yaml-flow 5.2.6 → 5.2.8
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 +57 -32
- package/browser/board-livegraph-engine.js.map +1 -1
- package/browser/card-compute.js +17 -17
- package/browser/live-cards.js +139 -12
- package/browser/live-cards.schema.json +14 -9
- package/dist/board-livegraph-runtime/index.cjs +57 -32
- 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 +57 -32
- package/dist/board-livegraph-runtime/index.js.map +1 -1
- package/dist/card-compute/index.cjs +96 -38
- 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 +96 -38
- package/dist/card-compute/index.js.map +1 -1
- package/dist/cli/board-live-cards-cli.cjs +7200 -201
- 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 +7199 -201
- package/dist/cli/board-live-cards-cli.js.map +1 -1
- package/dist/continuous-event-graph/index.cjs +55 -30
- 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 +55 -30
- package/dist/continuous-event-graph/index.js.map +1 -1
- package/dist/index.cjs +121 -53
- 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 +121 -53
- 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 +1 -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 +57 -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 +79 -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 +436 -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 +14 -9
|
@@ -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 });
|
|
@@ -123,52 +157,17 @@ function curlFetchJson(url, method, headers) {
|
|
|
123
157
|
return JSON.parse(raw);
|
|
124
158
|
}
|
|
125
159
|
|
|
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
160
|
function resolveCopilotPrompt(sourceDef) {
|
|
160
161
|
const cfg = sourceDef?.copilot && typeof sourceDef.copilot === 'object' ? sourceDef.copilot : {};
|
|
161
162
|
const template = cfg.prompt_template ?? sourceDef.prompt_template;
|
|
162
163
|
const args = cfg.args ?? cfg.prompt_args ?? sourceDef.prompt_args ?? sourceDef.args ?? {};
|
|
163
164
|
|
|
164
|
-
// Merge
|
|
165
|
-
//
|
|
166
|
-
//
|
|
165
|
+
// Merge _projections into template interpolation context.
|
|
166
|
+
// _projections contains the named data projections declared in source_defs[].projections,
|
|
167
|
+
// evaluated by the engine from card_data/requires before invoking this executor.
|
|
167
168
|
// Explicit args defined on the source take highest precedence.
|
|
168
169
|
const interpolationContext = {
|
|
169
|
-
...sourceDef.
|
|
170
|
-
...sourceDef._sourcesData,
|
|
171
|
-
...sourceDef._computed_values,
|
|
170
|
+
...sourceDef._projections,
|
|
172
171
|
...args,
|
|
173
172
|
};
|
|
174
173
|
|
|
@@ -176,93 +175,63 @@ function resolveCopilotPrompt(sourceDef) {
|
|
|
176
175
|
return interpolatePrompt(template, interpolationContext);
|
|
177
176
|
}
|
|
178
177
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Run a copilot prompt via copilot_wrapper.bat (Windows only).
|
|
180
|
+
*
|
|
181
|
+
* The wrapper handles:
|
|
182
|
+
* - Session management (--resume UUID for multi-turn continuity)
|
|
183
|
+
* - Noise/footer stripping (via copilot_wrapper_helper.ps1)
|
|
184
|
+
* - JSON mode extraction with optional result_shape key matching
|
|
185
|
+
* - Agentic retry: if the first response isn't valid JSON, the wrapper calls
|
|
186
|
+
* copilot again in the same session with a correction prompt, then re-extracts.
|
|
187
|
+
*
|
|
188
|
+
* @param {string} prompt - interpolated prompt string
|
|
189
|
+
* @param {object} sourceDef - source definition (may contain copilot.result_shape)
|
|
190
|
+
* @param {string} wrapperOutFile - path the wrapper writes its JSON output to
|
|
191
|
+
* @param {string} sessionDir - persistent dir for session UUID (enables --resume)
|
|
192
|
+
* @param {string} cwd - working directory for copilot (boardSetupRoot)
|
|
193
|
+
* @returns {unknown} parsed JSON result value
|
|
194
|
+
*/
|
|
195
|
+
function runCopilotViaWrapper(prompt, sourceDef, wrapperOutFile, sessionDir, cwd) {
|
|
196
|
+
const wrapperPath = path.join(__dirname, 'scripts', 'copilot_wrapper.bat');
|
|
197
|
+
|
|
198
|
+
const promptFile = wrapperOutFile + '.prompt.txt';
|
|
199
|
+
fs.writeFileSync(promptFile, prompt, 'utf-8');
|
|
200
|
+
|
|
201
|
+
// Optional result_shape_file: top-level keys the response JSON must contain.
|
|
202
|
+
// Sourced from sourceDef.copilot.result_shape or sourceDef.result_shape.
|
|
203
|
+
let shapeFile = '';
|
|
204
|
+
const shape = sourceDef?.copilot?.result_shape ?? sourceDef?.result_shape;
|
|
205
|
+
if (shape && typeof shape === 'object') {
|
|
206
|
+
shapeFile = wrapperOutFile + '.shape.json';
|
|
207
|
+
fs.writeFileSync(shapeFile, JSON.stringify(shape), 'utf-8');
|
|
202
208
|
}
|
|
203
209
|
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function runCopilotPrompt(prompt) {
|
|
208
|
-
const copilotBin = resolveCopilotExecutable();
|
|
209
|
-
const copilotArgs = ['--allow-all'];
|
|
210
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
210
211
|
|
|
211
212
|
try {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
execFileSync('cmd.exe', [
|
|
214
|
+
'/d', '/c',
|
|
215
|
+
wrapperPath,
|
|
216
|
+
wrapperOutFile, // OUTPUT_FILE
|
|
217
|
+
sessionDir, // SESSION_DIR
|
|
218
|
+
cwd || process.cwd(), // WORKING_DIR
|
|
219
|
+
'@' + promptFile, // REQUEST_OR_FILE (@ prefix = file path)
|
|
220
|
+
'json', // RESULT_TYPE — wrapper extracts JSON + retries
|
|
221
|
+
sourceDef.bindTo || 'executor', // AGENT_NAME (for log file naming)
|
|
222
|
+
'', // MODEL (empty = wrapper default)
|
|
223
|
+
shapeFile, // RESULT_SHAPE_FILE (empty = accept any JSON)
|
|
224
|
+
], {
|
|
215
225
|
encoding: 'utf-8',
|
|
216
|
-
stdio: ['
|
|
226
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
217
227
|
maxBuffer: 10 * 1024 * 1024,
|
|
218
228
|
});
|
|
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');
|
|
229
|
+
} finally {
|
|
230
|
+
try { fs.unlinkSync(promptFile); } catch {}
|
|
231
|
+
if (shapeFile) { try { fs.unlinkSync(shapeFile); } catch {} }
|
|
265
232
|
}
|
|
233
|
+
|
|
234
|
+
return JSON.parse(fs.readFileSync(wrapperOutFile, 'utf-8').replace(/^\uFEFF/, ''));
|
|
266
235
|
}
|
|
267
236
|
|
|
268
237
|
function fail(msg, errFile) {
|
|
@@ -279,9 +248,17 @@ function runSourceFetchSubcommand(argv) {
|
|
|
279
248
|
const inIdx = argv.indexOf('--in');
|
|
280
249
|
const outIdx = argv.indexOf('--out');
|
|
281
250
|
const errIdx = argv.indexOf('--err');
|
|
251
|
+
const extraIdx = argv.indexOf('--extra');
|
|
282
252
|
const inFile = inIdx !== -1 ? argv[inIdx + 1] : undefined;
|
|
283
253
|
const outFile = outIdx !== -1 ? argv[outIdx + 1] : undefined;
|
|
284
254
|
const errFile = errIdx !== -1 ? argv[errIdx + 1] : undefined;
|
|
255
|
+
const extraB64 = extraIdx !== -1 ? argv[extraIdx + 1] : undefined;
|
|
256
|
+
|
|
257
|
+
let extra = {};
|
|
258
|
+
if (extraB64) {
|
|
259
|
+
try { extra = JSON.parse(Buffer.from(extraB64, 'base64').toString('utf-8')); }
|
|
260
|
+
catch { console.warn('[demo-task-executor] bad --extra base64, ignoring'); }
|
|
261
|
+
}
|
|
285
262
|
|
|
286
263
|
if (!inFile || !outFile) {
|
|
287
264
|
fail('Usage: run-source-fetch --in <source.json> --out <result.json> [--err <error.txt>]', errFile);
|
|
@@ -300,119 +277,67 @@ function runSourceFetchSubcommand(argv) {
|
|
|
300
277
|
|
|
301
278
|
let resultValue;
|
|
302
279
|
|
|
303
|
-
if (sourceDef
|
|
280
|
+
if (sourceDef['url']) {
|
|
304
281
|
// ---------------------------------------------------------------------------
|
|
305
|
-
//
|
|
306
|
-
//
|
|
282
|
+
// url — single URL fetch via curl
|
|
283
|
+
// {{key}} interpolation applied to url from _projections and optional args.
|
|
284
|
+
// cacheTimeout: seconds to cache the response (default: CACHE_TTL_MS / 1000).
|
|
307
285
|
// ---------------------------------------------------------------------------
|
|
308
|
-
const
|
|
309
|
-
const
|
|
286
|
+
const cfg = sourceDef['url'];
|
|
287
|
+
const method = (cfg.method || 'GET').toUpperCase();
|
|
288
|
+
const headers = { ...(cfg.headers || {}) };
|
|
289
|
+
const cacheTimeoutSec = cfg.cacheTimeout != null ? Number(cfg.cacheTimeout) : null;
|
|
310
290
|
|
|
311
|
-
|
|
312
|
-
let tickers = [];
|
|
291
|
+
const fetchArgs = { ...(cfg.args || {}) };
|
|
313
292
|
if (sourceDef.tickersFrom) {
|
|
314
293
|
const dotIdx = sourceDef.tickersFrom.indexOf('.');
|
|
315
294
|
if (dotIdx > 0) {
|
|
316
|
-
const
|
|
295
|
+
const refKey = sourceDef.tickersFrom.slice(0, dotIdx);
|
|
317
296
|
const fieldName = sourceDef.tickersFrom.slice(dotIdx + 1);
|
|
318
|
-
const arr = sourceDef.
|
|
297
|
+
const arr = sourceDef._projections?.[refKey];
|
|
319
298
|
if (Array.isArray(arr)) {
|
|
320
|
-
tickers = arr.map(h => h[fieldName]).filter(Boolean);
|
|
299
|
+
fetchArgs.tickers = arr.map(h => h[fieldName]).filter(Boolean).join(',');
|
|
321
300
|
}
|
|
322
301
|
}
|
|
323
302
|
}
|
|
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
|
-
}
|
|
303
|
+
if (sourceDef.tickersFrom && !fetchArgs.tickers) {
|
|
304
|
+
fail('url: tickersFrom resolved to empty list — skipping fetch', errFile);
|
|
360
305
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
306
|
+
const urlContext = { ...(sourceDef._projections || {}), ...fetchArgs };
|
|
307
|
+
const url = interpolatePrompt(cfg.url, urlContext);
|
|
308
|
+
try {
|
|
309
|
+
resultValue = doFetchApi(url, method, headers, cacheTimeoutSec, errFile);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
fail(`url failed: ${err.message}`, errFile);
|
|
364
312
|
}
|
|
365
313
|
|
|
366
|
-
} else if (sourceDef
|
|
314
|
+
} else if (sourceDef['url-list']) {
|
|
367
315
|
// ---------------------------------------------------------------------------
|
|
368
|
-
//
|
|
316
|
+
// url-list — fan-out over a URL list, calling url logic per URL.
|
|
317
|
+
// url_list must be a string[] pre-resolved in _projections.url_list.
|
|
318
|
+
// cacheTimeout: seconds to cache each individual response.
|
|
369
319
|
// ---------------------------------------------------------------------------
|
|
370
|
-
const
|
|
320
|
+
const cfg = sourceDef['url-list'];
|
|
321
|
+
const method = (cfg.method || 'GET').toUpperCase();
|
|
322
|
+
const headers = { ...(cfg.headers || {}) };
|
|
323
|
+
const cacheTimeoutSec = cfg.cacheTimeout != null ? Number(cfg.cacheTimeout) : null;
|
|
371
324
|
|
|
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
|
-
}
|
|
325
|
+
const urlList = Array.isArray(sourceDef._projections?.url_list)
|
|
326
|
+
? sourceDef._projections.url_list : null;
|
|
327
|
+
|
|
328
|
+
if (!urlList || urlList.length === 0) {
|
|
329
|
+
fail('url-list: _projections.url_list must be a non-empty string array', errFile);
|
|
385
330
|
}
|
|
386
331
|
|
|
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 {
|
|
332
|
+
const results = [];
|
|
333
|
+
for (const u of urlList) {
|
|
406
334
|
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);
|
|
335
|
+
results.push(doFetchApi(u, method, headers, cacheTimeoutSec, errFile));
|
|
336
|
+
} catch (err) {
|
|
337
|
+
fail(`url-list fetch failed for ${u}: ${err.message}`, errFile);
|
|
414
338
|
}
|
|
415
339
|
}
|
|
340
|
+
resultValue = results;
|
|
416
341
|
|
|
417
342
|
} else if (sourceDef.copilot || sourceDef.prompt_template) {
|
|
418
343
|
const prompt = resolveCopilotPrompt(sourceDef);
|
|
@@ -420,22 +345,88 @@ function runSourceFetchSubcommand(argv) {
|
|
|
420
345
|
fail('Source definition missing copilot.prompt_template (or prompt_template)', errFile);
|
|
421
346
|
}
|
|
422
347
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
348
|
+
// Use boardSetupRoot (from --extra) as copilot working directory
|
|
349
|
+
const copilotCwd = extra.boardSetupRoot || undefined;
|
|
350
|
+
|
|
351
|
+
// On Windows, delegate entirely to copilot_wrapper.bat which handles:
|
|
352
|
+
// - session management (--resume UUID for multi-turn continuity)
|
|
353
|
+
// - noise/footer stripping, JSON extraction, agentic retry on bad shape
|
|
354
|
+
// On non-Windows, fall back to a basic direct invocation (no retry).
|
|
355
|
+
const wrapperPath = path.join(__dirname, 'scripts', 'copilot_wrapper.bat');
|
|
356
|
+
const useWrapper = process.platform === 'win32' && fs.existsSync(wrapperPath);
|
|
357
|
+
|
|
358
|
+
if (useWrapper) {
|
|
359
|
+
// Session dir is stable across refreshes so --resume continues the conversation.
|
|
360
|
+
const sessionDir = path.join(
|
|
361
|
+
extra.boardSetupRoot || os.tmpdir(),
|
|
362
|
+
'copilot-sessions',
|
|
363
|
+
String(sourceDef.bindTo || 'default').replace(/[^a-zA-Z0-9_-]/g, '_'),
|
|
364
|
+
);
|
|
365
|
+
const wrapperOutFile = outFile + '.wrapper-out.json';
|
|
366
|
+
try {
|
|
367
|
+
resultValue = runCopilotViaWrapper(prompt, sourceDef, wrapperOutFile, sessionDir, copilotCwd);
|
|
368
|
+
} catch (err) {
|
|
369
|
+
fail(`copilot invocation failed: ${String(err && err.message || err)}`, errFile);
|
|
370
|
+
} finally {
|
|
371
|
+
try { fs.unlinkSync(wrapperOutFile); } catch {}
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
// Non-Windows fallback: call copilot directly via cmd.exe and do basic JSON extraction.
|
|
375
|
+
let rawOutput = '';
|
|
376
|
+
try {
|
|
377
|
+
rawOutput = execFileSync('cmd.exe', ['/d', '/c', 'copilot --allow-all'], {
|
|
378
|
+
input: String(prompt),
|
|
379
|
+
encoding: 'utf-8',
|
|
380
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
381
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
382
|
+
...(copilotCwd ? { cwd: copilotCwd } : {}),
|
|
383
|
+
});
|
|
384
|
+
} catch (err) {
|
|
385
|
+
fail(`copilot invocation failed: ${String(err && err.message || err)}`, errFile);
|
|
386
|
+
}
|
|
387
|
+
// Basic JSON extraction: find first { or [ in output
|
|
388
|
+
const firstBrace = rawOutput.indexOf('{');
|
|
389
|
+
const firstBracket = rawOutput.indexOf('[');
|
|
390
|
+
const jsonStart = (firstBrace === -1) ? firstBracket
|
|
391
|
+
: (firstBracket === -1) ? firstBrace
|
|
392
|
+
: Math.min(firstBrace, firstBracket);
|
|
393
|
+
if (jsonStart !== -1) {
|
|
394
|
+
try {
|
|
395
|
+
const parsed = JSON.parse(rawOutput.slice(jsonStart));
|
|
396
|
+
resultValue = (parsed && typeof parsed === 'object') ? parsed : rawOutput;
|
|
397
|
+
} catch {
|
|
398
|
+
resultValue = rawOutput;
|
|
399
|
+
}
|
|
400
|
+
} else {
|
|
401
|
+
resultValue = rawOutput;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
} else if (sourceDef.workiq) {
|
|
405
|
+
const cfg = typeof sourceDef.workiq === 'object' ? sourceDef.workiq : {};
|
|
406
|
+
if (!cfg.query_template || typeof cfg.query_template !== 'string') {
|
|
407
|
+
fail('Source definition missing workiq.query_template', errFile);
|
|
429
408
|
}
|
|
409
|
+
const interpolationContext = { ...sourceDef._projections, ...(cfg.args ?? {}) };
|
|
410
|
+
const query = interpolatePrompt(cfg.query_template, interpolationContext);
|
|
430
411
|
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
412
|
+
const wrapperPath = path.join(__dirname, 'scripts', 'workiq_wrapper.mjs');
|
|
413
|
+
if (!fs.existsSync(wrapperPath)) {
|
|
414
|
+
fail('workiq source kind requires workiq_wrapper.js in scripts/', errFile);
|
|
415
|
+
}
|
|
434
416
|
try {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
417
|
+
execFileSync(process.execPath, [wrapperPath, outFile], {
|
|
418
|
+
encoding: 'utf-8',
|
|
419
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
420
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
421
|
+
env: {
|
|
422
|
+
...process.env,
|
|
423
|
+
WORKIQ_QUERY: query,
|
|
424
|
+
...(extra.serverUrl ? { WORKIQ_SERVER_URL: extra.serverUrl } : {}),
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
return; // wrapper wrote directly to outFile
|
|
428
|
+
} catch (err) {
|
|
429
|
+
fail(`workiq invocation failed: ${String(err && err.message || err)}`, errFile);
|
|
439
430
|
}
|
|
440
431
|
} else if (sourceDef.mock) {
|
|
441
432
|
// MOCK_DB lookup — data hardcoded at the top of this file
|
|
@@ -444,7 +435,7 @@ function runSourceFetchSubcommand(argv) {
|
|
|
444
435
|
fail(`Key "${sourceDef.mock}" not found in MOCK_DB`, errFile);
|
|
445
436
|
}
|
|
446
437
|
} else {
|
|
447
|
-
fail('Source definition has no recognised kind (
|
|
438
|
+
fail('Source definition has no recognised kind (url, url-list, copilot, workiq, mock)', errFile);
|
|
448
439
|
}
|
|
449
440
|
|
|
450
441
|
// Write result to --out as JSON payload, same contract as current mock mode.
|
|
@@ -456,12 +447,211 @@ function runSourceFetchSubcommand(argv) {
|
|
|
456
447
|
|
|
457
448
|
}
|
|
458
449
|
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
// validate-source-def — structural validation of a source definition
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
function validateSourceDefSubcommand(argv) {
|
|
454
|
+
const inIdx = argv.indexOf('--in');
|
|
455
|
+
const inFile = inIdx !== -1 ? argv[inIdx + 1] : undefined;
|
|
456
|
+
|
|
457
|
+
if (!inFile) {
|
|
458
|
+
console.error('[demo-task-executor] Usage: validate-source-def --in <source.json>');
|
|
459
|
+
process.exit(1);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (!fs.existsSync(inFile)) {
|
|
463
|
+
console.log(JSON.stringify({ ok: false, errors: [`Input file not found: ${inFile}`] }));
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
let sourceDef;
|
|
468
|
+
try {
|
|
469
|
+
sourceDef = readJson(inFile);
|
|
470
|
+
} catch (err) {
|
|
471
|
+
console.log(JSON.stringify({ ok: false, errors: [`Cannot parse source file: ${err && err.message || err}`] }));
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const errors = [];
|
|
476
|
+
|
|
477
|
+
// Determine source kind and validate required fields
|
|
478
|
+
const hasUrl = !!sourceDef['url'];
|
|
479
|
+
const hasUrlList = !!sourceDef['url-list'];
|
|
480
|
+
const hasCopilot = !!sourceDef.copilot;
|
|
481
|
+
const hasPromptTemplate = typeof sourceDef.prompt_template === 'string';
|
|
482
|
+
const hasWorkiq = !!sourceDef.workiq;
|
|
483
|
+
const hasMock = sourceDef.mock !== undefined;
|
|
484
|
+
|
|
485
|
+
const kindCount = [hasUrl, hasUrlList, hasCopilot || hasPromptTemplate, hasWorkiq, hasMock].filter(Boolean).length;
|
|
486
|
+
|
|
487
|
+
if (kindCount === 0) {
|
|
488
|
+
errors.push('No recognised source kind (url, url-list, copilot, workiq, mock). Add one of these fields.');
|
|
489
|
+
} else if (kindCount > 1) {
|
|
490
|
+
const kinds = [];
|
|
491
|
+
if (hasUrl) kinds.push('url');
|
|
492
|
+
if (hasUrlList) kinds.push('url-list');
|
|
493
|
+
if (hasCopilot || hasPromptTemplate) kinds.push('copilot');
|
|
494
|
+
if (hasWorkiq) kinds.push('workiq');
|
|
495
|
+
if (hasMock) kinds.push('mock');
|
|
496
|
+
errors.push(`Multiple source kinds specified: [${kinds.join(', ')}]. Use exactly one.`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (hasUrl) {
|
|
500
|
+
if (typeof sourceDef['url'] !== 'object') {
|
|
501
|
+
errors.push('url must be an object.');
|
|
502
|
+
} else if (!sourceDef['url'].url || typeof sourceDef['url'].url !== 'string') {
|
|
503
|
+
errors.push('url.url is required and must be a string.');
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (hasUrlList) {
|
|
508
|
+
if (typeof sourceDef['url-list'] !== 'object') {
|
|
509
|
+
errors.push('url-list must be an object.');
|
|
510
|
+
}
|
|
511
|
+
// url_list is supplied via _projections at runtime — no static validation needed.
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (hasCopilot) {
|
|
515
|
+
if (typeof sourceDef.copilot !== 'object') {
|
|
516
|
+
errors.push('copilot must be an object.');
|
|
517
|
+
} else {
|
|
518
|
+
if (!sourceDef.copilot.prompt_template && !hasPromptTemplate) {
|
|
519
|
+
errors.push('copilot.prompt_template is required (or use top-level prompt_template).');
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (hasWorkiq) {
|
|
525
|
+
if (typeof sourceDef.workiq !== 'object') {
|
|
526
|
+
errors.push('workiq must be an object.');
|
|
527
|
+
} else if (!sourceDef.workiq.query_template || typeof sourceDef.workiq.query_template !== 'string') {
|
|
528
|
+
errors.push('workiq.query_template is required and must be a string.');
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (hasMock) {
|
|
533
|
+
if (typeof sourceDef.mock !== 'string') {
|
|
534
|
+
errors.push('mock must be a string key.');
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const result = { ok: errors.length === 0, errors };
|
|
539
|
+
console.log(JSON.stringify(result));
|
|
540
|
+
process.exit(errors.length === 0 ? 0 : 1);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ---------------------------------------------------------------------------
|
|
544
|
+
// describe-capabilities — introspection metadata for this executor
|
|
545
|
+
// ---------------------------------------------------------------------------
|
|
546
|
+
const CAPABILITIES = {
|
|
547
|
+
version: '1.0',
|
|
548
|
+
executor: 'demo-task-executor',
|
|
549
|
+
subcommands: ['run-source-fetch', 'describe-capabilities', 'validate-source-def'],
|
|
550
|
+
sourceKinds: {
|
|
551
|
+
mock: {
|
|
552
|
+
description: 'Look up a key in a hardcoded MOCK_DB dictionary.',
|
|
553
|
+
inputSchema: {
|
|
554
|
+
mock: { type: 'string', required: true, description: 'Key in MOCK_DB (e.g. "quotes").' },
|
|
555
|
+
},
|
|
556
|
+
outputShape: 'Arbitrary JSON — depends on the mock key.',
|
|
557
|
+
example: {
|
|
558
|
+
input: { mock: 'quotes' },
|
|
559
|
+
output: { quoteResponse: { result: [{ symbol: 'AAPL', regularMarketPrice: 198.15 }], error: null } },
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
copilot: {
|
|
563
|
+
description: 'Invoke GitHub Copilot CLI with an interpolated prompt template.',
|
|
564
|
+
inputSchema: {
|
|
565
|
+
copilot: {
|
|
566
|
+
type: 'object', required: false,
|
|
567
|
+
description: 'Object with prompt_template (string) and optional args (object).',
|
|
568
|
+
properties: {
|
|
569
|
+
prompt_template: { type: 'string', required: true, description: 'Prompt with {{key}} placeholders.' },
|
|
570
|
+
args: { type: 'object', required: false, description: 'Extra interpolation args (highest precedence).' },
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
prompt_template: { type: 'string', required: false, description: 'Shorthand — top-level prompt template (alternative to copilot.prompt_template).' },
|
|
574
|
+
},
|
|
575
|
+
outputShape: 'string | object — raw Copilot text, or parsed JSON if the response is valid JSON.',
|
|
576
|
+
},
|
|
577
|
+
workiq: {
|
|
578
|
+
description: 'Query WorkIQ (Microsoft 365 Copilot) with an interpolated query template. Returns raw text response.',
|
|
579
|
+
inputSchema: {
|
|
580
|
+
workiq: {
|
|
581
|
+
type: 'object', required: true,
|
|
582
|
+
properties: {
|
|
583
|
+
query_template: { type: 'string', required: true, description: 'Query with {{key}} placeholders interpolated from _projections and args.' },
|
|
584
|
+
args: { type: 'object', required: false, description: 'Extra interpolation args (highest precedence).' },
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
outputShape: 'string — raw M365 Copilot response text.',
|
|
589
|
+
note: 'Requires workiq CLI installed and Azure CLI logged in (az login).',
|
|
590
|
+
},
|
|
591
|
+
'url': {
|
|
592
|
+
description: 'Single URL fetch via curl with {{key}} interpolation from _projections. Supports cacheTimeout.',
|
|
593
|
+
inputSchema: {
|
|
594
|
+
'url': {
|
|
595
|
+
type: 'object', required: true,
|
|
596
|
+
properties: {
|
|
597
|
+
url: { type: 'string', required: true, description: 'URL template with {{key}} placeholders.' },
|
|
598
|
+
method: { type: 'string', required: false, description: 'HTTP method (default: GET).' },
|
|
599
|
+
headers: { type: 'object', required: false, description: 'Request headers.' },
|
|
600
|
+
args: { type: 'object', required: false, description: 'Extra interpolation args (highest precedence).' },
|
|
601
|
+
cacheTimeout: { type: 'number', required: false, description: 'Cache TTL in seconds (default: 3600).' },
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
tickersFrom: { type: 'string', required: false, description: '"refKey.fieldName" — join tickers from _projections into {{tickers}}.' },
|
|
605
|
+
},
|
|
606
|
+
outputShape: 'Arbitrary JSON from the fetched URL.',
|
|
607
|
+
},
|
|
608
|
+
'url-list': {
|
|
609
|
+
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).',
|
|
610
|
+
inputSchema: {
|
|
611
|
+
'url-list': {
|
|
612
|
+
type: 'object', required: true,
|
|
613
|
+
properties: {
|
|
614
|
+
method: { type: 'string', required: false, description: 'HTTP method (default: GET).' },
|
|
615
|
+
headers: { type: 'object', required: false, description: 'Request headers.' },
|
|
616
|
+
cacheTimeout: { type: 'number', required: false, description: 'Cache TTL per URL in seconds (default: 3600).' },
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
outputShape: 'Array of raw JSON responses, one per URL in _projections.url_list.',
|
|
621
|
+
urlListNote: 'Declare `"projections": { "url_list": "<JSONata producing string[]>" }` on the source def. Example: `requires.holdings.ticker.(\'https://api.example.com/\' & $ & \'?q=1\')`',
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
extraSchema: {
|
|
625
|
+
description: 'Board topology context passed via --extra (base64-encoded JSON, baked at init).',
|
|
626
|
+
properties: {
|
|
627
|
+
boardSetupRoot: { type: 'string', description: 'Absolute path to board root.' },
|
|
628
|
+
boardId: { type: 'string', description: 'Board identifier.' },
|
|
629
|
+
boardRuntimeDir: { type: 'string', description: 'Relative path to runtime dir.' },
|
|
630
|
+
runtimeStatusDir: { type: 'string', description: 'Relative path to runtime-out dir.' },
|
|
631
|
+
cardsDir: { type: 'string', description: 'Relative path to cards dir.' },
|
|
632
|
+
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.' },
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
function describeCapabilities() {
|
|
638
|
+
console.log(JSON.stringify(CAPABILITIES, null, 2));
|
|
639
|
+
}
|
|
640
|
+
|
|
459
641
|
async function main() {
|
|
460
642
|
const sub = process.argv[2];
|
|
461
643
|
if (sub === 'run-source-fetch') {
|
|
462
644
|
runSourceFetchSubcommand(process.argv.slice(3));
|
|
463
645
|
return;
|
|
464
646
|
}
|
|
647
|
+
if (sub === 'describe-capabilities') {
|
|
648
|
+
describeCapabilities();
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (sub === 'validate-source-def') {
|
|
652
|
+
validateSourceDefSubcommand(process.argv.slice(3));
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
465
655
|
|
|
466
656
|
console.warn(`[demo-task-executor] Unknown subcommand: ${sub}`);
|
|
467
657
|
process.exit(0);
|