unreal-engine-mcp-server 0.5.1 → 0.5.2

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 (75) hide show
  1. package/.github/workflows/publish-mcp.yml +1 -4
  2. package/.github/workflows/release-drafter.yml +2 -1
  3. package/CHANGELOG.md +38 -0
  4. package/dist/automation/bridge.d.ts +1 -2
  5. package/dist/automation/bridge.js +24 -23
  6. package/dist/automation/connection-manager.d.ts +1 -0
  7. package/dist/automation/connection-manager.js +10 -0
  8. package/dist/automation/message-handler.js +5 -4
  9. package/dist/automation/request-tracker.d.ts +4 -0
  10. package/dist/automation/request-tracker.js +11 -3
  11. package/dist/tools/actors.d.ts +19 -1
  12. package/dist/tools/actors.js +15 -5
  13. package/dist/tools/assets.js +1 -1
  14. package/dist/tools/blueprint.d.ts +12 -0
  15. package/dist/tools/blueprint.js +43 -14
  16. package/dist/tools/consolidated-tool-definitions.js +2 -1
  17. package/dist/tools/editor.js +3 -2
  18. package/dist/tools/handlers/actor-handlers.d.ts +1 -1
  19. package/dist/tools/handlers/actor-handlers.js +14 -8
  20. package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
  21. package/dist/tools/handlers/sequence-handlers.js +24 -13
  22. package/dist/tools/introspection.d.ts +1 -1
  23. package/dist/tools/introspection.js +1 -1
  24. package/dist/tools/level.js +3 -3
  25. package/dist/tools/lighting.d.ts +54 -7
  26. package/dist/tools/lighting.js +4 -4
  27. package/dist/tools/materials.d.ts +1 -1
  28. package/dist/types/tool-types.d.ts +2 -0
  29. package/dist/unreal-bridge.js +4 -4
  30. package/dist/utils/command-validator.js +6 -5
  31. package/dist/utils/error-handler.d.ts +24 -2
  32. package/dist/utils/error-handler.js +58 -23
  33. package/dist/utils/normalize.d.ts +7 -4
  34. package/dist/utils/normalize.js +12 -10
  35. package/dist/utils/response-validator.js +88 -73
  36. package/dist/utils/unreal-command-queue.d.ts +2 -0
  37. package/dist/utils/unreal-command-queue.js +8 -1
  38. package/docs/handler-mapping.md +4 -2
  39. package/package.json +1 -1
  40. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +298 -33
  41. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +7 -8
  42. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +229 -319
  43. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +98 -0
  44. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +24 -0
  45. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +96 -0
  46. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +52 -5
  47. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +5 -268
  48. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +57 -2
  49. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +0 -1
  50. package/server.json +3 -3
  51. package/src/automation/bridge.ts +27 -25
  52. package/src/automation/connection-manager.ts +18 -0
  53. package/src/automation/message-handler.ts +33 -8
  54. package/src/automation/request-tracker.ts +39 -7
  55. package/src/server/tool-registry.ts +3 -3
  56. package/src/tools/actors.ts +44 -19
  57. package/src/tools/assets.ts +3 -3
  58. package/src/tools/blueprint.ts +115 -49
  59. package/src/tools/consolidated-tool-definitions.ts +2 -1
  60. package/src/tools/editor.ts +4 -3
  61. package/src/tools/handlers/actor-handlers.ts +14 -9
  62. package/src/tools/handlers/sequence-handlers.ts +86 -63
  63. package/src/tools/introspection.ts +7 -7
  64. package/src/tools/level.ts +6 -6
  65. package/src/tools/lighting.ts +19 -19
  66. package/src/tools/materials.ts +1 -1
  67. package/src/tools/sequence.ts +1 -1
  68. package/src/tools/ui.ts +1 -1
  69. package/src/types/tool-types.ts +4 -0
  70. package/src/unreal-bridge.ts +71 -26
  71. package/src/utils/command-validator.ts +46 -5
  72. package/src/utils/error-handler.ts +128 -45
  73. package/src/utils/normalize.ts +38 -16
  74. package/src/utils/response-validator.ts +103 -87
  75. package/src/utils/unreal-command-queue.ts +13 -1
