@aiscene/shared 8.0.2 → 8.0.4

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 (177) hide show
  1. package/dist/es/agent-tools/agent-behavior-init-args.mjs +44 -0
  2. package/dist/es/agent-tools/base-tools.mjs +163 -0
  3. package/dist/es/agent-tools/chrome-path.mjs +50 -0
  4. package/dist/es/agent-tools/cli-report-session.mjs +78 -0
  5. package/dist/es/agent-tools/error-formatter.mjs +106 -0
  6. package/dist/es/agent-tools/index.mjs +9 -0
  7. package/dist/es/agent-tools/init-arg-utils.mjs +38 -0
  8. package/dist/es/agent-tools/observation-artifact.mjs +5 -0
  9. package/dist/es/agent-tools/observation-record.mjs +248 -0
  10. package/dist/es/agent-tools/tool-defaults.mjs +54 -0
  11. package/dist/es/agent-tools/tool-generator.mjs +529 -0
  12. package/dist/es/agent-tools/types.mjs +3 -0
  13. package/dist/es/agent-tools/user-prompt.mjs +66 -0
  14. package/dist/es/cli/cli-args.mjs +95 -0
  15. package/dist/es/cli/cli-error.mjs +24 -0
  16. package/dist/es/cli/cli-runner.mjs +10 -40
  17. package/dist/es/cli/index.mjs +4 -2
  18. package/dist/es/cli/interrupt.mjs +116 -0
  19. package/dist/es/cli/record-command.mjs +130 -0
  20. package/dist/es/cli/screenshot-file.mjs +24 -0
  21. package/dist/es/cli/verbose-ai-act.mjs +230 -0
  22. package/dist/es/cli/verbose-screenshot.mjs +134 -0
  23. package/dist/es/cli/verbose.mjs +404 -0
  24. package/dist/es/constants/example-code.mjs +2 -2
  25. package/dist/es/constants/index.mjs +6 -1
  26. package/dist/es/env/model-config-manager.mjs +3 -0
  27. package/dist/es/env/parse-model-config.mjs +6 -1
  28. package/dist/es/env/types.mjs +4 -1
  29. package/dist/es/env/utils.mjs +1 -9
  30. package/dist/es/extractor/index.mjs +2 -2
  31. package/dist/es/img/index.mjs +2 -2
  32. package/dist/es/img/info.mjs +58 -17
  33. package/dist/es/img/photon-loader.mjs +4 -0
  34. package/dist/es/key-alias-utils.mjs +19 -0
  35. package/dist/es/mcp/base-server.mjs +10 -9
  36. package/dist/es/mcp/base-tools.mjs +72 -4
  37. package/dist/es/mcp/cli-report-session.mjs +78 -0
  38. package/dist/es/mcp/error-formatter.mjs +19 -0
  39. package/dist/es/mcp/index.mjs +2 -0
  40. package/dist/es/mcp/init-arg-utils.mjs +38 -0
  41. package/dist/es/mcp/inject-report-html-plugin.mjs +1 -1
  42. package/dist/es/mcp/tool-generator.mjs +65 -17
  43. package/dist/es/node/fs.mjs +1 -1
  44. package/dist/es/recorder.mjs +249 -0
  45. package/dist/lib/agent-tools/agent-behavior-init-args.js +87 -0
  46. package/dist/lib/agent-tools/base-tools.js +197 -0
  47. package/dist/lib/agent-tools/chrome-path.js +87 -0
  48. package/dist/lib/agent-tools/cli-report-session.js +121 -0
  49. package/dist/lib/agent-tools/error-formatter.js +149 -0
  50. package/dist/lib/agent-tools/index.js +114 -0
  51. package/dist/lib/agent-tools/init-arg-utils.js +78 -0
  52. package/dist/lib/agent-tools/observation-artifact.js +42 -0
  53. package/dist/lib/agent-tools/observation-record.js +297 -0
  54. package/dist/lib/agent-tools/tool-defaults.js +97 -0
  55. package/dist/lib/agent-tools/tool-generator.js +569 -0
  56. package/dist/lib/agent-tools/types.js +40 -0
  57. package/dist/lib/agent-tools/user-prompt.js +103 -0
  58. package/dist/lib/cli/cli-args.js +138 -0
  59. package/dist/lib/cli/cli-error.js +61 -0
  60. package/dist/lib/cli/cli-runner.js +19 -46
  61. package/dist/lib/cli/index.js +8 -3
  62. package/dist/lib/cli/interrupt.js +156 -0
  63. package/dist/lib/cli/record-command.js +164 -0
  64. package/dist/lib/cli/screenshot-file.js +58 -0
  65. package/dist/lib/cli/verbose-ai-act.js +273 -0
  66. package/dist/lib/cli/verbose-screenshot.js +177 -0
  67. package/dist/lib/cli/verbose.js +465 -0
  68. package/dist/lib/constants/example-code.js +2 -2
  69. package/dist/lib/constants/index.js +23 -3
  70. package/dist/lib/env/model-config-manager.js +3 -0
  71. package/dist/lib/env/parse-model-config.js +6 -1
  72. package/dist/lib/env/types.js +9 -0
  73. package/dist/lib/env/utils.js +0 -11
  74. package/dist/lib/extractor/index.js +3 -0
  75. package/dist/lib/img/index.js +3 -0
  76. package/dist/lib/img/info.js +65 -18
  77. package/dist/lib/img/photon-loader.js +38 -0
  78. package/dist/lib/key-alias-utils.js +62 -0
  79. package/dist/lib/mcp/base-server.js +11 -10
  80. package/dist/lib/mcp/base-tools.js +74 -6
  81. package/dist/lib/mcp/cli-report-session.js +121 -0
  82. package/dist/lib/mcp/error-formatter.js +53 -0
  83. package/dist/lib/mcp/index.js +24 -10
  84. package/dist/lib/mcp/init-arg-utils.js +78 -0
  85. package/dist/lib/mcp/inject-report-html-plugin.js +1 -1
  86. package/dist/lib/mcp/tool-generator.js +74 -26
  87. package/dist/lib/node/fs.js +1 -1
  88. package/dist/lib/recorder.js +307 -0
  89. package/dist/types/agent-tools/agent-behavior-init-args.d.ts +17 -0
  90. package/dist/types/agent-tools/base-tools.d.ts +158 -0
  91. package/dist/types/agent-tools/chrome-path.d.ts +2 -0
  92. package/dist/types/agent-tools/cli-report-session.d.ts +12 -0
  93. package/dist/types/agent-tools/error-formatter.d.ts +30 -0
  94. package/dist/types/agent-tools/index.d.ts +9 -0
  95. package/dist/types/agent-tools/init-arg-utils.d.ts +13 -0
  96. package/dist/types/agent-tools/observation-artifact.d.ts +10 -0
  97. package/dist/types/agent-tools/observation-record.d.ts +38 -0
  98. package/dist/types/agent-tools/tool-defaults.d.ts +63 -0
  99. package/dist/types/agent-tools/tool-generator.d.ts +13 -0
  100. package/dist/types/agent-tools/types.d.ts +213 -0
  101. package/dist/types/agent-tools/user-prompt.d.ts +13 -0
  102. package/dist/types/cli/cli-args.d.ts +8 -0
  103. package/dist/types/cli/cli-error.d.ts +5 -0
  104. package/dist/types/cli/cli-runner.d.ts +4 -7
  105. package/dist/types/cli/index.d.ts +3 -1
  106. package/dist/types/cli/interrupt.d.ts +49 -0
  107. package/dist/types/cli/record-command.d.ts +3 -0
  108. package/dist/types/cli/screenshot-file.d.ts +10 -0
  109. package/dist/types/cli/verbose-ai-act.d.ts +44 -0
  110. package/dist/types/cli/verbose-screenshot.d.ts +10 -0
  111. package/dist/types/cli/verbose.d.ts +40 -0
  112. package/dist/types/constants/example-code.d.ts +1 -1
  113. package/dist/types/constants/index.d.ts +5 -0
  114. package/dist/types/env/types.d.ts +14 -5
  115. package/dist/types/env/utils.d.ts +0 -31
  116. package/dist/types/extractor/index.d.ts +1 -1
  117. package/dist/types/img/index.d.ts +1 -1
  118. package/dist/types/img/info.d.ts +7 -0
  119. package/dist/types/img/photon-loader.d.ts +2 -0
  120. package/dist/types/key-alias-utils.d.ts +9 -0
  121. package/dist/types/mcp/base-tools.d.ts +74 -5
  122. package/dist/types/mcp/cli-report-session.d.ts +12 -0
  123. package/dist/types/mcp/error-formatter.d.ts +12 -0
  124. package/dist/types/mcp/index.d.ts +2 -0
  125. package/dist/types/mcp/init-arg-utils.d.ts +13 -0
  126. package/dist/types/mcp/inject-report-html-plugin.d.ts +2 -2
  127. package/dist/types/mcp/launcher-helper.d.ts +3 -3
  128. package/dist/types/mcp/tool-generator.d.ts +3 -3
  129. package/dist/types/mcp/types.d.ts +10 -0
  130. package/dist/types/recorder.d.ts +113 -0
  131. package/package.json +1 -1
  132. package/src/agent-tools/agent-behavior-init-args.ts +109 -0
  133. package/src/agent-tools/base-tools.ts +399 -0
  134. package/src/agent-tools/chrome-path.ts +74 -0
  135. package/src/agent-tools/cli-report-session.ts +130 -0
  136. package/src/agent-tools/error-formatter.ts +177 -0
  137. package/src/agent-tools/index.ts +9 -0
  138. package/src/agent-tools/init-arg-utils.ts +105 -0
  139. package/src/agent-tools/observation-artifact.ts +29 -0
  140. package/src/agent-tools/observation-record.ts +331 -0
  141. package/src/agent-tools/tool-defaults.ts +119 -0
  142. package/src/agent-tools/tool-generator.ts +866 -0
  143. package/src/agent-tools/types.ts +250 -0
  144. package/src/agent-tools/user-prompt.ts +102 -0
  145. package/src/cli/cli-args.ts +173 -0
  146. package/src/cli/cli-error.ts +24 -0
  147. package/src/cli/cli-runner.ts +37 -56
  148. package/src/cli/index.ts +3 -7
  149. package/src/cli/interrupt.ts +207 -0
  150. package/src/cli/record-command.ts +177 -0
  151. package/src/cli/screenshot-file.ts +61 -0
  152. package/src/cli/verbose-ai-act.ts +387 -0
  153. package/src/cli/verbose-screenshot.ts +269 -0
  154. package/src/cli/verbose.ts +753 -0
  155. package/src/constants/example-code.ts +2 -2
  156. package/src/constants/index.ts +10 -0
  157. package/src/env/model-config-manager.ts +3 -0
  158. package/src/env/parse-model-config.ts +19 -1
  159. package/src/env/types.ts +13 -2
  160. package/src/env/utils.ts +0 -50
  161. package/src/extractor/index.ts +1 -1
  162. package/src/img/index.ts +14 -0
  163. package/src/img/info.ts +97 -45
  164. package/src/img/photon-loader.ts +5 -0
  165. package/src/img/transform.ts +261 -2
  166. package/src/key-alias-utils.ts +23 -0
  167. package/src/mcp/base-server.ts +10 -12
  168. package/src/mcp/base-tools.ts +218 -11
  169. package/src/mcp/cli-report-session.ts +130 -0
  170. package/src/mcp/error-formatter.ts +52 -0
  171. package/src/mcp/index.ts +2 -0
  172. package/src/mcp/init-arg-utils.ts +105 -0
  173. package/src/mcp/inject-report-html-plugin.ts +3 -3
  174. package/src/mcp/launcher-helper.ts +3 -3
  175. package/src/mcp/tool-generator.ts +98 -20
  176. package/src/mcp/types.ts +18 -1
  177. package/src/recorder.ts +625 -0
