yaml-flow 5.1.0 → 5.2.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 (105) hide show
  1. package/{examples/example-board/reusable-server-runtime.js → board-livecards-server-runtime.js} +42 -20
  2. package/{examples/example-board/reusable-board-runtime-client.js → browser/board-livecards-runtime-client.js} +6 -2
  3. package/browser/{board-livegraph-runtime.js → board-livegraph-engine.js} +212 -16
  4. package/browser/board-livegraph-engine.js.map +1 -0
  5. package/browser/live-cards.js +362 -38
  6. package/browser/live-cards.schema.json +20 -4
  7. package/dist/board-livegraph-runtime/index.cjs +210 -14
  8. package/dist/board-livegraph-runtime/index.cjs.map +1 -1
  9. package/dist/board-livegraph-runtime/index.d.cts +49 -5
  10. package/dist/board-livegraph-runtime/index.d.ts +49 -5
  11. package/dist/board-livegraph-runtime/index.js +209 -15
  12. package/dist/board-livegraph-runtime/index.js.map +1 -1
  13. package/dist/card-compute/index.cjs +63 -7
  14. package/dist/card-compute/index.cjs.map +1 -1
  15. package/dist/card-compute/index.d.cts +2 -2
  16. package/dist/card-compute/index.d.ts +2 -2
  17. package/dist/card-compute/index.js +63 -7
  18. package/dist/card-compute/index.js.map +1 -1
  19. package/dist/cli/board-live-cards-cli.cjs +664 -75
  20. package/dist/cli/board-live-cards-cli.cjs.map +1 -1
  21. package/dist/cli/board-live-cards-cli.d.cts +33 -5
  22. package/dist/cli/board-live-cards-cli.d.ts +33 -5
  23. package/dist/cli/board-live-cards-cli.js +661 -76
  24. package/dist/cli/board-live-cards-cli.js.map +1 -1
  25. package/dist/{constants-ozjf1Ejw.d.cts → constants-BzZUyYlp.d.cts} +1 -1
  26. package/dist/{constants-DuzE5n03.d.ts → constants-oCEbNpul.d.ts} +1 -1
  27. package/dist/continuous-event-graph/index.cjs +47 -14
  28. package/dist/continuous-event-graph/index.cjs.map +1 -1
  29. package/dist/continuous-event-graph/index.d.cts +9 -9
  30. package/dist/continuous-event-graph/index.d.ts +9 -9
  31. package/dist/continuous-event-graph/index.js +47 -14
  32. package/dist/continuous-event-graph/index.js.map +1 -1
  33. package/dist/event-graph/index.cjs +29 -12
  34. package/dist/event-graph/index.cjs.map +1 -1
  35. package/dist/event-graph/index.d.cts +5 -5
  36. package/dist/event-graph/index.d.ts +5 -5
  37. package/dist/event-graph/index.js +29 -12
  38. package/dist/event-graph/index.js.map +1 -1
  39. package/dist/index.cjs +93 -20
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.cts +7 -7
  42. package/dist/index.d.ts +7 -7
  43. package/dist/index.js +93 -20
  44. package/dist/index.js.map +1 -1
  45. package/dist/inference/index.cjs +29 -12
  46. package/dist/inference/index.cjs.map +1 -1
  47. package/dist/inference/index.d.cts +2 -2
  48. package/dist/inference/index.d.ts +2 -2
  49. package/dist/inference/index.js +29 -12
  50. package/dist/inference/index.js.map +1 -1
  51. package/dist/{journal-NLYuqege.d.ts → journal-9HEgs7dU.d.ts} +1 -1
  52. package/dist/{journal-DRfJiheM.d.cts → journal-B-JCfQnh.d.cts} +1 -1
  53. package/dist/{live-cards-bridge-Or7fdEJV.d.ts → live-cards-bridge-CeNxiVcm.d.ts} +6 -2
  54. package/dist/{live-cards-bridge-vGJ6tMzN.d.cts → live-cards-bridge-z_rJCSbi.d.cts} +6 -2
  55. package/dist/{schedule-CMcZe5Ny.d.ts → schedule-Cszq9LYY.d.ts} +1 -1
  56. package/dist/{schedule-CiucyCan.d.cts → schedule-qWNL0RQh.d.cts} +1 -1
  57. package/dist/{types-CMFSIjpc.d.cts → types-BBhqYGhE.d.cts} +4 -0
  58. package/dist/{types-CMFSIjpc.d.ts → types-BBhqYGhE.d.ts} +4 -0
  59. package/dist/{types-BzLD8bjb.d.cts → types-CHSdoAAA.d.cts} +1 -1
  60. package/dist/{types-C2eJ7DAV.d.ts → types-CoW0gQl3.d.ts} +1 -1
  61. package/dist/{validate-DJQTQ6bP.d.ts → validate-BAVzUJWa.d.ts} +1 -1
  62. package/dist/{validate-ke92Cleg.d.cts → validate-Dbu7ygys.d.cts} +1 -1
  63. package/examples/browser/boards/portfolio-tracker/cards/portfolio-risk-assessment.json +28 -0
  64. package/examples/browser/boards/portfolio-tracker/cards/rebalancing-strategy.json +28 -0
  65. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-inference-adapter.js +187 -0
  66. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +139 -5
  67. package/examples/example-board/agent-instructions-cardlayout.md +28 -0
  68. package/examples/example-board/agent-instructions.md +603 -0
  69. package/examples/example-board/cards/card-concentration.json +42 -0
  70. package/examples/example-board/cards/card-market-prices.json +51 -0
  71. package/examples/example-board/cards/card-portfolio-action.json +19 -0
  72. package/examples/example-board/cards/card-portfolio-risks.json +19 -0
  73. package/examples/example-board/cards/card-portfolio-value.json +62 -0
  74. package/examples/example-board/cards/card-portfolio.json +44 -0
  75. package/examples/example-board/demo-chat-handler.js +373 -33
  76. package/examples/example-board/demo-server-config.json +0 -2
  77. package/examples/example-board/demo-server.js +46 -7
  78. package/examples/example-board/demo-shell-browser.html +75 -207
  79. package/examples/example-board/demo-shell-with-server.html +14 -9
  80. package/examples/example-board/demo-shell.html +1 -1
  81. package/examples/example-board/demo-task-executor.js +259 -41
  82. package/package.json +6 -2
  83. package/schema/live-cards.schema.json +20 -4
  84. package/browser/board-livegraph-runtime.js.map +0 -1
  85. package/examples/example-board/board.yaml +0 -23
  86. package/examples/example-board/bootstrap_payload.json +0 -1
  87. package/examples/example-board/cards/card-chain-region-alert.json +0 -39
  88. package/examples/example-board/cards/card-chain-region-totals.json +0 -26
  89. package/examples/example-board/cards/card-chain-top-region.json +0 -24
  90. package/examples/example-board/cards/card-ex-actions.json +0 -32
  91. package/examples/example-board/cards/card-ex-chart.json +0 -30
  92. package/examples/example-board/cards/card-ex-filter.json +0 -36
  93. package/examples/example-board/cards/card-ex-filtered-by-preference.json +0 -59
  94. package/examples/example-board/cards/card-ex-form.json +0 -91
  95. package/examples/example-board/cards/card-ex-list.json +0 -22
  96. package/examples/example-board/cards/card-ex-markdown.json +0 -17
  97. package/examples/example-board/cards/card-ex-metric.json +0 -19
  98. package/examples/example-board/cards/card-ex-narrative.json +0 -36
  99. package/examples/example-board/cards/card-ex-source-http.json +0 -28
  100. package/examples/example-board/cards/card-ex-source.json +0 -21
  101. package/examples/example-board/cards/card-ex-status.json +0 -35
  102. package/examples/example-board/cards/card-ex-table.json +0 -30
  103. package/examples/example-board/cards/card-ex-todo.json +0 -29
  104. package/examples/example-board/mock.db +0 -15
  105. package/examples/example-board/reusable-runtime-artifacts-adapter.js +0 -233
