@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
@@ -4,11 +4,12 @@ import { readFileSync } from 'node:fs';
4
4
  import { writeFile } from 'node:fs/promises';
5
5
  import path from 'node:path';
6
6
  import type { PhotonImage as PhotonImageType } from '@silvia-odwyer/photon-node';
7
- import type { Rect } from 'src/types';
7
+ import type { Rect, Size } from '../types';
8
8
  import { getDebug } from '../logger';
9
9
  import { ifInNode } from '../utils';
10
10
  import getPhoton from './get-photon';
11
11
  import getSharp from './get-sharp';
12
+ import { encodedImageInfoOfBuffer } from './info';
12
13
 
13
14
  const imgDebug = getDebug('img');
14
15
 
@@ -154,10 +155,268 @@ export async function resizeAndConvertImgBuffer(
154
155
  };
155
156
  }
156
157
 
158
+ const rawBase64BodyPattern = /^[A-Za-z0-9+/]+={0,2}$/;
159
+ const base64ImageDataUrlPattern =
160
+ /^data:image\/(?:png|jpe?g);base64,[A-Za-z0-9+/=\s]+$/i;
161
+ const supportedScreenshotDataUriPattern =
162
+ /^data:image\/(png|jpe?g);base64,([A-Za-z0-9+/=\s]+)$/i;
163
+
164
+ function normalizeBase64Body(body: string): string {
165
+ return body.replace(/\s/g, '');
166
+ }
167
+
168
+ function detectImageMimeTypeFromBuffer(buffer: Buffer): string | undefined {
169
+ if (
170
+ buffer.length >= 8 &&
171
+ buffer[0] === 0x89 &&
172
+ buffer[1] === 0x50 &&
173
+ buffer[2] === 0x4e &&
174
+ buffer[3] === 0x47 &&
175
+ buffer[4] === 0x0d &&
176
+ buffer[5] === 0x0a &&
177
+ buffer[6] === 0x1a &&
178
+ buffer[7] === 0x0a
179
+ ) {
180
+ return 'image/png';
181
+ }
182
+ if (
183
+ buffer.length >= 3 &&
184
+ buffer[0] === 0xff &&
185
+ buffer[1] === 0xd8 &&
186
+ buffer[2] === 0xff
187
+ ) {
188
+ return 'image/jpeg';
189
+ }
190
+ return undefined;
191
+ }
192
+
193
+ export function inferBase64ImageFormat(base64Body: string): string {
194
+ const buffer = Buffer.from(normalizeBase64Body(base64Body), 'base64');
195
+ const mimeType = detectImageMimeTypeFromBuffer(buffer);
196
+ if (mimeType === 'image/jpeg') return 'jpeg';
197
+ if (mimeType === 'image/png') return 'png';
198
+ return 'png';
199
+ }
200
+
157
201
  export const createImgBase64ByFormat = (format: string, body: string) => {
158
- return `data:image/${format};base64,${body}`;
202
+ return `data:image/${format};base64,${normalizeBase64Body(body)}`;
159
203
  };
160
204
 
