@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,207 @@
1
+ export type CliInterruptReason = 'sigint' | 'sigterm' | 'sighup' | 'watchdog';
2
+
3
+ type CliInterruptSignal = 'SIGINT' | 'SIGTERM' | 'SIGHUP';
4
+
5
+ export interface CliInterruptSource {
6
+ on(event: CliInterruptSignal, listener: () => void): unknown;
7
+ removeListener(event: CliInterruptSignal, listener: () => void): unknown;
8
+ }
9
+
10
+ export interface CliInterruptInputSource {
11
+ readonly isTTY?: boolean;
12
+ readonly isRaw?: boolean;
13
+ readonly readableFlowing?: boolean | null;
14
+ on(event: 'data', listener: (chunk: unknown) => void): unknown;
15
+ removeListener(event: 'data', listener: (chunk: unknown) => void): unknown;
16
+ setRawMode?(enabled: boolean): unknown;
17
+ pause?(): unknown;
18
+ }
19
+
20
+ export interface CliInterruptWaiter {
21
+ /** Resolves on the first stop signal or watchdog timeout. */
22
+ readonly result: Promise<CliInterruptReason>;
23
+ /** Release signal handlers after asynchronous finalization has completed. */
24
+ dispose(): void;
25
+ }
26
+
27
+ export interface CliInterruptWaiterOptions {
28
+ source?: CliInterruptSource;
29
+ input?: CliInterruptInputSource;
30
+ /** Called after restoring the terminal when Ctrl+C is pressed again. */
31
+ forceExit?: (exitCode: number) => void;
32
+ }
33
+
34
+ const activeInterruptWaiters = new WeakMap<object, number>();
35
+ const noop = () => {};
36
+ const sigintExitCode = 130;
37
+
38
+ function guardTerminalCtrlC(
39
+ input: CliInterruptInputSource | undefined,
40
+ onInterrupt: () => void,
41
+ ): () => void {
42
+ if (!input?.isTTY || !input.setRawMode) return noop;
43
+
44
+ const wasRaw = input.isRaw === true;
45
+ const wasFlowing = input.readableFlowing === true;
46
+ const onData = (chunk: unknown) => {
47
+ const includesCtrlC =
48
+ (typeof chunk === 'string' && chunk.includes('\u0003')) ||
49
+ (chunk instanceof Uint8Array && chunk.includes(3));
50
+ if (includesCtrlC) onInterrupt();
51
+ };
52
+ const dispose = () => {
53
+ input.removeListener('data', onData);
54
+ try {
55
+ if (!wasFlowing) input.pause?.();
56
+ } finally {
57
+ if (!wasRaw) input.setRawMode?.(false);
58
+ }
59
+ };
60
+
61
+ input.setRawMode(true);
62
+ try {
63
+ input.on('data', onData);
64
+ } catch (error) {
65
+ dispose();
66
+ throw error;
67
+ }
68
+ return dispose;
69
+ }
70
+
71
+ function registerCliInterruptWaiter(source: CliInterruptSource): () => void {
72
+ const key = source as object;
73
+ activeInterruptWaiters.set(key, (activeInterruptWaiters.get(key) ?? 0) + 1);
74
+
75
+ let active = true;
76
+ return () => {
77
+ if (!active) return;
78
+ active = false;
79
+
80
+ const remaining = (activeInterruptWaiters.get(key) ?? 1) - 1;
81
+ if (remaining > 0) {
82
+ activeInterruptWaiters.set(key, remaining);
83
+ } else {
84
+ activeInterruptWaiters.delete(key);
85
+ }
86
+ };
87
+ }
88
+
89
+ /** Whether a foreground CLI command is currently waiting for this source. */
90
+ export function hasActiveCliInterruptWaiter(
91
+ source: CliInterruptSource = process,
92
+ ): boolean {
93
+ return (activeInterruptWaiters.get(source as object) ?? 0) > 0;
94
+ }
95
+
96
+ /**
97
+ * Keep graceful-stop handlers installed until the caller has finished saving.
98
+ *
99
+ * Package runners such as pnpm can deliver SIGINT to the foreground child,
100
+ * immediately follow it with SIGTERM, then cause SIGHUP when the runner exits
101
+ * and its pseudo-terminal closes. Resolving on the first signal is not enough:
102
+ * removing any handler at that point lets a subsequent signal kill the child
103
+ * during asynchronous artifact finalization.
104
+ *
105
+ * On a TTY, Ctrl+C is captured as raw input so the package runner itself stays
106
+ * alive until the child has saved and restored the terminal. Signal handlers
107
+ * remain as the graceful-stop path for externally delivered termination.
108
+ */
109
+ export function createCliInterruptWaiter(
110
+ watchdogMs: number,
111
+ options: CliInterruptWaiterOptions = {},
112
+ ): CliInterruptWaiter {
113
+ const source = options.source ?? process;
114
+ const input =
115
+ options.input ?? (source === process ? process.stdin : undefined);
116
+ const forceExit =
117
+ options.forceExit ??
118
+ (source === process
119
+ ? (exitCode: number) => {
120
+ process.exit(exitCode);
121
+ }
122
+ : undefined);
123
+ const unregisterWaiter = registerCliInterruptWaiter(source);
124
+ let timer: ReturnType<typeof setTimeout> | undefined;
125
+ let finished = false;
126
+ let disposed = false;
127
+ let disposeInput = noop;
128
+ let resolveResult!: (reason: CliInterruptReason) => void;
129
+ let rejectResult!: (error: unknown) => void;
130
+
131
+ const result = new Promise<CliInterruptReason>((resolve, reject) => {
132
+ resolveResult = resolve;
133
+ rejectResult = reject;
134
+ });
135
+
136
+ const finish = (reason: CliInterruptReason) => {
137
+ if (finished) return;
138
+ finished = true;
139
+ if (timer) {
140
+ clearTimeout(timer);
141
+ timer = undefined;
142
+ }
143
+ resolveResult(reason);
144
+ };
145
+ const onSigint = () => finish('sigint');
146
+ const onSigterm = () => finish('sigterm');
147
+ const onSighup = () => finish('sighup');
148
+
149
+ function dispose() {
150
+ if (disposed) return;
151
+ disposed = true;
152
+ source.removeListener('SIGINT', onSigint);
153
+ source.removeListener('SIGTERM', onSigterm);
154
+ source.removeListener('SIGHUP', onSighup);
155
+ try {
156
+ disposeInput();
157
+ } finally {
158
+ if (timer) {
159
+ clearTimeout(timer);
160
+ timer = undefined;
161
+ }
162
+ unregisterWaiter();
163
+ }
164
+ }
165
+
166
+ const onTerminalCtrlC = () => {
167
+ if (!finished) {
168
+ finish('sigint');
169
+ return;
170
+ }
171
+
172
+ // The first Ctrl+C protects asynchronous artifact finalization. A second
173
+ // explicit Ctrl+C is the user's escape hatch if device or file I/O hangs.
174
+ dispose();
175
+ forceExit?.(sigintExitCode);
176
+ };
177
+
178
+ try {
179
+ source.on('SIGINT', onSigint);
180
+ source.on('SIGTERM', onSigterm);
181
+ source.on('SIGHUP', onSighup);
182
+ disposeInput = guardTerminalCtrlC(input, onTerminalCtrlC);
183
+ if (watchdogMs > 0) {
184
+ timer = setTimeout(() => finish('watchdog'), watchdogMs);
185
+ }
186
+ } catch (error) {
187
+ dispose();
188
+ rejectResult(error);
189
+ }
190
+
191
+ return { result, dispose };
192
+ }
193
+
194
+ /**
195
+ * Wait for one stop request and release the handlers immediately afterwards.
196
+ * Long-running finalizers should use {@link createCliInterruptWaiter} instead.
197
+ */
198
+ export function waitForCliInterrupt(
199
+ watchdogMs: number,
200
+ source: CliInterruptSource = process,
201
+ input: CliInterruptInputSource | undefined = source === process
202
+ ? process.stdin
203
+ : undefined,
204
+ ): Promise<CliInterruptReason> {
205
+ const waiter = createCliInterruptWaiter(watchdogMs, { source, input });
206
+ return waiter.result.finally(waiter.dispose);
207
+ }
@@ -0,0 +1,177 @@
1
+ import { z } from 'zod';
2
+ import { getErrorMessage } from '../agent-tools/error-formatter';
3
+ import { resolveObservationArtifactAdapter } from '../agent-tools/observation-artifact';
4
+ import { writeUIObservationRecord } from '../agent-tools/observation-record';
5
+ import type {
6
+ BaseAgent,
7
+ ToolCliMetadata,
8
+ ToolDefinition,
9
+ ToolResult,
10
+ ToolSchema,
11
+ } from '../agent-tools/types';
12
+ import { type CliInterruptWaiter, createCliInterruptWaiter } from './interrupt';
13
+ import { attachCliVerboseDumpListener, emitCliVerboseEvent } from './verbose';
14
+
15
+ const recordCliMetadata: ToolCliMetadata = {
16
+ positionals: ['action'],
17
+ options: {
18
+ intervalMs: {
19
+ preferredName: 'interval-ms',
20
+ aliases: ['intervalMs'],
21
+ },
22
+ maxFrames: {
23
+ preferredName: 'max-frames',
24
+ aliases: ['maxFrames'],
25
+ },
26
+ watchdogMs: {
27
+ preferredName: 'watchdog-ms',
28
+ aliases: ['watchdogMs'],
29
+ },
30
+ action: {
31
+ hidden: true,
32
+ },
33
+ },
34
+ };
35
+
36
+ function mergeCliMetadata(
37
+ base: ToolCliMetadata,
38
+ extra?: ToolCliMetadata,
39
+ ): ToolCliMetadata {
40
+ return {
41
+ positionals: base.positionals,
42
+ options: {
43
+ ...(base.options ?? {}),
44
+ ...(extra?.options ?? {}),
45
+ },
46
+ };
47
+ }
48
+
49
+ function createErrorResult(message: string): ToolResult {
50
+ return {
51
+ content: [{ type: 'text', text: message }],
52
+ isError: true,
53
+ };
54
+ }
55
+
56
+ /** Create the foreground recording command exposed only by Midscene CLIs. */
57
+ export function createRecordCliCommand(
58
+ getAgent: (args?: Record<string, unknown>) => Promise<BaseAgent>,
59
+ initArgSchema: ToolSchema = {},
60
+ initArgCliMetadata?: ToolCliMetadata,
61
+ ): ToolDefinition {
62
+ return {
63
+ name: 'record',
64
+ description:
65
+ 'Record the page/screen in the foreground until Ctrl+C or a termination signal, then save the ordered frame window for a later assert command.',
66
+ schema: {
67
+ action: z
68
+ .literal('start')
69
+ .describe('Start a foreground recording. Press Ctrl+C to finish.'),
70
+ output: z
71
+ .string()
72
+ .optional()
73
+ .describe(
74
+ 'Path for the JSON observation manifest. Frames are stored in an adjacent <name>.frames directory. Defaults to a generated path under midscene_run/output.',
75
+ ),
76
+ intervalMs: z
77
+ .number()
78
+ .min(200)
79
+ .optional()
80
+ .describe(
81
+ 'Sampling interval in milliseconds. Defaults to 1000; minimum 200 (5fps).',
82
+ ),
83
+ maxFrames: z
84
+ .number()
85
+ .int()
86
+ .min(2)
87
+ .optional()
88
+ .describe('Maximum buffered frames. Defaults to 30; minimum 2.'),
89
+ watchdogMs: z
90
+ .number()
91
+ .min(0)
92
+ .optional()
93
+ .describe(
94
+ 'Safety timeout in milliseconds. Defaults to 300000 (5 minutes); 0 disables it.',
95
+ ),
96
+ ...initArgSchema,
97
+ },
98
+ cli: mergeCliMetadata(recordCliMetadata, initArgCliMetadata),
99
+ handler: async (
100
+ args: Record<string, unknown> = {},
101
+ ): Promise<ToolResult> => {
102
+ if (args.action !== 'start') {
103
+ return createErrorResult(
104
+ 'record requires the start operation (for example: record start --output ./observation.json)',
105
+ );
106
+ }
107
+
108
+ try {
109
+ const agent = await getAgent(args);
110
+ emitCliVerboseEvent({ event: 'agent_ready', tool: 'record' });
111
+ if (!agent.startObserving) {
112
+ throw new Error(
113
+ 'record is not supported because this agent does not provide startObserving',
114
+ );
115
+ }
116
+ const observationArtifacts = resolveObservationArtifactAdapter(agent);
117
+ if (!observationArtifacts) {
118
+ throw new Error(
119
+ 'record is not supported because this agent does not provide observation artifact persistence',
120
+ );
121
+ }
122
+ const unsubscribeVerbose = attachCliVerboseDumpListener(agent, {
123
+ toolName: 'record',
124
+ });
125
+ let observer:
126
+ | Awaited<ReturnType<NonNullable<BaseAgent['startObserving']>>>
127
+ | undefined;
128
+ let interruptWaiter: CliInterruptWaiter | undefined;
129
+ try {
130
+ const watchdogMs = (args.watchdogMs as number | undefined) ?? 300_000;
131
+ observer = await agent.startObserving({
132
+ intervalMs: args.intervalMs as number | undefined,
133
+ maxFrames: args.maxFrames as number | undefined,
134
+ watchdogMs,
135
+ });
136
+ interruptWaiter = createCliInterruptWaiter(watchdogMs);
137
+ emitCliVerboseEvent({
138
+ event: 'recording_ready',
139
+ tool: 'record',
140
+ watchdogMs,
141
+ });
142
+ const stopReason = await interruptWaiter.result;
143
+ emitCliVerboseEvent({
144
+ event: 'recording_stopping',
145
+ tool: 'record',
146
+ reason: stopReason,
147
+ });
148
+ const observation = await observer.stop();
149
+ const record = await observationArtifacts.exportRecord(observation);
150
+ const outputPath = writeUIObservationRecord(
151
+ record,
152
+ args.output as string | undefined,
153
+ );
154
+ return {
155
+ content: [
156
+ {
157
+ type: 'text',
158
+ text: `Observation record saved: ${outputPath}`,
159
+ },
160
+ ],
161
+ };
162
+ } finally {
163
+ try {
164
+ await observer?.dispose?.();
165
+ } finally {
166
+ interruptWaiter?.dispose();
167
+ unsubscribeVerbose();
168
+ }
169
+ }
170
+ } catch (error: unknown) {
171
+ const errorMessage = getErrorMessage(error);
172
+ console.error('Error executing record:', errorMessage);
173
+ return createErrorResult(`Failed to execute record: ${errorMessage}`);
174
+ }
175
+ },
176
+ };
177
+ }
@@ -0,0 +1,61 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ export interface WriteCliScreenshotFileOptions {
6
+ id?: unknown;
7
+ mimeType?: unknown;
8
+ extension?: unknown;
9
+ directoryPath?: string;
10
+ directoryName?: string;
11
+ filenamePrefix?: string;
12
+ overwrite?: boolean;
13
+ }
14
+
15
+ function safeScreenshotFilenamePart(value: unknown): string {
16
+ const text = typeof value === 'string' && value.length > 0 ? value : 'shot';
17
+ return text.replace(/[^a-zA-Z0-9._-]/g, '_') || 'shot';
18
+ }
19
+
20
+ function extensionFromImageMetadata(
21
+ mimeType: unknown,
22
+ extension: unknown,
23
+ ): 'png' | 'jpeg' {
24
+ if (extension === 'jpeg' || extension === 'jpg') {
25
+ return 'jpeg';
26
+ }
27
+ if (extension === 'png') {
28
+ return 'png';
29
+ }
30
+ return mimeType === 'image/jpeg' ? 'jpeg' : 'png';
31
+ }
32
+
33
+ export function writeCliScreenshotFile(
34
+ rawBase64: string,
35
+ options: WriteCliScreenshotFileOptions = {},
36
+ ): string {
37
+ const extension = extensionFromImageMetadata(
38
+ options.mimeType,
39
+ options.extension,
40
+ );
41
+ const directory = options.directoryPath
42
+ ? options.directoryPath
43
+ : options.directoryName
44
+ ? join(tmpdir(), options.directoryName)
45
+ : tmpdir();
46
+ if (!existsSync(directory)) {
47
+ mkdirSync(directory, { recursive: true });
48
+ }
49
+
50
+ const filename =
51
+ options.id !== undefined
52
+ ? `${safeScreenshotFilenamePart(options.id)}.${extension}`
53
+ : `${options.filenamePrefix ?? 'screenshot'}-${Date.now()}.${extension}`;
54
+ const filePath = join(directory, filename);
55
+
56
+ if (options.overwrite !== false || !existsSync(filePath)) {
57
+ writeFileSync(filePath, Buffer.from(rawBase64, 'base64'));
58
+ }
59
+
60
+ return filePath;
61
+ }