@mseep/ai-tech-app-agent 1.0.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.
Files changed (67) hide show
  1. package/.env +24 -0
  2. package/.env.example +24 -0
  3. package/Jenkinsfile +210 -0
  4. package/MCP-SERVER-GUIDE.md +405 -0
  5. package/README.MD +450 -0
  6. package/dist/config/app.config.d.ts +65 -0
  7. package/dist/config/app.config.d.ts.map +1 -0
  8. package/dist/config/app.config.js +94 -0
  9. package/dist/config/app.config.js.map +1 -0
  10. package/dist/config/llm.config.d.ts +63 -0
  11. package/dist/config/llm.config.d.ts.map +1 -0
  12. package/dist/config/llm.config.js +158 -0
  13. package/dist/config/llm.config.js.map +1 -0
  14. package/dist/config/mcp.config.d.ts +175 -0
  15. package/dist/config/mcp.config.d.ts.map +1 -0
  16. package/dist/config/mcp.config.js +215 -0
  17. package/dist/config/mcp.config.js.map +1 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +175 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/llm/llamaClient.d.ts +14 -0
  23. package/dist/llm/llamaClient.d.ts.map +1 -0
  24. package/dist/llm/llamaClient.js +136 -0
  25. package/dist/llm/llamaClient.js.map +1 -0
  26. package/dist/mcp/mcpClient.d.ts +132 -0
  27. package/dist/mcp/mcpClient.d.ts.map +1 -0
  28. package/dist/mcp/mcpClient.js +784 -0
  29. package/dist/mcp/mcpClient.js.map +1 -0
  30. package/dist/models/testSpec.d.ts +78 -0
  31. package/dist/models/testSpec.d.ts.map +1 -0
  32. package/dist/models/testSpec.js +3 -0
  33. package/dist/models/testSpec.js.map +1 -0
  34. package/dist/orchestrator/aiTestRunner.d.ts +18 -0
  35. package/dist/orchestrator/aiTestRunner.d.ts.map +1 -0
  36. package/dist/orchestrator/aiTestRunner.js +247 -0
  37. package/dist/orchestrator/aiTestRunner.js.map +1 -0
  38. package/dist/utils/logger.d.ts +4 -0
  39. package/dist/utils/logger.d.ts.map +1 -0
  40. package/dist/utils/logger.js +49 -0
  41. package/dist/utils/logger.js.map +1 -0
  42. package/dist/utils/promptBuilder.d.ts +62 -0
  43. package/dist/utils/promptBuilder.d.ts.map +1 -0
  44. package/dist/utils/promptBuilder.js +333 -0
  45. package/dist/utils/promptBuilder.js.map +1 -0
  46. package/knowledge/app-knowledge.txt +100 -0
  47. package/logs/combined.log +486 -0
  48. package/logs/error.log +50 -0
  49. package/package.json +62 -0
  50. package/reports/screenshots/screenshot_1764535110518.png +0 -0
  51. package/reports/test-report.json +106 -0
  52. package/scripts/check-mcp-server.sh +100 -0
  53. package/scripts/extract-pom-knowledge.js +222 -0
  54. package/scripts/pre-test-setup.js +262 -0
  55. package/scripts/start-mcp-server.sh +76 -0
  56. package/src/config/app.config.ts +175 -0
  57. package/src/config/llm.config.ts +220 -0
  58. package/src/config/mcp.config.ts +291 -0
  59. package/src/index.ts +161 -0
  60. package/src/llm/llamaClient.ts +159 -0
  61. package/src/mcp/mcpClient.ts +878 -0
  62. package/src/models/testSpec.ts +85 -0
  63. package/src/orchestrator/aiTestRunner.ts +286 -0
  64. package/src/utils/logger.ts +59 -0
  65. package/src/utils/promptBuilder.ts +384 -0
  66. package/tests/nlp-specs/login-flow.yaml +31 -0
  67. package/tsconfig.json +31 -0
