yaml-flow 5.2.2 → 5.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/board-livecards-server-runtime.js +48 -9
- package/browser/live-cards.js +55 -9
- package/dist/cli/board-live-cards-cli.cjs +9 -6
- package/dist/cli/board-live-cards-cli.cjs.map +1 -1
- package/dist/cli/board-live-cards-cli.js +9 -6
- package/dist/cli/board-live-cards-cli.js.map +1 -1
- package/examples/example-board/demo-chat-handler.js +57 -50
- package/examples/example-board/demo-server.js +6 -0
- package/examples/example-board/demo-shell-browser.html +3 -3
- package/examples/example-board/demo-shell-with-server.html +4 -4
- package/examples/example-board/scripts/copilot_wrapper.bat +141 -0
- package/examples/example-board/scripts/copilot_wrapper_helper.ps1 +181 -0
- package/package.json +1 -1
|
@@ -4,9 +4,15 @@
|
|
|
4
4
|
* Invoked by reusable-server-runtime after a user message is persisted:
|
|
5
5
|
* node demo-chat-handler.js --boardId <id> --cardId <id> --extraEncJson <base64json>
|
|
6
6
|
*
|
|
7
|
-
* extraEncJson decodes to:
|
|
7
|
+
* extraEncJson decodes to:
|
|
8
|
+
* boardSetupRoot — absolute path to board root (parent of runtime/, surface/, runtime-out/)
|
|
9
|
+
* boardRuntimeDir — relative subdir: 'runtime'
|
|
10
|
+
* runtimeStatusDir— relative subdir: 'runtime-out'
|
|
11
|
+
* cardsDir — relative subdir: 'surface/tmp-cards'
|
|
12
|
+
* chatDir — absolute path to the card's chats directory
|
|
13
|
+
* lastChatFile — filename of the just-written user message, e.g. '001_user.txt'
|
|
8
14
|
*
|
|
9
|
-
*
|
|
15
|
+
* Invokes copilot_wrapper.bat with a prompt built from conversation history.
|
|
10
16
|
* Session dir is per-card: os.tmpdir()/demo-chat-handler-sessions/<boardId>_<cardId>
|
|
11
17
|
*/
|
|
12
18
|
|
|
@@ -27,19 +33,27 @@ function getArg(name) {
|
|
|
27
33
|
return idx !== -1 && args[idx + 1] !== undefined ? args[idx + 1] : null;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
const boardId
|
|
31
|
-
const cardId
|
|
32
|
-
const extraStr
|
|
36
|
+
const boardId = getArg('--boardId') || '';
|
|
37
|
+
const cardId = getArg('--cardId') || '';
|
|
38
|
+
const extraStr = getArg('--extraEncJson') || '';
|
|
39
|
+
const cleanOnExit = getArg('--cleanOnExit') || '';
|
|
33
40
|
|
|
34
41
|
let extra = {};
|
|
35
42
|
try { extra = JSON.parse(Buffer.from(extraStr, 'base64').toString('utf-8')); }
|
|
36
43
|
catch { console.error('[demo-chat-handler] bad --extraEncJson'); process.exit(0); }
|
|
37
44
|
|
|
38
|
-
const {
|
|
39
|
-
|
|
45
|
+
const { boardSetupRoot, boardRuntimeDir, runtimeStatusDir, cardsDir, chatDir, lastChatFile } = extra;
|
|
46
|
+
if (!boardSetupRoot || !chatDir || !lastChatFile) {
|
|
47
|
+
console.error('[demo-chat-handler] missing boardSetupRoot/chatDir/lastChatFile');
|
|
40
48
|
process.exit(0);
|
|
41
49
|
}
|
|
42
50
|
|
|
51
|
+
// Resolve absolute paths from the structured extra fields
|
|
52
|
+
const boardRuntimeDirAbs = path.join(boardSetupRoot, boardRuntimeDir || 'runtime');
|
|
53
|
+
const runtimeStatusDirAbs = path.join(boardSetupRoot, runtimeStatusDir || 'runtime-out');
|
|
54
|
+
const cardsDirAbs = path.join(boardSetupRoot, cardsDir || path.join('surface', 'tmp-cards'));
|
|
55
|
+
const chatDirAbs = chatDir;
|
|
56
|
+
|
|
43
57
|
// ---------------------------------------------------------------------------
|
|
44
58
|
// Read conversation history
|
|
45
59
|
// ---------------------------------------------------------------------------
|
|
@@ -57,29 +71,31 @@ function readHistory(dir) {
|
|
|
57
71
|
} catch { return []; }
|
|
58
72
|
}
|
|
59
73
|
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
// Check if card has a 'copilot' source entry
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
function hasCopilotSource(bDir, cId) {
|
|
64
|
-
try {
|
|
65
|
-
const boardGraph = JSON.parse(fs.readFileSync(path.join(bDir, 'board-graph.json'), 'utf-8'));
|
|
66
|
-
const nodes = boardGraph?.graph?.nodes ?? boardGraph?.nodes ?? {};
|
|
67
|
-
const card = nodes[cId];
|
|
68
|
-
const sources = card.sources ?? card.card_data?.sources ?? [];
|
|
69
|
-
if (Array.isArray(sources)) return sources.some(s => s && typeof s === 'object' && 'copilot' in s);
|
|
70
|
-
if (typeof sources === 'object') return 'copilot' in sources;
|
|
71
|
-
return false;
|
|
72
|
-
} catch { return false; }
|
|
73
|
-
}
|
|
74
|
-
|
|
75
74
|
// ---------------------------------------------------------------------------
|
|
76
75
|
// Build prompt
|
|
77
76
|
// ---------------------------------------------------------------------------
|
|
78
|
-
function buildPrompt(cId, bId, history) {
|
|
77
|
+
function buildPrompt(cId, bId, history, responseFileRel) {
|
|
78
|
+
const cardSetupDirRel = path.join(cardsDir, cId).replace(/\\/g, '/');
|
|
79
|
+
const runtimeDirRel = boardRuntimeDir || 'runtime';
|
|
80
|
+
const statusDirRel = runtimeStatusDir || 'runtime-out';
|
|
81
|
+
const chatDirRel = path.relative(boardSetupRoot, chatDir).replace(/\\/g, '/');
|
|
82
|
+
const lastQueryFileRel = path.join(chatDirRel, lastChatFile).replace(/\\/g, '/');
|
|
83
|
+
|
|
84
|
+
const contextBlock = [
|
|
85
|
+
'We are currently doing a three way orchestration.',
|
|
86
|
+
'You are the responder who has context of the cards in ' + cardSetupDirRel + ',',
|
|
87
|
+
'card runtime statuses in ' + runtimeDirRel + ',',
|
|
88
|
+
'and computed outputs in ' + statusDirRel + '.',
|
|
89
|
+
'I am just a mediator passing on the query.',
|
|
90
|
+
'The user sees the data available in cards which is rendered, and the status from ' + statusDirRel + '.',
|
|
91
|
+
'Everything else is internal detail not to be exposed to the user.',
|
|
92
|
+
'The conversation history can be found in ' + chatDirRel + ' and the last query is in ' + lastQueryFileRel + '.',
|
|
93
|
+
'Write your response to the user in ' + responseFileRel + ' (relative to your working directory).',
|
|
94
|
+
'Give me only a bare minimum log line on what you did — the response in ' + responseFileRel + ' is what the user will see.',
|
|
95
|
+
].join(' ');
|
|
96
|
+
|
|
79
97
|
return [
|
|
80
|
-
|
|
81
|
-
'Help the user understand and act on the data shown in this card.',
|
|
82
|
-
'Be concise.',
|
|
98
|
+
contextBlock,
|
|
83
99
|
'',
|
|
84
100
|
...history,
|
|
85
101
|
'Assistant:',
|
|
@@ -120,32 +136,23 @@ function runWrapper(prompt, sessionDir, workingDir) {
|
|
|
120
136
|
// ---------------------------------------------------------------------------
|
|
121
137
|
// Main
|
|
122
138
|
// ---------------------------------------------------------------------------
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
const
|
|
139
|
+
const serialMatch = String(lastChatFile).match(/^(\d+)/);
|
|
140
|
+
const nextSerial = serialMatch ? parseInt(serialMatch[1], 10) + 1 : 1;
|
|
141
|
+
const nextName = String(nextSerial).padStart(3, '0') + '-assistant.txt';
|
|
142
|
+
const responseFileRel = path.relative(boardSetupRoot, path.join(chatDir, nextName)).replace(/\\/g, '/');
|
|
127
143
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
} catch (err) {
|
|
133
|
-
response = 'Sorry, I could not reach the LLM right now. (' + String(err?.message ?? err).slice(0, 120) + ')';
|
|
134
|
-
console.error('[demo-chat-handler] wrapper failed: ' + (err?.message ?? err));
|
|
135
|
-
}
|
|
136
|
-
} else {
|
|
137
|
-
response = 'No copilot source configured for this card.';
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Write assistant response as next serial file
|
|
141
|
-
const serialMatch = String(lastChatFile).match(/^(\d+)/);
|
|
142
|
-
const nextSerial = serialMatch ? parseInt(serialMatch[1], 10) + 1 : 1;
|
|
143
|
-
const nextName = String(nextSerial).padStart(3, '0') + '-assistant.txt';
|
|
144
|
-
const nextPath = path.join(chatDir, nextName);
|
|
144
|
+
const history = readHistory(chatDirAbs);
|
|
145
|
+
const sessionDir = path.join(os.tmpdir(), 'demo-chat-handler-sessions', boardId + '_' + cardId);
|
|
146
|
+
const workingDir = boardSetupRoot;
|
|
147
|
+
const prompt = buildPrompt(cardId, boardId, history, responseFileRel);
|
|
145
148
|
|
|
146
149
|
try {
|
|
147
|
-
|
|
148
|
-
console.log('[demo-chat-handler] cardId="' + cardId + '"
|
|
150
|
+
runWrapper(prompt, sessionDir, workingDir);
|
|
151
|
+
console.log('[demo-chat-handler] cardId="' + cardId + '" copilot invoked, response expected at ' + responseFileRel);
|
|
149
152
|
} catch (err) {
|
|
150
|
-
console.error('[demo-chat-handler]
|
|
153
|
+
console.error('[demo-chat-handler] wrapper failed: ' + (err?.message ?? err));
|
|
154
|
+
} finally {
|
|
155
|
+
if (cleanOnExit) {
|
|
156
|
+
try { fs.unlinkSync(cleanOnExit); } catch {}
|
|
157
|
+
}
|
|
151
158
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import http from 'node:http';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
7
8
|
import { createRequire } from 'node:module';
|
|
8
9
|
|
|
@@ -90,6 +91,11 @@ function resetRuntime() {
|
|
|
90
91
|
fs.rmSync(setupDir, { recursive: true, force: true });
|
|
91
92
|
console.log(`[demo-server] reset: wiped ${setupDir}`);
|
|
92
93
|
}
|
|
94
|
+
const chatSessions = path.join(os.tmpdir(), 'demo-chat-handler-sessions');
|
|
95
|
+
if (fs.existsSync(chatSessions)) {
|
|
96
|
+
fs.rmSync(chatSessions, { recursive: true, force: true });
|
|
97
|
+
console.log(`[demo-server] reset: wiped ${chatSessions}`);
|
|
98
|
+
}
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
if (RESET_ON_START) {
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
<title>Example Board Demo (Browser Runtime)</title>
|
|
7
7
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
|
8
8
|
<script src="https://cdn.jsdelivr.net/npm/jsonata/jsonata.min.js"></script>
|
|
9
|
-
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.
|
|
10
|
-
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.
|
|
11
|
-
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.
|
|
9
|
+
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.6/browser/card-compute.js"></script>
|
|
10
|
+
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.6/browser/live-cards.js"></script>
|
|
11
|
+
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.6/browser/board-livegraph-engine.js"></script>
|
|
12
12
|
</head>
|
|
13
13
|
<body class="bg-light">
|
|
14
14
|
<div class="container-fluid py-3">
|
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
</style>
|
|
17
17
|
<script src="https://cdn.jsdelivr.net/npm/jsonata/jsonata.min.js"></script>
|
|
18
18
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
19
|
-
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.
|
|
20
|
-
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.
|
|
21
|
-
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.
|
|
22
|
-
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.
|
|
19
|
+
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.6/browser/card-compute.js"></script>
|
|
20
|
+
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.6/browser/live-cards.js"></script>
|
|
21
|
+
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.6/browser/board-livegraph-engine.js"></script>
|
|
22
|
+
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.6/browser/board-livecards-runtime-client.js"></script>
|
|
23
23
|
</head>
|
|
24
24
|
<body class="bg-light">
|
|
25
25
|
<div class="container-fluid py-3">
|
|
@@ -0,0 +1,141 @@
|
|
|
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
|
+
if exist "!CACHE_SESSION_PATH!" (
|
|
130
|
+
for %%f in ("!CACHE_SESSION_PATH!\*") do (
|
|
131
|
+
move /y "%%f" "%SESSION_DIR%\" >nul 2>&1
|
|
132
|
+
)
|
|
133
|
+
for /d %%d in ("!CACHE_SESSION_PATH!\*") do (
|
|
134
|
+
robocopy "%%d" "%SESSION_DIR%\%%~nxd" /E /MOVE /NFL /NDL /NJH /NJS >nul 2>&1
|
|
135
|
+
)
|
|
136
|
+
rmdir "!CACHE_SESSION_PATH!" 2>nul
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
del "%LOCK_FILE%" 2>nul
|
|
140
|
+
|
|
141
|
+
endlocal
|
|
@@ -0,0 +1,181 @@
|
|
|
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 sources.
|
|
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
|
+
)
|
|
20
|
+
|
|
21
|
+
if (-not (Test-Path $OutputFile)) { exit 0 }
|
|
22
|
+
|
|
23
|
+
$raw = [IO.File]::ReadAllText($OutputFile, [Text.Encoding]::UTF8)
|
|
24
|
+
if ([string]::IsNullOrWhiteSpace($raw)) { exit 0 }
|
|
25
|
+
|
|
26
|
+
# --- Step 1: Filter noise lines ---
|
|
27
|
+
$lines = $raw -split "`r?`n" | Where-Object {
|
|
28
|
+
$_ -notmatch "^error: unknown option '--no-warnings'" -and
|
|
29
|
+
$_ -notmatch "^Try 'copilot --help' for more information"
|
|
30
|
+
}
|
|
31
|
+
$cleaned = ($lines -join "`n").Trim()
|
|
32
|
+
|
|
33
|
+
# --- Step 1b: Strip copilot-cli tool operation lines ---
|
|
34
|
+
# These are internal tool invocations that leak into output:
|
|
35
|
+
# ● Create/Read/Edit/List directory/Glob/Check ...
|
|
36
|
+
# X Read ... (failed tool ops)
|
|
37
|
+
# $ Get-Content/Set-Content ... (PowerShell invocations)
|
|
38
|
+
# └ N lines/files found
|
|
39
|
+
# ├ ... (tree lines)
|
|
40
|
+
# "The agent decision has been simulated and saved to ..."
|
|
41
|
+
# session-state file paths
|
|
42
|
+
$noiseLines = New-Object System.Collections.Generic.List[string]
|
|
43
|
+
$contentLines2 = New-Object System.Collections.Generic.List[string]
|
|
44
|
+
foreach ($line in ($cleaned -split "`n")) {
|
|
45
|
+
$t = $line.TrimStart()
|
|
46
|
+
if ($t -match '^[\u25cf\u2022] ' -or # ● bullet tool ops
|
|
47
|
+
$t -match '^X ' -or # X failed tool ops
|
|
48
|
+
$t -match '^\$ ' -or # $ shell commands
|
|
49
|
+
$t -match '^[\u2514\u251c]' -or # └ ├ tree lines
|
|
50
|
+
$t -match 'session-state.*\.json' -or # session-state file refs
|
|
51
|
+
$t -match 'agent.decision has been simulated' -or
|
|
52
|
+
$t -match 'has been simulated and saved' -or
|
|
53
|
+
$t -match '^\d+ (files?|lines?|matches?) found$' -or # "3 files found"
|
|
54
|
+
$t -match '^No matches found$' -or
|
|
55
|
+
$t -match '^Path does not exist$' -or
|
|
56
|
+
$t -match '^\d+ lines?( read)?$') { # "1 line read"
|
|
57
|
+
$noiseLines.Add($line)
|
|
58
|
+
} else {
|
|
59
|
+
$contentLines2.Add($line)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
$cleaned = ($contentLines2 -join "`n").Trim()
|
|
63
|
+
|
|
64
|
+
# Write noise to sidecar file for upstream visibility
|
|
65
|
+
$NoiseFile = $OutputFile + '.noise'
|
|
66
|
+
if ($noiseLines.Count -gt 0) {
|
|
67
|
+
$noiseContent = "STRIPPED_LINES=$($noiseLines.Count)`n" + ($noiseLines -join "`n")
|
|
68
|
+
[IO.File]::WriteAllText($NoiseFile, $noiseContent, [Text.Encoding]::UTF8)
|
|
69
|
+
} elseif (Test-Path $NoiseFile) {
|
|
70
|
+
Remove-Item $NoiseFile -Force
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# --- Step 2: Strip trailing usage stats ---
|
|
74
|
+
$statsPrefixes = @('Total usage est:', 'API time spent:', 'Total session time:',
|
|
75
|
+
'Total code changes:', 'Breakdown by AI model:', 'Session:',
|
|
76
|
+
'Changes', 'Requests', 'Tokens')
|
|
77
|
+
$resultLines = New-Object System.Collections.Generic.List[string]
|
|
78
|
+
$hitStats = $false
|
|
79
|
+
foreach ($line in $cleaned -split "`n") {
|
|
80
|
+
if (-not $hitStats) {
|
|
81
|
+
foreach ($sp in $statsPrefixes) {
|
|
82
|
+
if ($line.TrimStart().StartsWith($sp)) { $hitStats = $true; break }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (-not $hitStats) { $resultLines.Add($line) }
|
|
86
|
+
}
|
|
87
|
+
$cleaned = ($resultLines -join "`n").Trim()
|
|
88
|
+
|
|
89
|
+
# --- raw result_type: write cleaned plain text and exit ---
|
|
90
|
+
if ($ResultType -eq 'raw') {
|
|
91
|
+
if ([string]::IsNullOrWhiteSpace($cleaned)) {
|
|
92
|
+
[IO.File]::WriteAllText($OutputFile, '', [Text.Encoding]::UTF8)
|
|
93
|
+
} else {
|
|
94
|
+
[IO.File]::WriteAllText($OutputFile, $cleaned, [Text.Encoding]::UTF8)
|
|
95
|
+
}
|
|
96
|
+
exit 0
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# --- json result_type: extract JSON object matching result_shape ---
|
|
100
|
+
|
|
101
|
+
# Load result_shape keys (if provided) to use as required-key filter
|
|
102
|
+
$shapeKeys = @()
|
|
103
|
+
if ($ResultShapeFile -and (Test-Path $ResultShapeFile)) {
|
|
104
|
+
try {
|
|
105
|
+
$shape = [IO.File]::ReadAllText($ResultShapeFile, [Text.Encoding]::UTF8) | ConvertFrom-Json -ErrorAction Stop
|
|
106
|
+
$shapeKeys = @($shape.PSObject.Properties.Name)
|
|
107
|
+
} catch {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if ([string]::IsNullOrWhiteSpace($cleaned)) {
|
|
111
|
+
$fallback = if ($shapeKeys.Count -gt 0) {
|
|
112
|
+
$obj = [ordered]@{}
|
|
113
|
+
foreach ($k in $shapeKeys) { $obj[$k] = $null }
|
|
114
|
+
$obj | ConvertTo-Json -Depth 2 -Compress
|
|
115
|
+
} else { '{}' }
|
|
116
|
+
[IO.File]::WriteAllText($OutputFile, $fallback, [Text.Encoding]::UTF8)
|
|
117
|
+
exit 0
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Helper: check if a parsed object has all required shape keys
|
|
121
|
+
function Test-ShapeMatch($obj) {
|
|
122
|
+
if ($shapeKeys.Count -eq 0) { return $true } # no shape constraint — accept any JSON object
|
|
123
|
+
foreach ($k in $shapeKeys) {
|
|
124
|
+
if (-not $obj.PSObject.Properties[$k]) { return $false }
|
|
125
|
+
}
|
|
126
|
+
return $true
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
$foundJson = $null
|
|
130
|
+
|
|
131
|
+
# 1: Look in ```json fenced blocks first
|
|
132
|
+
if ($cleaned -match '(?s)```json\s*(.*?)```') {
|
|
133
|
+
try {
|
|
134
|
+
$obj = $Matches[1].Trim() | ConvertFrom-Json -ErrorAction Stop
|
|
135
|
+
if (Test-ShapeMatch $obj) { $foundJson = $Matches[1].Trim() }
|
|
136
|
+
} catch {}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# 2: Scan for bare JSON objects
|
|
140
|
+
if (-not $foundJson) {
|
|
141
|
+
$depth = 0; $start = -1
|
|
142
|
+
for ($i = 0; $i -lt $cleaned.Length; $i++) {
|
|
143
|
+
if ($cleaned[$i] -eq '{') {
|
|
144
|
+
if ($depth -eq 0) { $start = $i }
|
|
145
|
+
$depth++
|
|
146
|
+
} elseif ($cleaned[$i] -eq '}') {
|
|
147
|
+
$depth--
|
|
148
|
+
if ($depth -eq 0 -and $start -ge 0) {
|
|
149
|
+
$candidate = $cleaned.Substring($start, $i - $start + 1)
|
|
150
|
+
try {
|
|
151
|
+
$obj = $candidate | ConvertFrom-Json -ErrorAction Stop
|
|
152
|
+
if (Test-ShapeMatch $obj) {
|
|
153
|
+
$foundJson = $candidate
|
|
154
|
+
break
|
|
155
|
+
}
|
|
156
|
+
} catch {}
|
|
157
|
+
$start = -1
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if ($foundJson) {
|
|
164
|
+
[IO.File]::WriteAllText($OutputFile, $foundJson, [Text.Encoding]::UTF8)
|
|
165
|
+
} else {
|
|
166
|
+
# No matching JSON found — record raw in noise file, write shape-skeleton fallback
|
|
167
|
+
$NoiseFile = $OutputFile + '.noise'
|
|
168
|
+
$fallbackNoise = "FALLBACK=no_json_match`nSHAPE_KEYS=$($shapeKeys -join ',')`nRAW_LENGTH=$($cleaned.Length)`n---`n$cleaned"
|
|
169
|
+
if (Test-Path $NoiseFile) {
|
|
170
|
+
$existing = [IO.File]::ReadAllText($NoiseFile, [Text.Encoding]::UTF8)
|
|
171
|
+
[IO.File]::WriteAllText($NoiseFile, "$existing`n$fallbackNoise", [Text.Encoding]::UTF8)
|
|
172
|
+
} else {
|
|
173
|
+
[IO.File]::WriteAllText($NoiseFile, $fallbackNoise, [Text.Encoding]::UTF8)
|
|
174
|
+
}
|
|
175
|
+
$fallback = if ($shapeKeys.Count -gt 0) {
|
|
176
|
+
$obj = [ordered]@{}
|
|
177
|
+
foreach ($k in $shapeKeys) { $obj[$k] = $null }
|
|
178
|
+
$obj | ConvertTo-Json -Depth 2 -Compress
|
|
179
|
+
} else { '{}' }
|
|
180
|
+
[IO.File]::WriteAllText($OutputFile, $fallback, [Text.Encoding]::UTF8)
|
|
181
|
+
}
|