205
+ export interface NormalizeScreenshotBase64Options {
206
+ label?: string;
207
+ }
208
+
209
+ export const normalizeScreenshotBase64 = (
210
+ base64: string,
211
+ options?: NormalizeScreenshotBase64Options,
212
+ ) => {
213
+ const label = options?.label ?? 'screenshot base64';
214
+ const trimmedBase64 = base64.trim();
215
+ if (!trimmedBase64) {
216
+ throw new Error(`${label} cannot be empty`);
217
+ }
218
+
219
+ const dataUriMatch = trimmedBase64.match(supportedScreenshotDataUriPattern);
220
+ if (dataUriMatch) {
221
+ const imageFormat =
222
+ dataUriMatch[1].toLowerCase() === 'jpg'
223
+ ? 'jpeg'
224
+ : dataUriMatch[1].toLowerCase();
225
+ const body = normalizeBase64Body(dataUriMatch[2]);
226
+ if (!body) {
227
+ throw new Error(`${label} cannot be empty`);
228
+ }
229
+ return createImgBase64ByFormat(imageFormat, body);
230
+ }
231
+
232
+ if (trimmedBase64.startsWith('data:')) {
233
+ throw new Error(
234
+ `${label} must be a PNG/JPEG data URI or raw PNG base64 string`,
235
+ );
236
+ }
237
+
238
+ if (!rawBase64BodyPattern.test(normalizeBase64Body(trimmedBase64))) {
239
+ throw new Error(
240
+ `${label} must be a PNG/JPEG data URI or raw PNG base64 string`,
241
+ );
242
+ }
243
+
244
+ return createImgBase64ByFormat('png', trimmedBase64);
245
+ };
246
+
247
+ export const normalizeBase64Image = (base64: string) => {
248
+ const trimmedBase64 = base64.trim();
249
+ if (base64ImageDataUrlPattern.test(trimmedBase64)) {
250
+ return trimmedBase64.replace(/\s/g, '');
251
+ }
252
+
253
+ const base64Body = normalizeBase64Body(trimmedBase64);
254
+ assert(base64Body, 'base64 image must include image data');
255
+ return createImgBase64ByFormat(
256
+ inferBase64ImageFormat(base64Body),
257
+ base64Body,
258
+ );
259
+ };
260
+
261
+ export type JpegBase64DataUrl = `data:image/jpeg;base64,${string}`;
262
+
263
+ export interface ResizeBase64ImageToJpegOptions {
264
+ sourceSize?: Size;
265
+ targetSize: Size;
266
+ jpegQuality?: number;
267
+ }
268
+
269
+ export interface ConstrainBase64ImageToMaxSizeOptions {
270
+ maxSize: number;
271
+ jpegQuality?: number;
272
+ }
273
+
274
+ function assertValidJpegQuality(jpegQuality: number): void {
275
+ if (!Number.isInteger(jpegQuality) || jpegQuality < 1 || jpegQuality > 100) {
276
+ throw new Error(
277
+ `jpegQuality must be an integer between 1 and 100. Received: ${jpegQuality}`,
278
+ );
279
+ }
280
+ }
281
+
282
+ function assertValidImageSize(size: Size, label: string): void {
283
+ if (
284
+ !Number.isInteger(size.width) ||
285
+ !Number.isInteger(size.height) ||
286
+ size.width <= 0 ||
287
+ size.height <= 0
288
+ ) {
289
+ throw new Error(
290
+ `${label} width and height must be positive integers. Received width: ${size.width}, height: ${size.height}`,
291
+ );
292
+ }
293
+ }
294
+
295
+ export async function convertImgBufferToJpeg(
296
+ inputData: Buffer,
297
+ jpegQuality = 90,
298
+ ): Promise<Buffer> {
299
+ assertValidJpegQuality(jpegQuality);
300
+
301
+ if (ifInNode) {
302
+ try {
303
+ const Sharp = await getSharp();
304
+ return await Sharp(inputData).jpeg({ quality: jpegQuality }).toBuffer();
305
+ } catch (error) {
306
+ imgDebug('Sharp failed, falling back to Photon:', error);
307
+ }
308
+ }
309
+
310
+ const { PhotonImage } = await getPhoton();
311
+ const inputBytes = new Uint8Array(inputData);
312
+ const bytesliceResult = PhotonImage.new_from_byteslice(inputBytes);
313
+ const image =
314
+ bytesliceResult instanceof Promise
315
+ ? await bytesliceResult
316
+ : bytesliceResult;
317
+ try {
318
+ return Buffer.from(image.get_bytes_jpeg(jpegQuality));
319
+ } finally {
320
+ image.free();
321
+ }
322
+ }
323
+
324
+ export async function convertBase64ImageToJpeg(
325
+ inputBase64: string,
326
+ jpegQuality = 90,
327
+ ): Promise<JpegBase64DataUrl> {
328
+ assertValidJpegQuality(jpegQuality);
329
+ const normalized = normalizeBase64Image(inputBase64);
330
+ const { body } = parseBase64(normalized);
331
+ const imageBuffer = Buffer.from(body, 'base64');
332
+ const detectedMimeType = detectImageMimeTypeFromBuffer(imageBuffer);
333
+ if (detectedMimeType === 'image/jpeg') {
334
+ return createImgBase64ByFormat('jpeg', body) as JpegBase64DataUrl;
335
+ }
336
+ if (detectedMimeType !== 'image/png') {
337
+ throw new Error(
338
+ `inputBase64 must contain a PNG or JPEG image. Detected: ${detectedMimeType ?? 'unsupported format'}`,
339
+ );
340
+ }
341
+
342
+ const jpegBuffer = await convertImgBufferToJpeg(imageBuffer, jpegQuality);
343
+ return createImgBase64ByFormat(
344
+ 'jpeg',
345
+ jpegBuffer.toString('base64'),
346
+ ) as JpegBase64DataUrl;
347
+ }
348
+
349
+ export async function resizeBase64ImageToJpeg(
350
+ inputBase64: string,
351
+ options: ResizeBase64ImageToJpegOptions,
352
+ ): Promise<JpegBase64DataUrl> {
353
+ const jpegQuality = options.jpegQuality ?? 90;
354
+ assertValidJpegQuality(jpegQuality);
355
+ assertValidImageSize(options.targetSize, 'targetSize');
356
+
357
+ const normalized = normalizeBase64Image(inputBase64);
358
+ const { body, mimeType } = parseBase64(normalized);
359
+ const imageBuffer = Buffer.from(body, 'base64');
360
+ const sourceSize = encodedImageInfoOfBuffer(imageBuffer);
361
+ if (options.sourceSize) {
362
+ assertValidImageSize(options.sourceSize, 'sourceSize');
363
+ if (
364
+ options.sourceSize.width !== sourceSize.width ||
365
+ options.sourceSize.height !== sourceSize.height
366
+ ) {
367
+ throw new Error(
368
+ `sourceSize ${options.sourceSize.width}x${options.sourceSize.height} does not match encoded image dimensions ${sourceSize.width}x${sourceSize.height}`,
369
+ );
370
+ }
371
+ }
372
+
373
+ const dimensionsUnchanged =
374
+ sourceSize.width === options.targetSize.width &&
375
+ sourceSize.height === options.targetSize.height;
376
+ if (dimensionsUnchanged) {
377
+ return convertBase64ImageToJpeg(normalized, jpegQuality);
378
+ }
379
+
380
+ const { buffer } = await resizeAndConvertImgBuffer(
381
+ mimeType.split('/')[1],
382
+ imageBuffer,
383
+ options.targetSize,
384
+ );
385
+ return createImgBase64ByFormat(
386
+ 'jpeg',
387
+ buffer.toString('base64'),
388
+ ) as JpegBase64DataUrl;
389
+ }
390
+
391
+ export async function constrainBase64ImageToMaxSize(
392
+ inputBase64: string,
393
+ options: ConstrainBase64ImageToMaxSizeOptions,
394
+ ): Promise<string> {
395
+ if (!Number.isInteger(options.maxSize) || options.maxSize <= 0) {
396
+ throw new Error(
397
+ `maxSize must be a positive integer. Received: ${options.maxSize}`,
398
+ );
399
+ }
400
+
401
+ const normalized = normalizeBase64Image(inputBase64);
402
+ const { body } = parseBase64(normalized);
403
+ const sourceSize = encodedImageInfoOfBuffer(Buffer.from(body, 'base64'));
404
+ const largestDimension = Math.max(sourceSize.width, sourceSize.height);
405
+ if (largestDimension <= options.maxSize) {
406
+ return normalized;
407
+ }
408
+
409
+ const scale = options.maxSize / largestDimension;
410
+ return resizeBase64ImageToJpeg(normalized, {
411
+ targetSize: {
412
+ width: Math.max(1, Math.round(sourceSize.width * scale)),
413
+ height: Math.max(1, Math.round(sourceSize.height * scale)),
414
+ },
415
+ sourceSize,
416
+ jpegQuality: options.jpegQuality,
417
+ });
418
+ }
419
+
161
420
  export async function resizeImgBase64(
162
421
  inputBase64: string,
163
422
  newSize: {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Internal-only helpers for CLI/MCP argument key aliasing.
3
+ * Not re-exported from the package entry point — keep consumers within
4
+ * `cli/` and `mcp/`.
5
+ */
6
+
7
+ export function kebabToCamel(str: string): string {
8
+ return str.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
9
+ }
10
+
11
+ export function camelToKebab(str: string): string {
12
+ return str
13
+ .replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)
14
+ .replace(/^-/, '');
15
+ }
16
+
17
+ export function getKeyAliases(key: string): string[] {
18
+ return [...new Set([key, kebabToCamel(key), camelToKebab(key)])];
19
+ }
20
+
21
+ export function isRecord(value: unknown): value is Record<string, unknown> {
22
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
23
+ }
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import type { ParseArgsConfig } from 'node:util';
3
+ import { setIsMcp } from '@aiscene/shared/utils';
3
4
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
6
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
@@ -8,7 +9,7 @@ import express, {
8
9
  type Request,
9
10
  type Response,
10
11
  } from 'express';
11
- import { setIsMcp } from '../utils';
12
+ import { getErrorMessage } from './error-formatter';
12
13
  import type { IMidsceneTools } from './types';
13
14
 
14
15
  export interface BaseMCPServerConfig {
@@ -119,7 +120,7 @@ export abstract class BaseMCPServer {
119
120
  try {
120
121
  await this.toolsManager.initTools();
121
122
  } catch (error: unknown) {
122
- const message = error instanceof Error ? error.message : String(error);
123
+ const message = getErrorMessage(error);
123
124
  console.error(`Failed to initialize tools: ${message}`);
124
125
  console.error('Tools will be initialized on first use');
125
126
  }
@@ -160,7 +161,7 @@ export abstract class BaseMCPServer {
160
161
  try {
161
162
  await this.mcpServer.connect(transport);
162
163
  } catch (error: unknown) {
163
- const message = error instanceof Error ? error.message : String(error);
164
+ const message = getErrorMessage(error);
164
165
  console.error(`Failed to connect MCP stdio transport: ${message}`);
165
166
  throw new Error(`Failed to initialize MCP stdio transport: ${message}`);
166
167
  }
@@ -282,7 +283,7 @@ export abstract class BaseMCPServer {
282
283
  .json({ error: 'Invalid session or GET without session' });
283
284
  }
284
285
  } catch (error: unknown) {
285
- const message = error instanceof Error ? error.message : String(error);
286
+ const message = getErrorMessage(error);
286
287
  const duration = Date.now() - startTime;
287
288
  console.error(
288
289
  `[${new Date().toISOString()}] [${requestId}] MCP request error after ${duration}ms: ${message}`,
@@ -336,8 +337,7 @@ export abstract class BaseMCPServer {
336
337
  try {
337
338
  await session.transport.close();
338
339
  } catch (error: unknown) {
339
- const message =
340
- error instanceof Error ? error.message : String(error);
340
+ const message = getErrorMessage(error);
341
341
  console.error(
342
342
  `Failed to close session ${session.transport.sessionId}: ${message}`,
343
343
  );
@@ -390,7 +390,7 @@ export abstract class BaseMCPServer {
390
390
  try {
391
391
  await this.mcpServer.connect(transport);
392
392
  } catch (error: unknown) {
393
- const message = error instanceof Error ? error.message : String(error);
393
+ const message = getErrorMessage(error);
394
394
  console.error(
395
395
  `[${new Date().toISOString()}] Failed to connect MCP transport: ${message}`,
396
396
  );
@@ -425,8 +425,7 @@ export abstract class BaseMCPServer {
425
425
  `[${new Date().toISOString()}] Session ${sid} cleaned up due to inactivity (remaining: ${sessions.size})`,
426
426
  );
427
427
  } catch (error: unknown) {
428
- const message =
429
- error instanceof Error ? error.message : String(error);
428
+ const message = getErrorMessage(error);
430
429
  console.error(
431
430
  `[${new Date().toISOString()}] Failed to close session ${sid} during cleanup: ${message}`,
432
431
  );
@@ -455,8 +454,7 @@ export abstract class BaseMCPServer {
455
454
  try {
456
455
  session.transport.close();
457
456
  } catch (error: unknown) {
458
- const message =
459
- error instanceof Error ? error.message : String(error);
457
+ const message = getErrorMessage(error);
460
458
  console.error(`Error closing session during shutdown: ${message}`);
461
459
  }
462
460
  }
@@ -475,7 +473,7 @@ export abstract class BaseMCPServer {
475
473
  this.performCleanup().finally(() => process.exit(1));
476
474
  }, 5000);
477
475
  } catch (error: unknown) {
478
- const message = error instanceof Error ? error.message : String(error);
476
+ const message = getErrorMessage(error);
479
477
  console.error(`Error closing HTTP server: ${message}`);
480
478
  this.performCleanup().finally(() => process.exit(1));
481
479
  }
@@ -1,6 +1,19 @@
1
+ import { parseBase64 } from '@aiscene/shared/img';
2
+ import { getDebug } from '@aiscene/shared/logger';
1
3
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { parseBase64 } from '../img';
3
- import { getDebug } from '../logger';
4
+ import type { z } from 'zod';
5
+ import { camelToKebab, getKeyAliases } from '../key-alias-utils';
6
+ import {
7
+ type CliReportSession,
8
+ generateCliReportSession,
9
+ readCliReportSession,
10
+ writeCliReportSession,
11
+ } from './cli-report-session';
12
+ import {
13
+ createNamespacedInitArgSchema,
14
+ extractNamespacedArgs,
15
+ sanitizeNamespacedArgs,
16
+ } from './init-arg-utils';
4
17
  import {
5
18
  generateCommonTools,
6
19
  generateToolsFromActionSpace,
@@ -10,22 +23,68 @@ import type {
10
23
  BaseAgent,
11
24
  BaseDevice,
12
25
  IMidsceneTools,
26
+ ToolCliMetadata,
13
27
  ToolDefinition,
28
+ ToolSchema,
14
29
  } from './types';
15
30
 
16
31
  const debug = getDebug('mcp:base-tools');
17
32
 
18
33
  /**
19
- * Base class for platform-specific MCP tools
20
- * Generic type TAgent allows subclasses to use their specific agent types
34
+ * Declarative description of a platform's agent init args.
35
+ * Collapses the `extractAgentInitParam` / `sanitizeToolArgs` /
36
+ * `getAgentInitArgSchema` trio into a single data declaration.
37
+ */
38
+ export interface InitArgSpec<TInitParam> {
39
+ /** Arg namespace, e.g. `android`, `ios`. */
40
+ namespace: string;
41
+ /** Zod shape describing the init args. Field names drive the MCP schema. */
42
+ shape: Record<string, z.ZodTypeAny>;
43
+ /**
44
+ * Optional CLI presentation hints. These affect `--help` output for
45
+ * single-platform CLIs but do not alter MCP/YAML protocol keys.
46
+ */
47
+ cli?: {
48
+ /** Prefer bare `--device-id`-style options in platform CLI help output. */
49
+ preferBareKeys?: boolean;
50
+ /** Override the displayed option name for specific init arg fields. */
51
+ preferredNames?: Record<string, string>;
52
+ };
53
+ /**
54
+ * Adapt extracted namespaced args into the concrete `TInitParam` passed to
55
+ * `ensureAgent`. Defaults to returning the raw extracted record.
56
+ */
57
+ adapt?: (
58
+ extracted: Record<string, unknown> | undefined,
59
+ ) => TInitParam | undefined;
60
+ }
61
+
62
+ /**
63
+ * Base class for platform-specific MCP tools.
64
+ * @typeParam TAgent - Platform-specific agent type.
65
+ * @typeParam TInitParam - Platform-specific init parameter consumed by
66
+ * `ensureAgent`. Defaults to `undefined` for platforms that take no args.
21
67
  */
22
- export abstract class BaseMidsceneTools<TAgent extends BaseAgent = BaseAgent>
23
- implements IMidsceneTools
68
+ export abstract class BaseMidsceneTools<
69
+ TAgent extends BaseAgent = BaseAgent,
70
+ TInitParam = unknown,
71
+ > implements IMidsceneTools
24
72
  {
25
73
  protected mcpServer?: McpServer;
26
74
  protected agent?: TAgent;
27
75
  protected toolDefinitions: ToolDefinition[] = [];
28
76
 
77
+ /**
78
+ * Declarative init-arg spec. Subclasses that accept CLI/MCP init args should
79
+ * set this once and get `extractAgentInitParam` / `sanitizeToolArgs` /
80
+ * `getAgentInitArgSchema` auto-implemented.
81
+ *
82
+ * Declared with `declare` so that TS doesn't emit an `Object.defineProperty`
83
+ * for this field on the base constructor, which would otherwise overwrite
84
+ * a subclass field initializer under `useDefineForClassFields`.
85
+ */
86
+ protected declare readonly initArgSpec?: InitArgSpec<TInitParam>;
87
+
29
88
  /**
30
89
  * Ensure agent is initialized and ready for use.
31
90
  * Must be implemented by subclasses to create platform-specific agent.
@@ -33,7 +92,102 @@ export abstract class BaseMidsceneTools<TAgent extends BaseAgent = BaseAgent>
33
92
  * @returns Promise resolving to initialized agent instance
34
93
  * @throws Error if agent initialization fails
35
94
  */
36
- protected abstract ensureAgent(initParam?: string): Promise<TAgent>;
95
+ protected abstract ensureAgent(initParam?: TInitParam): Promise<TAgent>;
96
+
97
+ private getInitArgKeys(): readonly string[] {
98
+ return this.initArgSpec ? Object.keys(this.initArgSpec.shape) : [];
99
+ }
100
+
101
+ /**
102
+ * Extract a platform-specific agent init parameter from CLI/MCP tool args.
103
+ */
104
+ protected extractAgentInitParam(
105
+ args: Record<string, unknown>,
106
+ ): TInitParam | undefined {
107
+ if (!this.initArgSpec) {
108
+ return undefined;
109
+ }
110
+ const extracted = extractNamespacedArgs(
111
+ args,
112
+ this.initArgSpec.namespace,
113
+ this.getInitArgKeys(),
114
+ );
115
+ if (this.initArgSpec.adapt) {
116
+ return this.initArgSpec.adapt(extracted);
117
+ }
118
+ return extracted as TInitParam | undefined;
119
+ }
120
+
121
+ /**
122
+ * Remove platform-specific init args before dispatching a tool payload to the action itself.
123
+ */
124
+ protected sanitizeToolArgs(
125
+ args: Record<string, unknown>,
126
+ ): Record<string, unknown> {
127
+ if (!this.initArgSpec) {
128
+ return args;
129
+ }
130
+ return sanitizeNamespacedArgs(
131
+ args,
132
+ this.initArgSpec.namespace,
133
+ this.getInitArgKeys(),
134
+ );
135
+ }
136
+
137
+ /**
138
+ * Expose platform-specific init args on action/common tool schemas.
139
+ */
140
+ protected getAgentInitArgSchema(): ToolSchema {
141
+ if (!this.initArgSpec) {
142
+ return {};
143
+ }
144
+ return createNamespacedInitArgSchema(
145
+ this.initArgSpec.namespace,
146
+ this.initArgSpec.shape,
147
+ );
148
+ }
149
+
150
+ /**
151
+ * Expose CLI-only metadata for platform init args so single-platform help can
152
+ * show ergonomic bare flags while the underlying schema stays namespaced.
153
+ * When `preferBareKeys` is enabled, single-platform CLIs only accept the
154
+ * bare spellings; namespaced dotted spellings remain available through the
155
+ * MCP/YAML schema instead of the platform CLI surface.
156
+ */
157
+ protected getAgentInitArgCliMetadata(): ToolCliMetadata | undefined {
158
+ if (!this.initArgSpec?.cli) {
159
+ return undefined;
160
+ }
161
+
162
+ const options = Object.fromEntries(
163
+ this.getInitArgKeys().map((key) => {
164
+ const canonicalKey = `${this.initArgSpec!.namespace}.${key}`;
165
+ const preferredName =
166
+ this.initArgSpec!.cli?.preferredNames?.[key] ??
167
+ (this.initArgSpec!.cli?.preferBareKeys
168
+ ? camelToKebab(key)
169
+ : canonicalKey);
170
+
171
+ const acceptedNames = new Set<string>([
172
+ preferredName,
173
+ ...(this.initArgSpec!.cli?.preferBareKeys
174
+ ? getKeyAliases(key)
175
+ : getKeyAliases(canonicalKey)),
176
+ ]);
177
+ acceptedNames.delete(preferredName);
178
+
179
+ return [
180
+ canonicalKey,
181
+ {
182
+ preferredName,
183
+ aliases: [...acceptedNames],
184
+ },
185
+ ];
186
+ }),
187
+ );
188
+
189
+ return { options };
190
+ }
37
191
 
38
192
  /**
39
193
  * Optional: prepare platform-specific tools (e.g., device connection)
@@ -42,6 +196,52 @@ export abstract class BaseMidsceneTools<TAgent extends BaseAgent = BaseAgent>
42
196
  return [];
43
197
  }
44
198
 
199
+ protected getCliReportSessionName(): string | undefined {
200
+ return undefined;
201
+ }
202
+
203
+ protected createNewCliReportSession(
204
+ targetIdentity?: string,
205
+ ): CliReportSession | undefined {
206
+ const sessionName = this.getCliReportSessionName();
207
+ if (!sessionName) {
208
+ return undefined;
209
+ }
210
+ return generateCliReportSession(sessionName, targetIdentity);
211
+ }
212
+
213
+ protected commitCliReportSession(session?: CliReportSession): void {
214
+ if (session) {
215
+ writeCliReportSession(session);
216
+ }
217
+ }
218
+
219
+ protected readCliReportFileName(): string | undefined {
220
+ const sessionName = this.getCliReportSessionName();
221
+ if (!sessionName) {
222
+ return undefined;
223
+ }
224
+ return readCliReportSession(sessionName)?.reportFileName;
225
+ }
226
+
227
+ protected readCliReportAgentOptions():
228
+ | {
229
+ reportFileName: string;
230
+ reportAttributes: Record<string, string>;
231
+ }
232
+ | undefined {
233
+ const reportFileName = this.readCliReportFileName();
234
+ if (!reportFileName) {
235
+ return undefined;
236
+ }
237
+ return {
238
+ reportFileName,
239
+ reportAttributes: {
240
+ 'data-group-id': reportFileName,
241
+ },
242
+ };
243
+ }
244
+
45
245
  /**
46
246
  * Must be implemented by subclasses to create a temporary device instance
47
247
  * This allows getting real actionSpace without connecting to device
@@ -83,13 +283,20 @@ export abstract class BaseMidsceneTools<TAgent extends BaseAgent = BaseAgent>
83
283
  }
84
284
 
85
285
  // 3. Generate tools from action space (core innovation)
86
- const actionTools = generateToolsFromActionSpace(actionSpace, () =>
87
- this.ensureAgent(),
286
+ const actionTools = generateToolsFromActionSpace(
287
+ actionSpace,
288
+ (args = {}) => this.ensureAgent(this.extractAgentInitParam(args)),
289
+ (args = {}) => this.sanitizeToolArgs(args),
290
+ this.getAgentInitArgSchema(),
291
+ this.getAgentInitArgCliMetadata(),
88
292
  );
89
293
 
90
294
  // 4. Add common tools (screenshot, waitFor)
91
- const commonTools = generateCommonTools(() => this.ensureAgent());
92
-
295
+ const commonTools = generateCommonTools(
296
+ (args = {}) => this.ensureAgent(this.extractAgentInitParam(args)),
297
+ this.getAgentInitArgSchema(),
298
+ this.getAgentInitArgCliMetadata(),
299
+ );
93
300
  this.toolDefinitions.push(...actionTools, ...commonTools);
94
301
 
95
302
  debug('Total tools prepared:', this.toolDefinitions.length);