yaml-flow 5.2.5 → 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 +157 -0
- package/examples/example-board/scripts/copilot_wrapper_helper.ps1 +190 -0
- 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
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
setlocal enabledelayedexpansion
|
|
3
|
+
|
|
4
|
+
REM Copilot Wrapper - Manages session isolation for GitHub Copilot CLI
|
|
5
|
+
REM Usage: copilot_wrapper.bat <output_file> <session_dir> <working_dir> <request_or_file> <result_type> [agent_name] [model] [result_shape_file]
|
|
6
|
+
REM
|
|
7
|
+
REM If request_or_file starts with @, it's treated as a file path containing the prompt.
|
|
8
|
+
REM Otherwise, it's treated as the prompt string directly.
|
|
9
|
+
REM result_type: "raw" to return plain text, "json" to extract JSON (passed to clean_copilot_output.ps1)
|
|
10
|
+
REM agent_name: name of the agent for log files (optional)
|
|
11
|
+
REM model: passed to copilot with --model flag (optional)
|
|
12
|
+
|
|
13
|
+
SET "OUTPUT_FILE=%~1"
|
|
14
|
+
SET "SESSION_DIR=%~2"
|
|
15
|
+
SET "WORKING_DIR=%~3"
|
|
16
|
+
SET "REQUEST_OR_FILE=%~4"
|
|
17
|
+
SET "RESULT_TYPE=%~5"
|
|
18
|
+
SET "AGENT_NAME=%~6"
|
|
19
|
+
SET "MODEL=%~7"
|
|
20
|
+
SET "RESULT_SHAPE_FILE=%~8"
|
|
21
|
+
|
|
22
|
+
if not defined RESULT_TYPE SET "RESULT_TYPE=raw"
|
|
23
|
+
|
|
24
|
+
SET "PROMPT_FILE="
|
|
25
|
+
SET "REQUEST="
|
|
26
|
+
echo !REQUEST_OR_FILE! | findstr /b "@" >nul
|
|
27
|
+
if !errorlevel! equ 0 (
|
|
28
|
+
SET "PROMPT_FILE=!REQUEST_OR_FILE:~1!"
|
|
29
|
+
) else (
|
|
30
|
+
SET "REQUEST=!REQUEST_OR_FILE!"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
SET "_WD_HASH=!WORKING_DIR:\=!"
|
|
34
|
+
SET "_WD_HASH=!_WD_HASH:/=!"
|
|
35
|
+
SET "_WD_HASH=!_WD_HASH::=!"
|
|
36
|
+
SET "_WD_HASH=!_WD_HASH:.=!"
|
|
37
|
+
SET "_WD_HASH=!_WD_HASH: =!"
|
|
38
|
+
SET "COPILOT_BASE=%TEMP%\copilot-sessions\!_WD_HASH!"
|
|
39
|
+
SET "COPILOT_CACHE=%COPILOT_BASE%\session-state"
|
|
40
|
+
SET "LOCK_FILE=%COPILOT_BASE%\copilot.lock"
|
|
41
|
+
SET "UUID_FILE=%SESSION_DIR%\session.uuid"
|
|
42
|
+
|
|
43
|
+
if not exist "%COPILOT_BASE%" mkdir "%COPILOT_BASE%"
|
|
44
|
+
if not exist "%COPILOT_CACHE%" mkdir "%COPILOT_CACHE%"
|
|
45
|
+
if not exist "%SESSION_DIR%" mkdir "%SESSION_DIR%"
|
|
46
|
+
|
|
47
|
+
if exist "%LOCK_FILE%" (
|
|
48
|
+
for /f "tokens=*" %%a in ('powershell -NoProfile -Command "if ((Get-Item '%LOCK_FILE%').LastWriteTime -lt (Get-Date).AddMinutes(-20)) { Write-Output 'STALE' }"') do (
|
|
49
|
+
if "%%a"=="STALE" (
|
|
50
|
+
del "%LOCK_FILE%" 2>nul
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
:acquire_lock
|
|
55
|
+
2>nul (
|
|
56
|
+
>"%LOCK_FILE%" (
|
|
57
|
+
echo %DATE% %TIME%
|
|
58
|
+
)
|
|
59
|
+
) || (
|
|
60
|
+
timeout /t 1 /nobreak >nul
|
|
61
|
+
goto acquire_lock
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
SET "SESSION_UUID="
|
|
65
|
+
if exist "%UUID_FILE%" (
|
|
66
|
+
set /p SESSION_UUID=<"%UUID_FILE%"
|
|
67
|
+
) else (
|
|
68
|
+
for /f "tokens=*" %%a in ('powershell -NoProfile -Command "[guid]::NewGuid().ToString()"') do (
|
|
69
|
+
SET "SESSION_UUID=%%a"
|
|
70
|
+
)
|
|
71
|
+
echo !SESSION_UUID!>"%UUID_FILE%"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
SET "CACHE_SESSION_PATH=%COPILOT_CACHE%\!SESSION_UUID!"
|
|
75
|
+
|
|
76
|
+
if exist "%SESSION_DIR%\workspace.yaml" (
|
|
77
|
+
if exist "!CACHE_SESSION_PATH!" rmdir /s /q "!CACHE_SESSION_PATH!" 2>nul
|
|
78
|
+
mkdir "!CACHE_SESSION_PATH!" 2>nul
|
|
79
|
+
for %%f in ("%SESSION_DIR%\*") do (
|
|
80
|
+
if /i not "%%~nxf"=="session.uuid" (
|
|
81
|
+
move /y "%%f" "!CACHE_SESSION_PATH!\" >nul 2>&1
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
for /d %%d in ("%SESSION_DIR%\*") do (
|
|
85
|
+
robocopy "%%d" "!CACHE_SESSION_PATH!\%%~nxd" /E /MOVE /NFL /NDL /NJH /NJS >nul 2>&1
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
cd /d "%WORKING_DIR%"
|
|
90
|
+
|
|
91
|
+
SET "MODEL_FLAG="
|
|
92
|
+
if defined MODEL (
|
|
93
|
+
SET "MODEL_FLAG=--model !MODEL!"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if defined PROMPT_FILE (
|
|
97
|
+
type "!PROMPT_FILE!" | call copilot --allow-all --resume !SESSION_UUID! !MODEL_FLAG! > "%OUTPUT_FILE%" 2>&1
|
|
98
|
+
) else (
|
|
99
|
+
call copilot -p "%REQUEST%" --allow-all --resume !SESSION_UUID! !MODEL_FLAG! > "%OUTPUT_FILE%" 2>&1
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
SET "LOG_DIR=%COPILOT_BASE%\copilot-logs"
|
|
103
|
+
if not exist "!LOG_DIR!" mkdir "!LOG_DIR!"
|
|
104
|
+
SET "LOG_AGENT=unknown"
|
|
105
|
+
if defined AGENT_NAME SET "LOG_AGENT=!AGENT_NAME!"
|
|
106
|
+
for /f "tokens=*" %%t in ('powershell -NoProfile -Command "Get-Date -Format 'yyyyMMdd-HHmmss'"') do SET "LOG_TS=%%t"
|
|
107
|
+
SET "LOG_FILE=!LOG_DIR!\!LOG_AGENT!_!LOG_TS!.log"
|
|
108
|
+
echo === PROMPT (!LOG_TS!) === > "!LOG_FILE!"
|
|
109
|
+
echo Agent: !LOG_AGENT! >> "!LOG_FILE!"
|
|
110
|
+
echo ResultType: !RESULT_TYPE! >> "!LOG_FILE!"
|
|
111
|
+
echo Working Dir: %WORKING_DIR% >> "!LOG_FILE!"
|
|
112
|
+
echo --- >> "!LOG_FILE!"
|
|
113
|
+
if defined PROMPT_FILE (
|
|
114
|
+
type "!PROMPT_FILE!" >> "!LOG_FILE!" 2>nul
|
|
115
|
+
) else (
|
|
116
|
+
echo %REQUEST% >> "!LOG_FILE!"
|
|
117
|
+
)
|
|
118
|
+
echo. >> "!LOG_FILE!"
|
|
119
|
+
echo === RESPONSE === >> "!LOG_FILE!"
|
|
120
|
+
type "%OUTPUT_FILE%" >> "!LOG_FILE!" 2>nul
|
|
121
|
+
echo. >> "!LOG_FILE!"
|
|
122
|
+
echo === END === >> "!LOG_FILE!"
|
|
123
|
+
for /f "skip=50 tokens=*" %%f in ('dir /b /o-d "!LOG_DIR!\!LOG_AGENT!_*.log" 2^>nul') do (
|
|
124
|
+
del "!LOG_DIR!\%%f" 2>nul
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0copilot_wrapper_helper.ps1" "%OUTPUT_FILE%" "!RESULT_TYPE!" "!RESULT_SHAPE_FILE!"
|
|
128
|
+
|
|
129
|
+
REM If JSON extraction failed (exit 2) and result_type is json, retry once with a
|
|
130
|
+
REM correction prompt in the same session (--resume SESSION_UUID continues the conversation).
|
|
131
|
+
SET "PS1_EXIT=!ERRORLEVEL!"
|
|
132
|
+
if "!PS1_EXIT!"=="2" (
|
|
133
|
+
SET "RETRY_PROMPT_FILE=%TEMP%\copilot-retry-!RANDOM!.txt"
|
|
134
|
+
(
|
|
135
|
+
echo Your previous response did not contain a valid JSON object.
|
|
136
|
+
echo Please respond with ONLY the JSON object — no markdown, no explanation, no preamble.
|
|
137
|
+
echo Start your response with { and end with }.
|
|
138
|
+
) > "!RETRY_PROMPT_FILE!"
|
|
139
|
+
type "!RETRY_PROMPT_FILE!" | call copilot --allow-all --resume !SESSION_UUID! !MODEL_FLAG! > "%OUTPUT_FILE%" 2>&1
|
|
140
|
+
del "!RETRY_PROMPT_FILE!" 2>nul
|
|
141
|
+
REM Re-run PS1 with -IsRetry so it writes skeleton on second failure rather than exit 2
|
|
142
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0copilot_wrapper_helper.ps1" "%OUTPUT_FILE%" "!RESULT_TYPE!" "!RESULT_SHAPE_FILE!" "-IsRetry"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if exist "!CACHE_SESSION_PATH!" (
|
|
146
|
+
for %%f in ("!CACHE_SESSION_PATH!\*") do (
|
|
147
|
+
move /y "%%f" "%SESSION_DIR%\" >nul 2>&1
|
|
148
|
+
)
|
|
149
|
+
for /d %%d in ("!CACHE_SESSION_PATH!\*") do (
|
|
150
|
+
robocopy "%%d" "%SESSION_DIR%\%%~nxd" /E /MOVE /NFL /NDL /NJH /NJS >nul 2>&1
|
|
151
|
+
)
|
|
152
|
+
rmdir "!CACHE_SESSION_PATH!" 2>nul
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
del "%LOCK_FILE%" 2>nul
|
|
156
|
+
|
|
157
|
+
endlocal
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# clean_copilot_output.ps1
|
|
2
|
+
# Cleans copilot CLI output: filters noise lines and stats footer.
|
|
3
|
+
# Called by copilot_wrapper.bat after copilot runs and after logging (raw log preserved).
|
|
4
|
+
#
|
|
5
|
+
# Usage: clean_copilot_output.ps1 <output_file> <result_type> [result_shape_file]
|
|
6
|
+
# output_file - file containing raw copilot output; overwritten with cleaned result
|
|
7
|
+
# result_type=raw - strip noise + stats, write plain text back to output_file
|
|
8
|
+
# result_type=json - extract first JSON object whose keys match result_shape;
|
|
9
|
+
# if result_shape_file is absent, accepts any valid JSON object
|
|
10
|
+
# result_shape_file - (json result_type only) JSON file whose top-level keys are required in output
|
|
11
|
+
#
|
|
12
|
+
# raw result_type: right for chat responses and task executor source_defs.
|
|
13
|
+
# json result_type: right for structured calls where the input contained {prompt, result_shape}.
|
|
14
|
+
|
|
15
|
+
param(
|
|
16
|
+
[Parameter(Mandatory)][string]$OutputFile,
|
|
17
|
+
[Parameter(Mandatory)][string]$ResultType,
|
|
18
|
+
[string]$ResultShapeFile = '',
|
|
19
|
+
# When $true (retry pass), write shape-skeleton fallback instead of exiting with code 2.
|
|
20
|
+
# On first pass, exit 2 signals the caller (.bat) to retry with a correction prompt.
|
|
21
|
+
[switch]$IsRetry
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if (-not (Test-Path $OutputFile)) { exit 0 }
|
|
25
|
+
|
|
26
|
+
$raw = [IO.File]::ReadAllText($OutputFile, [Text.Encoding]::UTF8)
|
|
27
|
+
if ([string]::IsNullOrWhiteSpace($raw)) { exit 0 }
|
|
28
|
+
|
|
29
|
+
# --- Step 1: Filter noise lines ---
|
|
30
|
+
$lines = $raw -split "`r?`n" | Where-Object {
|
|
31
|
+
$_ -notmatch "^error: unknown option '--no-warnings'" -and
|
|
32
|
+
$_ -notmatch "^Try 'copilot --help' for more information"
|
|
33
|
+
}
|
|
34
|
+
$cleaned = ($lines -join "`n").Trim()
|
|
35
|
+
|
|
36
|
+
# --- Step 1b: Strip copilot-cli tool operation lines ---
|
|
37
|
+
# These are internal tool invocations that leak into output:
|
|
38
|
+
# ● Create/Read/Edit/List directory/Glob/Check ...
|
|
39
|
+
# X Read ... (failed tool ops)
|
|
40
|
+
# $ Get-Content/Set-Content ... (PowerShell invocations)
|
|
41
|
+
# └ N lines/files found
|
|
42
|
+
# ├ ... (tree lines)
|
|
43
|
+
# "The agent decision has been simulated and saved to ..."
|
|
44
|
+
# session-state file paths
|
|
45
|
+
$noiseLines = New-Object System.Collections.Generic.List[string]
|
|
46
|
+
$contentLines2 = New-Object System.Collections.Generic.List[string]
|
|
47
|
+
foreach ($line in ($cleaned -split "`n")) {
|
|
48
|
+
$t = $line.TrimStart()
|
|
49
|
+
if ($t -match '^[\u25cf\u2022] ' -or # ● bullet tool ops
|
|
50
|
+
$t -match '^X ' -or # X failed tool ops
|
|
51
|
+
$t -match '^\$ ' -or # $ shell commands
|
|
52
|
+
$t -match '^[\u2514\u251c]' -or # └ ├ tree lines
|
|
53
|
+
$t -match 'session-state.*\.json' -or # session-state file projections
|
|
54
|
+
$t -match 'agent.decision has been simulated' -or
|
|
55
|
+
$t -match 'has been simulated and saved' -or
|
|
56
|
+
$t -match '^\d+ (files?|lines?|matches?) found$' -or # "3 files found"
|
|
57
|
+
$t -match '^No matches found$' -or
|
|
58
|
+
$t -match '^Path does not exist$' -or
|
|
59
|
+
$t -match '^\d+ lines?( read)?$') { # "1 line read"
|
|
60
|
+
$noiseLines.Add($line)
|
|
61
|
+
} else {
|
|
62
|
+
$contentLines2.Add($line)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
$cleaned = ($contentLines2 -join "`n").Trim()
|
|
66
|
+
|
|
67
|
+
# Write noise to sidecar file for upstream visibility
|
|
68
|
+
$NoiseFile = $OutputFile + '.noise'
|
|
69
|
+
if ($noiseLines.Count -gt 0) {
|
|
70
|
+
$noiseContent = "STRIPPED_LINES=$($noiseLines.Count)`n" + ($noiseLines -join "`n")
|
|
71
|
+
[IO.File]::WriteAllText($NoiseFile, $noiseContent, [Text.Encoding]::UTF8)
|
|
72
|
+
} elseif (Test-Path $NoiseFile) {
|
|
73
|
+
Remove-Item $NoiseFile -Force
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# --- Step 2: Strip trailing usage stats ---
|
|
77
|
+
$statsPrefixes = @('Total usage est:', 'API time spent:', 'Total session time:',
|
|
78
|
+
'Total code changes:', 'Breakdown by AI model:', 'Session:',
|
|
79
|
+
'Changes', 'Requests', 'Tokens')
|
|
80
|
+
$resultLines = New-Object System.Collections.Generic.List[string]
|
|
81
|
+
$hitStats = $false
|
|
82
|
+
foreach ($line in $cleaned -split "`n") {
|
|
83
|
+
if (-not $hitStats) {
|
|
84
|
+
foreach ($sp in $statsPrefixes) {
|
|
85
|
+
if ($line.TrimStart().StartsWith($sp)) { $hitStats = $true; break }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (-not $hitStats) { $resultLines.Add($line) }
|
|
89
|
+
}
|
|
90
|
+
$cleaned = ($resultLines -join "`n").Trim()
|
|
91
|
+
|
|
92
|
+
# --- raw result_type: write cleaned plain text and exit ---
|
|
93
|
+
if ($ResultType -eq 'raw') {
|
|
94
|
+
if ([string]::IsNullOrWhiteSpace($cleaned)) {
|
|
95
|
+
[IO.File]::WriteAllText($OutputFile, '', [Text.Encoding]::UTF8)
|
|
96
|
+
} else {
|
|
97
|
+
[IO.File]::WriteAllText($OutputFile, $cleaned, [Text.Encoding]::UTF8)
|
|
98
|
+
}
|
|
99
|
+
exit 0
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# --- json result_type: extract JSON object matching result_shape ---
|
|
103
|
+
|
|
104
|
+
# Load result_shape keys (if provided) to use as required-key filter
|
|
105
|
+
$shapeKeys = @()
|
|
106
|
+
if ($ResultShapeFile -and (Test-Path $ResultShapeFile)) {
|
|
107
|
+
try {
|
|
108
|
+
$shape = [IO.File]::ReadAllText($ResultShapeFile, [Text.Encoding]::UTF8) | ConvertFrom-Json -ErrorAction Stop
|
|
109
|
+
$shapeKeys = @($shape.PSObject.Properties.Name)
|
|
110
|
+
} catch {}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if ([string]::IsNullOrWhiteSpace($cleaned)) {
|
|
114
|
+
$fallback = if ($shapeKeys.Count -gt 0) {
|
|
115
|
+
$obj = [ordered]@{}
|
|
116
|
+
foreach ($k in $shapeKeys) { $obj[$k] = $null }
|
|
117
|
+
$obj | ConvertTo-Json -Depth 2 -Compress
|
|
118
|
+
} else { '{}' }
|
|
119
|
+
[IO.File]::WriteAllText($OutputFile, $fallback, [Text.Encoding]::UTF8)
|
|
120
|
+
exit 0
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Helper: check if a parsed object has all required shape keys
|
|
124
|
+
function Test-ShapeMatch($obj) {
|
|
125
|
+
if ($shapeKeys.Count -eq 0) { return $true } # no shape constraint — accept any JSON object
|
|
126
|
+
foreach ($k in $shapeKeys) {
|
|
127
|
+
if (-not $obj.PSObject.Properties[$k]) { return $false }
|
|
128
|
+
}
|
|
129
|
+
return $true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
$foundJson = $null
|
|
133
|
+
|
|
134
|
+
# 1: Look in ```json fenced blocks first
|
|
135
|
+
if ($cleaned -match '(?s)```json\s*(.*?)```') {
|
|
136
|
+
try {
|
|
137
|
+
$obj = $Matches[1].Trim() | ConvertFrom-Json -ErrorAction Stop
|
|
138
|
+
if (Test-ShapeMatch $obj) { $foundJson = $Matches[1].Trim() }
|
|
139
|
+
} catch {}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# 2: Scan for bare JSON objects
|
|
143
|
+
if (-not $foundJson) {
|
|
144
|
+
$depth = 0; $start = -1
|
|
145
|
+
for ($i = 0; $i -lt $cleaned.Length; $i++) {
|
|
146
|
+
if ($cleaned[$i] -eq '{') {
|
|
147
|
+
if ($depth -eq 0) { $start = $i }
|
|
148
|
+
$depth++
|
|
149
|
+
} elseif ($cleaned[$i] -eq '}') {
|
|
150
|
+
$depth--
|
|
151
|
+
if ($depth -eq 0 -and $start -ge 0) {
|
|
152
|
+
$candidate = $cleaned.Substring($start, $i - $start + 1)
|
|
153
|
+
try {
|
|
154
|
+
$obj = $candidate | ConvertFrom-Json -ErrorAction Stop
|
|
155
|
+
if (Test-ShapeMatch $obj) {
|
|
156
|
+
$foundJson = $candidate
|
|
157
|
+
break
|
|
158
|
+
}
|
|
159
|
+
} catch {}
|
|
160
|
+
$start = -1
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if ($foundJson) {
|
|
167
|
+
[IO.File]::WriteAllText($OutputFile, $foundJson, [Text.Encoding]::UTF8)
|
|
168
|
+
} else {
|
|
169
|
+
# No matching JSON found — record raw in noise file for upstream visibility
|
|
170
|
+
$NoiseFile = $OutputFile + '.noise'
|
|
171
|
+
$fallbackNoise = "FALLBACK=no_json_match`nSHAPE_KEYS=$($shapeKeys -join ',')`nRAW_LENGTH=$($cleaned.Length)`n---`n$cleaned"
|
|
172
|
+
if (Test-Path $NoiseFile) {
|
|
173
|
+
$existing = [IO.File]::ReadAllText($NoiseFile, [Text.Encoding]::UTF8)
|
|
174
|
+
[IO.File]::WriteAllText($NoiseFile, "$existing`n$fallbackNoise", [Text.Encoding]::UTF8)
|
|
175
|
+
} else {
|
|
176
|
+
[IO.File]::WriteAllText($NoiseFile, $fallbackNoise, [Text.Encoding]::UTF8)
|
|
177
|
+
}
|
|
178
|
+
if ($IsRetry) {
|
|
179
|
+
# Second pass — write shape-skeleton so the card gets a structured (empty) result
|
|
180
|
+
$fallback = if ($shapeKeys.Count -gt 0) {
|
|
181
|
+
$obj = [ordered]@{}
|
|
182
|
+
foreach ($k in $shapeKeys) { $obj[$k] = $null }
|
|
183
|
+
$obj | ConvertTo-Json -Depth 2 -Compress
|
|
184
|
+
} else { '{}' }
|
|
185
|
+
[IO.File]::WriteAllText($OutputFile, $fallback, [Text.Encoding]::UTF8)
|
|
186
|
+
} else {
|
|
187
|
+
# First pass — exit 2 to signal the caller (.bat) to retry with a correction prompt
|
|
188
|
+
exit 2
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* workiq_wrapper.mjs — Calls the demo-server /api/workiq/ask proxy endpoint.
|
|
4
|
+
*
|
|
5
|
+
* Usage: node workiq_wrapper.mjs <out_file>
|
|
6
|
+
* WORKIQ_QUERY env var: the interpolated query string
|
|
7
|
+
* WORKIQ_SERVER_URL env var: base URL of demo-server (default: http://127.0.0.1:7799)
|
|
8
|
+
*
|
|
9
|
+
* The demo-server has a TTY so workiq can produce output there.
|
|
10
|
+
* Writes raw WorkIQ response text to <out_file>.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import http from 'node:http';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
|
|
16
|
+
const outFile = process.argv[2];
|
|
17
|
+
const query = process.env.WORKIQ_QUERY;
|
|
18
|
+
const serverBase = (process.env.WORKIQ_SERVER_URL || 'http://127.0.0.1:7799').replace(/\/$/, '');
|
|
19
|
+
|
|
20
|
+
if (!outFile) { console.error('workiq_wrapper: missing <out_file> argument'); process.exit(1); }
|
|
21
|
+
if (!query) { console.error('workiq_wrapper: WORKIQ_QUERY env var not set'); process.exit(1); }
|
|
22
|
+
|
|
23
|
+
const body = JSON.stringify({ query });
|
|
24
|
+
const url = new URL('/api/workiq/ask', serverBase);
|
|
25
|
+
|
|
26
|
+
const reqOptions = {
|
|
27
|
+
hostname: url.hostname,
|
|
28
|
+
port: url.port || 80,
|
|
29
|
+
path: url.pathname,
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const req = http.request(reqOptions, (res) => {
|
|
35
|
+
let data = '';
|
|
36
|
+
res.on('data', chunk => { data += chunk; });
|
|
37
|
+
res.on('end', () => {
|
|
38
|
+
try {
|
|
39
|
+
const json = JSON.parse(data);
|
|
40
|
+
if (json.error) {
|
|
41
|
+
fs.writeFileSync(outFile, `workiq error: ${json.error}`, 'utf8');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
fs.writeFileSync(outFile, json.response || '', 'utf8');
|
|
45
|
+
process.exit(0);
|
|
46
|
+
} catch {
|
|
47
|
+
fs.writeFileSync(outFile, data, 'utf8');
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
req.on('error', (err) => {
|
|
54
|
+
fs.writeFileSync(outFile, `workiq proxy error: ${err.message}\nIs demo-server running at ${serverBase}?`, 'utf8');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
req.setTimeout(60_000, () => {
|
|
59
|
+
req.destroy();
|
|
60
|
+
fs.writeFileSync(outFile, 'workiq proxy timeout after 60s', 'utf8');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
req.write(body);
|
|
65
|
+
req.end();
|
|
66
|
+
|
|
@@ -46,9 +46,9 @@ const cards: LiveCard[] = [
|
|
|
46
46
|
card_data: {},
|
|
47
47
|
data: { requires: ['market-feed'] },
|
|
48
48
|
compute: {
|
|
49
|
-
total: { fn: 'sum', input: '
|
|
50
|
-
avg: { fn: 'avg', input: '
|
|
51
|
-
count: { fn: 'count', input: '
|
|
49
|
+
total: { fn: 'sum', input: 'source_defs.market-feed.prices' },
|
|
50
|
+
avg: { fn: 'avg', input: 'source_defs.market-feed.prices' },
|
|
51
|
+
count: { fn: 'count', input: 'source_defs.market-feed.prices' },
|
|
52
52
|
},
|
|
53
53
|
},
|
|
54
54
|
{
|
|
@@ -132,8 +132,8 @@ const board: LiveBoard = {
|
|
|
132
132
|
card_data: {},
|
|
133
133
|
data: { requires: ['equity-feed', 'bond-feed'] },
|
|
134
134
|
compute: {
|
|
135
|
-
equity_total: { fn: 'sum', input: '
|
|
136
|
-
bond_total: { fn: 'sum', input: '
|
|
135
|
+
equity_total: { fn: 'sum', input: 'source_defs.equity-feed.prices' },
|
|
136
|
+
bond_total: { fn: 'sum', input: 'source_defs.bond-feed.yields' },
|
|
137
137
|
},
|
|
138
138
|
},
|
|
139
139
|
{
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* SOC Incident Board — Correct Model
|
|
3
3
|
*
|
|
4
4
|
* A Security Operations Center dashboard demonstrating:
|
|
5
|
-
* • External data
|
|
5
|
+
* • External data source_defs (alert-feed, threat-intel) — NO handlers
|
|
6
6
|
* • Handler-driven compute (severity-score, blast-radius)
|
|
7
7
|
* • Fire-and-forget side effects (slack-alert, create-ticket)
|
|
8
8
|
* • Per-task refreshStrategy: 'once' on create-ticket (no duplicate tickets)
|
|
@@ -76,7 +76,7 @@ const config: GraphConfig = {
|
|
|
76
76
|
refreshStrategy: 'data-changed',
|
|
77
77
|
},
|
|
78
78
|
tasks: {
|
|
79
|
-
// External
|
|
79
|
+
// External source_defs — no handler, data pushed externally
|
|
80
80
|
'alert-feed': {
|
|
81
81
|
provides: ['alert-feed'],
|
|
82
82
|
description: 'SIEM alert feed (external push)',
|
|
@@ -203,7 +203,7 @@ console.log(`create-ticket: refreshStrategy 'once' (no duplicate tickets)\n`);
|
|
|
203
203
|
|
|
204
204
|
console.log(`--- T0: Initial incident (${siemAlerts.length} alerts, ${threatIntelV1.length} IOCs) ---\n`);
|
|
205
205
|
|
|
206
|
-
// Push both
|
|
206
|
+
// Push both source_defs simultaneously — engine stores data, auto-hash computed
|
|
207
207
|
graph.pushAll([
|
|
208
208
|
{
|
|
209
209
|
type: 'task-completed',
|
|
@@ -29,18 +29,18 @@ const graph: GraphConfig = {
|
|
|
29
29
|
},
|
|
30
30
|
tasks: {
|
|
31
31
|
fetch_sources: {
|
|
32
|
-
provides: ['raw-
|
|
32
|
+
provides: ['raw-asources'],
|
|
33
33
|
description: 'Fetch source documents from the web',
|
|
34
34
|
},
|
|
35
35
|
analyse_sentiment: {
|
|
36
|
-
requires: ['raw-
|
|
36
|
+
requires: ['raw-asources'],
|
|
37
37
|
provides: ['sentiment-result'],
|
|
38
|
-
description: 'Run sentiment analysis on
|
|
38
|
+
description: 'Run sentiment analysis on source_defs',
|
|
39
39
|
},
|
|
40
40
|
analyse_entities: {
|
|
41
|
-
requires: ['raw-
|
|
41
|
+
requires: ['raw-asources'],
|
|
42
42
|
provides: ['entity-result'],
|
|
43
|
-
description: 'Extract named entities from
|
|
43
|
+
description: 'Extract named entities from source_defs',
|
|
44
44
|
},
|
|
45
45
|
merge_analysis: {
|
|
46
46
|
requires: ['sentiment-result', 'entity-result'],
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - Fan-out / fan-in in the outer graph
|
|
14
14
|
*
|
|
15
15
|
* Outer graph:
|
|
16
|
-
* discover-
|
|
16
|
+
* discover-bsources → extract-batch → transform-batch → [load ∥ validate] → finalize
|
|
17
17
|
*
|
|
18
18
|
* Inner extract graph (event-graph, per source):
|
|
19
19
|
* connect → [fetch-metadata ∥ fetch-schema] → snapshot-data
|
|
@@ -129,11 +129,11 @@ const outerGraph: GraphConfig = {
|
|
|
129
129
|
id: 'etl-pipeline',
|
|
130
130
|
settings: { completion: 'all-tasks-complete' },
|
|
131
131
|
tasks: {
|
|
132
|
-
'discover-
|
|
133
|
-
provides: ['
|
|
132
|
+
'discover-bsources': {
|
|
133
|
+
provides: ['source_defs-discovered'],
|
|
134
134
|
},
|
|
135
135
|
'extract-batch': {
|
|
136
|
-
requires: ['
|
|
136
|
+
requires: ['source_defs-discovered'],
|
|
137
137
|
provides: ['extraction-complete'],
|
|
138
138
|
},
|
|
139
139
|
'transform-batch': {
|
|
@@ -191,7 +191,7 @@ async function runTransformFlow(record: { id: string; raw_data: string }) {
|
|
|
191
191
|
// 4. Sample data
|
|
192
192
|
// ============================================================================
|
|
193
193
|
|
|
194
|
-
const
|
|
194
|
+
const source_defs = [
|
|
195
195
|
{ id: 'src-orders', db: 'orders_db' },
|
|
196
196
|
{ id: 'src-users', db: 'users_db' },
|
|
197
197
|
{ id: 'src-products', db: 'products_db' },
|
|
@@ -211,13 +211,13 @@ const records = [
|
|
|
211
211
|
// ============================================================================
|
|
212
212
|
|
|
213
213
|
const outerHandlers: Record<string, () => Promise<void>> = {
|
|
214
|
-
'discover-
|
|
215
|
-
console.log(` Found ${
|
|
214
|
+
'discover-bsources': async () => {
|
|
215
|
+
console.log(` Found ${source_defs.length} data source_defs`);
|
|
216
216
|
},
|
|
217
217
|
|
|
218
218
|
'extract-batch': async () => {
|
|
219
|
-
console.log(` Extracting from ${
|
|
220
|
-
const result = await batch(
|
|
219
|
+
console.log(` Extracting from ${source_defs.length} source_defs (concurrency: 2, mode: event-graph)`);
|
|
220
|
+
const result = await batch(source_defs, {
|
|
221
221
|
concurrency: 2,
|
|
222
222
|
processor: runExtractGraph,
|
|
223
223
|
onItemComplete: (src, res) => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "holdings-table",
|
|
3
3
|
"meta": { "title": "Holdings Table" },
|
|
4
4
|
"requires": ["holdings", "prices"],
|
|
5
|
-
"provides": [{ "bindTo": "table", "
|
|
5
|
+
"provides": [{ "bindTo": "table", "ref": "computed_values.table" }],
|
|
6
6
|
"card_data": {},
|
|
7
7
|
"compute": [
|
|
8
8
|
{
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "portfolio-value",
|
|
3
3
|
"meta": { "title": "Portfolio Total Value" },
|
|
4
4
|
"requires": ["table"],
|
|
5
|
-
"provides": [{ "bindTo": "totalValue", "
|
|
5
|
+
"provides": [{ "bindTo": "totalValue", "ref": "computed_values.totalValue" }],
|
|
6
6
|
"card_data": {},
|
|
7
7
|
"compute": [
|
|
8
8
|
{ "bindTo": "totalValue", "expr": "$sum(requires.table.rows.value)" }
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
"id": "price-fetch",
|
|
3
3
|
"meta": { "title": "Fetch Market Prices" },
|
|
4
4
|
"requires": ["holdings"],
|
|
5
|
-
"provides": [{ "bindTo": "prices", "
|
|
5
|
+
"provides": [{ "bindTo": "prices", "ref": "fetched_sources.prices" }],
|
|
6
6
|
"card_data": {},
|
|
7
|
-
"
|
|
7
|
+
"source_defs": [
|
|
8
8
|
{ "cli": "node ../fetch-prices.js --tmp-file-name tmp_file1", "bindTo": "prices", "outputFile": "prices.json" }
|
|
9
9
|
],
|
|
10
10
|
"view": {
|
|
11
11
|
"elements": [
|
|
12
|
-
{ "kind": "table", "label": "Market Prices", "data": { "bind": "
|
|
12
|
+
{ "kind": "table", "label": "Market Prices", "data": { "bind": "fetched_sources.prices" } }
|
|
13
13
|
]
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -22,7 +22,7 @@ try {
|
|
|
22
22
|
card.state.holdings = holdings;
|
|
23
23
|
fs.writeFileSync(cardPath, `${JSON.stringify(card, null, 2)}\n`, 'utf-8');
|
|
24
24
|
|
|
25
|
-
runBoardCli(['
|
|
25
|
+
runBoardCli(['upsert-card', '--rg', boardDir, '--card', cardPath, '--restart']);
|
|
26
26
|
|
|
27
27
|
writeResult({
|
|
28
28
|
result: 'success',
|
|
@@ -44,7 +44,7 @@ steps:
|
|
|
44
44
|
expects_data: [board_dir, cards_template_dir]
|
|
45
45
|
produces_data: [cards_dir]
|
|
46
46
|
handler:
|
|
47
|
-
cli: node %%BOARDCLI_CMD%%
|
|
47
|
+
cli: node %%BOARDCLI_CMD%% upsert-card --rg "%%BOARD_DIR%%" --card-glob "%%CARDS_GLOB%%"
|
|
48
48
|
result-mode: exit-code
|
|
49
49
|
input-transforms:
|
|
50
50
|
BOARD_DIR: board_dir
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yaml-flow",
|
|
3
|
-
"version": "5.2.
|
|
3
|
+
"version": "5.2.8",
|
|
4
4
|
"description": "Unified workflow engine: step-machine (sequential) + event-graph (stateless DAG) with pluggable storage",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"scripts": {
|
|
102
102
|
"build": "tsup",
|
|
103
103
|
"build:browser": "tsup --config tsup.browser.config.ts",
|
|
104
|
-
"build:example": "node example-board
|
|
104
|
+
"build:example": "node demo-src/example-board/build.js",
|
|
105
105
|
"dev": "tsup --watch",
|
|
106
106
|
"test": "vitest",
|
|
107
107
|
"test:run": "vitest run",
|