@zhin.js/command 1.0.7 → 1.0.9

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.
package/lib/provider.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { basename, join, parse, sep } from 'node:path';
2
- import { featureId } from '@zhin.js/plugin-runtime';
2
+ import { featureId, isCapabilityLocalSegment } from '@zhin.js/plugin-runtime';
3
3
  import { defineFeatureProvider, } from '@zhin.js/feature-kit';
4
4
  import { CommandIndex } from './command-index.js';
5
5
  import { bindCommandParameter, parseCommandDefinition, } from './definition.js';
@@ -38,7 +38,7 @@ async function* discoverCommandDirectory(context, directory, ancestors) {
38
38
  }
39
39
  }
40
40
  for (const entry of entries) {
41
- if (entry.kind === 'directory' && isCommandSegment(entry.name)) {
41
+ if (entry.kind === 'directory' && isCapabilityLocalSegment(entry.name)) {
42
42
  yield* discoverCommandDirectory(context, join(directory, entry.name), [...ancestors, entry.name]);
43
43
  continue;
44
44
  }
@@ -56,18 +56,20 @@ async function* discoverCommandDirectory(context, directory, ancestors) {
56
56
  };
57
57
  }
58
58
  }
59
- function isCommandSegment(value) {
60
- return /^[a-z0-9][a-z0-9-]*$/.test(value);
61
- }
62
59
  const dynamicCommandFilePatterns = [
63
60
  { pattern: /^\[\[\.\.\.([a-zA-Z][a-zA-Z0-9]*)\]\]\.(?:tsx?|[cm]?js)$/, optional: true, rest: true },
64
61
  { pattern: /^\[\.\.\.([a-zA-Z][a-zA-Z0-9]*)\]\.(?:tsx?|[cm]?js)$/, optional: false, rest: true },
65
62
  { pattern: /^\[\[([a-zA-Z][a-zA-Z0-9]*)\]\]\.(?:tsx?|[cm]?js)$/, optional: true, rest: false },
66
63
  { pattern: /^\[([a-zA-Z][a-zA-Z0-9]*)\]\.(?:tsx?|[cm]?js)$/, optional: false, rest: false },
67
64
  ];
