@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,250 @@
1
+ import type { z } from 'zod';
2
+ import type { ToolDefaults } from './tool-defaults';
3
+
4
+ // Avoid circular dependency: don't import from @aiscene/core
5
+ // Instead, use generic types that will be provided by implementation
6
+
7
+ /**
8
+ * Default timeout constants for app loading verification
9
+ */
10
+ export const defaultAppLoadingTimeoutMs = 10000;
11
+ export const defaultAppLoadingCheckIntervalMs = 2000;
12
+
13
+ /**
14
+ * Content item types for tool results.
15
+ */
16
+ export type ToolResultContent =
17
+ | { type: 'text'; text: string }
18
+ | { type: 'image'; data: string; mimeType: string }
19
+ | { type: 'audio'; data: string; mimeType: string }
20
+ | {
21
+ type: 'resource';
22
+ resource:
23
+ | { text: string; uri: string; mimeType?: string }
24
+ | { uri: string; blob: string; mimeType?: string };
25
+ };
26
+
27
+ /**
28
+ * Result type for tool execution.
29
+ */
30
+ export interface ToolResult {
31
+ [x: string]: unknown;
32
+ content: ToolResultContent[];
33
+ isError?: boolean;
34
+ _meta?: Record<string, unknown>;
35
+ }
36
+
37
+ /**
38
+ * Tool handler function type
39
+ * Takes parsed arguments and returns a tool result
40
+ */
41
+ export type ToolHandler<T = Record<string, unknown>> = (
42
+ args: T,
43
+ ) => Promise<ToolResult>;
44
+
45
+ /**
46
+ * Tool schema type using Zod
47
+ */
48
+ export type ToolSchema = Record<string, z.ZodTypeAny>;
49
+
50
+ export interface ToolCliOption {
51
+ preferredName?: string;
52
+ aliases?: string[];
53
+ hidden?: boolean;
54
+ }
55
+
56
+ export interface ToolCliMetadata {
57
+ options?: Record<string, ToolCliOption>;
58
+ /** Schema keys populated from leading positional CLI arguments. */
59
+ positionals?: string[];
60
+ }
61
+
62
+ /**
63
+ * Tool definition for Midscene CLI and Skill surfaces.
64
+ */
65
+ export interface ToolDefinition<T = Record<string, unknown>> {
66
+ name: string;
67
+ description: string;
68
+ schema: ToolSchema;
69
+ handler: ToolHandler<T>;
70
+ cli?: ToolCliMetadata;
71
+ }
72
+
73
+ export type Tool = ToolDefinition;
74
+
75
+ /**
76
+ * Action space item definition
77
+ * Note: Intentionally no index signature to maintain compatibility with DeviceAction
78
+ */
79
+ export interface ActionSpaceItem {
80
+ name: string;
81
+ description?: string;
82
+ args?: Record<string, unknown>;
83
+ paramSchema?: z.ZodTypeAny;
84
+ }
85
+
86
+ /**
87
+ * Structural shape compatible with @aiscene/core `TUserPrompt`.
88
+ * Declared locally to avoid a circular dep on `@aiscene/core` from `@aiscene/shared`.
89
+ *
90
+ * Consumed by the `assert` and `act` tools in `generateCommonTools`, both of
91
+ * which forward reference images to core (`aiAssert` / `aiAct`). `aiWaitFor`
92
+ * stays string-only at the CLI surface because the tools generator does not
93
+ * yet expose a multimodal entry point for it.
94
+ */
95
+ export type UserPromptLike =
96
+ | string
97
+ | {
98
+ prompt: string;
99
+ images?: Array<{ name: string; url: string }>;
100
+ convertHttpImage2Base64?: boolean;
101
+ };
102
+
103
+ export interface RecordToReportScreenshot {
104
+ /**
105
+ * PNG/JPEG data URI, or raw PNG base64 body.
106
+ */
107
+ base64: string;
108
+ description?: string;
109
+ }
110
+
111
+ export interface RecordToReportOptions {
112
+ content?: string;
113
+ /**
114
+ * @deprecated Use `screenshots: [{ base64 }]` instead.
115
+ */
116
+ screenshotBase64?: string;
117
+ screenshots?: RecordToReportScreenshot[];
118
+ }
119
+
120
+ /** Generic progress-bus envelope as seen by tool consumers (all untrusted). */
121
+ export interface BaseAgentProgressEvent {
122
+ scope?: unknown;
123
+ phase?: unknown;
124
+ sequence?: unknown;
125
+ data?: unknown;
126
+ }
127
+
128
+ /** A single frame in a portable UI observation record. */
129
+ export interface UIObservationFrame {
130
+ /** Path to the captured image. Serialized manifests use relative paths. */
131
+ path: string;
132
+ /** MIME type of the captured image file. */
133
+ mimeType: 'image/png' | 'image/jpeg';
134
+ /** Capture timestamp in milliseconds. */
135
+ capturedAt: number;
136
+ }
137
+
138
+ /**
139
+ * Serializable observation window that can be persisted and loaded later.
140
+ * Frames are ordered from earliest to latest; the final frame represents the
141
+ * UI state at the end of the observation window.
142
+ */
143
+ export interface UIObservationRecord {
144
+ type: 'midscene_ui_observation';
145
+ version: 1;
146
+ /** Time when observation sampling started. */
147
+ startedAt: number;
148
+ /** Time when the final representative frame was captured. */
149
+ endedAt: number;
150
+ frames: UIObservationFrame[];
151
+ shotSize: {
152
+ width: number;
153
+ height: number;
154
+ };
155
+ shrunkShotToLogicalRatio: number;
156
+ }
157
+
158
+ /** Fixed observation window consumed by shared CLI surfaces. */
159
+ export interface BaseUIObservation {
160
+ readonly frameCount: number;
161
+ readonly startedAt: number;
162
+ readonly endedAt: number;
163
+ aiAssert(
164
+ assertion: UserPromptLike,
165
+ message?: string,
166
+ options?: Record<string, unknown>,
167
+ ): Promise<unknown>;
168
+ /** Release any resources owned by this observation. */
169
+ dispose?(): Promise<void>;
170
+ }
171
+
172
+ /** Minimal UI observation lifecycle required by shared tool surfaces. */
173
+ export interface BaseUIObserver {
174
+ readonly bufferedFrameCount: number;
175
+ /** Stop sampling and return the fixed observed window. */
176
+ stop(): Promise<BaseUIObservation>;
177
+ /** Release temporary backing files if observation does not complete. */
178
+ dispose?(): Promise<void>;
179
+ }
180
+
181
+ /** Options for {@link BaseAgent.startObserving}. */
182
+ export interface BaseUIObserverOptions {
183
+ /** Sampling interval in milliseconds. Defaults to 1000; minimum 200. */
184
+ intervalMs?: number;
185
+ /** Maximum number of buffered frames. Defaults to 30; minimum 2. */
186
+ maxFrames?: number;
187
+ /** Auto-stop timeout in milliseconds. Defaults to 300000; 0 disables it. */
188
+ watchdogMs?: number;
189
+ }
190
+
191
+ /**
192
+ * Base agent interface
193
+ * Represents a platform-specific agent (Android, iOS, Web)
194
+ * Note: Return types use `unknown` for compatibility with platform-specific implementations
195
+ */
196
+ export interface BaseAgent {
197
+ getActionSpace(): Promise<ActionSpaceItem[]>;
198
+ destroy?(): Promise<void>;
199
+ reportFile?: string | null;
200
+ page?: {
201
+ screenshotBase64(): Promise<string>;
202
+ };
203
+ addDumpUpdateListener?(
204
+ listener: (dump: string, executionDump?: unknown) => void,
205
+ ): () => void;
206
+ addProgressListener?(
207
+ listener: (event: BaseAgentProgressEvent) => void,
208
+ ): () => void;
209
+ recordToReport?(
210
+ title?: string,
211
+ opt?: RecordToReportOptions,
212
+ ): Promise<void>;
213
+ callActionInActionSpace?(
214
+ actionName: string,
215
+ params?: unknown,
216
+ ): Promise<unknown>;
217
+ aiAction?(
218
+ description: UserPromptLike,
219
+ params?: Record<string, unknown>,
220
+ ): Promise<unknown>;
221
+ aiWaitFor?(
222
+ assertion: string,
223
+ options: Record<string, unknown>,
224
+ ): Promise<unknown>;
225
+ aiAssert?(
226
+ assertion: UserPromptLike,
227
+ msg?: string,
228
+ options?: Record<string, unknown>,
229
+ ): Promise<unknown>;
230
+ /** Start a UI observation window and capture its baseline frame. */
231
+ startObserving?(options?: BaseUIObserverOptions): Promise<BaseUIObserver>;
232
+ }
233
+
234
+ /**
235
+ * Base device interface for temporary device instances
236
+ */
237
+ export interface BaseDevice {
238
+ actionSpace(): ActionSpaceItem[];
239
+ destroy?(): Promise<void>;
240
+ }
241
+
242
+ /**
243
+ * Interface for platform-specific tools manager.
244
+ */
245
+ export interface IMidsceneTools {
246
+ initTools(): Promise<void>;
247
+ destroy?(): Promise<void>;
248
+ getCliToolDefinitions?(): ToolDefinition[];
249
+ setToolDefaults?(toolDefaults: ToolDefaults): void;
250
+ }
@@ -0,0 +1,102 @@
1
+ import { z } from 'zod';
2
+ import type { UserPromptLike } from './types';
3
+
4
+ type PromptReferenceImage = { name: string; url: string };
5
+
6
+ function normalizeStringList(raw: unknown, fieldName: string): string[] {
7
+ if (raw === undefined || raw === null) return [];
8
+ if (typeof raw === 'string') {
9
+ const trimmed = raw.trim();
10
+ return trimmed ? [trimmed] : [];
11
+ }
12
+ if (Array.isArray(raw)) {
13
+ return raw.map((item, index) => {
14
+ if (typeof item !== 'string') {
15
+ throw new Error(`${fieldName}[${index}]: expected a string.`);
16
+ }
17
+ return item.trim();
18
+ });
19
+ }
20
+ throw new Error(
21
+ `${fieldName}: expected a string or string array, got ${typeof raw}.`,
22
+ );
23
+ }
24
+
25
+ function composeImages(input: {
26
+ image?: unknown;
27
+ imageName?: unknown;
28
+ }): PromptReferenceImage[] {
29
+ const urls = normalizeStringList(input.image, 'image');
30
+ const names = normalizeStringList(input.imageName, 'imageName');
31
+
32
+ if (urls.length !== names.length) {
33
+ throw new Error(
34
+ `image/imageName: expected the same number of --image and --image-name values, got ${urls.length} image(s) and ${names.length} image name(s).`,
35
+ );
36
+ }
37
+
38
+ return urls.map((url, index) => ({ name: names[index], url }));
39
+ }
40
+
41
+ function coerceBoolean(value: unknown): boolean | undefined {
42
+ if (value === undefined || value === null) return undefined;
43
+ if (typeof value === 'boolean') return value;
44
+ if (typeof value === 'string') {
45
+ const trimmed = value.trim();
46
+ if (!trimmed) return undefined;
47
+ const v = trimmed.toLowerCase();
48
+ if (v === 'true' || v === '1') return true;
49
+ if (v === 'false' || v === '0') return false;
50
+ throw new Error(
51
+ `convertHttpImage2Base64: expected "true", "false", "1", or "0"; got ${JSON.stringify(value)}.`,
52
+ );
53
+ }
54
+ throw new Error(
55
+ `convertHttpImage2Base64: expected a boolean, got ${typeof value}.`,
56
+ );
57
+ }
58
+
59
+ export function composeUserPrompt(input: {
60
+ prompt: string;
61
+ image?: unknown;
62
+ imageName?: unknown;
63
+ convertHttpImage2Base64?: unknown;
64
+ }): UserPromptLike {
65
+ const images = composeImages({
66
+ image: input.image,
67
+ imageName: input.imageName,
68
+ });
69
+ const convertFlag = coerceBoolean(input.convertHttpImage2Base64);
70
+
71
+ if (images.length === 0 && convertFlag === undefined) {
72
+ return input.prompt;
73
+ }
74
+
75
+ const payload: Exclude<UserPromptLike, string> = { prompt: input.prompt };
76
+ if (images.length > 0) {
77
+ payload.images = images;
78
+ }
79
+ if (convertFlag !== undefined) {
80
+ payload.convertHttpImage2Base64 = convertFlag;
81
+ }
82
+ return payload;
83
+ }
84
+
85
+ export const promptInputExtraSchema = {
86
+ image: z
87
+ .union([z.string(), z.array(z.string())])
88
+ .optional()
89
+ .describe('Reference image URL/path. Repeat --image for multiple images.'),
90
+ imageName: z
91
+ .union([z.string(), z.array(z.string())])
92
+ .optional()
93
+ .describe(
94
+ 'Reference image name. Repeat --image-name; must align with --image order.',
95
+ ),
96
+ convertHttpImage2Base64: z
97
+ .union([z.boolean(), z.string()])
98
+ .optional()
99
+ .describe(
100
+ 'If true, convert http(s) image URLs to base64 before sending to the model.',
101
+ ),
102
+ };
@@ -0,0 +1,173 @@
1
+ import { z } from 'zod';
2
+ import { getKeyAliases } from '../key-alias-utils';
3
+ import type { ToolCliOption, ToolDefinition } from '../mcp/types';
4
+
5
+ export function parseValue(raw: string): unknown {
6
+ if (raw.startsWith('{') || raw.startsWith('[')) {
7
+ try {
8
+ return JSON.parse(raw);
9
+ } catch {
10
+ // Not valid JSON, treat as string below
11
+ }
12
+ }
13
+
14
+ if (/^-?\d+(\.\d+)?$/.test(raw)) {
15
+ return Number(raw);
16
+ }
17
+
18
+ return raw;
19
+ }
20
+
21
+ function walkCliArgs(
22
+ args: string[],
23
+ setArgValue: (key: string, value: unknown) => void,
24
+ ): void {
25
+ for (let i = 0; i < args.length; i++) {
26
+ const arg = args[i];
27
+ if (!arg.startsWith('--')) continue;
28
+
29
+ const body = arg.slice(2);
30
+ const eqIdx = body.indexOf('=');
31
+
32
+ if (eqIdx >= 0) {
33
+ setArgValue(body.slice(0, eqIdx), parseValue(body.slice(eqIdx + 1)));
34
+ } else if (args[i + 1] && !args[i + 1].startsWith('--')) {
35
+ i++;
36
+ setArgValue(body, parseValue(args[i]));
37
+ } else {
38
+ setArgValue(body, true);
39
+ }
40
+ }
41
+ }
42
+
43
+ export function parseCliArgs(args: string[]): Record<string, unknown> {
44
+ const result: Record<string, unknown> = {};
45
+
46
+ walkCliArgs(args, (key, value) => {
47
+ result[key] = value;
48
+ });
49
+
50
+ return result;
51
+ }
52
+
53
+ function formatCliOptionName(name: string): string {
54
+ return `--${name}`;
55
+ }
56
+
57
+ export function getCliOptionDisplay(
58
+ key: string,
59
+ cliOption?: ToolCliOption,
60
+ ): { label: string; aliases: string[] } {
61
+ const label = formatCliOptionName(cliOption?.preferredName ?? key);
62
+ const aliases = [...new Set(cliOption?.aliases ?? [])]
63
+ .map((alias) => formatCliOptionName(alias))
64
+ .filter((alias) => alias !== label);
65
+
66
+ return { label, aliases };
67
+ }
68
+
69
+ function getAcceptedCliOptionNames(
70
+ key: string,
71
+ cliOption?: ToolCliOption,
72
+ ): string[] {
73
+ return [
74
+ ...new Set(
75
+ cliOption
76
+ ? [cliOption.preferredName ?? key, ...(cliOption.aliases ?? [])]
77
+ : [key, ...getKeyAliases(key)],
78
+ ),
79
+ ];
80
+ }
81
+
82
+ function toOptionalCliSchemaField(field: unknown): z.ZodTypeAny {
83
+ if (
84
+ typeof field === 'object' &&
85
+ field !== null &&
86
+ typeof (field as z.ZodTypeAny).optional === 'function'
87
+ ) {
88
+ return (field as z.ZodTypeAny).optional();
89
+ }
90
+
91
+ const description =
92
+ typeof field === 'object' &&
93
+ field !== null &&
94
+ 'description' in field &&
95
+ typeof (field as { description?: unknown }).description === 'string'
96
+ ? (field as { description: string }).description
97
+ : undefined;
98
+ return description ? z.any().describe(description) : z.any();
99
+ }
100
+
101
+ function buildCliArgSchema(def: ToolDefinition): Record<string, z.ZodTypeAny> {
102
+ return Object.fromEntries(
103
+ Object.entries(def.schema).flatMap(([key, zodType]) =>
104
+ getAcceptedCliOptionNames(key, def.cli?.options?.[key]).map((cliKey) => [
105
+ cliKey,
106
+ toOptionalCliSchemaField(zodType),
107
+ ]),
108
+ ),
109
+ );
110
+ }
111
+
112
+ function buildDisallowedCliSpellings(def: ToolDefinition): Set<string> {
113
+ const disallowedSpellings = new Set<string>();
114
+
115
+ for (const [key] of Object.entries(def.schema)) {
116
+ const cliOption = def.cli?.options?.[key];
117
+ const acceptedNames = new Set(getAcceptedCliOptionNames(key, cliOption));
118
+ const knownSpellings = new Set<string>([
119
+ key,
120
+ ...getKeyAliases(key),
121
+ ...(cliOption?.preferredName
122
+ ? getKeyAliases(cliOption.preferredName)
123
+ : []),
124
+ ...(cliOption?.aliases ?? []),
125
+ ]);
126
+
127
+ for (const spelling of knownSpellings) {
128
+ if (!acceptedNames.has(spelling)) {
129
+ disallowedSpellings.add(spelling);
130
+ }
131
+ }
132
+ }
133
+
134
+ return disallowedSpellings;
135
+ }
136
+
137
+ export function formatCliValidationError(
138
+ scriptName: string,
139
+ commandName: string,
140
+ def: ToolDefinition,
141
+ rawArgs: Record<string, unknown>,
142
+ ): string | undefined {
143
+ if (Object.keys(def.schema).length === 0) {
144
+ return undefined;
145
+ }
146
+
147
+ const cliSchema = z.object(buildCliArgSchema(def)).strict();
148
+ const parsed = cliSchema.safeParse(rawArgs);
149
+ if (parsed.success) {
150
+ return undefined;
151
+ }
152
+
153
+ const disallowedSpellings = buildDisallowedCliSpellings(def);
154
+ const unknownKeys = parsed.error.issues.flatMap((issue) =>
155
+ issue.code === 'unrecognized_keys' ? issue.keys : [],
156
+ );
157
+
158
+ if (unknownKeys.length > 0) {
159
+ return unknownKeys
160
+ .map((key) => {
161
+ if (disallowedSpellings.has(key)) {
162
+ return `Unsupported option "--${key}" for ${scriptName} ${commandName}.`;
163
+ }
164
+ return `Unknown option "--${key}" for ${scriptName} ${commandName}.`;
165
+ })
166
+ .join('\n');
167
+ }
168
+
169
+ const [issue] = parsed.error.issues;
170
+ const optionName =
171
+ typeof issue?.path[0] === 'string' ? `--${issue.path[0]}` : 'CLI arguments';
172
+ return `Invalid value for "${optionName}" in ${scriptName} ${commandName}: ${issue?.message ?? parsed.error.message}`;
173
+ }
@@ -0,0 +1,24 @@
1
+ export class CLIError extends Error {
2
+ constructor(
3
+ message: string,
4
+ public exitCode = 1,
5
+ ) {
6
+ super(message);
7
+ }
8
+ }
9
+
10
+ export function reportCLIError(
11
+ error: unknown,
12
+ log: (
13
+ message?: unknown,
14
+ ...optionalParams: unknown[]
15
+ ) => void = console.error,
16
+ ): number {
17
+ if (error instanceof CLIError) {
18
+ log(error.message);
19
+ return error.exitCode;
20
+ }
21
+
22
+ log(error);
23
+ return 1;
24
+ }
@@ -9,6 +9,12 @@ import type {
9
9
  ToolResult,
10
10
  ToolResultContent,
11
11
  } from '../mcp/types';
