yaml-flow 8.2.5 → 8.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/browser/asset-integrity.json +3 -3
  2. package/browser/board-livecards-client.js +1 -1
  3. package/browser/board-livecards-localstorage.js +4 -4
  4. package/browser/live-cards.js +19 -19
  5. package/cli/{board-live-cards-lib-Iq_XAC09.d.ts → board-live-cards-lib-tjYsPt5U.d.ts} +1 -1
  6. package/cli/browser-api/board-live-cards-browser-adapter.d.ts +3 -3
  7. package/cli/browser-api/card-store-browser-api.d.ts +1 -1
  8. package/cli/{execution-interface-ftO1W7Po.d.ts → execution-interface-CrG5gzAx.d.ts} +116 -2
  9. package/cli/node/batch-runner-cli.d.ts +3 -0
  10. package/cli/node/batch-runner-cli.js +2 -1
  11. package/cli/node/board-live-cards-cli.js +9 -9
  12. package/cli/node/chat-store-cli.d.ts +23 -0
  13. package/cli/node/chat-store-cli.js +8 -0
  14. package/cli/node/execution-adapter.d.ts +4 -2
  15. package/cli/node/execution-adapter.js +2 -2
  16. package/cli/node/fs-board-adapter.d.ts +7 -6
  17. package/cli/node/fs-board-adapter.js +8 -8
  18. package/cli/node/source-cli-task-executor.js +4 -4
  19. package/cli/node/step-machine-cli.js +3 -3
  20. package/cli/{types--rXGWbSR.d.ts → types-PUfPBxc_.d.ts} +4 -109
  21. package/examples/board/demo-shell-with-server.html +3 -196
  22. package/examples/board/doc.html +465 -0
  23. package/examples/board/server/board-server.js +20 -81
  24. package/examples/board/server/board-worker/source_def_flows.json +2 -2
  25. package/examples/board/server/chat-flow/copilot-chat/assistant.js +44 -185
  26. package/examples/board/server/chat-flow/copilot-chat/copilot_wrapper.bat +157 -0
  27. package/examples/board/server/chat-flow/copilot-chat/copilot_wrapper_helper.ps1 +190 -0
  28. package/examples/board/server/chat-flow/flow-steps.json +122 -56
  29. package/examples/board/test/server-http-test.js +252 -220
  30. package/examples/board-local/demo-shell-localstorage.html +3 -3
  31. package/lib/{artifacts-store-lib-public-C5UL5tyG.d.cts → artifacts-store-lib-public-Cz8-kdXG.d.cts} +1 -1
  32. package/lib/{artifacts-store-lib-public-GD4H-fFp.d.ts → artifacts-store-lib-public-ksGIExhc.d.ts} +1 -1
  33. package/lib/artifacts-store-public.d.cts +2 -2
  34. package/lib/artifacts-store-public.d.ts +2 -2
  35. package/lib/board-live-cards-node.cjs +8 -8
  36. package/lib/board-live-cards-node.d.cts +26 -6
  37. package/lib/board-live-cards-node.d.ts +26 -6
  38. package/lib/board-live-cards-node.js +8 -8
  39. package/lib/{board-live-cards-public-BLXbcBNk.d.cts → board-live-cards-public-BwZYGAsF.d.cts} +1 -1
  40. package/lib/{board-live-cards-public-BZaNb2mi.d.ts → board-live-cards-public-DWpZVDXN.d.ts} +1 -1
  41. package/lib/board-live-cards-public.d.cts +1 -1
  42. package/lib/board-live-cards-public.d.ts +1 -1
  43. package/lib/board-live-cards-server-runtime.cjs +3 -3
  44. package/lib/board-live-cards-server-runtime.d.cts +3 -2
  45. package/lib/board-live-cards-server-runtime.d.ts +3 -2
  46. package/lib/board-live-cards-server-runtime.js +3 -3
  47. package/lib/board-worker-adapter.cjs +2 -2
  48. package/lib/board-worker-adapter.js +2 -2
  49. package/lib/card-store-public.d.cts +1 -1
  50. package/lib/card-store-public.d.ts +1 -1
  51. package/lib/chat-storage-lib-BK5Njslc.d.ts +53 -0
  52. package/lib/chat-storage-lib-C5bQ7bGe.d.cts +53 -0
  53. package/lib/chat-store-public.cjs +2 -0
  54. package/lib/chat-store-public.d.cts +128 -0
  55. package/lib/chat-store-public.d.ts +128 -0
  56. package/lib/chat-store-public.js +2 -0
  57. package/lib/execution-refs.d.cts +10 -1
  58. package/lib/execution-refs.d.ts +10 -1
  59. package/lib/server-runtime/index.cjs +3 -3
  60. package/lib/server-runtime/index.d.cts +4 -3
  61. package/lib/server-runtime/index.d.ts +4 -3
  62. package/lib/server-runtime/index.js +3 -3
  63. package/lib/{types-Bztd1KoK.d.cts → types-D9B0Vrwg.d.cts} +4 -53
  64. package/lib/{types-D-xVWPdY.d.ts → types-DNYhCFNJ.d.ts} +4 -53
  65. package/package.json +8 -2
  66. package/examples/board/.board-ws/cards/store/_index.json +0 -17
  67. package/examples/board/.board-ws/cards/store/card-market-prices.json +0 -80
  68. package/examples/board/.board-ws/cards/store/card-portfolio-value.json +0 -90
  69. package/examples/board/.board-ws/cards/store/card-portfolio.json +0 -78
  70. package/examples/board/server/chat-flow/chat-clear-processing.js +0 -41
  71. package/examples/board/server/chat-flow/chat-open-turn.js +0 -144
  72. package/examples/board/server/chat-flow/chat-write-assistant.js +0 -44
  73. package/examples/board/server/chat-flow/echo-probe/assistant.js +0 -28