65
+ const commandModuleExtension = /\.(?:tsx?|[cm]?js)$/u;
68
66
  function parseCommandFile(value) {
69
- if (/^[a-z0-9][a-z0-9-]*\.(?:tsx?|[cm]?js)$/.test(value)) {
70
- return { localSegment: parse(value).name };
67
+ // 静态段:ASCII kebab(hello.ts)或 Unicode 名(赞我.ts);与 isCapabilityLocalSegment 对齐。
68
+ if (commandModuleExtension.test(value)) {
69
+ const localSegment = parse(value).name;
70
+ if (isCapabilityLocalSegment(localSegment)) {
71
+ return { localSegment };
72
+ }
71
73
  }
72
74
  for (const { pattern, optional, rest } of dynamicCommandFilePatterns) {
73
75
  const match = pattern.exec(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/command",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Convention-based Command Feature for Zhin Plugin Runtime",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -18,8 +18,9 @@
18
18
  ],
19
19
  "dependencies": {
20
20
  "segment-matcher": "^1.0.5",
21
- "@zhin.js/feature-kit": "1.0.6",
22
- "@zhin.js/plugin-runtime": "1.1.3"
21
+ "@zhin.js/feature-kit": "1.0.8",
22
+ "@zhin.js/plugin-runtime": "1.1.5",
23
+ "@zhin.js/permission": "1.0.1"
23
24
  },
24
25
  "devDependencies": {
25
26
  "@types/node": "^26.1.2",
@@ -10,12 +10,15 @@ import type {
10
10
  } from '@zhin.js/plugin-runtime';
11
11
  import {
12
12
  createCommandContext,
13
+ resolveCommandSession,
13
14
  type CommandDefinition,
14
15
  type CommandParameterDefinition,
15
16
  type CommandParameterType,
16
17
  type CommandParameterValue,
17
18
  type CommandSegment,
18
19
  } from './definition.js';
20
+ import { permissionHostToken, type PermissionHost } from '@zhin.js/permission';
21
+ import { toPermissionSubject } from '@zhin.js/permission';
19
22
 
20
23
  export interface CommandParameterDescriptor extends CommandParameterDefinition {
21
24
  readonly required: boolean;
@@ -26,6 +29,10 @@ export interface CommandDescriptor {
26
29
  readonly description?: string;
27
30
  readonly source: string;
28
31
  readonly parameters: readonly CommandParameterDescriptor[];
32
+ readonly alias?: readonly string[];
33
+ readonly permit?: readonly string[];
34
+ /** shortcut 触发键列表(不含预填 params)。 */
35
+ readonly shortcut?: readonly string[];
29
36
  }
30
37
 
31
38
  export interface CommandDispatchResult {
@@ -39,7 +46,18 @@ interface CommandRecord extends CommandDescriptor {
39
46
  readonly slot: Readonly<CapabilitySlot<CommandDefinition>>;
40
47
  readonly segments: readonly string[];
41
48
  readonly parameter?: CommandParameterDefinition;
49
+ }
50
+
51
+ interface CommandRoute {
52
+ readonly record: CommandRecord;
53
+ readonly segments: readonly string[];
42
54
  readonly matcher: SegmentMatcher;
55
+ readonly kind: 'primary' | 'alias';
56
+ }
57
+
58
+ interface ShortcutEntry {
59
+ readonly record: CommandRecord;
60
+ readonly params: Readonly<Record<string, CommandParameterValue>>;
43
61
  }
44
62
 
45
63
  interface CommandMatch {
@@ -66,19 +84,39 @@ const segmentFields = {
66
84
  export class CommandIndex {
67
85
  readonly $projection = 'zhin.command-index/1' as const;
68
86
  readonly #commands: readonly CommandRecord[];
87
+ readonly #routes: readonly CommandRoute[];
88
+ readonly #shortcuts: ReadonlyMap<string, ShortcutEntry>;
69
89
 
70
90
  constructor(
71
91
  slots: readonly Readonly<CapabilitySlot<CommandDefinition>>[],
72
92
  private readonly snapshot: RuntimeSnapshot,
73
93
  ) {
74
94
  const commands: CommandRecord[] = [];
75
- const staticCommands = new Map<string, CommandRecord>();
76
- const dynamicCommands = new Map<string, CommandRecord>();
95
+ const routes: CommandRoute[] = [];
96
+ const occupancy = new Map<string, string>();
97
+ const shortcuts = new Map<string, ShortcutEntry>();
98
+
99
+ const claim = (key: string, source: string): void => {
100
+ const existing = occupancy.get(key);
101
+ if (existing !== undefined) {
102
+ throw new Error(`Duplicate Command route "${key}" (${source} vs ${existing})`);
103
+ }
104
+ occupancy.set(key, source);
105
+ };
106
+
77
107
  for (const slot of slots) {
78
- const segments = runtimeSegments(slot.owner, slot.localName);
108
+ const primarySegments = runtimeSegments(slot.owner, slot.localName);
79
109
  const parameter = slot.definition.$parameter;
80
- assertParameterSegment(segments, parameter, slot.source);
81
- const name = displayName(segments, parameter);
110
+ assertParameterSegment(primarySegments, parameter, slot.source);
111
+ const name = displayName(primarySegments, parameter);
112
+ const alias = normalizeAliasList(slot.definition.alias);
113
+ const permit = slot.definition.permit
114
+ ? Object.freeze([...slot.definition.permit])
115
+ : undefined;
116
+ const shortcutKeys = slot.definition.shortcut
117
+ ? Object.freeze(Object.keys(slot.definition.shortcut).map((key) => key.trim()))
118
+ : undefined;
119
+
82
120
  const record: CommandRecord = Object.freeze({
83
121
  name,
84
122
  description: slot.definition.description,
@@ -87,23 +125,57 @@ export class CommandIndex {
87
125
  ...parameter,
88
126
  required: isRequiredParameter(parameter),
89
127
  }] : []),
128
+ ...(alias ? { alias } : {}),
129
+ ...(permit ? { permit } : {}),
130
+ ...(shortcutKeys && shortcutKeys.length > 0 ? { shortcut: shortcutKeys } : {}),
90
131
  slot,
91
- segments: Object.freeze(segments),
132
+ segments: Object.freeze(primarySegments),
92
133
  parameter,
93
- matcher: new SegmentMatcher(matcherPattern(segments, parameter), segmentFields),
94
134
  });
95
- if (!parameter) {
96
- const key = segments.join(' ');
97
- if (staticCommands.has(key)) throw duplicateCommand(key);
98
- staticCommands.set(key, record);
99
- } else {
100
- const shape = routeShape(segments);
101
- if (dynamicCommands.has(shape)) throw duplicateCommand(name);
102
- dynamicCommands.set(shape, record);
135
+
136
+ claim(occupancyKey(primarySegments, parameter), slot.source);
137
+ routes.push({
138
+ record,
139
+ segments: primarySegments,
140
+ matcher: new SegmentMatcher(matcherPattern(primarySegments, parameter), segmentFields),
141
+ kind: 'primary',
142
+ });
143
+
144
+ if (alias) {
145
+ for (const entry of alias) {
146
+ const aliasSegments = aliasRuntimeSegments(slot.owner, entry, primarySegments);
147
+ assertParameterSegment(aliasSegments, parameter, `${slot.source} alias ${JSON.stringify(entry)}`);
148
+ claim(occupancyKey(aliasSegments, parameter), `${slot.source} alias ${JSON.stringify(entry)}`);
149
+ routes.push({
150
+ record,
151
+ segments: Object.freeze(aliasSegments),
152
+ matcher: new SegmentMatcher(matcherPattern(aliasSegments, parameter), segmentFields),
153
+ kind: 'alias',
154
+ });
155
+ }
156
+ }
157
+
158
+ if (slot.definition.shortcut) {
159
+ for (const [rawTrigger, prefill] of Object.entries(slot.definition.shortcut)) {
160
+ const trigger = rawTrigger.trim();
161
+ claim(trigger, `${slot.source} shortcut ${JSON.stringify(trigger)}`);
162
+ shortcuts.set(trigger, {
163
+ record,
164
+ params: Object.freeze(resolveShortcutParams(
165
+ slot.definition,
166
+ prefill,
167
+ `${slot.source} shortcut ${JSON.stringify(trigger)}`,
168
+ )),
169
+ });
170
+ }
103
171
  }
172
+
104
173
  commands.push(record);
105
174
  }
106
- this.#commands = Object.freeze(commands.sort(compareCommands));
175
+
176
+ this.#commands = Object.freeze(commands.sort(compareRecords));
177
+ this.#routes = Object.freeze(routes.sort(compareRoutes));
178
+ this.#shortcuts = shortcuts;
107
179
  }
108
180
 
109
181
  list(): readonly CommandDescriptor[] {
@@ -125,6 +197,7 @@ export class CommandIndex {
125
197
  this.#diagnoseParameter(name);
126
198
  throw new Error(`Unknown Command: ${name}`);
127
199
  }
200
+ // Host / 无 session:跳过 permit。
128
201
  return match.command.slot.definition.execute(
129
202
  createCommandContext(
130
203
  this.snapshot,
@@ -139,8 +212,34 @@ export class CommandIndex {
139
212
  input: CommandMatchInput,
140
213
  source: unknown = undefined,
141
214
  ): Promise<CommandDispatchResult> {
215
+ const shortcut = this.#matchShortcut(input);
216
+ if (shortcut) {
217
+ if (!(await this.#permitAllows(shortcut.record, source))) {
218
+ return Object.freeze({ matched: false });
219
+ }
220
+ const value = await shortcut.record.slot.definition.execute(
221
+ createCommandContext(
222
+ this.snapshot,
223
+ shortcut.record.slot.owner,
224
+ Object.freeze([]),
225
+ shortcut.params,
226
+ source,
227
+ Object.freeze([]),
228
+ ),
229
+ );
230
+ return Object.freeze({
231
+ matched: true,
232
+ command: shortcut.record.name,
233
+ owner: shortcut.record.slot.owner,
234
+ value,
235
+ });
236
+ }
237
+
142
238
  const match = this.#match(input, false);
143
239
  if (!match) return Object.freeze({ matched: false });
240
+ if (!(await this.#permitAllows(match.command, source))) {
241
+ return Object.freeze({ matched: false });
242
+ }
144
243
  const args = textArgs(match.remaining);
145
244
  const value = await match.command.slot.definition.execute(
146
245
  createCommandContext(
@@ -160,6 +259,35 @@ export class CommandIndex {
160
259
  });
161
260
  }
162
261
 
262
+ async #permitAllows(record: CommandRecord, source: unknown): Promise<boolean> {
263
+ const permits = record.permit;
264
+ if (!permits || permits.length === 0) return true;
265
+ if (!hasImSession(source)) return true;
266
+ const host = this.#resolveHost();
267
+ if (!host) return false;
268
+ const subject = toPermissionSubject(resolveCommandSession(source));
269
+ return host.checkAll(permits, subject);
270
+ }
271
+
272
+ #resolveHost(): PermissionHost | undefined {
273
+ try {
274
+ const resources = this.snapshot.resources.get(this.snapshot.root);
275
+ if (!resources) return undefined;
276
+ const host = resources.get(permissionHostToken.id);
277
+ return host && typeof (host as PermissionHost).check === 'function'
278
+ ? host as PermissionHost
279
+ : undefined;
280
+ } catch {
281
+ return undefined;
282
+ }
283
+ }
284
+
285
+ #matchShortcut(input: CommandMatchInput): ShortcutEntry | undefined {
286
+ const text = exactMessageText(input);
287
+ if (text === undefined) return undefined;
288
+ return this.#shortcuts.get(text);
289
+ }
290
+
163
291
  #match(input: CommandMatchInput, exact: boolean): CommandMatch | undefined {
164
292
  const segments = normalizeSegments(
165
293
  typeof input === 'string'
@@ -170,29 +298,26 @@ export class CommandIndex {
170
298
  );
171
299
  if (segments.length === 0) return undefined;
172
300
 
173
- for (const command of this.#commands) {
174
- const result = command.matcher.match(asMatcherSegments(segments));
301
+ for (const route of this.#routes) {
302
+ const result = route.matcher.match(asMatcherSegments(segments));
175
303
  if (!result || !hasCommandBoundary(result.remaining)) continue;
176
- const parameter = command.parameter;
304
+ const parameter = route.record.parameter;
177
305
  const params: Record<string, CommandParameterValue> = { ...result.params };
178
306
  if (parameter?.rest) {
179
307
  const raw = result.params[parameter.name];
180
308
  const coerced = coerceRestValues(parameter, Array.isArray(raw) ? raw : []);
181
- // 必需 `[...name]` 捕获所有:零元素视为不匹配;标量逐词转换失败同样不匹配。
182
309
  if (!coerced || (isRequiredParameter(parameter) && coerced.length === 0)) continue;
183
310
  params[parameter.name] = coerced;
184
311
  }
185
312
  const remaining = normalizeSegments(result.remaining);
186
313
  if (exact && remaining.length > 0) continue;
187
- // `[[name]]` 无 default 且未命中时,matcher 对 text 回退 ''、其他类型回退 null;
188
- // 按契约(省略 default 时未匹配为 undefined)删除该键。
189
314
  if (parameter && !parameter.rest && parameter.optional === true
190
315
  && parameter.defaultValue === undefined
191
316
  && (params[parameter.name] === '' || params[parameter.name] === null)) {
192
317
  delete params[parameter.name];
193
318
  }
194
319
  return {
195
- command,
320
+ command: route.record,
196
321
  params: Object.freeze(params),
197
322
  remaining,
198
323
  };
@@ -202,12 +327,12 @@ export class CommandIndex {
202
327
 
203
328
  #diagnoseParameter(name: string): void {
204
329
  const words = splitCommand(name);
205
- for (const command of this.#commands) {
206
- const parameter = command.parameter;
330
+ for (const route of this.#routes) {
331
+ const parameter = route.record.parameter;
207
332
  if (!parameter || parameter.rest) continue;
208
- const parameterIndex = command.segments.findIndex((segment) => segment.startsWith('$'));
209
- if (words.length !== command.segments.length) continue;
210
- if (!command.segments.every((segment, index) =>
333
+ const parameterIndex = route.segments.findIndex((segment) => segment.startsWith('$'));
334
+ if (words.length !== route.segments.length) continue;
335
+ if (!route.segments.every((segment, index) =>
211
336
  index === parameterIndex || segment === words[index])) continue;
212
337
  const value = words[parameterIndex];
213
338
  if (value === undefined || matchesParameter(parameter.type, value)) continue;
@@ -234,6 +359,103 @@ function runtimeSegments(owner: string, localName: string): string[] {
234
359
  return [`${prefix}.${localSegments[0]}`, ...localSegments.slice(1)];
235
360
  }
236
361
 
362
+ /**
363
+ * 用 alias 词序列替换全部本地静态段,再按 owner 规则重挂前缀;动态段保留。
364
+ */
365
+ function aliasRuntimeSegments(
366
+ owner: string,
367
+ alias: string,
368
+ primarySegments: readonly string[],
369
+ ): string[] {
370
+ const aliasTokens = alias.trim().split(/\s+/u).filter(Boolean);
371
+ const dynamicTail = primarySegments.filter((segment) => segment.startsWith('$'));
372
+ if (owner === 'root') return [...aliasTokens, ...dynamicTail];
373
+ const prefix = owner.slice('root/'.length).split('/').join('.');
374
+ return [`${prefix}.${aliasTokens[0]}`, ...aliasTokens.slice(1), ...dynamicTail];
375
+ }
376
+
377
+ function occupancyKey(
378
+ segments: readonly string[],
379
+ parameter: CommandParameterDefinition | undefined,
380
+ ): string {
381
+ return parameter ? routeShape(segments) : segments.join(' ');
382
+ }
383
+
384
+ function normalizeAliasList(
385
+ alias: readonly string[] | undefined,
386
+ ): readonly string[] | undefined {
387
+ if (!alias || alias.length === 0) return undefined;
388
+ return Object.freeze(alias.map((entry) => entry.trim().split(/\s+/u).filter(Boolean).join(' ')));
389
+ }
390
+
391
+ function resolveShortcutParams(
392
+ definition: CommandDefinition,
393
+ prefill: Readonly<Record<string, CommandParameterValue>>,
394
+ source: string,
395
+ ): Record<string, CommandParameterValue> {
396
+ const allowed = new Set<string>();
397
+ const parameter = definition.$parameter;
398
+ if (parameter) allowed.add(parameter.name);
399
+ if (definition.params) {
400
+ for (const key of Object.keys(definition.params)) allowed.add(key);
401
+ }
402
+
403
+ for (const key of Object.keys(prefill)) {
404
+ if (!allowed.has(key)) {
405
+ throw new TypeError(
406
+ `Invalid shortcut params for ${source}: unknown key ${JSON.stringify(key)}`,
407
+ );
408
+ }
409
+ }
410
+
411
+ const result: Record<string, CommandParameterValue> = { ...prefill };
412
+
413
+ if (parameter) {
414
+ if (result[parameter.name] === undefined) {
415
+ if (parameter.defaultValue !== undefined) {
416
+ result[parameter.name] = parameter.defaultValue;
417
+ } else if (isRequiredParameter(parameter)) {
418
+ throw new TypeError(
419
+ `Invalid shortcut params for ${source}: missing required ${parameter.name}`,
420
+ );
421
+ }
422
+ }
423
+ } else if (allowed.size === 0 && Object.keys(prefill).length > 0) {
424
+ throw new TypeError(
425
+ `Invalid shortcut params for ${source}: command has no params declaration`,
426
+ );
427
+ }
428
+
429
+ if (definition.params) {
430
+ for (const [name, schema] of Object.entries(definition.params)) {
431
+ if (result[name] === undefined && schema.default !== undefined) {
432
+ result[name] = schema.default;
433
+ }
434
+ }
435
+ }
436
+
437
+ return result;
438
+ }
439
+
440
+ function hasImSession(source: unknown): boolean {
441
+ if (!source || typeof source !== 'object') return false;
442
+ const conversation = (source as { conversation?: unknown }).conversation;
443
+ return !!conversation && typeof conversation === 'object';
444
+ }
445
+
446
+ function exactMessageText(input: CommandMatchInput): string | undefined {
447
+ if (typeof input === 'string') {
448
+ const trimmed = input.trim();
449
+ return trimmed || undefined;
450
+ }
451
+ // 仅纯单 text 段可作整句 shortcut;含 mention/image 等则不走 shortcut。
452
+ if (input.length !== 1) return undefined;
453
+ const only = input[0];
454
+ if (!only || only.type !== 'text' || typeof only.data.text !== 'string') return undefined;
455
+ const trimmed = only.data.text.trim();
456
+ return trimmed || undefined;
457
+ }
458
+
237
459
  function assertParameterSegment(
238
460
  segments: readonly string[],
239
461
  parameter: CommandParameterDefinition | undefined,
@@ -280,7 +502,6 @@ function matcherPattern(
280
502
  if (!segment.startsWith('$')) return segment;
281
503
  if (!parameter) throw new Error(`Missing Command parameter metadata: ${segment}`);
282
504
  const type = matcherType(parameter.type);
283
- // rest:结构化类型按消息段收集;标量类型先按 text 段收集,再在 #match 里逐词切分转换。
284
505
  if (parameter.rest) {
285
506
  return `[...${parameter.name}:${isStructuredRestType(parameter.type) ? type : 'text'}]`;
286
507
  }
@@ -295,7 +516,6 @@ function matcherType(type: CommandParameterType): string {
295
516
  return type === 'string' ? 'word' : type;
296
517
  }
297
518
 
298
- /** rest 参数中按消息段收集(matcher 原生行为)的结构化类型。 */
299
519
  function isStructuredRestType(type: CommandParameterType): boolean {
300
520
  return type === 'mention'
301
521
  || type === 'image'
@@ -306,19 +526,12 @@ function isStructuredRestType(type: CommandParameterType): boolean {
306
526
  || type === 'rps';
307
527
  }
308
528
 
309
- /**
310
- * 捕获所有参数的取值粒度由类型决定:
311
- * - `text` / 结构化类型:逐消息段(matcher 原生结果);
312
- * - `word` / `string`:逐词(空白切分);
313
- * - `number` / `integer` / `float` / `boolean`:逐词切分后逐个转换,任一失败返回 undefined(不匹配)。
314
- */
315
529
  function coerceRestValues(
316
530
  parameter: CommandParameterDefinition,
317
531
  values: readonly unknown[],
318
532
  ): readonly (string | number | boolean)[] | undefined {
319
533
  const type = parameter.type;
320
534
  if (type === 'text' || isStructuredRestType(type)) {
321
- // 逐消息段,保持 matcher 原生提取值(text 为 string,结构化类型可能是 number 等)。
322
535
  return values as readonly (string | number | boolean)[];
323
536
  }
324
537
  const words = values.flatMap((value) =>
@@ -346,18 +559,21 @@ function routeShape(segments: readonly string[]): string {
346
559
  return segments.map((segment) => segment.startsWith('$') ? '$' : segment).join(' ');
347
560
  }
348
561
 
349
- function compareCommands(left: CommandRecord, right: CommandRecord): number {
562
+ function compareRecords(left: CommandRecord, right: CommandRecord): number {
563
+ return left.name.localeCompare(right.name);
564
+ }
565
+
566
+ function compareRoutes(left: CommandRoute, right: CommandRoute): number {
350
567
  return dynamicWeight(left) - dynamicWeight(right)
351
568
  || staticSegmentCount(right.segments) - staticSegmentCount(left.segments)
352
569
  || right.segments.length - left.segments.length
353
- || right.name.length - left.name.length
354
- || left.name.localeCompare(right.name);
570
+ || right.record.name.length - left.record.name.length
571
+ || left.record.name.localeCompare(right.record.name);
355
572
  }
356
573
 
357
- /** 静态 < 单参数 < 捕获所有:更具体的形状优先匹配。 */
358
- function dynamicWeight(command: CommandRecord): number {
359
- if (!command.parameter) return 0;
360
- return command.parameter.rest ? 2 : 1;
574
+ function dynamicWeight(route: CommandRoute): number {
575
+ if (!route.record.parameter) return 0;
576
+ return route.record.parameter.rest ? 2 : 1;
361
577
  }
362
578
 
363
579
  function staticSegmentCount(segments: readonly string[]): number {
@@ -435,16 +651,11 @@ function toDescriptor({
435
651
  slot: _slot,
436
652
  segments: _segments,
437
653
  parameter: _parameter,
438
- matcher: _matcher,
439
654
  ...descriptor
440
655
  }: CommandRecord): CommandDescriptor {
441
656
  return descriptor;
442
657
  }
443
658
 
444
- function duplicateCommand(name: string): Error {
445
- return new Error(`Duplicate runtime Command: ${name}`);
446
- }
447
-
448
659
  export class CommandParameterValueError extends TypeError {
449
660
  constructor(name: string, type: CommandParameterType, value: string) {
450
661
  super(`Invalid value for Command parameter ${name}:${type}: ${value}`);