12
+ import {
13
+ formatCliValidationError,
14
+ getCliOptionDisplay,
15
+ parseCliArgs,
16
+ } from './cli-args';
17
+ import { CLIError } from './cli-error';
12
18
 
13
19
  const debug = getDebug('cli-runner');
14
20
 
@@ -32,58 +38,8 @@ export interface CLIRunnerOptions {
32
38
  extraCommands?: CLIExtraCommand[];
33
39
  }
34
40
 
35
- export class CLIError extends Error {
36
- constructor(
37
- message: string,
38
- public exitCode = 1,
39
- ) {
40
- super(message);
41
- }
42
- }
43
-
44
- export function parseValue(raw: string): unknown {
45
- // JSON objects/arrays
46
- if (raw.startsWith('{') || raw.startsWith('[')) {
47
- try {
48
- return JSON.parse(raw);
49
- } catch {
50
- // Not valid JSON, treat as string below
51
- }
52
- }
53
-
54
- // Numbers
55
- if (/^-?\d+(\.\d+)?$/.test(raw)) {
56
- return Number(raw);
57
- }
58
-
59
- return raw;
60
- }
61
-
62
- export function parseCliArgs(args: string[]): Record<string, unknown> {
63
- const result: Record<string, unknown> = {};
64
-
65
- for (let i = 0; i < args.length; i++) {
66
- const arg = args[i];
67
- if (!arg.startsWith('--')) continue;
68
-
69
- const body = arg.slice(2);
70
- const eqIdx = body.indexOf('=');
71
-
72
- if (eqIdx >= 0) {
73
- // --key=value
74
- result[body.slice(0, eqIdx)] = parseValue(body.slice(eqIdx + 1));
75
- } else if (args[i + 1] && !args[i + 1].startsWith('--')) {
76
- // --key value
77
- i++;
78
- result[body] = parseValue(args[i]);
79
- } else {
80
- // --flag (boolean)
81
- result[body] = true;
82
- }
83
- }
84
-
85
- return result;
86
- }
41
+ export { parseCliArgs, parseValue } from './cli-args';
42
+ export { CLIError, reportCLIError } from './cli-error';
87
43
 