@@ -0,0 +1,387 @@
1
+ import {
2
+ type CliVerboseScreenshotCollectOptions,
3
+ collectScreenshotRefs,
4
+ pathForReportScreenshot,
5
+ } from './verbose-screenshot';
6
+
7
+ export interface CliVerboseLine {
8
+ key: string;
9
+ text: string;
10
+ }
11
+
12
+ /** Generic progress-bus envelope as seen by the CLI (all fields untrusted). */
13
+ export interface CliAgentProgressEvent {
14
+ scope?: unknown;
15
+ phase?: unknown;
16
+ sequence?: unknown;
17
+ data?: unknown;
18
+ }
19
+
20
+ /** Flattened, render-ready aiAct payload derived from the envelope. */
21
+ export interface CliAiActProgressPayload {
22
+ phase?: unknown;
23
+ sequence?: unknown;
24
+ prompt?: unknown;
25
+ planIndex?: unknown;
26
+ planLimit?: unknown;
27
+ action?: unknown;
28
+ thought?: unknown;
29
+ log?: unknown;
30
+ output?: unknown;
31
+ durationMs?: unknown;
32
+ error?: unknown;
33
+ screenshots?: Array<Record<string, unknown>>;
34
+ screenshotPath?: string;
35
+ // Lets the payload satisfy the generic `Record<string, unknown>` dispatch
36
+ // boundary while keeping the named fields documented.
37
+ [key: string]: unknown;
38
+ }
39
+
40
+ /** aiAct scope tag on the progress bus (mirrors core's `aiActProgressScope`). */
41
+ export const cliAiActProgressScope = 'aiAct';
42
+
43
+ function isRecord(value: unknown): value is Record<string, unknown> {
44
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
45
+ }
46
+
47
+ function compactText(value: unknown): string {
48
+ if (value === undefined || value === null || value === '') {
49
+ return '';
50
+ }
51
+
52
+ if (typeof value === 'string') {
53
+ return value.length > 180 ? `${value.slice(0, 177)}...` : value;
54
+ }
55
+
56
+ if (typeof value === 'number' || typeof value === 'boolean') {
57
+ return String(value);
58
+ }
59
+
60
+ try {
61
+ const json = JSON.stringify(value);
62
+ return json.length > 180 ? `${json.slice(0, 177)}...` : json;
63
+ } catch {
64
+ return String(value);
65
+ }
66
+ }
67
+
68
+ function numericValue(value: unknown): number | undefined {
69
+ return typeof value === 'number' && Number.isFinite(value)
70
+ ? value
71
+ : undefined;
72
+ }
73
+
74
+ function firstText(values: unknown[]): string {
75
+ for (const value of values) {
76
+ const text = compactText(value);
77
+ if (text) {
78
+ return text;
79
+ }
80
+ }
81
+ return '';
82
+ }
83
+
84
+ function planPrefix(event: CliAiActProgressPayload): string {
85
+ const planIndex = numericValue(event.planIndex);
86
+ if (!planIndex) {
87
+ return '[Midscene][aiAct]';
88
+ }
89
+
90
+ const planLimit = numericValue(event.planLimit);
91
+ return `[Midscene][aiAct][Plan ${Math.round(planIndex)}${
92
+ planLimit ? `/${Math.round(planLimit)}` : ''
93
+ }]`;
94
+ }
95
+
96
+ function eventKey(event: CliAiActProgressPayload, suffix: string): string {
97
+ const sequence = numericValue(event.sequence);
98
+ if (sequence) {
99
+ return `aiAct:${sequence}:${suffix}`;
100
+ }
101
+
102
+ return [
103
+ 'aiAct',
104
+ compactText(event.phase),
105
+ compactText(event.planIndex),
106
+ suffix,
107
+ ]
108
+ .filter(Boolean)
109
+ .join(':');
110
+ }
111
+
112
+ // --- Structured action -> human-readable text (presentation lives here) ---
113
+
114
+ interface ParsedAiActAction {
115
+ name: string;
116
+ target?: string;
117
+ point?: [number, number];
118
+ bbox?: [number, number, number, number];
119
+ param?: unknown;
120
+ }
121
+
122
+ function integerText(value: number): string {
123
+ return String(Math.round(value));
124
+ }
125
+
126
+ function numberTuple2(value: unknown): [number, number] | undefined {
127
+ if (
128
+ Array.isArray(value) &&
129
+ value.length >= 2 &&
130
+ typeof value[0] === 'number' &&
131
+ typeof value[1] === 'number'
132
+ ) {
133
+ return [value[0], value[1]];
134
+ }
135
+ return undefined;
136
+ }
137
+
138
+ function numberTuple4(
139
+ value: unknown,
140
+ ): [number, number, number, number] | undefined {
141
+ if (
142
+ Array.isArray(value) &&
143
+ value.length >= 4 &&
144
+ value.slice(0, 4).every((item) => typeof item === 'number')
145
+ ) {
146
+ return [value[0], value[1], value[2], value[3]];
147
+ }
148
+ return undefined;
149
+ }
150
+
151
+ function parseAction(value: unknown): ParsedAiActAction | undefined {
152
+ if (!isRecord(value) || typeof value.name !== 'string') {
153
+ return undefined;
154
+ }
155
+ return {
156
+ name: value.name,
157
+ target: typeof value.target === 'string' ? value.target : undefined,
158
+ point: numberTuple2(value.point),
159
+ bbox: numberTuple4(value.bbox),
160
+ param: value.param,
161
+ };
162
+ }
163
+
164
+ function formatPoint(point: [number, number]): string {
165
+ return `(${integerText(point[0])}, ${integerText(point[1])})`;
166
+ }
167
+
168
+ function formatBbox(bbox: [number, number, number, number]): string {
169
+ return `(${bbox.map(integerText).join(',')})`;
170
+ }
171
+
172
+ function sleepActionText(action: ParsedAiActAction): string | undefined {
173
+ if (action.name !== 'Sleep' || !isRecord(action.param)) {
174
+ return undefined;
175
+ }
176
+ const timeMs =
177
+ numericValue(action.param.timeMs) ??
178
+ numericValue(action.param.duration) ??
179
+ numericValue(action.param.timeoutMs);
180
+ return timeMs !== undefined ? `Sleep ${integerText(timeMs)}ms` : undefined;
181
+ }
182
+
183
+ function plannedActionText(action: ParsedAiActAction): string {
184
+ if (action.point) {
185
+ const targetSegment = action.target ? ` "${action.target}"` : '';
186
+ const bboxSegment = action.bbox ? `, bbox=${formatBbox(action.bbox)}` : '';
187
+ return `${action.name}${targetSegment} at ${formatPoint(action.point)}${bboxSegment}`;
188
+ }
189
+
190
+ const sleep = sleepActionText(action);
191
+ if (sleep) {
192
+ return sleep;
193
+ }
194
+
195
+ const paramText = compactText(action.param);
196
+ return paramText ? `${action.name}: ${paramText}` : action.name;
197
+ }
198
+
199
+ function runningActionText(action: ParsedAiActAction): string {
200
+ if (action.point) {
201
+ return `${action.name} at ${formatPoint(action.point)}`;
202
+ }
203
+ return plannedActionText(action);
204
+ }
205
+
206
+ export function normalizeAiActProgressEventForCli(
207
+ event: unknown,
208
+ screenshotOptions: CliVerboseScreenshotCollectOptions = {},
209
+ ): CliAiActProgressPayload | undefined {
210
+ if (!isRecord(event) || event.scope !== cliAiActProgressScope) {
211
+ return undefined;
212
+ }
213
+
214
+ const data = isRecord(event.data) ? event.data : {};
215
+
216
+ const screenshots = collectScreenshotRefs(data.screenshot, screenshotOptions);
217
+ const latestPath = screenshots
218
+ .slice()
219
+ .reverse()
220
+ .find(
221
+ (item) => typeof item.path === 'string' && item.path.length > 0,
222
+ )?.path;
223
+ const screenshotPath =
224
+ typeof latestPath === 'string'
225
+ ? pathForReportScreenshot(latestPath, screenshotOptions.reportFile)
226
+ : undefined;
227
+
228
+ const {
229
+ prompt,
230
+ planIndex,
231
+ planLimit,
232
+ action,
233
+ thought,
234
+ log,
235
+ output,
236
+ durationMs,
237
+ error,
238
+ } = data;
239
+
240
+ return {
241
+ phase: event.phase,
242
+ sequence: event.sequence,
243
+ prompt,
244
+ planIndex,
245
+ planLimit,
246
+ action,
247
+ thought,
248
+ log,
249
+ output,
250
+ durationMs,
251
+ error,
252
+ ...(screenshots.length > 0 ? { screenshots } : {}),
253
+ ...(screenshotPath ? { screenshotPath } : {}),
254
+ };
255
+ }
256
+
257
+ export function buildAiActProgressEventLines(
258
+ event: CliAiActProgressPayload,
259
+ ): CliVerboseLine[] {
260
+ const phase = typeof event.phase === 'string' ? event.phase : '';
261
+ const prefix = planPrefix(event);
262
+ const error = compactText(event.error);
263
+ const duration =
264
+ typeof event.durationMs === 'number'
265
+ ? ` cost=${Math.round(event.durationMs)}ms`
266
+ : '';
267
+ const action = parseAction(event.action);
268
+
269
+ switch (phase) {
270
+ case 'start': {
271
+ const prompt = compactText(event.prompt);
272
+ return [
273
+ {
274
+ key: `aiAct:start:${prompt}`,
275
+ text: prompt
276
+ ? `[Midscene][aiAct] Start: ${prompt}`
277
+ : '[Midscene][aiAct] Start',
278
+ },
279
+ ];
280
+ }
281
+ case 'plan_thinking':
282
+ return event.screenshotPath
283
+ ? [
284
+ {
285
+ key: eventKey(event, 'thinking'),
286
+ text: `${prefix} Thinking with the latest screenshot: ${event.screenshotPath}`,
287
+ },
288
+ ]
289
+ : [
290
+ {
291
+ key: eventKey(event, 'thinking'),
292
+ text: `${prefix} Thinking with the latest screenshot`,
293
+ },
294
+ ];
295
+ case 'plan_planned': {
296
+ const text = firstText([event.log, event.thought, event.output]);
297
+ return text
298
+ ? [
299
+ {
300
+ key: eventKey(event, 'planned'),
301
+ text: `${prefix} Planned: ${text}`,
302
+ },
303
+ ]
304
+ : [];
305
+ }
306
+ case 'plan_action':
307
+ return action
308
+ ? [
309
+ {
310
+ key: eventKey(event, 'action'),
311
+ text: `${prefix} Action: ${plannedActionText(action)}`,
312
+ },
313
+ ]
314
+ : [];
315
+ case 'plan_failed':
316
+ return [
317
+ {
318
+ key: eventKey(event, 'plan_failed'),
319
+ text: `${prefix} Failed${error ? `: ${error}` : ''}`,
320
+ },
321
+ ];
322
+ case 'action_running':
323
+ return action
324
+ ? [
325
+ {
326
+ key: eventKey(event, 'action_running'),
327
+ text: `[Midscene][aiAct][Action] Running: ${runningActionText(action)}`,
328
+ },
329
+ ]
330
+ : [];
331
+ case 'action_done':
332
+ return action
333
+ ? [
334
+ {
335
+ key: eventKey(event, 'action_done'),
336
+ text: `[Midscene][aiAct][Action] Done: ${action.name}${duration}`,
337
+ },
338
+ ]
339
+ : [];
340
+ case 'action_failed':
341
+ return [
342
+ {
343
+ key: eventKey(event, 'action_failed'),
344
+ text: `[Midscene][aiAct][Action] Failed${
345
+ action ? `: ${action.name}` : ''
346
+ }${duration}${error ? ` error=${error}` : ''}`,
347
+ },
348
+ ];
349
+ case 'complete': {
350
+ const text = firstText([event.output, event.log, event.thought]);
351
+ return [
352
+ {
353
+ key: eventKey(event, 'complete'),
354
+ text: `[Midscene][aiAct] Complete${text ? `: ${text}` : ''}`,
355
+ },
356
+ ];
357
+ }
358
+ case 'failed':
359
+ return [
360
+ {
361
+ key: eventKey(event, 'failed'),
362
+ text: `[Midscene][aiAct] Failed${error ? `: ${error}` : ''}`,
363
+ },
364
+ ];
365
+ default:
366
+ return [];
367
+ }
368
+ }
369
+
370
+ /**
371
+ * A producer-specific renderer for the generic CLI progress dispatcher: turn a
372
+ * raw progress envelope into a normalized payload, then that payload into
373
+ * verbose lines. New producers register their own renderer; the dispatcher core
374
+ * stays untouched.
375
+ */
376
+ export interface CliProgressRenderer {
377
+ normalize(
378
+ event: unknown,
379
+ screenshotOptions: CliVerboseScreenshotCollectOptions,
380
+ ): Record<string, unknown> | undefined;
381
+ buildLines(payload: Record<string, unknown>): CliVerboseLine[];
382
+ }
383
+
384
+ export const aiActCliProgressRenderer: CliProgressRenderer = {
385
+ normalize: normalizeAiActProgressEventForCli,
386
+ buildLines: buildAiActProgressEventLines,
387
+ };
@@ -0,0 +1,269 @@
1
+ import { basename, dirname, isAbsolute, join, relative } from 'node:path';
2
+ import { writeCliScreenshotFile } from './screenshot-file';
3
+
4
+ export type CliVerboseScreenshotExportMode = 'none' | 'tmp' | 'report';
5
+
6
+ export interface CliVerboseScreenshotCollectOptions {
7
+ reportFile?: unknown;
8
+ exportMode?: CliVerboseScreenshotExportMode;
9
+ cache?: Map<string, string>;
10
+ }
11
+
12
+ interface CliVerboseScreenshotRefLike {
13
+ type: 'midscene_screenshot_ref';
14
+ id?: unknown;
15
+ mimeType?: unknown;
16
+ storage?: unknown;
17
+ path?: string;
18
+ }
19
+
20
+ function isRecord(value: unknown): value is Record<string, unknown> {
21
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
22
+ }
23
+
24
+ function isCliVerboseScreenshotRefLike(
25
+ value: unknown,
26
+ ): value is CliVerboseScreenshotRefLike {
27
+ return isRecord(value) && value.type === 'midscene_screenshot_ref';
28
+ }
29
+
30
+ function toSerializableScreenshot(
31
+ value: unknown,
32
+ ): CliVerboseScreenshotRefLike | null {
33
+ if (!value || typeof value !== 'object') {
34
+ return null;
35
+ }
36
+
37
+ const maybeSerializable = value as {
38
+ toSerializable?: () => unknown;
39
+ };
40
+ if (typeof maybeSerializable.toSerializable === 'function') {
41
+ try {
42
+ const serialized = maybeSerializable.toSerializable();
43
+ return isCliVerboseScreenshotRefLike(serialized) ? serialized : null;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ return isCliVerboseScreenshotRefLike(value) ? value : null;
50
+ }
51
+
52
+ function getStringProperty(value: unknown, key: string): string | undefined {
53
+ if (!isRecord(value)) {
54
+ return undefined;
55
+ }
56
+
57
+ try {
58
+ const property = value[key];
59
+ return typeof property === 'string' && property.length > 0
60
+ ? property
61
+ : undefined;
62
+ } catch {
63
+ return undefined;
64
+ }
65
+ }
66
+
67
+ function screenshotRawBase64(value: unknown): string | undefined {
68
+ const rawBase64 = getStringProperty(value, 'rawBase64');
69
+ if (rawBase64) {
70
+ return rawBase64;
71
+ }
72
+
73
+ const base64 = getStringProperty(value, 'base64');
74
+ const match = base64?.match(/^data:image\/(?:png|jpeg|jpg);base64,(.+)$/);
75
+ return match?.[1];
76
+ }
77
+
78
+ function inlineScreenshotCacheKey(
79
+ rawBase64: string,
80
+ serialized: CliVerboseScreenshotRefLike,
81
+ directoryPath: string | undefined,
82
+ directoryName: string | undefined,
83
+ extension: string | undefined,
84
+ ): string {
85
+ return JSON.stringify([
86
+ serialized.id,
87
+ serialized.mimeType,
88
+ directoryPath,
89
+ directoryName,
90
+ extension,
91
+ rawBase64,
92
+ ]);
93
+ }
94
+
95
+ function exportInlineScreenshotForVerbose(
96
+ value: unknown,
97
+ serialized: CliVerboseScreenshotRefLike,
98
+ options: CliVerboseScreenshotCollectOptions,
99
+ ): string | undefined {
100
+ if (typeof serialized.path === 'string') {
101
+ return serialized.path;
102
+ }
103
+
104
+ const exportMode = options.exportMode ?? 'tmp';
105
+ if (exportMode === 'none') {
106
+ return undefined;
107
+ }
108
+
109
+ const rawBase64 = screenshotRawBase64(value);
110
+ if (!rawBase64) {
111
+ return undefined;
112
+ }
113
+
114
+ const directoryPath =
115
+ exportMode === 'report' &&
116
+ typeof options.reportFile === 'string' &&
117
+ options.reportFile.length > 0
118
+ ? join(dirname(options.reportFile), 'screenshots')
119
+ : undefined;
120
+ const directoryName = directoryPath ? undefined : 'midscene-cli-screenshots';
121
+ const extension = getStringProperty(value, 'extension');
122
+ const cacheKey = inlineScreenshotCacheKey(
123
+ rawBase64,
124
+ serialized,
125
+ directoryPath,
126
+ directoryName,
127
+ extension,
128
+ );
129
+ const cachedPath = options.cache?.get(cacheKey);
130
+ if (cachedPath) {
131
+ return cachedPath;
132
+ }
133
+
134
+ try {
135
+ const path = writeCliScreenshotFile(rawBase64, {
136
+ id: serialized.id,
137
+ mimeType: serialized.mimeType,
138
+ extension,
139
+ ...(directoryPath ? { directoryPath } : { directoryName }),
140
+ overwrite: false,
141
+ });
142
+ options.cache?.set(cacheKey, path);
143
+ return path;
144
+ } catch {
145
+ return undefined;
146
+ }
147
+ }
148
+
149
+ export function collectScreenshotRefs(
150
+ value: unknown,
151
+ options: CliVerboseScreenshotCollectOptions = {},
152
+ ): Array<Record<string, unknown>> {
153
+ const screenshots: Array<Record<string, unknown>> = [];
154
+ const visit = (candidate: unknown, timing?: unknown): void => {
155
+ if (Array.isArray(candidate)) {
156
+ for (const item of candidate) {
157
+ visit(item, timing);
158
+ }
159
+ return;
160
+ }
161
+
162
+ const serialized = toSerializableScreenshot(candidate);
163
+ if (serialized?.type === 'midscene_screenshot_ref') {
164
+ const screenshot: Record<string, unknown> = {
165
+ id: serialized.id,
166
+ storage: serialized.storage,
167
+ };
168
+ const exportedPath = exportInlineScreenshotForVerbose(
169
+ candidate,
170
+ serialized,
171
+ options,
172
+ );
173
+ if (exportedPath) {
174
+ screenshot.path = exportedPath;
175
+ screenshot.file = basename(exportedPath);
176
+ }
177
+ if (typeof timing === 'string') {
178
+ screenshot.timing = timing;
179
+ }
180
+ screenshots.push(screenshot);
181
+ return;
182
+ }
183
+
184
+ if (!isRecord(candidate)) {
185
+ return;
186
+ }
187
+
188
+ const screenshotRecord = candidate.screenshot;
189
+ if (screenshotRecord) {
190
+ visit(screenshotRecord, candidate.timing);
191
+ }
192
+ if (candidate.recorder) {
193
+ visit(candidate.recorder);
194
+ }
195
+ if (isRecord(candidate.uiContext)) {
196
+ visit(candidate.uiContext.screenshot);
197
+ }
198
+ };
199
+
200
+ visit(value);
201
+ return screenshots;
202
+ }
203
+
204
+ export function pathForReportScreenshot(
205
+ path: string,
206
+ reportFile?: unknown,
207
+ ): string {
208
+ const resolvedPath =
209
+ typeof reportFile === 'string' &&
210
+ reportFile.length > 0 &&
211
+ (path.startsWith('./') || path.startsWith('../'))
212
+ ? join(dirname(reportFile), path)
213
+ : path;
214
+
215
+ if (!isAbsolute(resolvedPath)) {
216
+ return resolvedPath;
217
+ }
218
+
219
+ const relativePath = relative(process.cwd(), resolvedPath);
220
+ if (
221
+ relativePath &&
222
+ !relativePath.startsWith('..') &&
223
+ !isAbsolute(relativePath)
224
+ ) {
225
+ return relativePath;
226
+ }
227
+
228
+ return resolvedPath;
229
+ }
230
+
231
+ export function latestScreenshotPathForAiAct(
232
+ value: unknown,
233
+ options: CliVerboseScreenshotCollectOptions = {},
234
+ ): string {
235
+ const screenshot = collectScreenshotRefs(value, {
236
+ ...options,
237
+ exportMode: options.exportMode ?? 'report',
238
+ })
239
+ .slice()
240
+ .reverse()
241
+ .find((item) => typeof item.path === 'string' && item.path.length > 0);
242
+ const path = typeof screenshot?.path === 'string' ? screenshot.path : '';
243
+ return path ? pathForReportScreenshot(path, options.reportFile) : '';
244
+ }
245
+
246
+ export function renderScreenshotList(screenshots: unknown): string {
247
+ if (!Array.isArray(screenshots) || screenshots.length === 0) {
248
+ return '';
249
+ }
250
+
251
+ return screenshots
252
+ .map((item) => {
253
+ if (!isRecord(item)) {
254
+ return '';
255
+ }
256
+ const path =
257
+ typeof item.path === 'string'
258
+ ? item.path
259
+ : typeof item.file === 'string'
260
+ ? item.file
261
+ : typeof item.id === 'string'
262
+ ? item.id
263
+ : '';
264
+ const timing = typeof item.timing === 'string' ? item.timing : '';
265
+ return [timing, path].filter(Boolean).join(' ');
266
+ })
267
+ .filter(Boolean)
268
+ .join(', ');
269
+ }