yaml-flow 8.2.4 → 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.
- package/browser/asset-integrity.json +3 -3
- package/browser/board-livecards-client.js +1 -1
- package/browser/board-livecards-localstorage.js +4 -4
- package/browser/live-cards.js +19 -19
- package/cli/{board-live-cards-lib-Iq_XAC09.d.ts → board-live-cards-lib-tjYsPt5U.d.ts} +1 -1
- package/cli/browser-api/board-live-cards-browser-adapter.d.ts +3 -3
- package/cli/browser-api/card-store-browser-api.d.ts +1 -1
- package/cli/browser-api/card-store-browser-api.js +1 -1
- package/cli/{execution-interface-ftO1W7Po.d.ts → execution-interface-CrG5gzAx.d.ts} +116 -2
- package/cli/node/batch-runner-cli.d.ts +3 -0
- package/cli/node/batch-runner-cli.js +2 -1
- package/cli/node/board-live-cards-cli.js +7 -7
- package/cli/node/card-store-cli.js +5 -5
- package/cli/node/chat-store-cli.d.ts +23 -0
- package/cli/node/chat-store-cli.js +8 -0
- package/cli/node/execution-adapter.d.ts +4 -2
- package/cli/node/execution-adapter.js +2 -2
- package/cli/node/fs-board-adapter.d.ts +7 -6
- package/cli/node/fs-board-adapter.js +8 -8
- package/cli/node/source-cli-task-executor.js +4 -4
- package/cli/node/step-machine-cli.js +3 -3
- package/cli/{types--rXGWbSR.d.ts → types-PUfPBxc_.d.ts} +4 -109
- package/examples/board/demo-shell-with-server.html +3 -3
- package/examples/board/doc.html +465 -0
- package/examples/board/server/board-server.js +20 -81
- package/examples/board/server/board-worker/source_def_flows.json +2 -2
- package/examples/board/server/chat-flow/copilot-chat/assistant.js +44 -185
- package/examples/board/server/chat-flow/copilot-chat/copilot_wrapper.bat +157 -0
- package/examples/board/server/chat-flow/copilot-chat/copilot_wrapper_helper.ps1 +190 -0
- package/examples/board/server/chat-flow/flow-steps.json +122 -56
- package/examples/board/test/server-http-test.js +252 -220
- package/examples/board-local/demo-shell-localstorage.html +3 -3
- package/lib/{artifacts-store-lib-public-C5UL5tyG.d.cts → artifacts-store-lib-public-Cz8-kdXG.d.cts} +1 -1
- package/lib/{artifacts-store-lib-public-GD4H-fFp.d.ts → artifacts-store-lib-public-ksGIExhc.d.ts} +1 -1
- package/lib/artifacts-store-public.d.cts +2 -2
- package/lib/artifacts-store-public.d.ts +2 -2
- package/lib/board-live-cards-node.cjs +8 -8
- package/lib/board-live-cards-node.d.cts +26 -6
- package/lib/board-live-cards-node.d.ts +26 -6
- package/lib/board-live-cards-node.js +8 -8
- package/lib/{board-live-cards-public-BLXbcBNk.d.cts → board-live-cards-public-BwZYGAsF.d.cts} +1 -1
- package/lib/{board-live-cards-public-BZaNb2mi.d.ts → board-live-cards-public-DWpZVDXN.d.ts} +1 -1
- package/lib/board-live-cards-public.cjs +2 -2
- package/lib/board-live-cards-public.d.cts +1 -1
- package/lib/board-live-cards-public.d.ts +1 -1
- package/lib/board-live-cards-public.js +2 -2
- package/lib/board-live-cards-server-runtime.cjs +3 -3
- package/lib/board-live-cards-server-runtime.d.cts +3 -2
- package/lib/board-live-cards-server-runtime.d.ts +3 -2
- package/lib/board-live-cards-server-runtime.js +3 -3
- package/lib/board-worker-adapter.cjs +2 -2
- package/lib/board-worker-adapter.js +2 -2
- package/lib/card-store-public.d.cts +1 -1
- package/lib/card-store-public.d.ts +1 -1
- package/lib/chat-storage-lib-BK5Njslc.d.ts +53 -0
- package/lib/chat-storage-lib-C5bQ7bGe.d.cts +53 -0
- package/lib/chat-store-public.cjs +2 -0
- package/lib/chat-store-public.d.cts +128 -0
- package/lib/chat-store-public.d.ts +128 -0
- package/lib/chat-store-public.js +2 -0
- package/lib/execution-refs.d.cts +10 -1
- package/lib/execution-refs.d.ts +10 -1
- package/lib/server-runtime/index.cjs +3 -3
- package/lib/server-runtime/index.d.cts +4 -3
- package/lib/server-runtime/index.d.ts +4 -3
- package/lib/server-runtime/index.js +3 -3
- package/lib/{types-Bztd1KoK.d.cts → types-D9B0Vrwg.d.cts} +4 -53
- package/lib/{types-D-xVWPdY.d.ts → types-DNYhCFNJ.d.ts} +4 -53
- package/package.json +8 -2
- package/examples/board/.board-ws/cards/store/_index.json +0 -17
- package/examples/board/.board-ws/cards/store/card-market-prices.json +0 -80
- package/examples/board/.board-ws/cards/store/card-portfolio-value.json +0 -90
- package/examples/board/.board-ws/cards/store/card-portfolio.json +0 -78
- package/examples/board/server/chat-flow/chat-clear-processing.js +0 -41
- package/examples/board/server/chat-flow/chat-open-turn.js +0 -144
- package/examples/board/server/chat-flow/chat-write-assistant.js +0 -44
- 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
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
const chatCopilotTimeoutMs = Number.isFinite(Number(
|
|
42
|
-
? Math.floor(Number(
|
|
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,
|
|
42
|
+
function buildPrompt(cId, historyDump, currentUserText) {
|
|
78
43
|
const cardSetupDirRel = path.join(cardsDir, cId).replace(/\\/g, '/');
|
|
79
|
-
const runtimeDirRel = boardRuntimeDir
|
|
80
|
-
const statusDirRel = runtimeStatusDir
|
|
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
|
|
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
|
-
|
|
100
|
-
|
|
64
|
+
'Chat history dump:',
|
|
65
|
+
historyDump,
|
|
66
|
+
'',
|
|
67
|
+
'Assistant response:',
|
|
101
68
|
].join('\n');
|
|
102
69
|
}
|
|
103
70
|
|
|
104
|
-
function
|
|
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
|
|
137
|
-
const
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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,
|
|
101
|
+
const prompt = buildPrompt(cardId, historyDump, userText.trim());
|
|
236
102
|
|
|
237
103
|
try {
|
|
238
|
-
|
|
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
|
|
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
|
+
}
|