88
44
  function outputContentItem(item: ToolResultContent, isError: boolean): void {
89
45
  switch (item.type) {
@@ -128,10 +84,23 @@ function printCommandHelp(scriptName: string, cmd: CLICommand): void {
128
84
 
129
85
  const schemaEntries = Object.entries(def.schema);
130
86
  if (schemaEntries.length > 0) {
87
+ const optionWidth = Math.max(
88
+ 22,
89
+ ...schemaEntries.map(
90
+ ([key]) =>
91
+ getCliOptionDisplay(key, def.cli?.options?.[key]).label.length,
92
+ ),
93
+ );
131
94
  console.log('\nOptions:');
132
95
  for (const [key, zodType] of schemaEntries) {
96
+ const { label, aliases } = getCliOptionDisplay(
97
+ key,
98
+ def.cli?.options?.[key],
99
+ );
133
100
  const desc = zodType.description ?? '';
134
- console.log(` --${key.padEnd(20)} ${desc}`);
101
+ const aliasText =
102
+ aliases.length > 0 ? ` (aliases: ${aliases.join(', ')})` : '';
103
+ console.log(` ${label.padEnd(optionWidth)} ${desc}${aliasText}`);
135
104
  }
136
105
  }
137
106
  }
@@ -158,8 +127,10 @@ function printHelp(
158
127
  console.log(`\nRun "${scriptName} <command> --help" for more info.`);
159
128
  }
160
129
 
130
+ type AnyMidsceneTools = BaseMidsceneTools<any, any>;
131
+
161
132
  export async function runToolsCLI(
162
- tools: BaseMidsceneTools,
133
+ tools: AnyMidsceneTools,
163
134
  scriptName: string,
164
135
  options?: CLIRunnerOptions,
165
136
  ): Promise<void> {
@@ -227,14 +198,24 @@ export async function runToolsCLI(
227
198
  }
228
199
 
229
200
  const parsedArgs = parseCliArgs(restArgs);
230
- debug('command: %s, args: %s', match.name, JSON.stringify(parsedArgs));
231
-
232
201
  if (parsedArgs.help === true) {
233
202
  debug('showing command help for: %s', match.name);
234
203
  printCommandHelp(scriptName, match);
235
204
  return;
236
205
  }
237
206
 
207
+ const cliValidationError = formatCliValidationError(
208
+ scriptName,
209
+ match.name,
210
+ match.def,
211
+ parsedArgs,
212
+ );
213
+ if (cliValidationError) {
214
+ throw new CLIError(cliValidationError);
215
+ }
216
+
217
+ debug('command: %s, args: %s', match.name, JSON.stringify(parsedArgs));
218
+
238
219
  const result = await match.def.handler(parsedArgs);
239
220
  debug(
240
221
  'command %s completed, isError: %s',
package/src/cli/index.ts CHANGED
@@ -1,8 +1,4 @@
1
- export {
2
- runToolsCLI,
3
- CLIError,
4
- parseValue,
5
- parseCliArgs,
6
- removePrefix,
7
- } from './cli-runner';
1
+ export { CLIError, reportCLIError } from './cli-error';
2
+ export { parseCliArgs, parseValue } from './cli-args';
3
+ export { runToolsCLI, removePrefix } from './cli-runner';
8
4
  export type { CLIRunnerOptions, CLIExtraCommand } from './cli-runner';