@@ -5,14 +5,59 @@ import { DEFAULT_AUTOMATION_HOST, DEFAULT_AUTOMATION_PORT } from './constants.js
5
5
  import { UnrealCommandQueue } from './utils/unreal-command-queue.js';
6
6
  import { CommandValidator } from './utils/command-validator.js';
7
7
 
8
+ /** Connection event payload for automation bridge events */
9
+ interface ConnectionEventInfo {
10
+ host?: string;
11
+ port?: number;
12
+ reason?: string;
13
+ error?: string;
14
+ [key: string]: unknown;
15
+ }
16
+
17
+ /** Result object from automation requests */
18
+ interface AutomationResult {
19
+ value?: unknown;
20
+ propertyValue?: unknown;
21
+ message?: string;
22
+ warnings?: string[];
23
+ [key: string]: unknown;
24
+ }
25
+
26
+ /** Subsystems feature flags */
27
+ interface SubsystemFlags {
28
+ unrealEditor?: boolean;
29
+ levelEditor?: boolean;
30
+ editorActor?: boolean;
31
+ [key: string]: unknown;
32
+ }
33
+
34
+ /** Engine version result */
35
+ interface EngineVersionResult {
36
+ version?: string;
37
+ major?: number;
38
+ minor?: number;
39
+ patch?: number;
40
+ isUE56OrAbove?: boolean;
41
+ [key: string]: unknown;
42
+ }
43
+
44
+ /** Console command response */
45
+ interface ConsoleCommandResponse {
46
+ success?: boolean;
47
+ message?: string;
48
+ error?: string;
49
+ transport?: string;
50
+ [key: string]: unknown;
51
+ }
52
+
8
53
  export class UnrealBridge {
9
54
  private log = new Logger('UnrealBridge');
10
55
  private connected = false;
11
56
  private automationBridge?: AutomationBridge;
12
57
  private automationBridgeListeners?: {
13
- connected: (info: any) => void;
14
- disconnected: (info: any) => void;
15
- handshakeFailed: (info: any) => void;
58
+ connected: (info: ConnectionEventInfo) => void;
59
+ disconnected: (info: ConnectionEventInfo) => void;
60
+ handshakeFailed: (info: ConnectionEventInfo) => void;
16
61
  };
17
62
 
18
63
  // Command queue for throttling
@@ -35,17 +80,17 @@ export class UnrealBridge {
35
80
  return;
36
81
  }
37
82
 
38
- const onConnected = (info: any) => {
83
+ const onConnected = (info: ConnectionEventInfo) => {
39
84
  this.connected = true;
40
85
  this.log.debug('Automation bridge connected', info);
41
86
  };
42
87
 
43
- const onDisconnected = (info: any) => {
88
+ const onDisconnected = (info: ConnectionEventInfo) => {
44
89
  this.connected = false;
45
90
  this.log.debug('Automation bridge disconnected', info);
46
91
  };
47
92
 
48
- const onHandshakeFailed = (info: any) => {
93
+ const onHandshakeFailed = (info: ConnectionEventInfo) => {
49
94
  this.connected = false;
50
95
  this.log.warn('Automation bridge handshake failed', info);
51
96
  };
@@ -279,13 +324,13 @@ export class UnrealBridge {
279
324
  );
280
325
 
281
326
  const success = response.success !== false;
282
- const rawResult =
327
+ const rawResult: AutomationResult | undefined =
283
328
  response.result && typeof response.result === 'object'
284
329
  ? { ...(response.result as Record<string, unknown>) }
285
- : response.result;
330
+ : undefined;
286
331
  const value =
287
- (rawResult as any)?.value ??
288
- (rawResult as any)?.propertyValue ??
332
+ rawResult?.value ??
333
+ rawResult?.propertyValue ??
289
334
  (success ? rawResult : undefined);
290
335
 
291
336
  if (success) {
@@ -297,8 +342,8 @@ export class UnrealBridge {
297
342
  propertyValue: value,
298
343
  transport: 'automation_bridge',
299
344
  message: response.message,
300
- warnings: Array.isArray((rawResult as any)?.warnings)
301
- ? (rawResult as any).warnings
345
+ warnings: Array.isArray(rawResult?.warnings)
346
+ ? rawResult.warnings
302
347
  : undefined,
303
348
  raw: rawResult,
304
349
  bridge: {
@@ -389,10 +434,10 @@ export class UnrealBridge {
389
434
  );
390
435
 
391
436
  const success = response.success !== false;
392
- const rawResult =
437
+ const rawResult: AutomationResult | undefined =
393
438
  response.result && typeof response.result === 'object'
394
439
  ? { ...(response.result as Record<string, unknown>) }
395
- : response.result;
440
+ : undefined;
396
441
 
397
442
  if (success) {
398
443
  return {
@@ -401,7 +446,7 @@ export class UnrealBridge {
401
446
  propertyName,
402
447
  message:
403
448
  response.message ||
404
- (typeof (rawResult as any)?.message === 'string' ? (rawResult as any).message : undefined),
449
+ (typeof rawResult?.message === 'string' ? rawResult.message : undefined),
405
450
  transport: 'automation_bridge',
406
451
  raw: rawResult,
407
452
  bridge: {
@@ -469,14 +514,14 @@ export class UnrealBridge {
469
514
  throw new Error('Automation bridge not connected');
470
515
  }
471
516
 
472
- const pluginResp: any = await this.automationBridge.sendAutomationRequest(
517
+ const pluginResp: ConsoleCommandResponse = await this.automationBridge.sendAutomationRequest(
473
518
  'console_command',
474
519
  { command: cmdTrimmed },
475
520
  { timeoutMs: 30000 }
476
521
  );
477
522
 
478
523
  if (pluginResp && pluginResp.success) {
479
- return { ...(pluginResp as any), transport: 'automation_bridge' };
524
+ return { ...pluginResp, transport: 'automation_bridge' };
480
525
  }
481
526
 
482
527
  const errMsg = pluginResp?.message || pluginResp?.error || 'Plugin execution failed';
@@ -553,14 +598,14 @@ export class UnrealBridge {
553
598
 
554
599
  const bridge = this.getAutomationBridge();
555
600
  try {
556
- const resp: any = await bridge.sendAutomationRequest(
601
+ const resp = await bridge.sendAutomationRequest(
557
602
  'system_control',
558
603
  { action: 'get_engine_version' },
559
604
  { timeoutMs: 15000 }
560
605
  );
561
- const raw = resp && typeof resp.result === 'object'
562
- ? (resp.result as any)
563
- : (resp?.result ?? resp ?? {});
606
+ const raw: EngineVersionResult = resp && typeof resp.result === 'object'
607
+ ? (resp.result as Record<string, unknown>)
608
+ : (resp?.result as Record<string, unknown>) ?? resp ?? {};
564
609
  const version = typeof raw.version === 'string' ? raw.version : 'unknown';
565
610
  const major = typeof raw.major === 'number' ? raw.major : 0;
566
611
  const minor = typeof raw.minor === 'number' ? raw.minor : 0;
@@ -596,16 +641,16 @@ export class UnrealBridge {
596
641
 
597
642
  const bridge = this.getAutomationBridge();
598
643
  try {
599
- const resp: any = await bridge.sendAutomationRequest(
644
+ const resp = await bridge.sendAutomationRequest(
600
645
  'system_control',
601
646
  { action: 'get_feature_flags' },
602
647
  { timeoutMs: 15000 }
603
648
  );
604
649
  const raw = resp && typeof resp.result === 'object'
605
- ? (resp.result as any)
606
- : (resp?.result ?? resp ?? {});
607
- const subs = raw && typeof raw.subsystems === 'object'
608
- ? (raw.subsystems as any)
650
+ ? (resp.result as Record<string, unknown>)
651
+ : (resp?.result as Record<string, unknown>) ?? resp ?? {};
652
+ const subs: SubsystemFlags = raw && typeof raw.subsystems === 'object'
653
+ ? (raw.subsystems as SubsystemFlags)
609
654
  : {};
610
655
  return {
611
656
  subsystems: {
@@ -1,28 +1,58 @@
1
+ /**
2
+ * Validates console commands before execution to prevent dangerous operations.
3
+ * Blocks crash-inducing commands, shell injection, and Python execution.
4
+ */
1
5
  export class CommandValidator {
6
+ /**
7
+ * Commands that can crash the engine or cause severe instability.
8
+ * These are blocked unconditionally.
9
+ */
2
10
  private static readonly DANGEROUS_COMMANDS = [
3
- 'quit', 'exit', 'delete', 'destroy', 'kill', 'crash',
11
+ // Engine termination commands
12
+ 'quit', 'exit', 'kill', 'crash',
13
+ // Crash-inducing commands
14
+ 'r.gpucrash', 'r.crash', 'debug crash', 'forcecrash', 'debug break',
15
+ 'assert false', 'check(false)',
16
+ // View buffer commands that can crash on some hardware
4
17
  'viewmode visualizebuffer basecolor',
5
18
  'viewmode visualizebuffer worldnormal',
6
- 'r.gpucrash',
7
- 'buildpaths', // Can cause access violation if nav system not initialized
8
- 'rebuildnavigation', // Can also crash without proper nav setup
9
- 'obj garbage', 'obj list', 'memreport' // Heavy debug commands that can stall
19
+ // Heavy operations that can cause access violations if systems not initialized
20
+ 'buildpaths', 'rebuildnavigation',
21
+ // Heavy debug commands that can stall or crash
22
+ 'obj garbage', 'obj list', 'memreport',
23
+ // Potentially destructive without proper setup
24
+ 'delete', 'destroy'
10
25
  ];
11
26
 
27
+ /**
28
+ * Tokens that indicate shell injection or external system access attempts.
29
+ * Any command containing these is blocked.
30
+ */
12
31
  private static readonly FORBIDDEN_TOKENS = [
32
+ // Shell commands (Windows/Unix)
13
33
  'rm ', 'rm-', 'del ', 'format ', 'shutdown', 'reboot',
14
34
  'rmdir', 'mklink', 'copy ', 'move ', 'start "', 'system(',
35
+ // Python injection attempts
15
36
  'import os', 'import subprocess', 'subprocess.', 'os.system',
16
37
  'exec(', 'eval(', '__import__', 'import sys', 'import importlib',
17
38
  'with open', 'open(', 'write(', 'read('
18
39
  ];
19
40
 
41
+ /**
42
+ * Patterns that indicate obviously invalid commands.
43
+ * Used to warn about likely typos or invalid input.
44
+ */
20
45
  private static readonly INVALID_PATTERNS = [
21
46
  /^\d+$/, // Just numbers
22
47
  /^invalid_command/i,
23
48
  /^this_is_not_a_valid/i,
24
49
  ];
25
50
 
51
+ /**
52
+ * Validates a console command for safety before execution.
53
+ * @param command - The console command string to validate
54
+ * @throws Error if the command is dangerous, contains forbidden tokens, or is invalid
55
+ */
26
56
  static validate(command: string): void {
27
57
  if (!command || typeof command !== 'string') {
28
58
  throw new Error('Invalid command: must be a non-empty string');
@@ -56,11 +86,22 @@ export class CommandValidator {
56
86
  }
57
87
  }
58
88
 
89
+ /**
90
+ * Check if a command looks like an obviously invalid or mistyped command.
91
+ * @param command - The command to check
92
+ * @returns true if the command matches known invalid patterns
93
+ */
59
94
  static isLikelyInvalid(command: string): boolean {
60
95
  const cmdTrimmed = command.trim();
61
96
  return this.INVALID_PATTERNS.some(pattern => pattern.test(cmdTrimmed));
62
97
  }
63
98
 
99
+ /**
100
+ * Get the priority level of a command for throttling purposes.
101
+ * Lower numbers indicate heavier operations that need more throttling.
102
+ * @param command - The command to evaluate
103
+ * @returns Priority level (1=heavy, 5=medium, 7=default, 8-9=light)
104
+ */
64
105
  static getPriority(command: string): number {
65
106
  if (command.includes('BuildLighting') || command.includes('BuildPaths')) {
66
107
  return 1; // Heavy operation
@@ -16,61 +16,134 @@ export enum ErrorType {
16
16
  UNKNOWN = 'UNKNOWN'
17
17
  }
18
18
 
19
+ /**
20
+ * Debug information attached to error responses in development mode
21
+ */
22
+ interface ErrorResponseDebug {
23
+ errorType: ErrorType;
24
+ originalError: string;
25
+ stack?: string;
26
+ context?: Record<string, unknown>;
27
+ retriable: boolean;
28
+ scope: string;
29
+ }
30
+
31
+ /**
32
+ * Extended error response with optional debug info
33
+ */
34
+ interface ErrorToolResponse extends BaseToolResponse {
35
+ _debug?: ErrorResponseDebug;
36
+ }
37
+
38
+ /**
39
+ * Represents any error object with common properties
40
+ */
41
+ interface ErrorLike {
42
+ message?: string;
43
+ code?: string;
44
+ type?: string;
45
+ errorType?: string;
46
+ stack?: string;
47
+ response?: { status?: number };
48
+ }
49
+
50
+ /**
51
+ * Normalize any error type to ErrorLike interface
52
+ */
53
+ function normalizeErrorToLike(error: unknown): ErrorLike {
54
+ if (error instanceof Error) {
55
+ return {
56
+ message: error.message,
57
+ stack: error.stack,
58
+ code: (error as NodeJS.ErrnoException).code
59
+ };
60
+ }
61
+ if (typeof error === 'object' && error !== null) {
62
+ const obj = error as Record<string, unknown>;
63
+ return {
64
+ message: typeof obj.message === 'string' ? obj.message : undefined,
65
+ code: typeof obj.code === 'string' ? obj.code : undefined,
66
+ type: typeof obj.type === 'string' ? obj.type : undefined,
67
+ errorType: typeof obj.errorType === 'string' ? obj.errorType : undefined,
68
+ stack: typeof obj.stack === 'string' ? obj.stack : undefined,
69
+ response: typeof obj.response === 'object' && obj.response !== null
70
+ ? {
71
+ status: typeof (obj.response as Record<string, unknown>).status === 'number'
72
+ ? (obj.response as Record<string, unknown>).status as number
73
+ : undefined
74
+ }
75
+ : undefined
76
+ };
77
+ }
78
+ return { message: String(error) };
79
+ }
80
+
19
81
  /**
20
82
  * Consistent error handling for all tools
21
83
  */
22
84
  export class ErrorHandler {
23
85
  /**
24
86
  * Create a standardized error response
87
+ * @param error - The error object (can be Error, string, object with message, or unknown)
88
+ * @param toolName - Name of the tool that failed
89
+ * @param context - Optional additional context for debugging
25
90
  */
26
91
  static createErrorResponse(
27
- error: any,
92
+ error: unknown,
28
93
  toolName: string,
29
- context?: any
30
- ): BaseToolResponse {
31
- const errorType = this.categorizeError(error);
32
- const userMessage = this.getUserFriendlyMessage(errorType, error);
33
- const retriable = this.isRetriable(error);
34
- const scope = context?.scope || `tool-call/${toolName}`;
35
-
94
+ context?: Record<string, unknown>
95
+ ): ErrorToolResponse {
96
+ const errorObj = normalizeErrorToLike(error);
97
+ const errorType = this.categorizeError(errorObj);
98
+ const userMessage = this.getUserFriendlyMessage(errorType, errorObj);
99
+ const retriable = this.isRetriable(errorObj);
100
+ const scope = (context?.scope as string) || `tool-call/${toolName}`;
101
+ const errorMessage = errorObj.message || String(error);
102
+ const errorStack = errorObj.stack;
103
+
36
104
  log.error(`Tool ${toolName} failed:`, {
37
105
  type: errorType,
38
- message: error.message || error,
106
+ message: errorMessage,
39
107
  retriable,
40
108
  scope,
41
109
  context
42
110
  });
43
111
 
44
- return {
112
+ const response: ErrorToolResponse = {
45
113
  success: false,
46
114
  error: userMessage,
47
115
  message: `Failed to execute ${toolName}: ${userMessage}`,
48
- retriable: retriable as any,
49
- scope: scope as any,
50
- // Add debug info in development
51
- ...(process.env.NODE_ENV === 'development' && {
52
- _debug: {
53
- errorType,
54
- originalError: error.message || String(error),
55
- stack: error.stack,
56
- context,
57
- retriable,
58
- scope
59
- }
60
- })
61
- } as any;
116
+ retriable,
117
+ scope
118
+ };
119
+
120
+ // Add debug info in development
121
+ if (process.env.NODE_ENV === 'development') {
122
+ response._debug = {
123
+ errorType,
124
+ originalError: errorMessage,
125
+ stack: errorStack,
126
+ context,
127
+ retriable,
128
+ scope
129
+ };
130
+ }
131
+
132
+ return response;
62
133
  }
63
134
 
64
135
  /**
65
136
  * Categorize error by type
137
+ * @param error - The error to categorize
66
138
  */
67
- private static categorizeError(error: any): ErrorType {
68
- const explicitType = (error?.type || error?.errorType || '').toString().toUpperCase();
139
+ private static categorizeError(error: ErrorLike | Error | string): ErrorType {
140
+ const errorObj = typeof error === 'object' ? error as ErrorLike : null;
141
+ const explicitType = (errorObj?.type || errorObj?.errorType || '').toString().toUpperCase();
69
142
  if (explicitType && Object.values(ErrorType).includes(explicitType as ErrorType)) {
70
143
  return explicitType as ErrorType;
71
144
  }
72
145
 
73
- const errorMessage = error?.message?.toLowerCase() || String(error).toLowerCase();
146
+ const errorMessage = (errorObj?.message || String(error)).toLowerCase();
74
147
 
75
148
  // Connection errors
76
149
  if (
@@ -122,49 +195,59 @@ export class ErrorHandler {
122
195
 
123
196
  /**
124
197
  * Get user-friendly error message
198
+ * @param type - The categorized error type
199
+ * @param error - The original error
125
200
  */
126
- private static getUserFriendlyMessage(type: ErrorType, error: any): string {
127
- const originalMessage = error.message || String(error);
201
+ private static getUserFriendlyMessage(type: ErrorType, error: ErrorLike | Error | string): string {
202
+ const originalMessage = (typeof error === 'object' && error !== null && 'message' in error)
203
+ ? (error as { message?: string }).message || String(error)
204
+ : String(error);
128
205
 
129
206
  switch (type) {
130
207
  case ErrorType.CONNECTION:
131
208
  return 'Failed to connect to Unreal Engine. Please ensure the Automation Bridge plugin is active and the editor is running.';
132
-
209
+
133
210
  case ErrorType.VALIDATION:
134
211
  return `Invalid input: ${originalMessage}`;
135
-
212
+
136
213
  case ErrorType.UNREAL_ENGINE:
137
214
  return `Unreal Engine error: ${originalMessage}`;
138
-
215
+
139
216
  case ErrorType.PARAMETER:
140
217
  return `Invalid parameters: ${originalMessage}`;
141
-
218
+
142
219
  case ErrorType.TIMEOUT:
143
220
  return 'Operation timed out. Unreal Engine may be busy or unresponsive.';
144
-
221
+
145
222
  case ErrorType.EXECUTION:
146
223
  return `Execution failed: ${originalMessage}`;
147
-
224
+
148
225
  default:
149
226
  return originalMessage;
150
227
  }
151
228
  }
152
229
 
153
- /** Determine if an error is likely retriable */
154
- private static isRetriable(error: any): boolean {
230
+ /**
231
+ * Determine if an error is likely retriable
232
+ * @param error - The error to check
233
+ */
234
+ private static isRetriable(error: ErrorLike | Error | string): boolean {
155
235
  try {
156
- const code = (error?.code || '').toString().toUpperCase();
157
- const msg = (error?.message || String(error) || '').toLowerCase();
158
- const status = Number((error?.response?.status));
159
- if (['ECONNRESET','ECONNREFUSED','ETIMEDOUT','EPIPE'].includes(code)) return true;
236
+ const errorObj = typeof error === 'object' ? error as ErrorLike : null;
237
+ const code = (errorObj?.code || '').toString().toUpperCase();
238
+ const msg = (errorObj?.message || String(error) || '').toLowerCase();
239
+ const status = Number(errorObj?.response?.status);
240
+ if (['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE'].includes(code)) return true;
160
241
  if (/timeout|timed out|network|connection|closed|unavailable|busy|temporar/.test(msg)) return true;
161
242
  if (!isNaN(status) && (status === 429 || (status >= 500 && status < 600))) return true;
162
- } catch {}
243
+ } catch { }
163
244
  return false;
164
245
  }
165
246
 
166
247
  /**
167
248
  * Retry a function with exponential backoff
249
+ * @param fn - The async function to retry
250
+ * @param options - Retry configuration options
168
251
  */
169
252
  static async retryWithBackoff<T>(
170
253
  fn: () => Promise<T>,
@@ -173,14 +256,14 @@ export class ErrorHandler {
173
256
  initialDelay?: number;
174
257
  maxDelay?: number;
175
258
  backoffMultiplier?: number;
176
- shouldRetry?: (error: any) => boolean;
259
+ shouldRetry?: (error: ErrorLike | Error | unknown) => boolean;
177
260
  } = {}
178
261
  ): Promise<T> {
179
262
  const maxRetries = options.maxRetries ?? 3;
180
263
  const initialDelay = options.initialDelay ?? 1000;
181
264
  const maxDelay = options.maxDelay ?? 10000;
182
265
  const multiplier = options.backoffMultiplier ?? 2;
183
- const shouldRetry = options.shouldRetry ?? ((err) => this.isRetriable(err));
266
+ const shouldRetry = options.shouldRetry ?? ((err: unknown) => this.isRetriable(err as ErrorLike));
184
267
 
185
268
  let delay = initialDelay;
186
269
 
@@ -191,7 +274,7 @@ export class ErrorHandler {
191
274
  if (attempt === maxRetries || !shouldRetry(error)) {
192
275
  throw error;
193
276
  }
194
-
277
+
195
278
  await new Promise(resolve => setTimeout(resolve, delay));
196
279
  delay = Math.min(delay * multiplier, maxDelay);
197
280
  }
@@ -4,47 +4,66 @@ export interface Rot3Obj { pitch: number; yaw: number; roll: number; }
4
4
  export type Vec3Tuple = [number, number, number];
5
5
  export type Rot3Tuple = [number, number, number];
6
6
 
7
- export function toVec3Object(input: any): Vec3Obj | null {
7
+ /** Input that may represent a 3D vector */
8
+ type VectorInput = Vec3Obj | Vec3Tuple | Record<string, unknown> | unknown[];
9
+
10
+ /** Input that may represent a 3D rotation */
11
+ type RotationInput = Rot3Obj | Rot3Tuple | Record<string, unknown> | unknown[];
12
+
13
+ /**
14
+ * Convert various input formats to a Vec3 object
15
+ * @param input - Array, object with x/y/z or X/Y/Z properties
16
+ */
17
+ export function toVec3Object(input: VectorInput | unknown): Vec3Obj | null {
8
18
  try {
9
19
  if (Array.isArray(input) && input.length === 3) {
10
20
  const [x, y, z] = input;
11
21
  if ([x, y, z].every(v => typeof v === 'number' && isFinite(v))) {
12
- return { x, y, z };
22
+ return { x: x as number, y: y as number, z: z as number };
13
23
  }
14
24
  }
15
- if (input && typeof input === 'object') {
16
- const x = Number((input as any).x ?? (input as any).X);
17
- const y = Number((input as any).y ?? (input as any).Y);
18
- const z = Number((input as any).z ?? (input as any).Z);
25
+ if (input && typeof input === 'object' && !Array.isArray(input)) {
26
+ const obj = input as Record<string, unknown>;
27
+ const x = Number(obj.x ?? obj.X);
28
+ const y = Number(obj.y ?? obj.Y);
29
+ const z = Number(obj.z ?? obj.Z);
19
30
  if ([x, y, z].every(v => typeof v === 'number' && !isNaN(v) && isFinite(v))) {
20
31
  return { x, y, z };
21
32
  }
22
33
  }
23
- } catch {}
34
+ } catch { }
24
35
  return null;
25
36
  }
26
37
 
27
- export function toRotObject(input: any): Rot3Obj | null {
38
+ /**
39
+ * Convert various input formats to a Rotation object
40
+ * @param input - Array, object with pitch/yaw/roll or Pitch/Yaw/Roll properties
41
+ */
42
+ export function toRotObject(input: RotationInput | unknown): Rot3Obj | null {
28
43
  try {
29
44
  if (Array.isArray(input) && input.length === 3) {
30
45
  const [pitch, yaw, roll] = input;
31
46
  if ([pitch, yaw, roll].every(v => typeof v === 'number' && isFinite(v))) {
32
- return { pitch, yaw, roll };
47
+ return { pitch: pitch as number, yaw: yaw as number, roll: roll as number };
33
48
  }
34
49
  }
35
- if (input && typeof input === 'object') {
36
- const pitch = Number((input as any).pitch ?? (input as any).Pitch);
37
- const yaw = Number((input as any).yaw ?? (input as any).Yaw);
38
- const roll = Number((input as any).roll ?? (input as any).Roll);
50
+ if (input && typeof input === 'object' && !Array.isArray(input)) {
51
+ const obj = input as Record<string, unknown>;
52
+ const pitch = Number(obj.pitch ?? obj.Pitch);
53
+ const yaw = Number(obj.yaw ?? obj.Yaw);
54
+ const roll = Number(obj.roll ?? obj.Roll);
39
55
  if ([pitch, yaw, roll].every(v => typeof v === 'number' && !isNaN(v) && isFinite(v))) {
40
56
  return { pitch, yaw, roll };
41
57
  }
42
58
  }
43
- } catch {}
59
+ } catch { }
44
60
  return null;
45
61
  }
46
62
 
47
- export function toVec3Tuple(input: any): Vec3Tuple | null {
63
+ /**
64
+ * Convert vector input to a tuple format [x, y, z]
65
+ */
66
+ export function toVec3Tuple(input: VectorInput | unknown): Vec3Tuple | null {
48
67
  const vec = toVec3Object(input);
49
68
  if (!vec) {
50
69
  return null;
@@ -53,7 +72,10 @@ export function toVec3Tuple(input: any): Vec3Tuple | null {
53
72
  return [x, y, z];
54
73
  }
55
74
 
56
- export function toRotTuple(input: any): Rot3Tuple | null {
75
+ /**
76
+ * Convert rotation input to a tuple format [pitch, yaw, roll]
77
+ */
78
+ export function toRotTuple(input: RotationInput | unknown): Rot3Tuple | null {
57
79
  const rot = toRotObject(input);
58
80
  if (!rot) {
59
81
  return null;