@@ -0,0 +1,51 @@
1
+ {
2
+ "id": "card-market-prices",
3
+ "meta": {
4
+ "title": "Market Prices",
5
+ "tags": ["prices", "market"],
6
+ "desc": "Fetches live prices for portfolio tickers. Publishes enriched quote data for downstream portfolio value calculations."
7
+ },
8
+ "requires": ["holdings"],
9
+ "sources": [
10
+ {
11
+ "bindTo": "quotes",
12
+ "outputFile": "market-prices-quotes.json",
13
+ "chartApi": {
14
+ "url": "https://query1.finance.yahoo.com/v8/finance/chart/{{ticker}}?interval=1d&range=1d",
15
+ "headers": {
16
+ "User-Agent": "Mozilla/5.0 (compatible; portfolio-tracker-demo/1.0)"
17
+ }
18
+ },
19
+ "tickersFrom": "holdings.ticker",
20
+ "mock": "quotes"
21
+ }
22
+ ],
23
+ "compute": [
24
+ {
25
+ "bindTo": "prices",
26
+ "expr": "$map(fetched_sources.quotes.quoteResponse.result, function($q) { {\"ticker\": $q.symbol, \"name\": $q.shortName, \"price\": $round($q.regularMarketPrice, 2), \"change\": $round($q.regularMarketChange, 2), \"chg_pct\": $round($q.regularMarketChangePercent, 2)} })"
27
+ }
28
+ ],
29
+ "provides": [
30
+ { "bindTo": "quotes", "src": "fetched_sources.quotes" }
31
+ ],
32
+ "view": {
33
+ "elements": [
34
+ {
35
+ "kind": "table",
36
+ "label": "Prices",
37
+ "data": {
38
+ "bind": "computed_values.prices",
39
+ "columns": ["ticker", "name", "price", "change", "chg_pct"],
40
+ "sortable": true
41
+ }
42
+ }
43
+ ],
44
+ "layout": {
45
+ "board": { "col": 4, "order": 2 },
46
+ "canvas": { "x": 400, "y": 50, "w": 400, "h": 340 }
47
+ },
48
+ "features": { "refresh": true }
49
+ },
50
+ "card_data": {}
51
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "id": "card-portfolio-action",
3
+ "meta": {
4
+ "title": "Action Signal",
5
+ "tags": ["portfolio", "action"],
6
+ "desc": "Actionable recommendation sourced from Portfolio Intelligence."
7
+ },
8
+ "requires": ["portfolio_action"],
9
+ "view": {
10
+ "elements": [
11
+ { "kind": "markdown", "data": { "bind": "requires.portfolio_action" } }
12
+ ],
13
+ "layout": {
14
+ "board": { "col": 4, "order": 6 },
15
+ "canvas": { "x": 1660, "y": 50, "w": 300, "h": 160 }
16
+ }
17
+ },
18
+ "card_data": {}
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "id": "card-portfolio-risks",
3
+ "meta": {
4
+ "title": "Risk Watch",
5
+ "tags": ["portfolio", "risk"],
6
+ "desc": "Per-ticker risk signals sourced from Portfolio Intelligence."
7
+ },
8
+ "requires": ["portfolio_risks"],
9
+ "view": {
10
+ "elements": [
11
+ { "kind": "markdown", "data": { "bind": "requires.portfolio_risks" } }
12
+ ],
13
+ "layout": {
14
+ "board": { "col": 3, "order": 5 },
15
+ "canvas": { "x": 1300, "y": 270, "w": 340, "h": 220 }
16
+ }
17
+ },
18
+ "card_data": {}
19
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "id": "card-portfolio-value",
3
+ "meta": {
4
+ "title": "Portfolio Value",
5
+ "tags": ["portfolio", "value"],
6
+ "desc": "Computes total portfolio value, gainers/losers summary, and per-position P&L from holdings and live market quotes."
7
+ },
8
+ "requires": ["holdings", "quotes"],
9
+ "provides": [
10
+ { "bindTo": "positions", "src": "computed_values.positions" }
11
+ ],
12
+ "compute": [
13
+ {
14
+ "bindTo": "positions",
15
+ "expr": "($hMap := $merge(requires.holdings.{ticker: $}); $map(requires.quotes.quoteResponse.result, function($q) { ($h := $lookup($hMap, $q.symbol); $qty := $h.quantity; $cb := $h.cost_basis; $val := $round($q.regularMarketPrice * $qty, 2); $cost := $round($cb * $qty, 2); $chg := $round($q.regularMarketChange * $qty, 2); {\"ticker\": $q.symbol, \"quantity\": $qty, \"cost_basis\": $cb, \"price\": $round($q.regularMarketPrice, 2), \"value\": $val, \"gain_$\": $round($val - $cost, 2), \"gain_%\": $round(($val - $cost) / $cost * 100, 2), \"chg_$\": $chg, \"chg_pct\": $round($q.regularMarketChangePercent, 2)}) }))"
16
+ },
17
+ {
18
+ "bindTo": "totalValue",
19
+ "expr": "$round($sum(computed_values.positions.value), 2)"
20
+ },
21
+ {
22
+ "bindTo": "gainers",
23
+ "expr": "$count($filter(computed_values.positions, function($p){ $p.chg_pct > 0 }))"
24
+ },
25
+ {
26
+ "bindTo": "losers",
27
+ "expr": "$count($filter(computed_values.positions, function($p){ $p.chg_pct < 0 }))"
28
+ },
29
+ {
30
+ "bindTo": "gainersLosers",
31
+ "expr": "computed_values.gainers & ' up · ' & computed_values.losers & ' down'"
32
+ }
33
+ ],
34
+ "view": {
35
+ "elements": [
36
+ {
37
+ "kind": "metric",
38
+ "label": "Portfolio Value ($)",
39
+ "data": { "bind": "computed_values.totalValue" }
40
+ },
41
+ {
42
+ "kind": "text",
43
+ "data": { "bind": "computed_values.gainersLosers" }
44
+ },
45
+ {
46
+ "kind": "table",
47
+ "label": "Positions",
48
+ "data": {
49
+ "bind": "computed_values.positions",
50
+ "columns": ["ticker", "value", "gain_$", "gain_%"],
51
+ "sortable": true
52
+ }
53
+ }
54
+ ],
55
+ "layout": {
56
+ "board": { "col": 4, "order": 3 },
57
+ "canvas": { "x": 840, "y": 50, "w": 420, "h": 380 }
58
+ },
59
+ "features": { "chat": true }
60
+ },
61
+ "card_data": {}
62
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "id": "card-portfolio",
3
+ "meta": {
4
+ "title": "My Portfolio",
5
+ "tags": ["portfolio"],
6
+ "desc": "Manage your stock holdings — edit tickers and quantities inline, add or delete rows. Changes propagate downstream immediately."
7
+ },
8
+ "provides": [
9
+ { "bindTo": "holdings", "src": "card_data.holdings" }
10
+ ],
11
+ "compute": [],
12
+ "view": {
13
+ "elements": [
14
+ {
15
+ "kind": "editable-table",
16
+ "label": "Holdings",
17
+ "data": {
18
+ "bind": "card_data.holdings",
19
+ "writeTo": "card_data.holdings",
20
+ "columns": ["ticker", "quantity", "cost_basis"],
21
+ "schema": {
22
+ "properties": {
23
+ "quantity": { "type": "number" },
24
+ "cost_basis": { "type": "number" }
25
+ }
26
+ }
27
+ }
28
+ }
29
+ ],
30
+ "layout": {
31
+ "board": { "col": 4, "order": 1 },
32
+ "canvas": { "x": 50, "y": 50, "w": 320, "h": 340 }
33
+ },
34
+ "features": { "chat": true }
35
+ },
36
+ "card_data": {
37
+ "holdings": [
38
+ { "ticker": "AAPL", "quantity": 10, "cost_basis": 150.00 },
39
+ { "ticker": "MSFT", "quantity": 5, "cost_basis": 310.00 },
40
+ { "ticker": "GOOGL", "quantity": 2, "cost_basis": 280.00 },
41
+ { "ticker": "TSLA", "quantity": 3, "cost_basis": 200.00 }
42
+ ]
43
+ }
44
+ }
@@ -1,21 +1,26 @@
1
1
  #!/usr/bin/env node