@@ -0,0 +1,784 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MCPClient = void 0;
7
+ const child_process_1 = require("child_process");
8
+ const axios_1 = __importDefault(require("axios"));
9
+ const fast_xml_parser_1 = require("fast-xml-parser");
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const mcp_config_1 = __importDefault(require("../config/mcp.config"));
13
+ const app_config_1 = __importDefault(require("../config/app.config"));
14
+ const logger_1 = __importDefault(require("../utils/logger"));
15
+ class MCPClient {
16
+ constructor(sessionId) {
17
+ this.mcpProcess = null;
18
+ this.sessionId = null;
19
+ this.platform = null;
20
+ this.appiumServerUrl = mcp_config_1.default.appium.serverUrl;
21
+ this.sessionId = sessionId || null;
22
+ // Initialize Appium HTTP client
23
+ this.appiumClient = axios_1.default.create({
24
+ baseURL: this.appiumServerUrl,
25
+ timeout: 120000, // Increased to 2 minutes for LambdaTest session creation
26
+ headers: {
27
+ 'Content-Type': 'application/json',
28
+ },
29
+ });
30
+ // Initialize XML parser
31
+ this.xmlParser = new fast_xml_parser_1.XMLParser({
32
+ ignoreAttributes: false,
33
+ attributeNamePrefix: '',
34
+ textNodeName: 'text',
35
+ parseAttributeValue: true,
36
+ });
37
+ logger_1.default.info('MCPClient initialized');
38
+ }
39
+ /**
40
+ * Initialize MCP server and establish connection
41
+ */
42
+ async initialize() {
43
+ try {
44
+ logger_1.default.info('Initializing MCP-Appium server...');
45
+ // If using stdio protocol, spawn the MCP server process
46
+ if (mcp_config_1.default.server.protocol === 'stdio') {
47
+ await this.startMCPProcess();
48
+ }
49
+ // Verify Appium connection
50
+ await this.verifyAppiumConnection();
51
+ // If no session ID provided, create a new session
52
+ if (!this.sessionId) {
53
+ await this.createSession();
54
+ }
55
+ logger_1.default.info('MCP-Appium server initialized successfully');
56
+ }
57
+ catch (error) {
58
+ logger_1.default.error(`Failed to initialize MCP server: ${error.message}`);
59
+ throw error;
60
+ }
61
+ }
62
+ /**
63
+ * Start MCP server process (if using stdio)
64
+ */
65
+ async startMCPProcess() {
66
+ return new Promise((resolve, reject) => {
67
+ try {
68
+ // Start mcp-appium server
69
+ this.mcpProcess = (0, child_process_1.spawn)('npx', ['mcp-appium'], {
70
+ env: {
71
+ ...process.env,
72
+ APPIUM_SERVER_URL: this.appiumServerUrl,
73
+ APPIUM_SESSION_ID: this.sessionId || '',
74
+ },
75
+ stdio: ['pipe', 'pipe', 'pipe'],
76
+ });
77
+ this.mcpProcess.stdout?.on('data', (data) => {
78
+ logger_1.default.debug(`MCP stdout: ${data.toString()}`);
79
+ });
80
+ this.mcpProcess.stderr?.on('data', (data) => {
81
+ logger_1.default.debug(`MCP stderr: ${data.toString()}`);
82
+ });
83
+ this.mcpProcess.on('error', (error) => {
84
+ logger_1.default.error(`MCP process error: ${error.message}`);
85
+ reject(error);
86
+ });
87
+ // Give it a moment to start
88
+ setTimeout(() => resolve(), 2000);
89
+ }
90
+ catch (error) {
91
+ reject(error);
92
+ }
93
+ });
94
+ }
95
+ /**
96
+ * Verify Appium server connection
97
+ */
98
+ async verifyAppiumConnection() {
99
+ try {
100
+ const response = await this.appiumClient.get('/status');
101
+ logger_1.default.info('Appium server is reachable');
102
+ logger_1.default.debug(`Appium status: ${JSON.stringify(response.data)}`);
103
+ }
104
+ catch (error) {
105
+ throw new Error(`Cannot connect to Appium server at ${this.appiumServerUrl}: ${error.message}`);
106
+ }
107
+ }
108
+ /**
109
+ * Create a new Appium session with device capabilities
110
+ */
111
+ async createSession() {
112
+ const maxRetries = 3;
113
+ let lastError;
114
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
115
+ try {
116
+ logger_1.default.info(`Creating new Appium session (attempt ${attempt}/${maxRetries})...`);
117
+ // LambdaTest capabilities matching the provided format
118
+ const capabilities = {
119
+ platformName: 'Android',
120
+ 'appium:platformVersion': '13',
121
+ 'appium:deviceName': 'Galaxy S23',
122
+ 'appium:automationName': 'UiAutomator2',
123
+ 'appium:app': 'lt://APP1016060671764436009495779', // Upload your app to LambdaTest
124
+ 'appium:isRealMobile': true,
125
+ 'appium:newCommandTimeout': 300,
126
+ 'appium:autoGrantPermissions': true,
127
+ 'appium:noReset': true,
128
+ 'appium:fullReset': false,
129
+ // LambdaTest authentication (kept for backward compatibility)
130
+ username: app_config_1.default.lambdatest.username,
131
+ accessKey: app_config_1.default.lambdatest.accessKey,
132
+ // LambdaTest specific capabilities
133
+ 'lt:options': {
134
+ build: 'Hybrid AI Automation',
135
+ name: 'AI Test Session - ' + Date.now(),
136
+ video: true,
137
+ visual: true,
138
+ network: false,
139
+ console: true,
140
+ w3c: true,
141
+ retry: 0 // No retry
142
+ }
143
+ };
144
+ const sessionPayload = {
145
+ desiredCapabilities: capabilities
146
+ };
147
+ logger_1.default.debug(`Creating session with capabilities: ${JSON.stringify(sessionPayload, null, 2)}`);
148
+ const response = await this.appiumClient.post('/session', sessionPayload);
149
+ this.sessionId = response.data.sessionId || response.data.value?.sessionId;
150
+ if (!this.sessionId) {
151
+ throw new Error('Failed to get session ID from response');
152
+ }
153
+ // Auto-detect platform
154
+ this.platform = this.detectPlatformFromCapabilities(capabilities);
155
+ logger_1.default.info(`Appium session created successfully: ${this.sessionId}`);
156
+ logger_1.default.info(`Platform detected: ${this.platform}`);
157
+ return; // Success, exit the retry loop
158
+ }
159
+ catch (error) {
160
+ lastError = error;
161
+ if (error.response) {
162
+ logger_1.default.error(`Session creation failed with status ${error.response.status}`);
163
+ logger_1.default.error(`Response body: ${JSON.stringify(error.response.data, null, 2)}`);
164
+ }
165
+ else if (error.code === 'ECONNABORTED') {
166
+ logger_1.default.error(`Session creation timed out (attempt ${attempt}/${maxRetries})`);
167
+ }
168
+ else {
169
+ logger_1.default.error(`Session creation failed: ${error.message} (attempt ${attempt}/${maxRetries})`);
170
+ }
171
+ // If not the last attempt, wait before retrying
172
+ if (attempt < maxRetries) {
173
+ const waitTime = attempt * 10000; // 10s, 20s backoff
174
+ logger_1.default.info(`Waiting ${waitTime / 1000}s before retry...`);
175
+ await this.delay(waitTime);
176
+ }
177
+ }
178
+ }
179
+ // All retries failed
180
+ throw new Error(`Failed to create Appium session after ${maxRetries} attempts: ${lastError.message}`);
181
+ }
182
+ /**
183
+ * Detect platform from capabilities
184
+ */
185
+ detectPlatformFromCapabilities(capabilities) {
186
+ const platformName = (capabilities.platformName || '').toLowerCase();
187
+ return platformName.includes('ios') ? 'ios' : 'android';
188
+ }
189
+ /**
190
+ * Set the active Appium session ID
191
+ */
192
+ setSessionId(sessionId, platform) {
193
+ this.sessionId = sessionId;
194
+ this.platform = platform || null;
195
+ logger_1.default.info(`MCP client configured with session ID: ${sessionId}, platform: ${platform || 'auto-detect'}`);
196
+ }
197
+ /**
198
+ * Get current UI context (page source + parsed elements)
199
+ */
200
+ async getUIContext() {
201
+ try {
202
+ if (!this.sessionId) {
203
+ throw new Error('No active Appium session');
204
+ }
205
+ logger_1.default.debug('Fetching UI context from Appium...');
206
+ // Get page source (XML hierarchy)
207
+ const pageSource = await this.getPageSource();
208
+ // Parse visible elements from page source
209
+ const visibleElements = mcp_config_1.default.uiContext.parseXML
210
+ ? this.parseUIElements(pageSource)
211
+ : [];
212
+ // Get current activity/package (Android) or bundle ID (iOS)
213
+ const currentActivity = await this.getCurrentActivity();
214
+ // Auto-detect platform if not set
215
+ if (!this.platform) {
216
+ this.platform = this.detectPlatformFromPageSource(pageSource);
217
+ }
218
+ const uiContext = {
219
+ pageSource,
220
+ currentActivity,
221
+ visibleElements,
222
+ currentPackage: this.platform === 'android' ? await this.getCurrentPackage() : undefined,
223
+ };
224
+ logger_1.default.debug(`UI context fetched: ${visibleElements.length} elements found, activity: ${currentActivity}`);
225
+ return uiContext;
226
+ }
227
+ catch (error) {
228
+ logger_1.default.error(`Failed to get UI context: ${error.message}`);
229
+ throw error;
230
+ }
231
+ }
232
+ /**
233
+ * Get page source (XML hierarchy)
234
+ */
235
+ async getPageSource() {
236
+ try {
237
+ const response = await this.appiumClient.get(`/session/${this.sessionId}/source`);
238
+ return response.data.value || '';
239
+ }
240
+ catch (error) {
241
+ logger_1.default.error(`Failed to get page source: ${error.message}`);
242
+ throw error;
243
+ }
244
+ }
245
+ /**
246
+ * Get current activity (Android) or current view (iOS)
247
+ */
248
+ async getCurrentActivity() {
249
+ try {
250
+ if (!this.platform) {
251
+ this.platform = await this.detectPlatform();
252
+ }
253
+ if (this.platform === 'android') {
254
+ const response = await this.appiumClient.get(`/session/${this.sessionId}/appium/device/current_activity`);
255
+ return response.data.value || 'Unknown';
256
+ }
257
+ else {
258
+ // For iOS, we can get the active app bundle ID
259
+ const response = await this.appiumClient.get(`/session/${this.sessionId}/appium/device/current_package`);
260
+ return response.data.value || 'Unknown';
261
+ }
262
+ }
263
+ catch (error) {
264
+ logger_1.default.warn(`Could not get current activity: ${error.message}`);
265
+ return 'Unknown';
266
+ }
267
+ }
268
+ /**
269
+ * Get current package (Android only)
270
+ */
271
+ async getCurrentPackage() {
272
+ try {
273
+ if (this.platform === 'android') {
274
+ const response = await this.appiumClient.get(`/session/${this.sessionId}/appium/device/current_package`);
275
+ return response.data.value;
276
+ }
277
+ }
278
+ catch (error) {
279
+ logger_1.default.warn(`Could not get current package: ${error.message}`);
280
+ }
281
+ return undefined;
282
+ }
283
+ /**
284
+ * Detect platform from session capabilities
285
+ */
286
+ async detectPlatform() {
287
+ try {
288
+ const response = await this.appiumClient.get(`/session/${this.sessionId}`);
289
+ const caps = response.data.value || response.data;
290
+ const platformName = (caps.platformName || '').toLowerCase();
291
+ if (platformName.includes('android')) {
292
+ return 'android';
293
+ }
294
+ else if (platformName.includes('ios')) {
295
+ return 'ios';
296
+ }
297
+ }
298
+ catch (error) {
299
+ logger_1.default.warn(`Could not detect platform: ${error.message}`);
300
+ }
301
+ // Default to android
302
+ return 'android';
303
+ }
304
+ /**
305
+ * Detect platform from page source XML
306
+ */
307
+ detectPlatformFromPageSource(pageSource) {
308
+ if (pageSource.includes('XCUIElement') || pageSource.includes('AppiumAUT')) {
309
+ return 'ios';
310
+ }
311
+ return 'android';
312
+ }
313
+ /**
314
+ * Parse UI elements from XML page source
315
+ */
316
+ parseUIElements(pageSource) {
317
+ try {
318
+ const parsed = this.xmlParser.parse(pageSource);
319
+ const elements = [];
320
+ // Recursively extract elements
321
+ this.extractElementsRecursive(parsed, elements, 0);
322
+ // Apply filters
323
+ return this.filterElements(elements);
324
+ }
325
+ catch (error) {
326
+ logger_1.default.error(`Failed to parse XML: ${error.message}`);
327
+ return [];
328
+ }
329
+ }
330
+ /**
331
+ * Recursively extract elements from parsed XML
332
+ */
333
+ extractElementsRecursive(node, elements, depth) {
334
+ if (depth > mcp_config_1.default.uiContext.maxElementDepth) {
335
+ return;
336
+ }
337
+ if (!node || typeof node !== 'object') {
338
+ return;
339
+ }
340
+ // Check if this is an element node
341
+ if (node.class || node['resource-id'] || node['content-desc'] || node.name) {
342
+ const element = {
343
+ type: node.class || node.type || 'Unknown',
344
+ text: node.text || node.label || node.value,
345
+ resourceId: node['resource-id'] || node.name,
346
+ contentDesc: node['content-desc'] || node.label,
347
+ className: node.class || node.type,
348
+ bounds: node.bounds,
349
+ clickable: node.clickable === 'true' || node.clickable === true,
350
+ enabled: node.enabled === 'true' || node.enabled === true || node.enabled === undefined,
351
+ index: node.index,
352
+ };
353
+ elements.push(element);
354
+ }
355
+ // Recursively process children
356
+ Object.keys(node).forEach(key => {
357
+ if (typeof node[key] === 'object') {
358
+ if (Array.isArray(node[key])) {
359
+ node[key].forEach((child) => {
360
+ this.extractElementsRecursive(child, elements, depth + 1);
361
+ });
362
+ }
363
+ else {
364
+ this.extractElementsRecursive(node[key], elements, depth + 1);
365
+ }
366
+ }
367
+ });
368
+ }
369
+ /**
370
+ * Filter elements based on configuration
371
+ */
372
+ filterElements(elements) {
373
+ const { excludeClasses, includeOnlyInteractive, minTextLength } = mcp_config_1.default.uiContext.elementFilters;
374
+ return elements.filter(element => {
375
+ // Exclude certain classes
376
+ if (excludeClasses.some(cls => element.className?.includes(cls))) {
377
+ return false;
378
+ }
379
+ // Include only interactive elements
380
+ if (includeOnlyInteractive && !element.clickable && !element.type.includes('Edit')) {
381
+ return false;
382
+ }
383
+ // Filter by text length
384
+ if (element.text && element.text.length < minTextLength) {
385
+ return false;
386
+ }
387
+ return true;
388
+ });
389
+ }
390
+ /**
391
+ * Execute an action
392
+ */
393
+ async executeAction(action) {
394
+ try {
395
+ logger_1.default.info(`Executing action: ${action.type}`);
396
+ switch (action.type) {
397
+ case 'tap':
398
+ return await this.tap(action);
399
+ case 'type':
400
+ return await this.type(action);
401
+ case 'scroll':
402
+ return await this.scroll(action);
403
+ case 'swipe':
404
+ return await this.swipe(action);
405
+ case 'wait':
406
+ return await this.wait(action);
407
+ case 'assert':
408
+ return await this.assert(action);
409
+ case 'screenshot':
410
+ return await this.screenshot();
411
+ case 'launch':
412
+ return await this.launch(action);
413
+ default:
414
+ logger_1.default.warn(`Unknown action type: ${action.type}`);
415
+ return false;
416
+ }
417
+ }
418
+ catch (error) {
419
+ logger_1.default.error(`Action execution failed: ${error.message}`);
420
+ if (mcp_config_1.default.errorHandling.captureScreenshotOnError) {
421
+ await this.screenshot().catch(() => { });
422
+ }
423
+ return false;
424
+ }
425
+ }
426
+ /**
427
+ * Tap/click on an element
428
+ */
429
+ async tap(action) {
430
+ if (!action.selector) {
431
+ logger_1.default.error('Tap action requires a selector');
432
+ return false;
433
+ }
434
+ logger_1.default.debug(`Tapping element: ${action.selector.strategy}=${action.selector.value}`);
435
+ try {
436
+ const element = await this.findElement(action.selector);
437
+ if (!element) {
438
+ throw new Error('Element not found');
439
+ }
440
+ await this.appiumClient.post(`/session/${this.sessionId}/element/${element}/click`, {});
441
+ // Small delay after tap
442
+ await this.delay(app_config_1.default.interaction.actionDelay);
443
+ return true;
444
+ }
445
+ catch (error) {
446
+ logger_1.default.error(`Tap failed: ${error.message}`);
447
+ return false;
448
+ }
449
+ }
450
+ /**
451
+ * Type text into an element
452
+ */
453
+ async type(action) {
454
+ if (!action.selector || !action.value) {
455
+ logger_1.default.error('Type action requires selector and value');
456
+ return false;
457
+ }
458
+ logger_1.default.debug(`Typing "${action.value}" into element: ${action.selector.strategy}=${action.selector.value}`);
459
+ try {
460
+ const element = await this.findElement(action.selector);
461
+ if (!element) {
462
+ throw new Error('Element not found');
463
+ }
464
+ // Clear existing text if needed
465
+ await this.appiumClient.post(`/session/${this.sessionId}/element/${element}/clear`, {});
466
+ // Type the text
467
+ await this.appiumClient.post(`/session/${this.sessionId}/element/${element}/value`, {
468
+ text: action.value,
469
+ value: action.value.split(''),
470
+ });
471
+ await this.delay(app_config_1.default.interaction.actionDelay);
472
+ return true;
473
+ }
474
+ catch (error) {
475
+ logger_1.default.error(`Type failed: ${error.message}`);
476
+ return false;
477
+ }
478
+ }
479
+ /**
480
+ * Scroll the screen
481
+ */
482
+ async scroll(action) {
483
+ const direction = action.direction || 'down';
484
+ logger_1.default.debug(`Scrolling ${direction}`);
485
+ try {
486
+ // Get screen size
487
+ const size = await this.getScreenSize();
488
+ const { width, height } = size;
489
+ const centerX = width / 2;
490
+ const startY = direction === 'down' ? height * 0.8 : height * 0.2;
491
+ const endY = direction === 'down' ? height * 0.2 : height * 0.8;
492
+ await this.performSwipeGesture(centerX, startY, centerX, endY, app_config_1.default.interaction.swipeSpeed);
493
+ return true;
494
+ }
495
+ catch (error) {
496
+ logger_1.default.error(`Scroll failed: ${error.message}`);
497
+ return false;
498
+ }
499
+ }
500
+ /**
501
+ * Swipe gesture
502
+ */
503
+ async swipe(action) {
504
+ const direction = action.direction || 'left';
505
+ logger_1.default.debug(`Swiping ${direction}`);
506
+ try {
507
+ const size = await this.getScreenSize();
508
+ const { width, height } = size;
509
+ let startX, startY, endX, endY;
510
+ switch (direction) {
511
+ case 'left':
512
+ startX = width * 0.8;
513
+ startY = height / 2;
514
+ endX = width * 0.2;
515
+ endY = height / 2;
516
+ break;
517
+ case 'right':
518
+ startX = width * 0.2;
519
+ startY = height / 2;
520
+ endX = width * 0.8;
521
+ endY = height / 2;
522
+ break;
523
+ case 'up':
524
+ startX = width / 2;
525
+ startY = height * 0.8;
526
+ endX = width / 2;
527
+ endY = height * 0.2;
528
+ break;
529
+ case 'down':
530
+ startX = width / 2;
531
+ startY = height * 0.2;
532
+ endX = width / 2;
533
+ endY = height * 0.8;
534
+ break;
535
+ default:
536
+ startX = width / 2;
537
+ startY = height / 2;
538
+ endX = width / 2;
539
+ endY = height / 2;
540
+ }
541
+ await this.performSwipeGesture(startX, startY, endX, endY, action.duration || app_config_1.default.interaction.swipeSpeed);
542
+ return true;
543
+ }
544
+ catch (error) {
545
+ logger_1.default.error(`Swipe failed: ${error.message}`);
546
+ return false;
547
+ }
548
+ }
549
+ /**
550
+ * Perform swipe gesture using touch actions
551
+ */
552
+ async performSwipeGesture(startX, startY, endX, endY, duration) {
553
+ const actions = [
554
+ {
555
+ type: 'pointer',
556
+ id: 'finger1',
557
+ parameters: { pointerType: 'touch' },
558
+ actions: [
559
+ { type: 'pointerMove', duration: 0, x: Math.round(startX), y: Math.round(startY) },
560
+ { type: 'pointerDown', button: 0 },
561
+ { type: 'pause', duration: 100 },
562
+ { type: 'pointerMove', duration, x: Math.round(endX), y: Math.round(endY) },
563
+ { type: 'pointerUp', button: 0 },
564
+ ],
565
+ },
566
+ ];
567
+ await this.appiumClient.post(`/session/${this.sessionId}/actions`, { actions });
568
+ }
569
+ /**
570
+ * Wait for duration or element
571
+ */
572
+ async wait(action) {
573
+ const duration = action.duration || 2000;
574
+ logger_1.default.debug(`Waiting for ${duration}ms`);
575
+ await this.delay(duration);
576
+ return true;
577
+ }
578
+ /**
579
+ * Assert element state
580
+ */
581
+ async assert(action) {
582
+ if (!action.selector || !action.assertionType) {
583
+ logger_1.default.error('Assert action requires selector and assertionType');
584
+ return false;
585
+ }
586
+ logger_1.default.debug(`Asserting: ${action.assertionType} on ${action.selector.value}`);
587
+ try {
588
+ const element = await this.findElement(action.selector);
589
+ switch (action.assertionType) {
590
+ case 'exists':
591
+ return element !== null;
592
+ case 'visible':
593
+ if (!element)
594
+ return false;
595
+ const displayed = await this.appiumClient.get(`/session/${this.sessionId}/element/${element}/displayed`);
596
+ return displayed.data.value === true;
597
+ case 'enabled':
598
+ if (!element)
599
+ return false;
600
+ const enabled = await this.appiumClient.get(`/session/${this.sessionId}/element/${element}/enabled`);
601
+ return enabled.data.value === true;
602
+ case 'text':
603
+ if (!element)
604
+ return false;
605
+ const text = await this.appiumClient.get(`/session/${this.sessionId}/element/${element}/text`);
606
+ return text.data.value === action.expectedValue;
607
+ default:
608
+ logger_1.default.warn(`Unknown assertion type: ${action.assertionType}`);
609
+ return false;
610
+ }
611
+ }
612
+ catch (error) {
613
+ logger_1.default.error(`Assertion failed: ${error.message}`);
614
+ return false;
615
+ }
616
+ }
617
+ /**
618
+ * Launch an app or return to home screen
619
+ */
620
+ async launch(action) {
621
+ logger_1.default.debug('Launching app or returning to home');
622
+ try {
623
+ // If no specific app package is specified, we're already in a session
624
+ // Just return true as the device is ready
625
+ if (!action.value) {
626
+ logger_1.default.info('Device session ready - no specific app launch required');
627
+ return true;
628
+ }
629
+ // If a specific app package is provided, try to launch it
630
+ const appPackage = action.value;
631
+ logger_1.default.debug(`Attempting to launch app: ${appPackage}`);
632
+ // For Android, we can try to activate the app
633
+ if (this.platform === 'android') {
634
+ await this.appiumClient.post(`/session/${this.sessionId}/appium/device/activate_app`, {
635
+ appId: appPackage
636
+ });
637
+ }
638
+ else {
639
+ // For iOS, we can try to launch the app by bundle ID
640
+ await this.appiumClient.post(`/session/${this.sessionId}/appium/device/activate_app`, {
641
+ bundleId: appPackage
642
+ });
643
+ }
644
+ await this.delay(app_config_1.default.interaction.actionDelay);
645
+ return true;
646
+ }
647
+ catch (error) {
648
+ logger_1.default.warn(`Launch action completed with warning: ${error.message}`);
649
+ // Return true anyway as the session is established
650
+ return true;
651
+ }
652
+ }
653
+ /**
654
+ * Take screenshot
655
+ */
656
+ async screenshot() {
657
+ logger_1.default.debug('Taking screenshot');
658
+ try {
659
+ const response = await this.appiumClient.get(`/session/${this.sessionId}/screenshot`);
660
+ const screenshotData = response.data.value;
661
+ // Save screenshot
662
+ const timestamp = Date.now();
663
+ const filename = `screenshot_${timestamp}.png`;
664
+ const filepath = path_1.default.join(app_config_1.default.paths.screenshots, filename);
665
+ // Ensure directory exists
666
+ if (!fs_1.default.existsSync(app_config_1.default.paths.screenshots)) {
667
+ fs_1.default.mkdirSync(app_config_1.default.paths.screenshots, { recursive: true });
668
+ }
669
+ fs_1.default.writeFileSync(filepath, screenshotData, 'base64');
670
+ logger_1.default.info(`Screenshot saved: ${filepath}`);
671
+ return true;
672
+ }
673
+ catch (error) {
674
+ logger_1.default.error(`Screenshot failed: ${error.message}`);
675
+ return false;
676
+ }
677
+ }
678
+ /**
679
+ * Find element using selector
680
+ */
681
+ async findElement(selector) {
682
+ try {
683
+ const { strategy, value, index } = selector;
684
+ // Map our strategy names to Appium's
685
+ const strategyMap = {
686
+ 'text': this.platform === 'android' ? 'xpath' : '-ios predicate string',
687
+ 'id': this.platform === 'android' ? 'id' : 'accessibility id',
688
+ 'accessibility-id': 'accessibility id',
689
+ 'xpath': 'xpath',
690
+ 'class': 'class name',
691
+ };
692
+ let appiumStrategy = strategyMap[strategy] || strategy;
693
+ let appiumValue = value;
694
+ // Special handling for text search
695
+ if (strategy === 'text') {
696
+ if (this.platform === 'android') {
697
+ appiumStrategy = 'xpath';
698
+ appiumValue = `//*[@text='${value}' or contains(@text,'${value}')]`;
699
+ }
700
+ else {
701
+ appiumStrategy = '-ios predicate string';
702
+ appiumValue = `label == "${value}" OR name == "${value}"`;
703
+ }
704
+ }
705
+ const payload = {
706
+ using: appiumStrategy,
707
+ value: appiumValue,
708
+ };
709
+ const response = await this.appiumClient.post(`/session/${this.sessionId}/element`, payload);
710
+ if (index !== undefined && index > 0) {
711
+ // If index is specified, find multiple and return the one at index
712
+ const multiResponse = await this.appiumClient.post(`/session/${this.sessionId}/elements`, payload);
713
+ const elements = multiResponse.data.value;
714
+ if (elements && elements.length > index) {
715
+ return elements[index].ELEMENT || elements[index]['element-6066-11e4-a52e-4f735466cecf'];
716
+ }
717
+ return null;
718
+ }
719
+ const elementId = response.data.value?.ELEMENT || response.data.value?.['element-6066-11e4-a52e-4f735466cecf'];
720
+ return elementId || null;
721
+ }
722
+ catch (error) {
723
+ logger_1.default.debug(`Element not found: ${selector.strategy}=${selector.value}`);
724
+ return null;
725
+ }
726
+ }
727
+ /**
728
+ * Get screen size
729
+ */
730
+ async getScreenSize() {
731
+ try {
732
+ const response = await this.appiumClient.get(`/session/${this.sessionId}/window/rect`);
733
+ return {
734
+ width: response.data.value.width,
735
+ height: response.data.value.height,
736
+ };
737
+ }
738
+ catch (error) {
739
+ logger_1.default.warn(`Could not get screen size: ${error.message}`);
740
+ // Default fallback
741
+ return { width: 1080, height: 1920 };
742
+ }
743
+ }
744
+ /**
745
+ * Delay helper
746
+ */
747
+ delay(ms) {
748
+ return new Promise(resolve => setTimeout(resolve, ms));
749
+ }
750
+ /**
751
+ * Stop the current Appium session
752
+ */
753
+ async stopSession() {
754
+ if (this.sessionId) {
755
+ try {
756
+ logger_1.default.info(`Stopping Appium session: ${this.sessionId}`);
757
+ await this.appiumClient.delete(`/session/${this.sessionId}`);
758
+ logger_1.default.info('Appium session stopped successfully');
759
+ }
760
+ catch (error) {
761
+ logger_1.default.warn(`Failed to stop session: ${error.message}`);
762
+ }
763
+ finally {
764
+ this.sessionId = null;
765
+ this.platform = null;
766
+ }
767
+ }
768
+ }
769
+ /**
770
+ * Cleanup and shutdown
771
+ */
772
+ async cleanup() {
773
+ // Stop the Appium session first
774
+ await this.stopSession();
775
+ if (this.mcpProcess) {
776
+ logger_1.default.info('Shutting down MCP server...');
777
+ this.mcpProcess.kill();
778
+ this.mcpProcess = null;
779
+ }
780
+ }
781
+ }
782
+ exports.MCPClient = MCPClient;
783
+ exports.default = MCPClient;
784
+ //# sourceMappingURL=mcpClient.js.map