@midscene/shared 1.7.5-beta-20260420035759.0 → 1.7.5-beta-20260420052829.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,7 @@ import { existsSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import dotenv from "dotenv";
5
+ import { z } from "zod";
5
6
  import { getKeyAliases, isRecord } from "../key-alias-utils.mjs";
6
7
  import { getDebug } from "../logger.mjs";
7
8
  function _define_property(obj, key, value) {
@@ -27,9 +28,29 @@ function parseValue(raw) {
27
28
  if (/^-?\d+(\.\d+)?$/.test(raw)) return Number(raw);
28
29
  return raw;
29
30
  }
31
+ function walkCliArgs(args, setArgValue) {
32
+ for(let i = 0; i < args.length; i++){
33
+ const arg = args[i];
34
+ if (!arg.startsWith('--')) continue;
35
+ const body = arg.slice(2);
36
+ const eqIdx = body.indexOf('=');
37
+ if (eqIdx >= 0) setArgValue(body.slice(0, eqIdx), parseValue(body.slice(eqIdx + 1)));
38
+ else if (args[i + 1] && !args[i + 1].startsWith('--')) {
39
+ i++;
40
+ setArgValue(body, parseValue(args[i]));
41
+ } else setArgValue(body, true);
42
+ }
43
+ }
44
+ function parseRawCliArgs(args) {
45
+ const result = {};
46
+ walkCliArgs(args, (key, value)=>{
47
+ result[key] = value;
48
+ });
49
+ return result;
50
+ }
30
51
  function parseCliArgs(args) {
31
52
  const result = {};
32
- const setArgValue = (key, value)=>{
53
+ walkCliArgs(args, (key, value)=>{
33
54
  if (!key.includes('.')) {
34
55
  result[key] = value;
35
56
  return;
@@ -48,18 +69,7 @@ function parseCliArgs(args) {
48
69
  }
49
70
  const leafSegment = segments[segments.length - 1];
50
71
  for (const alias of getKeyAliases(leafSegment))current[alias] = value;
51
- };
52
- for(let i = 0; i < args.length; i++){
53
- const arg = args[i];
54
- if (!arg.startsWith('--')) continue;
55
- const body = arg.slice(2);
56
- const eqIdx = body.indexOf('=');
57
- if (eqIdx >= 0) setArgValue(body.slice(0, eqIdx), parseValue(body.slice(eqIdx + 1)));
58
- else if (args[i + 1] && !args[i + 1].startsWith('--')) {
59
- i++;
60
- setArgValue(body, parseValue(args[i]));
61
- } else setArgValue(body, true);
62
- }
72
+ });
63
73
  return result;
64
74
  }
65
75
  function outputContentItem(item, isError) {
@@ -100,28 +110,59 @@ function getCliOptionDisplay(key, cliOption) {
100
110
  aliases
101
111
  };
102
112
  }
103
- function validateRestrictedCliArgSpellings(args, scriptName, commandName, def) {
113
+ function getAcceptedCliOptionNames(key, cliOption) {
114
+ return [
115
+ ...new Set(cliOption ? [
116
+ cliOption.preferredName ?? key,
117
+ ...cliOption.aliases ?? []
118
+ ] : [
119
+ key,
120
+ ...getKeyAliases(key)
121
+ ])
122
+ ];
123
+ }
124
+ function toOptionalCliSchemaField(field) {
125
+ if ('object' == typeof field && null !== field && 'function' == typeof field.optional) return field.optional();
126
+ const description = 'object' == typeof field && null !== field && "description" in field && 'string' == typeof field.description ? field.description : void 0;
127
+ return description ? z.any().describe(description) : z.any();
128
+ }
129
+ function buildCliArgSchema(def) {
130
+ return Object.fromEntries(Object.entries(def.schema).flatMap(([key, zodType])=>getAcceptedCliOptionNames(key, def.cli?.options?.[key]).map((cliKey)=>[
131
+ cliKey,
132
+ toOptionalCliSchemaField(zodType)
133
+ ])));
134
+ }
135
+ function buildDisallowedCliSpellings(def) {
104
136
  const disallowedSpellings = new Map();
105
137
  for (const [key] of Object.entries(def.schema)){
106
138
  const cliOption = def.cli?.options?.[key];
107
- if (!cliOption?.acceptedNames?.length) continue;
108
- const acceptedNames = new Set(cliOption.acceptedNames);
109
- const preferredLabel = formatCliOptionName(cliOption.preferredName ?? key);
139
+ const preferredLabel = formatCliOptionName(cliOption?.preferredName ?? key);
140
+ const acceptedNames = new Set(getAcceptedCliOptionNames(key, cliOption));
110
141
  const knownSpellings = new Set([
111
142
  key,
112
143
  ...getKeyAliases(key),
113
- cliOption.preferredName ?? key,
114
- ...cliOption.aliases ?? [],
115
- ...cliOption.acceptedNames
144
+ ...cliOption?.preferredName ? getKeyAliases(cliOption.preferredName) : [],
145
+ ...cliOption?.aliases ?? []
116
146
  ]);
117
147
  for (const spelling of knownSpellings)if (!acceptedNames.has(spelling)) disallowedSpellings.set(spelling, preferredLabel);
118
148
  }
119
- for (const arg of args){
120
- if (!arg.startsWith('--')) continue;
121
- const key = arg.slice(2).split('=')[0];
149
+ return disallowedSpellings;
150
+ }
151
+ function formatCliValidationError(scriptName, commandName, def, rawArgs) {
152
+ if (0 === Object.keys(def.schema).length) return;
153
+ const cliSchema = z.object(buildCliArgSchema(def)).strict();
154
+ const parsed = cliSchema.safeParse(rawArgs);
155
+ if (parsed.success) return;
156
+ const disallowedSpellings = buildDisallowedCliSpellings(def);
157
+ const unknownKeys = parsed.error.issues.flatMap((issue)=>'unrecognized_keys' === issue.code ? issue.keys : []);
158
+ if (unknownKeys.length > 0) return unknownKeys.map((key)=>{
122
159
  const preferredLabel = disallowedSpellings.get(key);
123
- if (preferredLabel) throw new CLIError(`Unsupported option "--${key}" for ${scriptName} ${commandName}. Use "${preferredLabel}" instead.`);
124
- }
160
+ if (preferredLabel) return `Unsupported option "--${key}" for ${scriptName} ${commandName}. Use "${preferredLabel}" instead.`;
161
+ return `Unknown option "--${key}" for ${scriptName} ${commandName}.`;
162
+ }).join('\n');
163
+ const [issue] = parsed.error.issues;
164
+ const optionName = 'string' == typeof issue?.path[0] ? `--${issue.path[0]}` : 'CLI arguments';
165
+ return `Invalid value for "${optionName}" in ${scriptName} ${commandName}: ${issue?.message ?? parsed.error.message}`;
125
166
  }
126
167
  function printCommandHelp(scriptName, cmd) {
127
168
  const { def } = cmd;
@@ -196,14 +237,16 @@ async function runToolsCLI(tools, scriptName, options) {
196
237
  printHelp(scriptName, commands, cliVersion);
197
238
  throw new CLIError(`Unknown command: ${commandName}`);
198
239
  }
199
- validateRestrictedCliArgSpellings(restArgs, scriptName, match.name, match.def);
200
- const parsedArgs = parseCliArgs(restArgs);
201
- debug('command: %s, args: %s', match.name, JSON.stringify(parsedArgs));
202
- if (true === parsedArgs.help) {
240
+ const rawCliArgs = parseRawCliArgs(restArgs);
241
+ if (true === rawCliArgs.help) {
203
242
  debug('showing command help for: %s', match.name);
204
243
  printCommandHelp(scriptName, match);
205
244
  return;
206
245
  }
246
+ const cliValidationError = formatCliValidationError(scriptName, match.name, match.def, rawCliArgs);
247
+ if (cliValidationError) throw new CLIError(cliValidationError);
248
+ const parsedArgs = parseCliArgs(restArgs);
249
+ debug('command: %s, args: %s', match.name, JSON.stringify(parsedArgs));
207
250
  const result = await match.def.handler(parsedArgs);
208
251
  debug('command %s completed, isError: %s', match.name, result.isError ?? false);
209
252
  outputResult(result);
@@ -37,26 +37,18 @@ class BaseMidsceneTools {
37
37
  const options = Object.fromEntries(this.getInitArgKeys().map((key)=>{
38
38
  const canonicalKey = `${this.initArgSpec.namespace}.${key}`;
39
39
  const preferredName = this.initArgSpec.cli?.preferredNames?.[key] ?? (this.initArgSpec.cli?.preferBareKeys ? camelToKebab(key) : canonicalKey);
40
- const acceptedNames = this.initArgSpec.cli?.preferBareKeys ? [
41
- ...new Set([
42
- preferredName,
43
- ...getKeyAliases(key)
44
- ])
45
- ] : void 0;
46
- const aliases = new Set(acceptedNames ?? getKeyAliases(key));
47
- if (this.initArgSpec.cli?.preferBareKeys) aliases.delete(preferredName);
48
- else {
49
- for (const alias of getKeyAliases(canonicalKey))aliases.add(alias);
50
- aliases.delete(preferredName);
51
- }
40
+ const acceptedNames = new Set([
41
+ preferredName,
42
+ ...this.initArgSpec.cli?.preferBareKeys ? getKeyAliases(key) : getKeyAliases(canonicalKey)
43
+ ]);
44
+ acceptedNames.delete(preferredName);
52
45
  return [
53
46
  canonicalKey,
54
47
  {
55
48
  preferredName,
56
49
  aliases: [
57
- ...aliases
58
- ],
59
- acceptedNames
50
+ ...acceptedNames
51
+ ]
60
52
  }
61
53
  ];
62
54
  }));
@@ -44,6 +44,7 @@ const external_node_os_namespaceObject = require("node:os");
44
44
  const external_node_path_namespaceObject = require("node:path");
45
45
  const external_dotenv_namespaceObject = require("dotenv");
46
46
  var external_dotenv_default = /*#__PURE__*/ __webpack_require__.n(external_dotenv_namespaceObject);
47
+ const external_zod_namespaceObject = require("zod");
47
48
  const external_key_alias_utils_js_namespaceObject = require("../key-alias-utils.js");
48
49
  const external_logger_js_namespaceObject = require("../logger.js");
49
50
  function _define_property(obj, key, value) {
@@ -69,9 +70,29 @@ function parseValue(raw) {
69
70
  if (/^-?\d+(\.\d+)?$/.test(raw)) return Number(raw);
70
71
  return raw;
71
72
  }
73
+ function walkCliArgs(args, setArgValue) {
74
+ for(let i = 0; i < args.length; i++){
75
+ const arg = args[i];
76
+ if (!arg.startsWith('--')) continue;
77
+ const body = arg.slice(2);
78
+ const eqIdx = body.indexOf('=');
79
+ if (eqIdx >= 0) setArgValue(body.slice(0, eqIdx), parseValue(body.slice(eqIdx + 1)));
80
+ else if (args[i + 1] && !args[i + 1].startsWith('--')) {
81
+ i++;
82
+ setArgValue(body, parseValue(args[i]));
83
+ } else setArgValue(body, true);
84
+ }
85
+ }
86
+ function parseRawCliArgs(args) {
87
+ const result = {};
88
+ walkCliArgs(args, (key, value)=>{
89
+ result[key] = value;
90
+ });
91
+ return result;
92
+ }
72
93
  function parseCliArgs(args) {
73
94
  const result = {};
74
- const setArgValue = (key, value)=>{
95
+ walkCliArgs(args, (key, value)=>{
75
96
  if (!key.includes('.')) {
76
97
  result[key] = value;
77
98
  return;
@@ -90,18 +111,7 @@ function parseCliArgs(args) {
90
111
  }
91
112
  const leafSegment = segments[segments.length - 1];
92
113
  for (const alias of (0, external_key_alias_utils_js_namespaceObject.getKeyAliases)(leafSegment))current[alias] = value;
93
- };
94
- for(let i = 0; i < args.length; i++){
95
- const arg = args[i];
96
- if (!arg.startsWith('--')) continue;
97
- const body = arg.slice(2);
98
- const eqIdx = body.indexOf('=');
99
- if (eqIdx >= 0) setArgValue(body.slice(0, eqIdx), parseValue(body.slice(eqIdx + 1)));
100
- else if (args[i + 1] && !args[i + 1].startsWith('--')) {
101
- i++;
102
- setArgValue(body, parseValue(args[i]));
103
- } else setArgValue(body, true);
104
- }
114
+ });
105
115
  return result;
106
116
  }
107
117
  function outputContentItem(item, isError) {
@@ -142,28 +152,59 @@ function getCliOptionDisplay(key, cliOption) {
142
152
  aliases
143
153
  };
144
154
  }
145
- function validateRestrictedCliArgSpellings(args, scriptName, commandName, def) {
155
+ function getAcceptedCliOptionNames(key, cliOption) {
156
+ return [
157
+ ...new Set(cliOption ? [
158
+ cliOption.preferredName ?? key,
159
+ ...cliOption.aliases ?? []
160
+ ] : [
161
+ key,
162
+ ...(0, external_key_alias_utils_js_namespaceObject.getKeyAliases)(key)
163
+ ])
164
+ ];
165
+ }
166
+ function toOptionalCliSchemaField(field) {
167
+ if ('object' == typeof field && null !== field && 'function' == typeof field.optional) return field.optional();
168
+ const description = 'object' == typeof field && null !== field && "description" in field && 'string' == typeof field.description ? field.description : void 0;
169
+ return description ? external_zod_namespaceObject.z.any().describe(description) : external_zod_namespaceObject.z.any();
170
+ }
171
+ function buildCliArgSchema(def) {
172
+ return Object.fromEntries(Object.entries(def.schema).flatMap(([key, zodType])=>getAcceptedCliOptionNames(key, def.cli?.options?.[key]).map((cliKey)=>[
173
+ cliKey,
174
+ toOptionalCliSchemaField(zodType)
175
+ ])));
176
+ }
177
+ function buildDisallowedCliSpellings(def) {
146
178
  const disallowedSpellings = new Map();
147
179
  for (const [key] of Object.entries(def.schema)){
148
180
  const cliOption = def.cli?.options?.[key];
149
- if (!cliOption?.acceptedNames?.length) continue;
150
- const acceptedNames = new Set(cliOption.acceptedNames);
151
- const preferredLabel = formatCliOptionName(cliOption.preferredName ?? key);
181
+ const preferredLabel = formatCliOptionName(cliOption?.preferredName ?? key);
182
+ const acceptedNames = new Set(getAcceptedCliOptionNames(key, cliOption));
152
183
  const knownSpellings = new Set([
153
184
  key,
154
185
  ...(0, external_key_alias_utils_js_namespaceObject.getKeyAliases)(key),
155
- cliOption.preferredName ?? key,
156
- ...cliOption.aliases ?? [],
157
- ...cliOption.acceptedNames
186
+ ...cliOption?.preferredName ? (0, external_key_alias_utils_js_namespaceObject.getKeyAliases)(cliOption.preferredName) : [],
187
+ ...cliOption?.aliases ?? []
158
188
  ]);
159
189
  for (const spelling of knownSpellings)if (!acceptedNames.has(spelling)) disallowedSpellings.set(spelling, preferredLabel);
160
190
  }
161
- for (const arg of args){
162
- if (!arg.startsWith('--')) continue;
163
- const key = arg.slice(2).split('=')[0];
191
+ return disallowedSpellings;
192
+ }
193
+ function formatCliValidationError(scriptName, commandName, def, rawArgs) {
194
+ if (0 === Object.keys(def.schema).length) return;
195
+ const cliSchema = external_zod_namespaceObject.z.object(buildCliArgSchema(def)).strict();
196
+ const parsed = cliSchema.safeParse(rawArgs);
197
+ if (parsed.success) return;
198
+ const disallowedSpellings = buildDisallowedCliSpellings(def);
199
+ const unknownKeys = parsed.error.issues.flatMap((issue)=>'unrecognized_keys' === issue.code ? issue.keys : []);
200
+ if (unknownKeys.length > 0) return unknownKeys.map((key)=>{
164
201
  const preferredLabel = disallowedSpellings.get(key);
165
- if (preferredLabel) throw new CLIError(`Unsupported option "--${key}" for ${scriptName} ${commandName}. Use "${preferredLabel}" instead.`);
166
- }
202
+ if (preferredLabel) return `Unsupported option "--${key}" for ${scriptName} ${commandName}. Use "${preferredLabel}" instead.`;
203
+ return `Unknown option "--${key}" for ${scriptName} ${commandName}.`;
204
+ }).join('\n');
205
+ const [issue] = parsed.error.issues;
206
+ const optionName = 'string' == typeof issue?.path[0] ? `--${issue.path[0]}` : 'CLI arguments';
207
+ return `Invalid value for "${optionName}" in ${scriptName} ${commandName}: ${issue?.message ?? parsed.error.message}`;
167
208
  }
168
209
  function printCommandHelp(scriptName, cmd) {
169
210
  const { def } = cmd;
@@ -238,14 +279,16 @@ async function runToolsCLI(tools, scriptName, options) {
238
279
  printHelp(scriptName, commands, cliVersion);
239
280
  throw new CLIError(`Unknown command: ${commandName}`);
240
281
  }
241
- validateRestrictedCliArgSpellings(restArgs, scriptName, match.name, match.def);
242
- const parsedArgs = parseCliArgs(restArgs);
243
- debug('command: %s, args: %s', match.name, JSON.stringify(parsedArgs));
244
- if (true === parsedArgs.help) {
282
+ const rawCliArgs = parseRawCliArgs(restArgs);
283
+ if (true === rawCliArgs.help) {
245
284
  debug('showing command help for: %s', match.name);
246
285
  printCommandHelp(scriptName, match);
247
286
  return;
248
287
  }
288
+ const cliValidationError = formatCliValidationError(scriptName, match.name, match.def, rawCliArgs);
289
+ if (cliValidationError) throw new CLIError(cliValidationError);
290
+ const parsedArgs = parseCliArgs(restArgs);
291
+ debug('command: %s, args: %s', match.name, JSON.stringify(parsedArgs));
249
292
  const result = await match.def.handler(parsedArgs);
250
293
  debug('command %s completed, isError: %s', match.name, result.isError ?? false);
251
294
  outputResult(result);
@@ -65,26 +65,18 @@ class BaseMidsceneTools {
65
65
  const options = Object.fromEntries(this.getInitArgKeys().map((key)=>{
66
66
  const canonicalKey = `${this.initArgSpec.namespace}.${key}`;
67
67
  const preferredName = this.initArgSpec.cli?.preferredNames?.[key] ?? (this.initArgSpec.cli?.preferBareKeys ? (0, external_key_alias_utils_js_namespaceObject.camelToKebab)(key) : canonicalKey);
68
- const acceptedNames = this.initArgSpec.cli?.preferBareKeys ? [
69
- ...new Set([
70
- preferredName,
71
- ...(0, external_key_alias_utils_js_namespaceObject.getKeyAliases)(key)
72
- ])
73
- ] : void 0;
74
- const aliases = new Set(acceptedNames ?? (0, external_key_alias_utils_js_namespaceObject.getKeyAliases)(key));
75
- if (this.initArgSpec.cli?.preferBareKeys) aliases.delete(preferredName);
76
- else {
77
- for (const alias of (0, external_key_alias_utils_js_namespaceObject.getKeyAliases)(canonicalKey))aliases.add(alias);
78
- aliases.delete(preferredName);
79
- }
68
+ const acceptedNames = new Set([
69
+ preferredName,
70
+ ...this.initArgSpec.cli?.preferBareKeys ? (0, external_key_alias_utils_js_namespaceObject.getKeyAliases)(key) : (0, external_key_alias_utils_js_namespaceObject.getKeyAliases)(canonicalKey)
71
+ ]);
72
+ acceptedNames.delete(preferredName);
80
73
  return [
81
74
  canonicalKey,
82
75
  {
83
76
  preferredName,
84
77
  aliases: [
85
- ...aliases
86
- ],
87
- acceptedNames
78
+ ...acceptedNames
79
+ ]
88
80
  }
89
81
  ];
90
82
  }));
@@ -52,7 +52,6 @@ export type ToolSchema = Record<string, z.ZodTypeAny>;
52
52
  export interface ToolCliOption {
53
53
  preferredName?: string;
54
54
  aliases?: string[];
55
- acceptedNames?: string[];
56
55
  }
57
56
  export interface ToolCliMetadata {
58
57
  options?: Record<string, ToolCliOption>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@midscene/shared",
3
- "version": "1.7.5-beta-20260420035759.0",
3
+ "version": "1.7.5-beta-20260420052829.0",
4
4
  "repository": "https://github.com/web-infra-dev/midscene",
5
5
  "homepage": "https://midscenejs.com/",
6
6
  "types": "./dist/types/index.d.ts",
@@ -2,6 +2,7 @@ import { existsSync, writeFileSync } from 'node:fs';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import dotenv from 'dotenv';
5
+ import { z } from 'zod';
5
6
  import { getKeyAliases, isRecord } from '../key-alias-utils';
6
7
  import { getDebug } from '../logger';
7
8
  import type { BaseMidsceneTools } from '../mcp/base-tools';
@@ -61,10 +62,43 @@ export function parseValue(raw: string): unknown {
61
62
  return raw;
62
63
  }
63
64
 
65
+ function walkCliArgs(
66
+ args: string[],
67
+ setArgValue: (key: string, value: unknown) => void,
68
+ ): void {
69
+ for (let i = 0; i < args.length; i++) {
70
+ const arg = args[i];
71
+ if (!arg.startsWith('--')) continue;
72
+
73
+ const body = arg.slice(2);
74
+ const eqIdx = body.indexOf('=');
75
+
76
+ if (eqIdx >= 0) {
77
+ // --key=value
78
+ setArgValue(body.slice(0, eqIdx), parseValue(body.slice(eqIdx + 1)));
79
+ } else if (args[i + 1] && !args[i + 1].startsWith('--')) {
80
+ // --key value
81
+ i++;
82
+ setArgValue(body, parseValue(args[i]));
83
+ } else {
84
+ // --flag (boolean)
85
+ setArgValue(body, true);
86
+ }
87
+ }
88
+ }
89
+
90
+ function parseRawCliArgs(args: string[]): Record<string, unknown> {
91
+ const result: Record<string, unknown> = {};
92
+ walkCliArgs(args, (key, value) => {
93
+ result[key] = value;
94
+ });
95
+ return result;
96
+ }
97
+
64
98
  export function parseCliArgs(args: string[]): Record<string, unknown> {
65
99
  const result: Record<string, unknown> = {};
66
100
 
67
- const setArgValue = (key: string, value: unknown) => {
101
+ walkCliArgs(args, (key, value) => {
68
102
  if (!key.includes('.')) {
69
103
  result[key] = value;
70
104
  return;
@@ -98,27 +132,7 @@ export function parseCliArgs(args: string[]): Record<string, unknown> {
98
132
  for (const alias of getKeyAliases(leafSegment)) {
99
133
  current[alias] = value;
100
134
  }
101
- };
102
-
103
- for (let i = 0; i < args.length; i++) {
104
- const arg = args[i];
105
- if (!arg.startsWith('--')) continue;
106
-
107
- const body = arg.slice(2);
108
- const eqIdx = body.indexOf('=');
109
-
110
- if (eqIdx >= 0) {
111
- // --key=value
112
- setArgValue(body.slice(0, eqIdx), parseValue(body.slice(eqIdx + 1)));
113
- } else if (args[i + 1] && !args[i + 1].startsWith('--')) {
114
- // --key value
115
- i++;
116
- setArgValue(body, parseValue(args[i]));
117
- } else {
118
- // --flag (boolean)
119
- setArgValue(body, true);
120
- }
121
- }
135
+ });
122
136
 
123
137
  return result;
124
138
  }
@@ -175,28 +189,63 @@ function getCliOptionDisplay(
175
189
  return { label, aliases };
176
190
  }
177
191
 
178
- function validateRestrictedCliArgSpellings(
179
- args: string[],
180
- scriptName: string,
181
- commandName: string,
182
- def: ToolDefinition,
183
- ): void {
192
+ function getAcceptedCliOptionNames(
193
+ key: string,
194
+ cliOption?: ToolCliOption,
195
+ ): string[] {
196
+ return [
197
+ ...new Set(
198
+ cliOption
199
+ ? [cliOption.preferredName ?? key, ...(cliOption.aliases ?? [])]
200
+ : [key, ...getKeyAliases(key)],
201
+ ),
202
+ ];
203
+ }
204
+
205
+ function toOptionalCliSchemaField(field: unknown): z.ZodTypeAny {
206
+ if (
207
+ typeof field === 'object' &&
208
+ field !== null &&
209
+ typeof (field as z.ZodTypeAny).optional === 'function'
210
+ ) {
211
+ return (field as z.ZodTypeAny).optional();
212
+ }
213
+
214
+ const description =
215
+ typeof field === 'object' &&
216
+ field !== null &&
217
+ 'description' in field &&
218
+ typeof (field as { description?: unknown }).description === 'string'
219
+ ? (field as { description: string }).description
220
+ : undefined;
221
+ return description ? z.any().describe(description) : z.any();
222
+ }
223
+
224
+ function buildCliArgSchema(def: ToolDefinition): Record<string, z.ZodTypeAny> {
225
+ return Object.fromEntries(
226
+ Object.entries(def.schema).flatMap(([key, zodType]) =>
227
+ getAcceptedCliOptionNames(key, def.cli?.options?.[key]).map((cliKey) => [
228
+ cliKey,
229
+ toOptionalCliSchemaField(zodType),
230
+ ]),
231
+ ),
232
+ );
233
+ }
234
+
235
+ function buildDisallowedCliSpellings(def: ToolDefinition): Map<string, string> {
184
236
  const disallowedSpellings = new Map<string, string>();
185
237
 
186
238
  for (const [key] of Object.entries(def.schema)) {
187
239
  const cliOption = def.cli?.options?.[key];
188
- if (!cliOption?.acceptedNames?.length) {
189
- continue;
190
- }
191
-
192
- const acceptedNames = new Set(cliOption.acceptedNames);
193
- const preferredLabel = formatCliOptionName(cliOption.preferredName ?? key);
240
+ const preferredLabel = formatCliOptionName(cliOption?.preferredName ?? key);
241
+ const acceptedNames = new Set(getAcceptedCliOptionNames(key, cliOption));
194
242
  const knownSpellings = new Set<string>([
195
243
  key,
196
244
  ...getKeyAliases(key),
197
- cliOption.preferredName ?? key,
198
- ...(cliOption.aliases ?? []),
199
- ...cliOption.acceptedNames,
245
+ ...(cliOption?.preferredName
246
+ ? getKeyAliases(cliOption.preferredName)
247
+ : []),
248
+ ...(cliOption?.aliases ?? []),
200
249
  ]);
201
250
 
202
251
  for (const spelling of knownSpellings) {
@@ -206,21 +255,46 @@ function validateRestrictedCliArgSpellings(
206
255
  }
207
256
  }
208
257
 
209
- for (const arg of args) {
210
- if (!arg.startsWith('--')) {
211
- continue;
212
- }
258
+ return disallowedSpellings;
259
+ }
213
260
 
214
- const key = arg.slice(2).split('=')[0];
215
- const preferredLabel = disallowedSpellings.get(key);
216
- if (!preferredLabel) {
217
- continue;
218
- }
261
+ function formatCliValidationError(
262
+ scriptName: string,
263
+ commandName: string,
264
+ def: ToolDefinition,
265
+ rawArgs: Record<string, unknown>,
266
+ ): string | undefined {
267
+ if (Object.keys(def.schema).length === 0) {
268
+ return undefined;
269
+ }
219
270
 
220
- throw new CLIError(
221
- `Unsupported option "--${key}" for ${scriptName} ${commandName}. Use "${preferredLabel}" instead.`,
222
- );
271
+ const cliSchema = z.object(buildCliArgSchema(def)).strict();
272
+ const parsed = cliSchema.safeParse(rawArgs);
273
+ if (parsed.success) {
274
+ return undefined;
223
275
  }
276
+
277
+ const disallowedSpellings = buildDisallowedCliSpellings(def);
278
+ const unknownKeys = parsed.error.issues.flatMap((issue) =>
279
+ issue.code === 'unrecognized_keys' ? issue.keys : [],
280
+ );
281
+
282
+ if (unknownKeys.length > 0) {
283
+ return unknownKeys
284
+ .map((key) => {
285
+ const preferredLabel = disallowedSpellings.get(key);
286
+ if (preferredLabel) {
287
+ return `Unsupported option "--${key}" for ${scriptName} ${commandName}. Use "${preferredLabel}" instead.`;
288
+ }
289
+ return `Unknown option "--${key}" for ${scriptName} ${commandName}.`;
290
+ })
291
+ .join('\n');
292
+ }
293
+
294
+ const [issue] = parsed.error.issues;
295
+ const optionName =
296
+ typeof issue?.path[0] === 'string' ? `--${issue.path[0]}` : 'CLI arguments';
297
+ return `Invalid value for "${optionName}" in ${scriptName} ${commandName}: ${issue?.message ?? parsed.error.message}`;
224
298
  }
225
299
 
226
300
  function printCommandHelp(scriptName: string, cmd: CLICommand): void {
@@ -343,21 +417,26 @@ export async function runToolsCLI(
343
417
  throw new CLIError(`Unknown command: ${commandName}`);
344
418
  }
345
419
 
346
- validateRestrictedCliArgSpellings(
347
- restArgs,
420
+ const rawCliArgs = parseRawCliArgs(restArgs);
421
+ if (rawCliArgs.help === true) {
422
+ debug('showing command help for: %s', match.name);
423
+ printCommandHelp(scriptName, match);
424
+ return;
425
+ }
426
+
427
+ const cliValidationError = formatCliValidationError(
348
428
  scriptName,
349
429
  match.name,
350
430
  match.def,
431
+ rawCliArgs,
351
432
  );
433
+ if (cliValidationError) {
434
+ throw new CLIError(cliValidationError);
435
+ }
436
+
352
437
  const parsedArgs = parseCliArgs(restArgs);
353
438
  debug('command: %s, args: %s', match.name, JSON.stringify(parsedArgs));
354
439
 
355
- if (parsedArgs.help === true) {
356
- debug('showing command help for: %s', match.name);
357
- printCommandHelp(scriptName, match);
358
- return;
359
- }
360
-
361
440
  const result = await match.def.handler(parsedArgs);
362
441
  debug(
363
442
  'command %s completed, isError: %s',
@@ -162,25 +162,19 @@ export abstract class BaseMidsceneTools<
162
162
  ? camelToKebab(key)
163
163
  : canonicalKey);
164
164
 
165
- const acceptedNames = this.initArgSpec!.cli?.preferBareKeys
166
- ? [...new Set([preferredName, ...getKeyAliases(key)])]
167
- : undefined;
168
- const aliases = new Set<string>(acceptedNames ?? getKeyAliases(key));
169
- if (this.initArgSpec!.cli?.preferBareKeys) {
170
- aliases.delete(preferredName);
171
- } else {
172
- for (const alias of getKeyAliases(canonicalKey)) {
173
- aliases.add(alias);
174
- }
175
- aliases.delete(preferredName);
176
- }
165
+ const acceptedNames = new Set<string>([
166
+ preferredName,
167
+ ...(this.initArgSpec!.cli?.preferBareKeys
168
+ ? getKeyAliases(key)
169
+ : getKeyAliases(canonicalKey)),
170
+ ]);
171
+ acceptedNames.delete(preferredName);
177
172
 
178
173
  return [
179
174
  canonicalKey,
180
175
  {
181
176
  preferredName,
182
- aliases: [...aliases],
183
- acceptedNames,
177
+ aliases: [...acceptedNames],
184
178
  },
185
179
  ];
186
180
  }),
package/src/mcp/types.ts CHANGED
@@ -50,7 +50,6 @@ export type ToolSchema = Record<string, z.ZodTypeAny>;
50
50
  export interface ToolCliOption {
51
51
  preferredName?: string;
52
52
  aliases?: string[];
53
- acceptedNames?: string[];
54
53
  }
55
54
 
56
55
  export interface ToolCliMetadata {