2
- // demo-chat-handler.js — Echo chat handler for demo/example boards.
3
- //
4
- // Invoked by reusable-server-runtime after a user chat message is persisted,
5
- // when a .chat-handler file is present in the board runtime directory.
6
- //
7
- // Invocation contract:
8
- // node demo-chat-handler.js --boardId <id> --cardId <id> --extra <json>
9
- //
10
- // --extra JSON shape: { chatDir: "<abs path>", boardDir: "<abs path>", lastChatFile: "<filename>" }
11
- //
12
- // This demo handler:
13
- // 1. Reads the content of the last chat file (the user message just written).
14
- // 2. Computes the next serial by incrementing the leading number from lastChatFile.
15
- // 3. Writes <nextSerial>-assistant.txt to chatDir with: "Echoing <original content>"
2
+ /**
3
+ * demo-chat-handler.js — LLM-based chat handler for example-board.
4
+ *
5
+ * Protocol (invoked by reusable-server-runtime after a user message is persisted):
6
+ * node demo-chat-handler.js --boardId <id> --cardId <id> --extraEncJson <base64json>
7
+ *
8
+ * --extraEncJson decodes to: { chatDir: "<abs>", boardDir: "<abs>", lastChatFile: "<filename>" }
9
+ *
10
+ * Design:
11
+ * The chat handler is universal — it does not depend on any source definition or card model.
12
+ * It reads the full conversation history from chatDir, builds a grounded system prompt
13
+ * (scoped to the card and board), and calls the LLM directly (Copilot CLI).
14
+ * Copilot is invoked from boardDir (cwd), so it naturally has access to board files.
15
+ *
16
+ * The LLM is the sole decision-maker. No rule-based fallback is used here — if the LLM
17
+ * is unavailable, the handler writes a short error acknowledgment so the user isn't left
18
+ * with a silent failure.
19
+ */
16
20
 