@@ -3,9 +3,14 @@
3
3
  import * as fs from 'node:fs';
4
4
  import * as path from 'node:path';
5
5
  import * as os from 'node:os';
6
- import { execFileSync, spawnSync } from 'node:child_process';
6
+ import { execFileSync } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const HANDLER_DIR = path.dirname(fileURLToPath(import.meta.url));
10
+ const WRAPPER_BAT = path.join(HANDLER_DIR, 'copilot_wrapper.bat');
7
11
 
8
12
  function readJsonStdin() {
13
+ if (process.stdin.isTTY) return {};
9
14
  try {
10
15
  const raw = fs.readFileSync(0, 'utf-8').trim();
11
16
  if (!raw) return {};
@@ -16,68 +21,28 @@ function readJsonStdin() {
16
21
  }
17
22
  }
18
23
 
19
- function resolveChatDir(extra) {
20
- if (typeof extra.chatDir === 'string' && extra.chatDir.trim()) return extra.chatDir;
21
- if (typeof extra.chatsBlobBasePath === 'string' && typeof extra.chatsKeyPrefix === 'string') {
22
- const cardPart = String(extra.chatsKeyPrefix).split('/')[0];
23
- return path.join(extra.chatsBlobBasePath, cardPart);
24
- }
25
- return '';
26
- }
27
-
28
24
  const extra = readJsonStdin();
29
- const boardId = typeof extra.boardId === 'string' ? extra.boardId : '';
30
- const cardId = typeof extra.cardId === 'string' ? extra.cardId : '';
31
- const boardSetupRoot = typeof extra.boardSetupRoot === 'string' ? extra.boardSetupRoot : '';
32
- const boardRuntimeDir = typeof extra.boardRuntimeDir === 'string' ? extra.boardRuntimeDir : 'runtime';
33
- const runtimeStatusDir = typeof extra.runtimeStatusDir === 'string' ? extra.runtimeStatusDir : 'runtime-out';
34
- const cardsDir = typeof extra.cardsDir === 'string' ? extra.cardsDir : 'cards';
35
- const projectRoot = typeof extra.projectRoot === 'string' ? extra.projectRoot : '';
36
- const chatFlowRoot = typeof extra.chatFlowRoot === 'string' ? extra.chatFlowRoot : '';
37
- const serverUrl = typeof extra.serverUrl === 'string' ? extra.serverUrl.replace(/\/$/, '') : '';
38
- const apiBasePath = typeof extra.apiBasePath === 'string' ? extra.apiBasePath : '/api/board';
39
- const lastChatEntryId = typeof extra.lastChatEntryId === 'string' ? extra.lastChatEntryId : '';
40
- const userText = typeof extra.userText === 'string' ? extra.userText : '';
41
- const chatCopilotTimeoutMs = Number.isFinite(Number(extra.chatCopilotTimeoutMs)) && Number(extra.chatCopilotTimeoutMs) > 0
42
- ? Math.floor(Number(extra.chatCopilotTimeoutMs))
25
+ const {
26
+ cardId = '',
27
+ boardSetupRoot = '',
28
+ boardRuntimeDir = '',
29
+ runtimeStatusDir = '',
30
+ cardsDir = '',
31
+ chatMessages: rawChatMessages = [],
32
+ userText = 'what is two plus two?',
33
+ chatCopilotTimeoutMs: rawChatCopilotTimeoutMs = 300000,
34
+ } = extra;
35
+
36
+ const chatMessages = Array.isArray(rawChatMessages) ? rawChatMessages : [];
37
+ const chatCopilotTimeoutMs = Number.isFinite(Number(rawChatCopilotTimeoutMs)) && Number(rawChatCopilotTimeoutMs) > 0
38
+ ? Math.floor(Number(rawChatCopilotTimeoutMs))
43
39
  : 300000;
44
40
 
45
- if (!boardSetupRoot || !serverUrl || !cardId) {
46
- process.stderr.write('missing boardSetupRoot/serverUrl/cardId\n');
47
- process.exit(1);
48
- }
49
-
50
- const boardRuntimeDirAbs = path.join(boardSetupRoot, boardRuntimeDir || 'runtime');
51
- const runtimeStatusDirAbs = path.join(boardSetupRoot, runtimeStatusDir || 'runtime-out');
52
- const cardsDirAbs = path.join(boardSetupRoot, cardsDir || 'cards');
53
-
54
- async function fetchChatMessages() {
55
- const chatsUrl = `${serverUrl}${apiBasePath}/cards/${encodeURIComponent(cardId)}/chats`;
56
- const res = await fetch(chatsUrl);
57
- if (!res.ok) {
58
- throw new Error(`could not fetch chat history: HTTP ${res.status}`);
59
- }
60
- const data = await res.json();
61
- return Array.isArray(data?.messages) ? data.messages : [];
62
- }
63
-
64
- function readHistory(messages) {
65
- return (Array.isArray(messages) ? messages : [])
66
- .filter((message) => typeof message?.role === 'string' && typeof message?.text === 'string')
67
- .map((message) => {
68
- const role = String(message.role || 'system').replace(/^./, (s) => s.toUpperCase());
69
- const text = String(message.text || '').trim();
70
- const files = Array.isArray(message.files) && message.files.length > 0
71
- ? ` [files: ${message.files.length}]`
72
- : '';
73
- return `${role}: ${text}${files}`.trim();
74
- });
75
- }
76
41
 
77
- function buildPrompt(cId, history, currentUserText) {
42
+ function buildPrompt(cId, historyDump, currentUserText) {
78
43
  const cardSetupDirRel = path.join(cardsDir, cId).replace(/\\/g, '/');
79
- const runtimeDirRel = boardRuntimeDir || 'runtime';
80
- const statusDirRel = runtimeStatusDir || 'runtime-out';
44
+ const runtimeDirRel = boardRuntimeDir;
45
+ const statusDirRel = runtimeStatusDir;
81
46
 
82
47
  const contextBlock = [
83
48
  'We are currently doing a three way orchestration.',
@@ -87,7 +52,7 @@ function buildPrompt(cId, history, currentUserText) {
87
52
  'I am just a mediator passing on the query.',
88
53
  'The user sees the data available in cards which is rendered, and the status from ' + statusDirRel + '.',
89
54
  'Everything else is internal detail not to be exposed to the user.',
90
- 'The conversation history is provided below as chat messages from the runtime API.',
55
+ 'The conversation history is provided below exactly as received from the runtime API as a string dump.',
91
56
  'The current user query is: ' + currentUserText,
92
57
  'Return only the assistant response text for the user.',
93
58
  'Do not write files, and do not include any internal notes, logs, or orchestration details in the response.',
@@ -96,102 +61,28 @@ function buildPrompt(cId, history, currentUserText) {
96
61
  return [
97
62
  contextBlock,
98
63
  '',
99
- ...history,
100
- 'Assistant:',
64
+ 'Chat history dump:',
65
+ historyDump,
66
+ '',
67
+ 'Assistant response:',
101
68
  ].join('\n');
102
69
  }
103
70
 
104
- function localFallbackReply(currentUserText) {
105
- const text = String(currentUserText || '').trim();
106
- const normalized = text.toLowerCase();
107
- if (normalized.includes('capital of france')) return 'paris';
108
- if (normalized.includes('capital of japan')) return 'tokyo';
109
- if (!text) return 'I could not determine the user request.';
110
- return `Echo: ${text}`;
111
- }
112
-
113
- function isNonInteractiveCopilotError(err) {
114
- const detail = [
115
- err instanceof Error ? err.message : String(err),
116
- typeof err?.stderr === 'string' ? err.stderr : Buffer.isBuffer(err?.stderr) ? err.stderr.toString('utf-8') : '',
117
- typeof err?.stdout === 'string' ? err.stdout : Buffer.isBuffer(err?.stdout) ? err.stdout.toString('utf-8') : '',
118
- ].join('\n').toLowerCase();
119
- return detail.includes('stdout is not a tty') || detail.includes('not a tty');
120
- }
121
-
122
- function runWrapper(prompt, sessionDir, workingDir) {
123
- const fallbackProjectRoot = chatFlowRoot ? path.resolve(chatFlowRoot, '..', '..') : process.cwd();
124
- const effectiveProjectRoot = projectRoot || fallbackProjectRoot;
125
- const wrapperPath = path.resolve(
126
- effectiveProjectRoot,
127
- 'server',
128
- 'board-worker',
129
- 'source-def-flows',
130
- 'copilot-handler',
131
- 'copilot-wrapper.py',
132
- );
133
- const python = process.platform === 'win32' ? 'python' : 'python3';
134
- const tmpBase = os.tmpdir();
71
+ function runCopilot(prompt, workingDir) {
135
72
  const ts = Date.now();
136
- const outFile = path.join(tmpBase, 'dch-out-' + cardId + '-' + ts + '.txt');
137
- const promptFile = path.join(tmpBase, 'dch-prompt-' + cardId + '-' + ts + '.txt');
138
- const windowsWrapperPath = path.resolve(
139
- effectiveProjectRoot,
140
- '..',
141
- 'public-examples',
142
- 'board',
143
- 'scripts',
144
- 'copilot_wrapper.bat',
145
- );
146
-
147
- fs.mkdirSync(sessionDir, { recursive: true });
73
+ const promptFile = path.join(os.tmpdir(), `asst-prompt-${ts}.txt`);
74
+ const outFile = path.join(os.tmpdir(), `asst-out-${ts}.txt`);
148
75
  fs.writeFileSync(promptFile, prompt, 'utf-8');
149
-
150
- if (process.platform === 'win32' && fs.existsSync(windowsWrapperPath)) {
151
- try {
152
- execFileSync('cmd.exe', [
153
- '/d', '/c',
154
- windowsWrapperPath,
155
- outFile,
156
- sessionDir,
157
- workingDir,
158
- '@' + promptFile,
159
- 'raw',
160
- 'demo-chat',
161
- '',
162
- '',
163
- ], {
164
- encoding: 'utf-8',
165
- stdio: ['ignore', 'pipe', 'pipe'],
166
- maxBuffer: 10 * 1024 * 1024,
167
- timeout: chatCopilotTimeoutMs,
168
- windowsHide: true,
169
- });
170
- return fs.existsSync(outFile) ? fs.readFileSync(outFile, 'utf-8').trim() : '';
171
- } finally {
172
- try { fs.unlinkSync(promptFile); } catch {}
173
- try { fs.unlinkSync(outFile); } catch {}
174
- }
175
- }
176
-
177
- const pyArgs = [
178
- wrapperPath,
179
- '--output-file', outFile,
180
- '--session-dir', sessionDir,
181
- '--cwd', workingDir,
182
- '--prompt-file', promptFile,
183
- '--result-type', 'raw',
184
- '--agent-name', 'demo-chat',
185
- '--add-dir', boardRuntimeDirAbs,
186
- '--add-dir', runtimeStatusDirAbs,
187
- '--add-dir', cardsDirAbs,
188
- ];
189
-
190
76
  try {
191
- if (!fs.existsSync(wrapperPath)) {
192
- throw new Error(`copilot wrapper not found at ${wrapperPath}`);
193
- }
194
- execFileSync(python, pyArgs, {
77
+ execFileSync('cmd.exe', [
78
+ '/d', '/c', WRAPPER_BAT,
79
+ outFile,
80
+ os.tmpdir(),
81
+ workingDir || process.cwd(),
82
+ '@' + promptFile,
83
+ 'raw',
84
+ 'demo-chat',
85
+ ], {
195
86
  encoding: 'utf-8',
196
87
  stdio: ['ignore', 'pipe', 'pipe'],
197
88
  maxBuffer: 10 * 1024 * 1024,
@@ -205,47 +96,15 @@ function runWrapper(prompt, sessionDir, workingDir) {
205
96
  }
206
97
  }
207
98
 
208
- function upsertCardsIfChanged() {
209
- const cliJs = process.env.BOARD_LIVE_CARDS_CLI_JS;
210
- if (!cliJs || !fs.existsSync(cliJs)) return;
211
- const rg = boardRuntimeDirAbs;
212
- const glob = path.join(cardsDirAbs, '*.json');
213
- try {
214
- const result = spawnSync(process.execPath, [cliJs, 'upsert-card', '--rg', rg, '--card-glob', glob], {
215
- stdio: ['ignore', 'pipe', 'pipe'],
216
- timeout: 30000,
217
- });
218
- if (result.status !== 0) {
219
- const err = (result.stderr || '').toString().trim();
220
- if (err) console.error('[copilot-chat-assistant] upsert-card: ' + err);
221
- }
222
- } catch (err) {
223
- console.error('[copilot-chat-assistant] upsert-card failed: ' + (err?.message ?? err));
224
- }
225
- }
226
-
227
- const messages = await fetchChatMessages();
228
- const currentUser = messages.find((message) => typeof message?.id === 'string' && message.id === lastChatEntryId && message.role === 'user');
229
- const currentUserText = typeof currentUser?.text === 'string' && currentUser.text.trim()
230
- ? currentUser.text.trim()
231
- : userText.trim();
232
- const history = readHistory(messages);
233
- const sessionDir = path.join(os.tmpdir(), 'demo-chat-handler-sessions', boardId + '_' + cardId);
99
+ const historyDump = JSON.stringify(chatMessages, null, 2);
234
100
  const workingDir = boardSetupRoot;
235
- const prompt = buildPrompt(cardId, history, currentUserText);
101
+ const prompt = buildPrompt(cardId, historyDump, userText.trim());
236
102
 
237
103
  try {
238
- let replyText = '';
239
- try {
240
- replyText = runWrapper(prompt, sessionDir, workingDir).trim();
241
- } catch (err) {
242
- if (!isNonInteractiveCopilotError(err)) throw err;
243
- replyText = localFallbackReply(currentUserText);
244
- }
104
+ const replyText = runCopilot(prompt, workingDir).trim();
245
105
  if (!replyText) {
246
- throw new Error('Copilot wrapper returned an empty response');
106
+ throw new Error('Copilot returned an empty response');
247
107
  }
248
- upsertCardsIfChanged();
249
108
  process.stdout.write(JSON.stringify({ replyText }));
250
109
  } catch (err) {
251
110
  process.stderr.write((err?.message ?? String(err)) + '\n');
@@ -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
+ }