yaml-flow 5.2.6 → 5.3.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/README.md +6 -6
- package/board-livecards-server-runtime.js +260 -35
- package/browser/board-livegraph-engine.js +61 -33
- package/browser/board-livegraph-engine.js.map +1 -1
- package/browser/card-compute.js +18 -18
- package/browser/live-cards.js +317 -156
- package/browser/live-cards.schema.json +15 -10
- package/dist/board-livegraph-runtime/index.cjs +61 -33
- package/dist/board-livegraph-runtime/index.cjs.map +1 -1
- package/dist/board-livegraph-runtime/index.d.cts +1 -1
- package/dist/board-livegraph-runtime/index.d.ts +1 -1
- package/dist/board-livegraph-runtime/index.js +61 -33
- package/dist/board-livegraph-runtime/index.js.map +1 -1
- package/dist/card-compute/index.cjs +101 -39
- package/dist/card-compute/index.cjs.map +1 -1
- package/dist/card-compute/index.d.cts +13 -8
- package/dist/card-compute/index.d.ts +13 -8
- package/dist/card-compute/index.js +101 -39
- package/dist/card-compute/index.js.map +1 -1
- package/dist/cli/board-live-cards-cli.cjs +7205 -202
- package/dist/cli/board-live-cards-cli.cjs.map +1 -1
- package/dist/cli/board-live-cards-cli.d.cts +6 -6
- package/dist/cli/board-live-cards-cli.d.ts +6 -6
- package/dist/cli/board-live-cards-cli.js +7204 -202
- package/dist/cli/board-live-cards-cli.js.map +1 -1
- package/dist/continuous-event-graph/index.cjs +59 -31
- package/dist/continuous-event-graph/index.cjs.map +1 -1
- package/dist/continuous-event-graph/index.d.cts +2 -2
- package/dist/continuous-event-graph/index.d.ts +2 -2
- package/dist/continuous-event-graph/index.js +59 -31
- package/dist/continuous-event-graph/index.js.map +1 -1
- package/dist/index.cjs +126 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +126 -54
- package/dist/index.js.map +1 -1
- package/dist/{live-cards-bridge-CeNxiVcm.d.ts → live-cards-bridge-EQjytzI_.d.ts} +10 -5
- package/dist/{live-cards-bridge-z_rJCSbi.d.cts → live-cards-bridge-x5XREkXm.d.cts} +10 -5
- package/examples/browser/boards/portfolio-tracker/cards/holdings-table.json +1 -1
- package/examples/browser/boards/portfolio-tracker/cards/portfolio-form.json +1 -1
- package/examples/browser/boards/portfolio-tracker/cards/portfolio-risk-assessment.json +1 -1
- package/examples/browser/boards/portfolio-tracker/cards/portfolio-value.json +1 -1
- package/examples/browser/boards/portfolio-tracker/cards/price-fetch.json +2 -2
- package/examples/browser/boards/portfolio-tracker/cards/rebalancing-strategy.json +1 -1
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +10 -10
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/holdings-table.json +1 -1
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +1 -1
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +1 -1
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/price-fetch.json +2 -2
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +1 -1
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +1 -1
- package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +1 -1
- package/examples/example-board/agent-instructions-cardlayout.md +29 -1
- package/examples/example-board/agent-instructions.md +271 -45
- package/examples/example-board/cards/card-concentration.json +8 -5
- package/examples/example-board/cards/card-market-prices.json +14 -9
- package/examples/example-board/cards/card-my-identity.json +28 -0
- package/examples/example-board/cards/card-portfolio-value.json +1 -1
- package/examples/example-board/cards/card-portfolio.json +1 -1
- package/examples/example-board/cards/card-rebalance-impact.json +65 -0
- package/examples/example-board/cards/card-rebalance-sim.json +67 -0
- package/examples/example-board/demo-chat-handler.js +2 -1
- package/examples/example-board/demo-server-config.json +6 -1
- package/examples/example-board/demo-server.js +91 -8
- package/examples/example-board/demo-shell-browser.html +6 -6
- package/examples/example-board/demo-shell-with-server.html +4 -4
- package/examples/example-board/demo-task-executor.js +457 -246
- package/examples/example-board/scripts/copilot_wrapper.bat +16 -0
- package/examples/example-board/scripts/copilot_wrapper_helper.ps1 +19 -10
- package/examples/example-board/scripts/workiq_wrapper.mjs +66 -0
- package/examples/npm-libs/continuous-event-graph/live-cards-board.ts +5 -5
- package/examples/npm-libs/continuous-event-graph/soc-incident-board.ts +3 -3
- package/examples/npm-libs/event-graph/research-pipeline.ts +5 -5
- package/examples/npm-libs/graph-of-graphs/multi-stage-etl.ts +9 -9
- package/examples/step-machine-cli/portfolio-tracker/cards/holdings-table.json +1 -1
- package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +1 -1
- package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +1 -1
- package/examples/step-machine-cli/portfolio-tracker/cards/price-fetch.json +3 -3
- package/examples/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +1 -1
- package/examples/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +1 -1
- package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +1 -1
- package/package.json +2 -2
- package/schema/live-cards.schema.json +15 -10
|
@@ -3,35 +3,54 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* demo-task-executor.js — Simple mock source executor for example-board.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Subcommands:
|
|
7
|
+
* run-source-fetch — fetch data for one source entry
|
|
8
|
+
* describe-capabilities — print supported source kinds + schemas to stdout (JSON)
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
+
* CLI args:
|
|
11
|
+
* --in <source.json> Required. Path to a temp JSON file containing the source definition.
|
|
12
|
+
* --out <result.json> Required. Path where this executor must write its JSON result.
|
|
13
|
+
* --err <error.txt> Optional. Path where this executor writes an error message on failure.
|
|
14
|
+
* --extra <base64json> Optional. Base64-encoded JSON with board topology context
|
|
15
|
+
* (baked into .task-executor at board init time, passed blindly by the CLI).
|
|
16
|
+
*
|
|
17
|
+
* --in payload (source definition):
|
|
10
18
|
* {
|
|
11
|
-
* "bindTo":
|
|
12
|
-
* "outputFile": "
|
|
13
|
-
*
|
|
14
|
-
* "
|
|
15
|
-
* "
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
+
* "bindTo": "token_name",
|
|
20
|
+
* "outputFile": "relative/path.json",
|
|
21
|
+
* "cwd": "<card directory>", // injected by CLI
|
|
22
|
+
* "boardDir":"<board runtime directory>", // injected by CLI
|
|
23
|
+
* "_projections": { "refKey": <resolvedValue> }, // named projections from card_data/requires,
|
|
24
|
+
* // declared in source_defs[].projections and resolved
|
|
25
|
+
* // by the engine before invoking the executor
|
|
26
|
+
* // ...plus any custom fields authored on the source entry (bindTo, outputFile, projections, etc.)
|
|
19
27
|
* }
|
|
20
28
|
*
|
|
21
|
-
*
|
|
29
|
+
* --extra (decoded):
|
|
30
|
+
* {
|
|
31
|
+
* "boardSetupRoot": "<abs path>", // board root (parent of runtime/, surface/, runtime-out/)
|
|
32
|
+
* "boardId": "<board id>", // e.g. "default"
|
|
33
|
+
* "boardRuntimeDir": "<relative>", // e.g. "runtime"
|
|
34
|
+
* "runtimeStatusDir": "<relative>", // e.g. "runtime-out"
|
|
35
|
+
* "cardsDir": "<relative>" // e.g. "surface/tmp-cards"
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* Supported source kinds (based on custom fields in --in):
|
|
22
39
|
* - { mock: "key" } → look up key in MOCK_DB (hardcoded below)
|
|
23
40
|
* - { copilot: { prompt_template, args? } } → call Copilot CLI with interpolated prompt
|
|
24
41
|
* - { prompt_template: "..." } → shorthand copilot call (top-level template)
|
|
25
|
-
* - {
|
|
26
|
-
* - {
|
|
27
|
-
*
|
|
42
|
+
* - { workiq: { query_template, args? } } → call WorkIQ (M365 Copilot) with interpolated query
|
|
43
|
+
* - { "url": { url, method?, headers?, args?, cacheTimeout? }, tickersFrom? }
|
|
44
|
+
* → single URL fetch via curl with {{key}} interpolation from _projections
|
|
45
|
+
* - { "url-list": { method?, headers?, cacheTimeout? } }
|
|
46
|
+
* → fan-out over _projections.url_list (string[]); returns array of responses.
|
|
47
|
+
* Build url_list in projections: e.g. `requires.holdings.ticker.('https://host/' & $ & '?q=1')`
|
|
48
|
+
* - { chartApi: { url, headers? }, tickersFrom } → removed; use url-list instead
|
|
49
|
+
* prefer url-list for new sources
|
|
28
50
|
* A real executor can also handle: graphapi, teams, mail, incidentdb, script, etc.
|
|
29
51
|
*
|
|
30
|
-
*
|
|
31
|
-
* -
|
|
32
|
-
* - tickersFrom: "tokenName.fieldName" extracts tickers from a _requires array
|
|
33
|
-
* - http and chartApi results are cached in os.tmpdir()/demo-executor-cache/ for 1 hour
|
|
34
|
-
* so Yahoo Finance is not hammered on every card refresh during demos
|
|
52
|
+
* url / url-list notes:
|
|
53
|
+
* - Results cached in os.tmpdir()/demo-executor-cache/ per URL (default 1 hour, override via cacheTimeout)
|
|
35
54
|
*/
|
|
36
55
|
|
|
37
56
|
import fs from 'node:fs';
|
|
@@ -62,7 +81,7 @@ const MOCK_DB = {
|
|
|
62
81
|
};
|
|
63
82
|
|
|
64
83
|
// ---------------------------------------------------------------------------
|
|
65
|
-
// Simple
|
|
84
|
+
// Simple file cache for url / url-list results.
|
|
66
85
|
// Stored in os.tmpdir()/demo-executor-cache/<hash>.json
|
|
67
86
|
// ---------------------------------------------------------------------------
|
|
68
87
|
const CACHE_DIR = path.join(os.tmpdir(), 'demo-executor-cache');
|
|
@@ -72,17 +91,32 @@ function cacheKey(str) {
|
|
|
72
91
|
return crypto.createHash('sha1').update(str).digest('hex');
|
|
73
92
|
}
|
|
74
93
|
|
|
75
|
-
function readCache(key) {
|
|
94
|
+
function readCache(key, ttlMs = CACHE_TTL_MS) {
|
|
76
95
|
const file = path.join(CACHE_DIR, `${key}.json`);
|
|
77
96
|
try {
|
|
78
97
|
const stat = fs.statSync(file);
|
|
79
|
-
if (Date.now() - stat.mtimeMs <
|
|
98
|
+
if (Date.now() - stat.mtimeMs < ttlMs) {
|
|
80
99
|
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
81
100
|
}
|
|
82
101
|
} catch {}
|
|
83
102
|
return null;
|
|
84
103
|
}
|
|
85
104
|
|
|
105
|
+
// Shared single-URL fetch helper used by both url and url-list.
|
|
106
|
+
// cacheTimeoutSec: override TTL in seconds (null → use CACHE_TTL_MS default).
|
|
107
|
+
function doFetchApi(url, method, headers, cacheTimeoutSec, errFile) {
|
|
108
|
+
const ttlMs = cacheTimeoutSec != null ? cacheTimeoutSec * 1000 : CACHE_TTL_MS;
|
|
109
|
+
const k = cacheKey(`url:${method}:${url}`);
|
|
110
|
+
const cached = readCache(k, ttlMs);
|
|
111
|
+
if (cached) {
|
|
112
|
+
console.warn(`[demo-task-executor] url: cache hit for ${url}`);
|
|
113
|
+
return cached;
|
|
114
|
+
}
|
|
115
|
+
const data = curlFetchJson(url, method, headers);
|
|
116
|
+
writeCache(k, data);
|
|
117
|
+
return data;
|
|
118
|
+
}
|
|
119
|
+
|
|
86
120
|
function writeCache(key, value) {
|
|
87
121
|
try {
|
|
88
122
|
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
@@ -103,6 +137,26 @@ function interpolatePrompt(template, args) {
|
|
|
103
137
|
});
|
|
104
138
|
}
|
|
105
139
|
|
|
140
|
+
// Reusable prompt fragments available to all copilot source templates.
|
|
141
|
+
// Source definitions can interpolate them with {{view_kind_guidance}} and {{card_layout_guidance}}.
|
|
142
|
+
const COPILOT_PROMPT_CONTEXT = {
|
|
143
|
+
view_kind_guidance: [
|
|
144
|
+
'VIEW KIND GUIDANCE (for dynamic ref rendering):',
|
|
145
|
+
'- Return a _view object whenever your output data is meant for a ref element.',
|
|
146
|
+
'- Allowed _view.kind values only: table, editable-table, chart, metric, list, badge, text, narrative, markdown, form, filter, todo, alert.',
|
|
147
|
+
'- If uncertain, use "table".',
|
|
148
|
+
'- For array rows that users should edit, prefer "editable-table" and set _view.data.writeTo to a card_data path.',
|
|
149
|
+
'- For chart, set _view.data.chartType and _view.data.columns with [labelField, valueField].',
|
|
150
|
+
'- Keep _view.data minimal and valid JSON (no comments, no trailing text).',
|
|
151
|
+
].join('\n'),
|
|
152
|
+
card_layout_guidance: [
|
|
153
|
+
'CARD LAYOUT GUIDANCE:',
|
|
154
|
+
'- Prefer compact outputs that fit a card: one primary structure plus concise rationale text.',
|
|
155
|
+
'- Avoid repeating values already present in upstream inputs.',
|
|
156
|
+
'- If you produce both machine-readable and human-readable content, keep machine-readable fields top-level and concise prose in a separate field.',
|
|
157
|
+
].join('\n'),
|
|
158
|
+
};
|
|
159
|
+
|
|
106
160
|
/**
|
|
107
161
|
* Fetch a URL using the system curl binary (synchronous, no Node event-loop handles).
|
|
108
162
|
* Throws if curl exits non-zero (e.g. HTTP 4xx/5xx with -f, or network error).
|
|
@@ -123,52 +177,18 @@ function curlFetchJson(url, method, headers) {
|
|
|
123
177
|
return JSON.parse(raw);
|
|
124
178
|
}
|
|
125
179
|
|
|
126
|
-
function stripCopilotFooter(rawText) {
|
|
127
|
-
const lines = String(rawText ?? '').split(/\r?\n/);
|
|
128
|
-
|
|
129
|
-
// Strip CLI-level tool-call telemetry lines emitted by the copilot binary when
|
|
130
|
-
// --allow-all activates MCP tools. These are NOT model output — the prompt cannot
|
|
131
|
-
// suppress them. They look like:
|
|
132
|
-
// ● Web Search (MCP: github-mcp-server) · ...
|
|
133
|
-
// └ {"type":"output_text",...}
|
|
134
|
-
const filtered = lines.filter(line => {
|
|
135
|
-
const t = line.trimStart();
|
|
136
|
-
return !(
|
|
137
|
-
/^[●•]\s+/.test(t) || // tool invocation lines
|
|
138
|
-
/^└\s+/.test(t) // tool result lines
|
|
139
|
-
);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// Remove trailing blank lines.
|
|
143
|
-
while (filtered.length > 0 && filtered[filtered.length - 1].trim() === '') filtered.pop();
|
|
144
|
-
|
|
145
|
-
// Remove the standard trailing Copilot metadata footer, if present.
|
|
146
|
-
if (
|
|
147
|
-
filtered.length >= 3 &&
|
|
148
|
-
/^Changes\b/i.test(filtered[filtered.length - 3]) &&
|
|
149
|
-
/^Requests\b/i.test(filtered[filtered.length - 2]) &&
|
|
150
|
-
/^Tokens\b/i.test(filtered[filtered.length - 1])
|
|
151
|
-
) {
|
|
152
|
-
filtered.splice(filtered.length - 3, 3);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
while (filtered.length > 0 && filtered[filtered.length - 1].trim() === '') filtered.pop();
|
|
156
|
-
return filtered.join('\n');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
180
|
function resolveCopilotPrompt(sourceDef) {
|
|
160
181
|
const cfg = sourceDef?.copilot && typeof sourceDef.copilot === 'object' ? sourceDef.copilot : {};
|
|
161
182
|
const template = cfg.prompt_template ?? sourceDef.prompt_template;
|
|
162
183
|
const args = cfg.args ?? cfg.prompt_args ?? sourceDef.prompt_args ?? sourceDef.args ?? {};
|
|
163
184
|
|
|
164
|
-
// Merge
|
|
165
|
-
//
|
|
166
|
-
//
|
|
185
|
+
// Merge _projections into template interpolation context.
|
|
186
|
+
// _projections contains the named data projections declared in source_defs[].projections,
|
|
187
|
+
// evaluated by the engine from card_data/requires before invoking this executor.
|
|
167
188
|
// Explicit args defined on the source take highest precedence.
|
|
168
189
|
const interpolationContext = {
|
|
169
|
-
...
|
|
170
|
-
...sourceDef.
|
|
171
|
-
...sourceDef._computed_values,
|
|
190
|
+
...COPILOT_PROMPT_CONTEXT,
|
|
191
|
+
...sourceDef._projections,
|
|
172
192
|
...args,
|
|
173
193
|
};
|
|
174
194
|
|
|
@@ -176,93 +196,63 @@ function resolveCopilotPrompt(sourceDef) {
|
|
|
176
196
|
return interpolatePrompt(template, interpolationContext);
|
|
177
197
|
}
|
|
178
198
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Run a copilot prompt via copilot_wrapper.bat (Windows only).
|
|
201
|
+
*
|
|
202
|
+
* The wrapper handles:
|
|
203
|
+
* - Session management (--resume UUID for multi-turn continuity)
|
|
204
|
+
* - Noise/footer stripping (via copilot_wrapper_helper.ps1)
|
|
205
|
+
* - JSON mode extraction with optional result_shape key matching
|
|
206
|
+
* - Agentic retry: if the first response isn't valid JSON, the wrapper calls
|
|
207
|
+
* copilot again in the same session with a correction prompt, then re-extracts.
|
|
208
|
+
*
|
|
209
|
+
* @param {string} prompt - interpolated prompt string
|
|
210
|
+
* @param {object} sourceDef - source definition (may contain copilot.result_shape)
|
|
211
|
+
* @param {string} wrapperOutFile - path the wrapper writes its JSON output to
|
|
212
|
+
* @param {string} sessionDir - persistent dir for session UUID (enables --resume)
|
|
213
|
+
* @param {string} cwd - working directory for copilot (boardSetupRoot)
|
|
214
|
+
* @returns {unknown} parsed JSON result value
|
|
215
|
+
*/
|
|
216
|
+
function runCopilotViaWrapper(prompt, sourceDef, wrapperOutFile, sessionDir, cwd) {
|
|
217
|
+
const wrapperPath = path.join(__dirname, 'scripts', 'copilot_wrapper.bat');
|
|
218
|
+
|
|
219
|
+
const promptFile = wrapperOutFile + '.prompt.txt';
|
|
220
|
+
fs.writeFileSync(promptFile, prompt, 'utf-8');
|
|
221
|
+
|
|
222
|
+
// Optional result_shape_file: top-level keys the response JSON must contain.
|
|
223
|
+
// Sourced from sourceDef.copilot.result_shape or sourceDef.result_shape.
|
|
224
|
+
let shapeFile = '';
|
|
225
|
+
const shape = sourceDef?.copilot?.result_shape ?? sourceDef?.result_shape;
|
|
226
|
+
if (shape && typeof shape === 'object') {
|
|
227
|
+
shapeFile = wrapperOutFile + '.shape.json';
|
|
228
|
+
fs.writeFileSync(shapeFile, JSON.stringify(shape), 'utf-8');
|
|
202
229
|
}
|
|
203
230
|
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function runCopilotPrompt(prompt) {
|
|
208
|
-
const copilotBin = resolveCopilotExecutable();
|
|
209
|
-
const copilotArgs = ['--allow-all'];
|
|
231
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
210
232
|
|
|
211
233
|
try {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
234
|
+
execFileSync('cmd.exe', [
|
|
235
|
+
'/d', '/c',
|
|
236
|
+
wrapperPath,
|
|
237
|
+
wrapperOutFile, // OUTPUT_FILE
|
|
238
|
+
sessionDir, // SESSION_DIR
|
|
239
|
+
cwd || process.cwd(), // WORKING_DIR
|
|
240
|
+
'@' + promptFile, // REQUEST_OR_FILE (@ prefix = file path)
|
|
241
|
+
'json', // RESULT_TYPE — wrapper extracts JSON + retries
|
|
242
|
+
sourceDef.bindTo || 'executor', // AGENT_NAME (for log file naming)
|
|
243
|
+
'', // MODEL (empty = wrapper default)
|
|
244
|
+
shapeFile, // RESULT_SHAPE_FILE (empty = accept any JSON)
|
|
245
|
+
], {
|
|
215
246
|
encoding: 'utf-8',
|
|
216
|
-
stdio: ['
|
|
247
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
217
248
|
maxBuffer: 10 * 1024 * 1024,
|
|
218
249
|
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (
|
|
222
|
-
const isCmdShim = /\.(bat|cmd)$/i.test(copilotBin);
|
|
223
|
-
|
|
224
|
-
if (isCmdShim) {
|
|
225
|
-
try {
|
|
226
|
-
return execFileSync(copilotBin, copilotArgs, {
|
|
227
|
-
input: String(prompt),
|
|
228
|
-
encoding: 'utf-8',
|
|
229
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
230
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
231
|
-
shell: true,
|
|
232
|
-
});
|
|
233
|
-
} catch {}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
try {
|
|
237
|
-
// Final fallback: resolve through cmd PATH lookup, still piping prompt on stdin.
|
|
238
|
-
return execFileSync('cmd.exe', ['/d', '/c', 'copilot --allow-all'], {
|
|
239
|
-
input: String(prompt),
|
|
240
|
-
encoding: 'utf-8',
|
|
241
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
242
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
243
|
-
});
|
|
244
|
-
} catch (cmdErr) {
|
|
245
|
-
const stderrDirect = directErr && typeof directErr === 'object' && 'stderr' in directErr
|
|
246
|
-
? String(directErr.stderr || '')
|
|
247
|
-
: '';
|
|
248
|
-
const stderrCmd = cmdErr && typeof cmdErr === 'object' && 'stderr' in cmdErr
|
|
249
|
-
? String(cmdErr.stderr || '')
|
|
250
|
-
: '';
|
|
251
|
-
const msg = [stderrDirect.trim(), stderrCmd.trim(), String(cmdErr && cmdErr.message || cmdErr)]
|
|
252
|
-
.filter(Boolean)
|
|
253
|
-
.join(' | ');
|
|
254
|
-
throw new Error(msg || 'copilot invocation failed');
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const stderrDirect = directErr && typeof directErr === 'object' && 'stderr' in directErr
|
|
259
|
-
? String(directErr.stderr || '')
|
|
260
|
-
: '';
|
|
261
|
-
const msg = [stderrDirect.trim(), String(directErr && directErr.message || directErr)]
|
|
262
|
-
.filter(Boolean)
|
|
263
|
-
.join(' | ');
|
|
264
|
-
throw new Error(msg || 'copilot invocation failed');
|
|
250
|
+
} finally {
|
|
251
|
+
try { fs.unlinkSync(promptFile); } catch {}
|
|
252
|
+
if (shapeFile) { try { fs.unlinkSync(shapeFile); } catch {} }
|
|
265
253
|
}
|
|
254
|
+
|
|
255
|
+
return JSON.parse(fs.readFileSync(wrapperOutFile, 'utf-8').replace(/^\uFEFF/, ''));
|
|
266
256
|
}
|
|
267
257
|
|
|
268
258
|
function fail(msg, errFile) {
|
|
@@ -279,9 +269,17 @@ function runSourceFetchSubcommand(argv) {
|
|
|
279
269
|
const inIdx = argv.indexOf('--in');
|
|
280
270
|
const outIdx = argv.indexOf('--out');
|
|
281
271
|
const errIdx = argv.indexOf('--err');
|
|
272
|
+
const extraIdx = argv.indexOf('--extra');
|
|
282
273
|
const inFile = inIdx !== -1 ? argv[inIdx + 1] : undefined;
|
|
283
274
|
const outFile = outIdx !== -1 ? argv[outIdx + 1] : undefined;
|
|
284
275
|
const errFile = errIdx !== -1 ? argv[errIdx + 1] : undefined;
|
|
276
|
+
const extraB64 = extraIdx !== -1 ? argv[extraIdx + 1] : undefined;
|
|
277
|
+
|
|
278
|
+
let extra = {};
|
|
279
|
+
if (extraB64) {
|
|
280
|
+
try { extra = JSON.parse(Buffer.from(extraB64, 'base64').toString('utf-8')); }
|
|
281
|
+
catch { console.warn('[demo-task-executor] bad --extra base64, ignoring'); }
|
|
282
|
+
}
|
|
285
283
|
|
|
286
284
|
if (!inFile || !outFile) {
|
|
287
285
|
fail('Usage: run-source-fetch --in <source.json> --out <result.json> [--err <error.txt>]', errFile);
|
|
@@ -300,119 +298,67 @@ function runSourceFetchSubcommand(argv) {
|
|
|
300
298
|
|
|
301
299
|
let resultValue;
|
|
302
300
|
|
|
303
|
-
if (sourceDef
|
|
301
|
+
if (sourceDef['url']) {
|
|
304
302
|
// ---------------------------------------------------------------------------
|
|
305
|
-
//
|
|
306
|
-
//
|
|
303
|
+
// url — single URL fetch via curl
|
|
304
|
+
// {{key}} interpolation applied to url from _projections and optional args.
|
|
305
|
+
// cacheTimeout: seconds to cache the response (default: CACHE_TTL_MS / 1000).
|
|
307
306
|
// ---------------------------------------------------------------------------
|
|
308
|
-
const
|
|
309
|
-
const
|
|
307
|
+
const cfg = sourceDef['url'];
|
|
308
|
+
const method = (cfg.method || 'GET').toUpperCase();
|
|
309
|
+
const headers = { ...(cfg.headers || {}) };
|
|
310
|
+
const cacheTimeoutSec = cfg.cacheTimeout != null ? Number(cfg.cacheTimeout) : null;
|
|
310
311
|
|
|
311
|
-
|
|
312
|
-
let tickers = [];
|
|
312
|
+
const fetchArgs = { ...(cfg.args || {}) };
|
|
313
313
|
if (sourceDef.tickersFrom) {
|
|
314
314
|
const dotIdx = sourceDef.tickersFrom.indexOf('.');
|
|
315
315
|
if (dotIdx > 0) {
|
|
316
|
-
const
|
|
316
|
+
const refKey = sourceDef.tickersFrom.slice(0, dotIdx);
|
|
317
317
|
const fieldName = sourceDef.tickersFrom.slice(dotIdx + 1);
|
|
318
|
-
const arr = sourceDef.
|
|
318
|
+
const arr = sourceDef._projections?.[refKey];
|
|
319
319
|
if (Array.isArray(arr)) {
|
|
320
|
-
tickers = arr.map(h => h[fieldName]).filter(Boolean);
|
|
320
|
+
fetchArgs.tickers = arr.map(h => h[fieldName]).filter(Boolean).join(',');
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
323
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
console.warn('[demo-task-executor] chartApi: tickersFrom resolved to empty list — falling back to mock');
|
|
327
|
-
} else {
|
|
328
|
-
const chartCacheKey = cacheKey('chartApi:' + tickers.sort().join(',') + chartCfg.url);
|
|
329
|
-
const cached = readCache(chartCacheKey);
|
|
330
|
-
if (cached) {
|
|
331
|
-
console.warn(`[demo-task-executor] chartApi: cache hit for [${tickers.join(', ')}]`);
|
|
332
|
-
resultValue = cached;
|
|
333
|
-
} else {
|
|
334
|
-
try {
|
|
335
|
-
const results = [];
|
|
336
|
-
for (const ticker of tickers) {
|
|
337
|
-
const url = interpolatePrompt(chartCfg.url, { ticker });
|
|
338
|
-
const data = curlFetchJson(url, 'GET', headers);
|
|
339
|
-
const meta = data?.chart?.result?.[0]?.meta;
|
|
340
|
-
if (!meta) throw new Error(`No chart meta for ${ticker}`);
|
|
341
|
-
// Map to quote-compatible shape; compute change from chartPreviousClose
|
|
342
|
-
const price = meta.regularMarketPrice ?? 0;
|
|
343
|
-
const prevClose = meta.chartPreviousClose ?? price;
|
|
344
|
-
const change = price - prevClose;
|
|
345
|
-
const changePct = prevClose !== 0 ? (change / prevClose) * 100 : 0;
|
|
346
|
-
results.push({
|
|
347
|
-
symbol: meta.symbol ?? ticker,
|
|
348
|
-
shortName: meta.shortName ?? meta.longName ?? ticker,
|
|
349
|
-
regularMarketPrice: price,
|
|
350
|
-
regularMarketChange: change,
|
|
351
|
-
regularMarketChangePercent: changePct,
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
resultValue = { quoteResponse: { result: results, error: null } };
|
|
355
|
-
writeCache(chartCacheKey, resultValue);
|
|
356
|
-
} catch (chartErr) {
|
|
357
|
-
fail(`chartApi fetch failed: ${chartErr.message}`, errFile);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
324
|
+
if (sourceDef.tickersFrom && !fetchArgs.tickers) {
|
|
325
|
+
fail('url: tickersFrom resolved to empty list — skipping fetch', errFile);
|
|
360
326
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
327
|
+
const urlContext = { ...(sourceDef._projections || {}), ...fetchArgs };
|
|
328
|
+
const url = interpolatePrompt(cfg.url, urlContext);
|
|
329
|
+
try {
|
|
330
|
+
resultValue = doFetchApi(url, method, headers, cacheTimeoutSec, errFile);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
fail(`url failed: ${err.message}`, errFile);
|
|
364
333
|
}
|
|
365
334
|
|
|
366
|
-
} else if (sourceDef
|
|
335
|
+
} else if (sourceDef['url-list']) {
|
|
367
336
|
// ---------------------------------------------------------------------------
|
|
368
|
-
//
|
|
337
|
+
// url-list — fan-out over a URL list, calling url logic per URL.
|
|
338
|
+
// url_list must be a string[] pre-resolved in _projections.url_list.
|
|
339
|
+
// cacheTimeout: seconds to cache each individual response.
|
|
369
340
|
// ---------------------------------------------------------------------------
|
|
370
|
-
const
|
|
341
|
+
const cfg = sourceDef['url-list'];
|
|
342
|
+
const method = (cfg.method || 'GET').toUpperCase();
|
|
343
|
+
const headers = { ...(cfg.headers || {}) };
|
|
344
|
+
const cacheTimeoutSec = cfg.cacheTimeout != null ? Number(cfg.cacheTimeout) : null;
|
|
371
345
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (
|
|
376
|
-
|
|
377
|
-
if (dotIdx > 0) {
|
|
378
|
-
const tokenName = sourceDef.tickersFrom.slice(0, dotIdx);
|
|
379
|
-
const fieldName = sourceDef.tickersFrom.slice(dotIdx + 1);
|
|
380
|
-
const arr = sourceDef._requires?.[tokenName];
|
|
381
|
-
if (Array.isArray(arr)) {
|
|
382
|
-
httpArgs.tickers = arr.map(h => h[fieldName]).filter(Boolean).join(',');
|
|
383
|
-
}
|
|
384
|
-
}
|
|
346
|
+
const urlList = Array.isArray(sourceDef._projections?.url_list)
|
|
347
|
+
? sourceDef._projections.url_list : null;
|
|
348
|
+
|
|
349
|
+
if (!urlList || urlList.length === 0) {
|
|
350
|
+
fail('url-list: _projections.url_list must be a non-empty string array', errFile);
|
|
385
351
|
}
|
|
386
352
|
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
...(sourceDef._requires || {}),
|
|
390
|
-
...(sourceDef._computed_values || {}),
|
|
391
|
-
...httpArgs,
|
|
392
|
-
};
|
|
393
|
-
const url = interpolatePrompt(httpCfg.url, urlContext);
|
|
394
|
-
const method = (httpCfg.method || 'GET').toUpperCase();
|
|
395
|
-
const headers = { ...(httpCfg.headers || {}) };
|
|
396
|
-
|
|
397
|
-
// Skip fetch entirely if tickers ended up empty (guard against empty ?symbols=)
|
|
398
|
-
const httpFetchSkipped = sourceDef.tickersFrom && !httpArgs.tickers;
|
|
399
|
-
|
|
400
|
-
const httpCacheKey = cacheKey(`http:${method}:${url}`);
|
|
401
|
-
const httpCached = readCache(httpCacheKey);
|
|
402
|
-
if (httpCached && !httpFetchSkipped) {
|
|
403
|
-
console.warn(`[demo-task-executor] http: cache hit for ${url}`);
|
|
404
|
-
resultValue = httpCached;
|
|
405
|
-
} else {
|
|
353
|
+
const results = [];
|
|
354
|
+
for (const u of urlList) {
|
|
406
355
|
try {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
resultValue = curlFetchJson(url, method, headers);
|
|
411
|
-
writeCache(httpCacheKey, resultValue);
|
|
412
|
-
} catch (httpErr) {
|
|
413
|
-
fail(`HTTP fetch failed: ${httpErr.message}`, errFile);
|
|
356
|
+
results.push(doFetchApi(u, method, headers, cacheTimeoutSec, errFile));
|
|
357
|
+
} catch (err) {
|
|
358
|
+
fail(`url-list fetch failed for ${u}: ${err.message}`, errFile);
|
|
414
359
|
}
|
|
415
360
|
}
|
|
361
|
+
resultValue = results;
|
|
416
362
|
|
|
417
363
|
} else if (sourceDef.copilot || sourceDef.prompt_template) {
|
|
418
364
|
const prompt = resolveCopilotPrompt(sourceDef);
|
|
@@ -420,22 +366,88 @@ function runSourceFetchSubcommand(argv) {
|
|
|
420
366
|
fail('Source definition missing copilot.prompt_template (or prompt_template)', errFile);
|
|
421
367
|
}
|
|
422
368
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
369
|
+
// Use boardSetupRoot (from --extra) as copilot working directory
|
|
370
|
+
const copilotCwd = extra.boardSetupRoot || undefined;
|
|
371
|
+
|
|
372
|
+
// On Windows, delegate entirely to copilot_wrapper.bat which handles:
|
|
373
|
+
// - session management (--resume UUID for multi-turn continuity)
|
|
374
|
+
// - noise/footer stripping, JSON extraction, agentic retry on bad shape
|
|
375
|
+
// On non-Windows, fall back to a basic direct invocation (no retry).
|
|
376
|
+
const wrapperPath = path.join(__dirname, 'scripts', 'copilot_wrapper.bat');
|
|
377
|
+
const useWrapper = process.platform === 'win32' && fs.existsSync(wrapperPath);
|
|
378
|
+
|
|
379
|
+
if (useWrapper) {
|
|
380
|
+
// Session dir is stable across refreshes so --resume continues the conversation.
|
|
381
|
+
const sessionDir = path.join(
|
|
382
|
+
extra.boardSetupRoot || os.tmpdir(),
|
|
383
|
+
'copilot-sessions',
|
|
384
|
+
String(sourceDef.bindTo || 'default').replace(/[^a-zA-Z0-9_-]/g, '_'),
|
|
385
|
+
);
|
|
386
|
+
const wrapperOutFile = outFile + '.wrapper-out.json';
|
|
387
|
+
try {
|
|
388
|
+
resultValue = runCopilotViaWrapper(prompt, sourceDef, wrapperOutFile, sessionDir, copilotCwd);
|
|
389
|
+
} catch (err) {
|
|
390
|
+
fail(`copilot invocation failed: ${String(err && err.message || err)}`, errFile);
|
|
391
|
+
} finally {
|
|
392
|
+
try { fs.unlinkSync(wrapperOutFile); } catch {}
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
// Non-Windows fallback: call copilot directly via cmd.exe and do basic JSON extraction.
|
|
396
|
+
let rawOutput = '';
|
|
397
|
+
try {
|
|
398
|
+
rawOutput = execFileSync('cmd.exe', ['/d', '/c', 'copilot --allow-all'], {
|
|
399
|
+
input: String(prompt),
|
|
400
|
+
encoding: 'utf-8',
|
|
401
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
402
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
403
|
+
...(copilotCwd ? { cwd: copilotCwd } : {}),
|
|
404
|
+
});
|
|
405
|
+
} catch (err) {
|
|
406
|
+
fail(`copilot invocation failed: ${String(err && err.message || err)}`, errFile);
|
|
407
|
+
}
|
|
408
|
+
// Basic JSON extraction: find first { or [ in output
|
|
409
|
+
const firstBrace = rawOutput.indexOf('{');
|
|
410
|
+
const firstBracket = rawOutput.indexOf('[');
|
|
411
|
+
const jsonStart = (firstBrace === -1) ? firstBracket
|
|
412
|
+
: (firstBracket === -1) ? firstBrace
|
|
413
|
+
: Math.min(firstBrace, firstBracket);
|
|
414
|
+
if (jsonStart !== -1) {
|
|
415
|
+
try {
|
|
416
|
+
const parsed = JSON.parse(rawOutput.slice(jsonStart));
|
|
417
|
+
resultValue = (parsed && typeof parsed === 'object') ? parsed : rawOutput;
|
|
418
|
+
} catch {
|
|
419
|
+
resultValue = rawOutput;
|
|
420
|
+
}
|
|
421
|
+
} else {
|
|
422
|
+
resultValue = rawOutput;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
} else if (sourceDef.workiq) {
|
|
426
|
+
const cfg = typeof sourceDef.workiq === 'object' ? sourceDef.workiq : {};
|
|
427
|
+
if (!cfg.query_template || typeof cfg.query_template !== 'string') {
|
|
428
|
+
fail('Source definition missing workiq.query_template', errFile);
|
|
429
429
|
}
|
|
430
|
+
const interpolationContext = { ...sourceDef._projections, ...(cfg.args ?? {}) };
|
|
431
|
+
const query = interpolatePrompt(cfg.query_template, interpolationContext);
|
|
430
432
|
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
433
|
+
const wrapperPath = path.join(__dirname, 'scripts', 'workiq_wrapper.mjs');
|
|
434
|
+
if (!fs.existsSync(wrapperPath)) {
|
|
435
|
+
fail('workiq source kind requires workiq_wrapper.js in scripts/', errFile);
|
|
436
|
+
}
|
|
434
437
|
try {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
438
|
+
execFileSync(process.execPath, [wrapperPath, outFile], {
|
|
439
|
+
encoding: 'utf-8',
|
|
440
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
441
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
442
|
+
env: {
|
|
443
|
+
...process.env,
|
|
444
|
+
WORKIQ_QUERY: query,
|
|
445
|
+
...(extra.serverUrl ? { WORKIQ_SERVER_URL: extra.serverUrl } : {}),
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
return; // wrapper wrote directly to outFile
|
|
449
|
+
} catch (err) {
|
|
450
|
+
fail(`workiq invocation failed: ${String(err && err.message || err)}`, errFile);
|
|
439
451
|
}
|
|
440
452
|
} else if (sourceDef.mock) {
|
|
441
453
|
// MOCK_DB lookup — data hardcoded at the top of this file
|
|
@@ -444,7 +456,7 @@ function runSourceFetchSubcommand(argv) {
|
|
|
444
456
|
fail(`Key "${sourceDef.mock}" not found in MOCK_DB`, errFile);
|
|
445
457
|
}
|
|
446
458
|
} else {
|
|
447
|
-
fail('Source definition has no recognised kind (
|
|
459
|
+
fail('Source definition has no recognised kind (url, url-list, copilot, workiq, mock)', errFile);
|
|
448
460
|
}
|
|
449
461
|
|
|
450
462
|
// Write result to --out as JSON payload, same contract as current mock mode.
|
|
@@ -456,12 +468,211 @@ function runSourceFetchSubcommand(argv) {
|
|
|
456
468
|
|
|
457
469
|
}
|
|
458
470
|
|
|
471
|
+
// ---------------------------------------------------------------------------
|
|
472
|
+
// validate-source-def — structural validation of a source definition
|
|
473
|
+
// ---------------------------------------------------------------------------
|
|
474
|
+
function validateSourceDefSubcommand(argv) {
|
|
475
|
+
const inIdx = argv.indexOf('--in');
|
|
476
|
+
const inFile = inIdx !== -1 ? argv[inIdx + 1] : undefined;
|
|
477
|
+
|
|
478
|
+
if (!inFile) {
|
|
479
|
+
console.error('[demo-task-executor] Usage: validate-source-def --in <source.json>');
|
|
480
|
+
process.exit(1);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (!fs.existsSync(inFile)) {
|
|
484
|
+
console.log(JSON.stringify({ ok: false, errors: [`Input file not found: ${inFile}`] }));
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
let sourceDef;
|
|
489
|
+
try {
|
|
490
|
+
sourceDef = readJson(inFile);
|
|
491
|
+
} catch (err) {
|
|
492
|
+
console.log(JSON.stringify({ ok: false, errors: [`Cannot parse source file: ${err && err.message || err}`] }));
|
|
493
|
+
process.exit(1);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const errors = [];
|
|
497
|
+
|
|
498
|
+
// Determine source kind and validate required fields
|
|
499
|
+
const hasUrl = !!sourceDef['url'];
|
|
500
|
+
const hasUrlList = !!sourceDef['url-list'];
|
|
501
|
+
const hasCopilot = !!sourceDef.copilot;
|
|
502
|
+
const hasPromptTemplate = typeof sourceDef.prompt_template === 'string';
|
|
503
|
+
const hasWorkiq = !!sourceDef.workiq;
|
|
504
|
+
const hasMock = sourceDef.mock !== undefined;
|
|
505
|
+
|
|
506
|
+
const kindCount = [hasUrl, hasUrlList, hasCopilot || hasPromptTemplate, hasWorkiq, hasMock].filter(Boolean).length;
|
|
507
|
+
|
|
508
|
+
if (kindCount === 0) {
|
|
509
|
+
errors.push('No recognised source kind (url, url-list, copilot, workiq, mock). Add one of these fields.');
|
|
510
|
+
} else if (kindCount > 1) {
|
|
511
|
+
const kinds = [];
|
|
512
|
+
if (hasUrl) kinds.push('url');
|
|
513
|
+
if (hasUrlList) kinds.push('url-list');
|
|
514
|
+
if (hasCopilot || hasPromptTemplate) kinds.push('copilot');
|
|
515
|
+
if (hasWorkiq) kinds.push('workiq');
|
|
516
|
+
if (hasMock) kinds.push('mock');
|
|
517
|
+
errors.push(`Multiple source kinds specified: [${kinds.join(', ')}]. Use exactly one.`);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (hasUrl) {
|
|
521
|
+
if (typeof sourceDef['url'] !== 'object') {
|
|
522
|
+
errors.push('url must be an object.');
|
|
523
|
+
} else if (!sourceDef['url'].url || typeof sourceDef['url'].url !== 'string') {
|
|
524
|
+
errors.push('url.url is required and must be a string.');
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (hasUrlList) {
|
|
529
|
+
if (typeof sourceDef['url-list'] !== 'object') {
|
|
530
|
+
errors.push('url-list must be an object.');
|
|
531
|
+
}
|
|
532
|
+
// url_list is supplied via _projections at runtime — no static validation needed.
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (hasCopilot) {
|
|
536
|
+
if (typeof sourceDef.copilot !== 'object') {
|
|
537
|
+
errors.push('copilot must be an object.');
|
|
538
|
+
} else {
|
|
539
|
+
if (!sourceDef.copilot.prompt_template && !hasPromptTemplate) {
|
|
540
|
+
errors.push('copilot.prompt_template is required (or use top-level prompt_template).');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (hasWorkiq) {
|
|
546
|
+
if (typeof sourceDef.workiq !== 'object') {
|
|
547
|
+
errors.push('workiq must be an object.');
|
|
548
|
+
} else if (!sourceDef.workiq.query_template || typeof sourceDef.workiq.query_template !== 'string') {
|
|
549
|
+
errors.push('workiq.query_template is required and must be a string.');
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (hasMock) {
|
|
554
|
+
if (typeof sourceDef.mock !== 'string') {
|
|
555
|
+
errors.push('mock must be a string key.');
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const result = { ok: errors.length === 0, errors };
|
|
560
|
+
console.log(JSON.stringify(result));
|
|
561
|
+
process.exit(errors.length === 0 ? 0 : 1);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ---------------------------------------------------------------------------
|
|
565
|
+
// describe-capabilities — introspection metadata for this executor
|
|
566
|
+
// ---------------------------------------------------------------------------
|
|
567
|
+
const CAPABILITIES = {
|
|
568
|
+
version: '1.0',
|
|
569
|
+
executor: 'demo-task-executor',
|
|
570
|
+
subcommands: ['run-source-fetch', 'describe-capabilities', 'validate-source-def'],
|
|
571
|
+
sourceKinds: {
|
|
572
|
+
mock: {
|
|
573
|
+
description: 'Look up a key in a hardcoded MOCK_DB dictionary.',
|
|
574
|
+
inputSchema: {
|
|
575
|
+
mock: { type: 'string', required: true, description: 'Key in MOCK_DB (e.g. "quotes").' },
|
|
576
|
+
},
|
|
577
|
+
outputShape: 'Arbitrary JSON — depends on the mock key.',
|
|
578
|
+
example: {
|
|
579
|
+
input: { mock: 'quotes' },
|
|
580
|
+
output: { quoteResponse: { result: [{ symbol: 'AAPL', regularMarketPrice: 198.15 }], error: null } },
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
copilot: {
|
|
584
|
+
description: 'Invoke GitHub Copilot CLI with an interpolated prompt template.',
|
|
585
|
+
inputSchema: {
|
|
586
|
+
copilot: {
|
|
587
|
+
type: 'object', required: false,
|
|
588
|
+
description: 'Object with prompt_template (string) and optional args (object).',
|
|
589
|
+
properties: {
|
|
590
|
+
prompt_template: { type: 'string', required: true, description: 'Prompt with {{key}} placeholders.' },
|
|
591
|
+
args: { type: 'object', required: false, description: 'Extra interpolation args (highest precedence).' },
|
|
592
|
+
},
|
|
593
|
+
},
|
|
594
|
+
prompt_template: { type: 'string', required: false, description: 'Shorthand — top-level prompt template (alternative to copilot.prompt_template).' },
|
|
595
|
+
},
|
|
596
|
+
outputShape: 'string | object — raw Copilot text, or parsed JSON if the response is valid JSON.',
|
|
597
|
+
},
|
|
598
|
+
workiq: {
|
|
599
|
+
description: 'Query WorkIQ (Microsoft 365 Copilot) with an interpolated query template. Returns raw text response.',
|
|
600
|
+
inputSchema: {
|
|
601
|
+
workiq: {
|
|
602
|
+
type: 'object', required: true,
|
|
603
|
+
properties: {
|
|
604
|
+
query_template: { type: 'string', required: true, description: 'Query with {{key}} placeholders interpolated from _projections and args.' },
|
|
605
|
+
args: { type: 'object', required: false, description: 'Extra interpolation args (highest precedence).' },
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
outputShape: 'string — raw M365 Copilot response text.',
|
|
610
|
+
note: 'Requires workiq CLI installed and Azure CLI logged in (az login).',
|
|
611
|
+
},
|
|
612
|
+
'url': {
|
|
613
|
+
description: 'Single URL fetch via curl with {{key}} interpolation from _projections. Supports cacheTimeout.',
|
|
614
|
+
inputSchema: {
|
|
615
|
+
'url': {
|
|
616
|
+
type: 'object', required: true,
|
|
617
|
+
properties: {
|
|
618
|
+
url: { type: 'string', required: true, description: 'URL template with {{key}} placeholders.' },
|
|
619
|
+
method: { type: 'string', required: false, description: 'HTTP method (default: GET).' },
|
|
620
|
+
headers: { type: 'object', required: false, description: 'Request headers.' },
|
|
621
|
+
args: { type: 'object', required: false, description: 'Extra interpolation args (highest precedence).' },
|
|
622
|
+
cacheTimeout: { type: 'number', required: false, description: 'Cache TTL in seconds (default: 3600).' },
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
tickersFrom: { type: 'string', required: false, description: '"refKey.fieldName" — join tickers from _projections into {{tickers}}.' },
|
|
626
|
+
},
|
|
627
|
+
outputShape: 'Arbitrary JSON from the fetched URL.',
|
|
628
|
+
},
|
|
629
|
+
'url-list': {
|
|
630
|
+
description: 'Fan-out over a pre-resolved URL list — calls url logic per URL and returns an array of responses. url_list must be a string[] in _projections.url_list (built via projections JSONata).',
|
|
631
|
+
inputSchema: {
|
|
632
|
+
'url-list': {
|
|
633
|
+
type: 'object', required: true,
|
|
634
|
+
properties: {
|
|
635
|
+
method: { type: 'string', required: false, description: 'HTTP method (default: GET).' },
|
|
636
|
+
headers: { type: 'object', required: false, description: 'Request headers.' },
|
|
637
|
+
cacheTimeout: { type: 'number', required: false, description: 'Cache TTL per URL in seconds (default: 3600).' },
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
outputShape: 'Array of raw JSON responses, one per URL in _projections.url_list.',
|
|
642
|
+
urlListNote: 'Declare `"projections": { "url_list": "<JSONata producing string[]>" }` on the source def. Example: `requires.holdings.ticker.(\'https://api.example.com/\' & $ & \'?q=1\')`',
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
extraSchema: {
|
|
646
|
+
description: 'Board topology context passed via --extra (base64-encoded JSON, baked at init).',
|
|
647
|
+
properties: {
|
|
648
|
+
boardSetupRoot: { type: 'string', description: 'Absolute path to board root.' },
|
|
649
|
+
boardId: { type: 'string', description: 'Board identifier.' },
|
|
650
|
+
boardRuntimeDir: { type: 'string', description: 'Relative path to runtime dir.' },
|
|
651
|
+
runtimeStatusDir: { type: 'string', description: 'Relative path to runtime-out dir.' },
|
|
652
|
+
cardsDir: { type: 'string', description: 'Relative path to cards dir.' },
|
|
653
|
+
serverUrl: { type: 'string', description: 'Base URL of the hosting server (e.g. http://127.0.0.1:7799). Used by source kinds that call server-side proxy endpoints.' },
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
function describeCapabilities() {
|
|
659
|
+
console.log(JSON.stringify(CAPABILITIES, null, 2));
|
|
660
|
+
}
|
|
661
|
+
|
|
459
662
|
async function main() {
|
|
460
663
|
const sub = process.argv[2];
|
|
461
664
|
if (sub === 'run-source-fetch') {
|
|
462
665
|
runSourceFetchSubcommand(process.argv.slice(3));
|
|
463
666
|
return;
|
|
464
667
|
}
|
|
668
|
+
if (sub === 'describe-capabilities') {
|
|
669
|
+
describeCapabilities();
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (sub === 'validate-source-def') {
|
|
673
|
+
validateSourceDefSubcommand(process.argv.slice(3));
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
465
676
|
|
|
466
677
|
console.warn(`[demo-task-executor] Unknown subcommand: ${sub}`);
|
|
467
678
|
process.exit(0);
|