17
21
  import * as fs from 'node:fs';
18
22
  import * as path from 'node:path';
23
+ import { execFileSync } from 'node:child_process';
19
24
 
20
25
  const args = process.argv.slice(2);
21
26
 
@@ -24,46 +29,381 @@ function getArg(name) {
24
29
  return idx !== -1 && args[idx + 1] !== undefined ? args[idx + 1] : null;
25
30
  }
26
31
 
27
- const boardId = getArg('--boardId') || '';
28
- const cardId = getArg('--cardId') || '';
29
- const extraStr = getArg('--extraEncJson') || '';
32
+ const boardId = getArg('--boardId') || '';
33
+ const cardId = getArg('--cardId') || '';
34
+ const extraStr = getArg('--extraEncJson') || '';
30
35
 
31
36
  let extra = {};
32
37
  try {
33
38
  extra = JSON.parse(Buffer.from(extraStr, 'base64').toString('utf-8'));
34
39
  } catch {
35
- console.error('[demo-chat-handler] could not parse --extra JSON');
40
+ console.error('[demo-chat-handler] could not parse --extraEncJson');
36
41
  process.exit(0);
37
42
  }
38
43
 
39
- const { chatDir, lastChatFile } = extra;
44
+ const { chatDir, boardDir, lastChatFile } = extra;
40
45
 
41
46
  if (!chatDir || !lastChatFile) {
42
- console.error('[demo-chat-handler] --extra must contain chatDir and lastChatFile');
47
+ console.error('[demo-chat-handler] --extraEncJson must contain chatDir and lastChatFile');
43
48
  process.exit(0);
44
49
  }
45
50
 
46
- // Read the user message from the last chat file.
47
- const lastChatPath = path.join(chatDir, lastChatFile);
48
- let content = '';
51
+ // ---------------------------------------------------------------------------
52
+ // Read full conversation history from chatDir (all user + assistant turns)
53
+ // ---------------------------------------------------------------------------
54
+ function readConversationHistory(dir) {
55
+ let files;
56
+ try {
57
+ files = fs.readdirSync(dir).filter(f => /^\d+[-_](user|assistant)\.txt$/i.test(f));
58
+ files.sort();
59
+ } catch {
60
+ return [];
61
+ }
62
+ return files.map(f => {
63
+ const role = /user/i.test(f) ? 'User' : 'Assistant';
64
+ let text = '';
65
+ try { text = fs.readFileSync(path.join(dir, f), 'utf-8').trim(); } catch {}
66
+ return `${role}: ${text}`;
67
+ });
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Build prompt: system instruction + conversation turns
72
+ // ---------------------------------------------------------------------------
73
+ function buildPrompt(bId, cId, history) {
74
+ return [
75
+ `You are a helpful assistant embedded in a live data card (card: "${cId}", board: "${bId}").`,
76
+ 'Help the user understand and act on the data shown in this card.',
77
+ 'Be concise — this is an inline card chat, not a full conversation window.',
78
+ 'Ground answers in the card\'s data context. Ask one short question if the intent is ambiguous.',
79
+ '',
80
+ ...history,
81
+ 'Assistant:',
82
+ ].join('\n');
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Call Copilot CLI — same pattern as demo-task-executor.js
87
+ // ---------------------------------------------------------------------------
88
+ function stripCopilotFooter(rawText) {
89
+ const lines = String(rawText ?? '').split(/\r?\n/);
90
+ while (lines.length > 0 && lines[lines.length - 1].trim() === '') lines.pop();
91
+ if (
92
+ lines.length >= 3 &&
93
+ /^Changes\b/i.test(lines[lines.length - 3]) &&
94
+ /^Requests\b/i.test(lines[lines.length - 2]) &&
95
+ /^Tokens\b/i.test(lines[lines.length - 1])
96
+ ) {
97
+ lines.splice(lines.length - 3, 3);
98
+ }
99
+ while (lines.length > 0 && lines[lines.length - 1].trim() === '') lines.pop();
100
+ return lines.join('\n');
101
+ }
102
+
103
+ function resolveCopilotExecutable() {
104
+ const envBin = process.env.COPILOT_BIN;
105
+ if (envBin && fs.existsSync(envBin)) return envBin;
106
+ if (process.platform === 'win32') {
107
+ try {
108
+ const out = execFileSync('where.exe', ['copilot'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
109
+ const candidates = out.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
110
+ return candidates.find(p => /\.(cmd|exe|bat)$/i.test(p)) ?? candidates[0] ?? 'copilot';
111
+ } catch {}
112
+ } else {
113
+ try {
114
+ const out = execFileSync('which', ['copilot'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
115
+ return out.split(/\r?\n/).map(s => s.trim()).find(Boolean) ?? 'copilot';
116
+ } catch {}
117
+ }
118
+ return 'copilot';
119
+ }
120
+
121
+ function runCopilotPrompt(prompt, cwd) {
122
+ const copilotBin = resolveCopilotExecutable();
123
+ const opts = {
124
+ input: String(prompt),
125
+ encoding: 'utf-8',
126
+ stdio: ['pipe', 'pipe', 'pipe'],
127
+ maxBuffer: 10 * 1024 * 1024,
128
+ timeout: 60000,
129
+ ...(cwd ? { cwd } : {}),
130
+ };
131
+ try {
132
+ return execFileSync(copilotBin, ['--allow-all'], opts);
133
+ } catch (directErr) {
134
+ if (process.platform === 'win32') {
135
+ try {
136
+ return execFileSync('cmd.exe', ['/d', '/c', 'copilot --allow-all'], opts);
137
+ } catch (cmdErr) {
138
+ const msg = [
139
+ directErr?.stderr?.trim?.(),
140
+ cmdErr?.stderr?.trim?.(),
141
+ String(cmdErr?.message ?? cmdErr),
142
+ ].filter(Boolean).join(' | ');
143
+ throw new Error(msg || 'copilot invocation failed');
144
+ }
145
+ }
146
+ const msg = [directErr?.stderr?.trim?.(), String(directErr?.message ?? directErr)].filter(Boolean).join(' | ');
147
+ throw new Error(msg || 'copilot invocation failed');
148
+ }
149
+ }
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Main
153
+ // ---------------------------------------------------------------------------
154
+ const history = readConversationHistory(chatDir);
155
+ const prompt = buildPrompt(boardId, cardId, history);
156
+ const cwd = boardDir && fs.existsSync(boardDir) ? boardDir : undefined;
157
+
158
+ let response = '';
49
159
  try {
50
- content = fs.readFileSync(lastChatPath, 'utf-8').trim();
160
+ response = stripCopilotFooter(runCopilotPrompt(prompt, cwd)).trim();
51
161
  } catch (err) {
52
- console.error(`[demo-chat-handler] could not read ${lastChatPath}: ${err.message}`);
53
- process.exit(0);
162
+ const lastUser = [...history].reverse().find(l => l.startsWith('User:')) ?? '';
163
+ response = `Sorry, I could not reach the LLM right now. (${String(err?.message ?? err).slice(0, 120)})`;
164
+ console.error(`[demo-chat-handler] LLM call failed: ${err?.message ?? err}`);
54
165
  }
55
166
 
56
- // Derive next serial by incrementing the leading digits in lastChatFile.
57
- // e.g. "007_user.txt" → 7 → next = 8 → "008-assistant.txt"
167
+ // Write assistant response as next serial file
58
168
  const serialMatch = String(lastChatFile).match(/^(\d+)/);
59
- const nextSerial = serialMatch ? parseInt(serialMatch[1], 10) + 1 : 1;
60
- const nextName = `${String(nextSerial).padStart(3, '0')}-assistant.txt`;
61
- const nextPath = path.join(chatDir, nextName);
169
+ const nextSerial = serialMatch ? parseInt(serialMatch[1], 10) + 1 : 1;
170
+ const nextName = `${String(nextSerial).padStart(3, '0')}-assistant.txt`;
171
+ const nextPath = path.join(chatDir, nextName);
62
172
 
63
173
  try {
64
- fs.writeFileSync(nextPath, `Echoing ${content}\n`, 'utf-8');
65
- console.log(`[demo-chat-handler] boardId="${boardId}" cardId="${cardId}" wrote echo → ${nextPath}`);
174
+ fs.writeFileSync(nextPath, response + '\n', 'utf-8');
175
+ console.log(`[demo-chat-handler] boardId="${boardId}" cardId="${cardId}" → ${nextPath}`);
66
176
  } catch (err) {
67
177
  console.error(`[demo-chat-handler] write failed: ${err.message}`);
178
+ }
179
+ *
180
+ * Protocol (invoked by reusable-server-runtime after a user message is persisted):
181
+ * node demo-chat-handler.js --boardId <id> --cardId <id> --extraEncJson <base64json>
182
+ *
183
+ * --extraEncJson decodes to: { chatDir: "<abs>", boardDir: "<abs>", lastChatFile: "<filename>" }
184
+ *
185
+ * Responsibilities:
186
+ * 1. Read the full conversation history from chatDir (all *_user.txt / *-assistant.txt files).
187
+ * 2. Read the current card state from boardDir/board-graph.json (card_data, fetched_sources,
188
+ * computed_values for cardId) to use as grounding context.
189
+ * 3. Build a system prompt that situates the LLM as an assistant for this specific card,
190
+ * including the card's current data as context.
191
+ * 4. Send the conversation + context to the LLM (Copilot via CLI).
192
+ * 5. Write the response as <nextSerial>-assistant.txt to chatDir.
193
+ *
194
+ * Design principle:
195
+ * The chat is always scoped to the card where the chat button is embedded.
196
+ * The card's current state (card_data, computed_values, fetched_sources) is the primary
197
+ * grounding context. The LLM should help the user understand, explore, or act on that card's
198
+ * data — not give generic answers disconnected from the card's content.
199
+ *
200
+ * The system prompt should encourage the LLM to:
201
+ * - Reference the card's actual values when answering
202
+ * - Ask clarifying questions if the user's intent is ambiguous
203
+ * - Suggest next steps relevant to the card's domain
204
+ * - Be concise (the chat is embedded in a card, not a full chat window)
205
+ */
206
+
207
+ import * as fs from 'node:fs';
208
+ import * as path from 'node:path';
209
+ import { execFileSync } from 'node:child_process';
210
+
211
+ const args = process.argv.slice(2);
212
+
213
+ function getArg(name) {
214
+ const idx = args.indexOf(name);
215
+ return idx !== -1 && args[idx + 1] !== undefined ? args[idx + 1] : null;
216
+ }
217
+
218
+ const boardId = getArg('--boardId') || '';
219
+ const cardId = getArg('--cardId') || '';
220
+ const extraStr = getArg('--extraEncJson') || '';
221
+
222
+ let extra = {};
223
+ try {
224
+ extra = JSON.parse(Buffer.from(extraStr, 'base64').toString('utf-8'));
225
+ } catch {
226
+ console.error('[demo-chat-handler] could not parse --extraEncJson');
68
227
  process.exit(0);
69
228
  }
229
+
230
+ const { chatDir, boardDir, lastChatFile } = extra;
231
+
232
+ if (!chatDir || !lastChatFile) {
233
+ console.error('[demo-chat-handler] --extraEncJson must contain chatDir and lastChatFile');
234
+ process.exit(0);
235
+ }
236
+
237
+ // ---------------------------------------------------------------------------
238
+ // 1. Read full conversation history from chatDir
239
+ // ---------------------------------------------------------------------------
240
+ function readConversationHistory(dir) {
241
+ let files;
242
+ try {
243
+ files = fs.readdirSync(dir).filter(f => /^\d+[-_](user|assistant)\.txt$/i.test(f));
244
+ files.sort();
245
+ } catch {
246
+ return [];
247
+ }
248
+ return files.map(f => {
249
+ const role = /user/i.test(f) ? 'user' : 'assistant';
250
+ let text = '';
251
+ try { text = fs.readFileSync(path.join(dir, f), 'utf-8').trim(); } catch {}
252
+ return { role, text };
253
+ });
254
+ }
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // 2. Read card state from board-graph.json
258
+ // ---------------------------------------------------------------------------
259
+ function readCardState(bDir, cId) {
260
+ if (!bDir) return null;
261
+ try {
262
+ const boardGraph = JSON.parse(fs.readFileSync(path.join(bDir, 'board-graph.json'), 'utf-8'));
263
+ // board-graph.json wraps a LiveGraph snapshot; cards live under graph.nodes
264
+ const nodes = boardGraph?.graph?.nodes ?? boardGraph?.nodes ?? {};
265
+ return nodes[cId] ?? null;
266
+ } catch {
267
+ return null;
268
+ }
269
+ }
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // 3. Build system prompt grounded in the card's current state
273
+ // ---------------------------------------------------------------------------
274
+ function buildSystemPrompt(cId, cardState) {
275
+ const lines = [
276
+ `You are a helpful assistant embedded inside a live card (id: "${cId}") on a data dashboard.`,
277
+ 'Your role is to help the user understand, interpret, and act on the data shown in this card.',
278
+ 'Always ground your answers in the card\'s actual current values. Be concise — this is an embedded card chat, not a full conversation window.',
279
+ 'If the user\'s question is ambiguous, ask one short clarifying question.',
280
+ 'Suggest relevant next steps or insights when appropriate.',
281
+ '',
282
+ '--- Current card state ---',
283
+ ];
284
+
285
+ if (cardState) {
286
+ if (cardState.card_data && Object.keys(cardState.card_data).length > 0) {
287
+ lines.push('card_data: ' + JSON.stringify(cardState.card_data, null, 2));
288
+ }
289
+ if (cardState.computed_values && Object.keys(cardState.computed_values).length > 0) {
290
+ lines.push('computed_values: ' + JSON.stringify(cardState.computed_values, null, 2));
291
+ }
292
+ if (cardState.fetched_sources && Object.keys(cardState.fetched_sources).length > 0) {
293
+ lines.push('fetched_sources: ' + JSON.stringify(cardState.fetched_sources, null, 2));
294
+ }
295
+ } else {
296
+ lines.push('(card state not available)');
297
+ }
298
+
299
+ lines.push('--- End card state ---');
300
+ return lines.join('\n');
301
+ }
302
+
303
+ // ---------------------------------------------------------------------------
304
+ // 4. Build the full prompt (system + conversation turns)
305
+ // ---------------------------------------------------------------------------
306
+ function buildPrompt(systemPrompt, history) {
307
+ const parts = [systemPrompt, ''];
308
+ for (const turn of history) {
309
+ parts.push(`${turn.role === 'user' ? 'User' : 'Assistant'}: ${turn.text}`);
310
+ }
311
+ parts.push('Assistant:');
312
+ return parts.join('\n');
313
+ }
314
+
315
+ // ---------------------------------------------------------------------------
316
+ // 5. Call LLM (Copilot CLI)
317
+ // ---------------------------------------------------------------------------
318
+ function resolveCopilotExecutable() {
319
+ const envBin = process.env.COPILOT_BIN;
320
+ if (envBin && fs.existsSync(envBin)) return envBin;
321
+
322
+ if (process.platform === 'win32') {
323
+ try {
324
+ const out = execFileSync('where.exe', ['copilot'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
325
+ const candidates = out.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
326
+ return candidates.find(p => /\.(cmd|exe|bat)$/i.test(p)) ?? candidates[0] ?? 'copilot';
327
+ } catch {}
328
+ } else {
329
+ try {
330
+ const out = execFileSync('which', ['copilot'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
331
+ return out.split(/\r?\n/).map(s => s.trim()).find(Boolean) ?? 'copilot';
332
+ } catch {}
333
+ }
334
+ return 'copilot';
335
+ }
336
+
337
+ function stripCopilotFooter(rawText) {
338
+ const lines = String(rawText ?? '').split(/\r?\n/);
339
+ while (lines.length > 0 && lines[lines.length - 1].trim() === '') lines.pop();
340
+ if (
341
+ lines.length >= 3 &&
342
+ /^Changes\b/i.test(lines[lines.length - 3]) &&
343
+ /^Requests\b/i.test(lines[lines.length - 2]) &&
344
+ /^Tokens\b/i.test(lines[lines.length - 1])
345
+ ) {
346
+ lines.splice(lines.length - 3, 3);
347
+ }
348
+ while (lines.length > 0 && lines[lines.length - 1].trim() === '') lines.pop();
349
+ return lines.join('\n');
350
+ }
351
+
352
+ function runLLM(prompt) {
353
+ const copilotBin = resolveCopilotExecutable();
354
+ try {
355
+ const raw = execFileSync(copilotBin, ['--allow-all'], {
356
+ input: String(prompt),
357
+ encoding: 'utf-8',
358
+ stdio: ['pipe', 'pipe', 'pipe'],
359
+ maxBuffer: 10 * 1024 * 1024,
360
+ timeout: 60000,
361
+ });
362
+ return stripCopilotFooter(raw).trim();
363
+ } catch (err) {
364
+ if (process.platform === 'win32') {
365
+ try {
366
+ const raw = execFileSync('cmd.exe', ['/d', '/c', 'copilot --allow-all'], {
367
+ input: String(prompt),
368
+ encoding: 'utf-8',
369
+ stdio: ['pipe', 'pipe', 'pipe'],
370
+ maxBuffer: 10 * 1024 * 1024,
371
+ timeout: 60000,
372
+ });
373
+ return stripCopilotFooter(raw).trim();
374
+ } catch {}
375
+ }
376
+ throw err;
377
+ }
378
+ }
379
+
380
+ // ---------------------------------------------------------------------------
381
+ // Main
382
+ // ---------------------------------------------------------------------------
383
+ const history = readConversationHistory(chatDir);
384
+ const cardState = readCardState(boardDir, cardId);
385
+ const systemPmt = buildSystemPrompt(cardId, cardState);
386
+ const fullPrompt = buildPrompt(systemPmt, history);
387
+
388
+ let response = '';
389
+ try {
390
+ response = runLLM(fullPrompt);
391
+ } catch (err) {
392
+ // Fallback: acknowledge the message so the user sees something
393
+ const lastUserMsg = [...history].reverse().find(t => t.role === 'user')?.text ?? '';
394
+ response = `I received your message ("${lastUserMsg.slice(0, 80)}") but could not reach the LLM right now. Please try again.`;
395
+ console.error(`[demo-chat-handler] LLM call failed: ${err && err.message || err}`);
396
+ }
397
+
398
+ // Derive next serial and write assistant response
399
+ const serialMatch = String(lastChatFile).match(/^(\d+)/);
400
+ const nextSerial = serialMatch ? parseInt(serialMatch[1], 10) + 1 : 1;
401
+ const nextName = `${String(nextSerial).padStart(3, '0')}-assistant.txt`;
402
+ const nextPath = path.join(chatDir, nextName);
403
+
404
+ try {
405
+ fs.writeFileSync(nextPath, response + '\n', 'utf-8');
406
+ console.log(`[demo-chat-handler] boardId="${boardId}" cardId="${cardId}" wrote response → ${nextPath}`);
407
+ } catch (err) {
408
+ console.error(`[demo-chat-handler] write failed: ${err.message}`);
409
+ }
@@ -1,7 +1,5 @@
1
1
  {
2
2
  "port": 7799,
3
- "boardLiveCardsCliJs": "../../dist/cli/board-live-cards-cli.js",
4
- "stepMachineCliPath": "../../step-machine-cli.js",
5
3
  "taskExecutorPath": "./demo-task-executor.js",
6
4
  "chatHandlerPath": "./demo-chat-handler.js"
7
5
  }