devicely 1.0.5

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 (54) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +243 -0
  3. package/bin/devicely.js +3 -0
  4. package/config/apps_presets.conf +271 -0
  5. package/config/devices.conf +20 -0
  6. package/lib/aiProviders.js +518 -0
  7. package/lib/aiProviders.js.backup +301 -0
  8. package/lib/aiProvidersConfig.js +176 -0
  9. package/lib/aiProviders_new.js +70 -0
  10. package/lib/androidDeviceDetection.js +2 -0
  11. package/lib/appMappings.js +1 -0
  12. package/lib/deviceDetection.js +2 -0
  13. package/lib/devices.conf +20 -0
  14. package/lib/devices.js +1 -0
  15. package/lib/doctor.js +1 -0
  16. package/lib/executor.js +1 -0
  17. package/lib/fix_logs.sh +18 -0
  18. package/lib/frontend/asset-manifest.json +13 -0
  19. package/lib/frontend/index.html +1 -0
  20. package/lib/frontend/static/css/main.23bd35c0.css +2 -0
  21. package/lib/frontend/static/css/main.23bd35c0.css.map +1 -0
  22. package/lib/frontend/static/js/main.3f13aeaf.js +1 -0
  23. package/lib/frontend/static/js/main.3f13aeaf.js.LICENSE.txt +48 -0
  24. package/lib/frontend/static/js/main.3f13aeaf.js.map +1 -0
  25. package/lib/frontend/voice-test.html +156 -0
  26. package/lib/index.js +1 -0
  27. package/lib/package-lock.json +1678 -0
  28. package/lib/package.json +30 -0
  29. package/lib/server.js +1 -0
  30. package/lib/server.js.bak +3380 -0
  31. package/package.json +78 -0
  32. package/scripts/postinstall.js +110 -0
  33. package/scripts/shell/android_device_control +0 -0
  34. package/scripts/shell/connect_android_usb +0 -0
  35. package/scripts/shell/connect_android_usb_multi_final +0 -0
  36. package/scripts/shell/connect_android_wireless +0 -0
  37. package/scripts/shell/connect_android_wireless_multi_final +0 -0
  38. package/scripts/shell/connect_ios_usb +0 -0
  39. package/scripts/shell/connect_ios_usb_multi_final +0 -0
  40. package/scripts/shell/connect_ios_wireless_multi_final +0 -0
  41. package/scripts/shell/find_element_coordinates +0 -0
  42. package/scripts/shell/find_wda +0 -0
  43. package/scripts/shell/install_uiautomator2 +0 -0
  44. package/scripts/shell/ios_device_control +0 -0
  45. package/scripts/shell/setup +0 -0
  46. package/scripts/shell/setup_android +0 -0
  47. package/scripts/shell/start +0 -0
  48. package/scripts/shell/test_android_locators +0 -0
  49. package/scripts/shell/test_connect +0 -0
  50. package/scripts/shell/test_device_detection +0 -0
  51. package/scripts/shell/test_fixes +0 -0
  52. package/scripts/shell/test_getlocators_fix +0 -0
  53. package/scripts/shell/test_recording_feature +0 -0
  54. package/scripts/shell/verify_distribution +0 -0
@@ -0,0 +1,518 @@
1
+ // AI Provider Abstraction Layer - Enhanced Multi-Provider Support
2
+ // Supports: OpenAI, Google Gemini, Anthropic Claude, GitHub Copilot, Groq, Cohere, Mistral AI
3
+
4
+ const { OpenAI } = require('openai');
5
+ const { GoogleGenerativeAI } = require('@google/generative-ai');
6
+ const { Anthropic } = require('@anthropic-ai/sdk');
7
+ const { getPackageId, APP_MAPPINGS } = require('./appMappings');
8
+
9
+ class AIProviderManager {
10
+ constructor() {
11
+ this.provider = process.env.AI_PROVIDER || 'gemini';
12
+ this.model = process.env.AI_MODEL || null; // Auto-select default if null
13
+ this.initializeProviders();
14
+ }
15
+
16
+ initializeProviders() {
17
+ // OpenAI
18
+ if (process.env.OPENAI_API_KEY) {
19
+ this.openai = new OpenAI({
20
+ apiKey: process.env.OPENAI_API_KEY,
21
+ });
22
+ }
23
+
24
+ // Google Gemini
25
+ if (process.env.GEMINI_API_KEY) {
26
+ this.gemini = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
27
+ }
28
+
29
+ // Anthropic Claude
30
+ if (process.env.CLAUDE_API_KEY) {
31
+ this.claude = new Anthropic({
32
+ apiKey: process.env.CLAUDE_API_KEY,
33
+ });
34
+ }
35
+
36
+ // GitHub Copilot
37
+ if (process.env.GITHUB_TOKEN) {
38
+ this.copilot = new OpenAI({
39
+ apiKey: process.env.GITHUB_TOKEN,
40
+ baseURL: 'https://api.githubcopilot.com',
41
+ });
42
+ }
43
+
44
+ // Groq (ultra-fast inference)
45
+ if (process.env.GROQ_API_KEY) {
46
+ this.groq = new OpenAI({
47
+ apiKey: process.env.GROQ_API_KEY,
48
+ baseURL: 'https://api.groq.com/openai/v1',
49
+ });
50
+ }
51
+
52
+ // Cohere
53
+ if (process.env.COHERE_API_KEY) {
54
+ this.cohere = new OpenAI({
55
+ apiKey: process.env.COHERE_API_KEY,
56
+ baseURL: 'https://api.cohere.ai/v1',
57
+ });
58
+ }
59
+
60
+ // Mistral AI
61
+ if (process.env.MISTRAL_API_KEY) {
62
+ this.mistral = new OpenAI({
63
+ apiKey: process.env.MISTRAL_API_KEY,
64
+ baseURL: 'https://api.mistral.ai/v1',
65
+ });
66
+ }
67
+ }
68
+
69
+ getSystemPrompt(platform = null) {
70
+ // Generate app list based on platform
71
+ let appListSection = '';
72
+
73
+ if (platform === 'both') {
74
+ // Multi-platform mode - use generic app names
75
+ const commonApps = Object.keys(APP_MAPPINGS)
76
+ .filter(app => APP_MAPPINGS[app].ios && APP_MAPPINGS[app].android)
77
+ .slice(0, 30);
78
+
79
+ appListSection = `\n\nMULTI-PLATFORM MODE (iOS + Android devices)
80
+ Available apps: ${commonApps.join(', ')}
81
+
82
+ IMPORTANT: Use generic app names (e.g., "launch settings", "launch chrome")
83
+ The system will automatically convert to platform-specific package IDs:
84
+ ${commonApps.slice(0, 15).map(app => `- ${app} -> iOS: ${APP_MAPPINGS[app].ios} / Android: ${APP_MAPPINGS[app].android}`).join('\n')}
85
+
86
+ Commands will execute SIMULTANEOUSLY on all devices with correct package IDs.
87
+ `;
88
+ } else if (platform) {
89
+ const availableApps = Object.keys(APP_MAPPINGS)
90
+ .filter(app => APP_MAPPINGS[app][platform])
91
+ .slice(0, 50);
92
+
93
+ appListSection = `\n\nPLATFORM: ${platform.toUpperCase()}
94
+ Available apps for ${platform}: ${availableApps.join(', ')}
95
+
96
+ APP PACKAGE MAPPINGS:
97
+ When user says "launch chrome", "open chrome", etc., use the correct package ID:
98
+ ${availableApps.slice(0, 20).map(app => `- ${app} -> launch ${APP_MAPPINGS[app][platform]}`).join('\n')}
99
+ `;
100
+ }
101
+
102
+ return `You are a mobile device automation command converter. Convert natural language requests into executable commands for iOS and Android devices.
103
+
104
+ Available commands (work on both iOS & Android):
105
+ - launch <app_name>: Launch an app using generic name (e.g., "launch settings", "launch chrome")
106
+ - kill <app_name>: Close/force stop an app
107
+ - home: Go to home screen (press home button)
108
+ - back: Navigate back (Android/iOS)
109
+ - url <url>: Open URL in browser
110
+ - click <text>: Click on a button or element by visible text
111
+ - click <x,y>: Click at specific coordinates (e.g., click 500,1000)
112
+ - tap <text/coords>: Same as click
113
+ - longpress <text/coords>: Long press on element or coordinates
114
+ - swipe <direction>: Swipe up/down/left/right (use for scrolling)
115
+ - type <text>: Type text into focused field (just the text, no "type" prefix)
116
+ - screenshot: Take screenshot
117
+ - restart: Restart device
118
+ - rotate <left/right/portrait/landscape>: Rotate screen
119
+
120
+ iOS-specific commands:
121
+ - darkmode/lightmode: Change appearance
122
+ - airplane <on/off>: Toggle airplane mode
123
+ - wifi <on/off>: Toggle WiFi
124
+ - volume <up/down/mute>: Control volume
125
+
126
+ Android-specific commands:
127
+ - getLocators: Get all interactive elements on current screen
128
+ - recent: Open recent apps
129
+ - notifications: Open notification panel
130
+ - quicksettings: Open quick settings
131
+ ${appListSection}
132
+
133
+ COMMON PHRASE MAPPINGS:
134
+ - "scroll up" OR "scroll down" -> swipe up OR swipe down
135
+ - "go to <url>" OR "open <url>" OR "visit <url>" -> url https://<url>
136
+ - "press home" OR "go home" OR "home button" -> home
137
+ - "open settings" OR "launch settings" -> launch settings
138
+ - "open camera" OR "launch camera" -> launch camera
139
+
140
+ Examples:
141
+ - "open chrome" -> launch chrome
142
+ - "launch settings" -> launch settings
143
+ - "open camera" -> launch camera
144
+ - "scroll up" -> swipe up
145
+ - "scroll down" -> swipe down
146
+ - "click on the login button" -> click Login
147
+ - "tap at center of screen" -> click 540,1000
148
+ - "swipe down" -> swipe down
149
+ - "type hello world" -> hello world
150
+ - "take a screenshot" -> screenshot
151
+ - "go back" -> back
152
+ - "press home" -> home
153
+ - "go to google.com" -> url https://www.google.com
154
+ - "visit youtube.com" -> url https://www.youtube.com
155
+ - "launch Chrome and search google.com" -> launch chrome
156
+ WAIT 3000
157
+ url https://www.google.com
158
+ - "launch settings scroll up launch camera go to google.com press home" ->
159
+ launch settings
160
+ WAIT 3000
161
+ swipe up
162
+ WAIT 1000
163
+ home
164
+ WAIT 500
165
+ launch camera
166
+ WAIT 3000
167
+ home
168
+ WAIT 500
169
+ url https://www.google.com
170
+ WAIT 2000
171
+ home
172
+
173
+ Convert this request to commands: "{INPUT}"
174
+
175
+ CRITICAL RULES - YOU MUST FOLLOW THESE EXACTLY:
176
+ 1. Output ONLY executable commands, one per line
177
+ 2. NO explanations, NO markdown code blocks, NO comments, NO extra text
178
+ 3. For multi-step actions, insert WAIT <milliseconds> between commands
179
+ 4. Use GENERIC app names (e.g., "launch settings", "launch chrome", "launch camera")
180
+ 5. Do NOT use platform-specific package IDs - use simple app names
181
+ 6. The system will automatically convert to correct package IDs for each platform
182
+ 5. For URLs, always use: url https://example.com
183
+ 6. For scrolling, use: swipe up OR swipe down (never "scroll")
184
+ 7. For text input, output ONLY the text (never include "type" prefix)
185
+ 8. For home button, output: home (never "press home" or "go home")
186
+ 9. WAIT timings: apps=3000ms, pages=2000ms, UI=1000ms, quick=500ms
187
+ 10. Parse compound requests into individual steps with WAIT between each
188
+
189
+ OUTPUT FORMAT EXAMPLE:
190
+ launch com.apple.Preferences
191
+ WAIT 3000
192
+ swipe up
193
+ WAIT 1000
194
+ home
195
+
196
+ DO NOT include any other text. Start your response with the first command.`;
197
+ }
198
+
199
+ async convertCommand(text, platform = null, providerOverride = null) {
200
+ const provider = providerOverride || this.provider;
201
+
202
+ try {
203
+ switch (provider) {
204
+ case 'openai':
205
+ return await this.convertWithOpenAI(text, platform);
206
+ case 'gemini':
207
+ return await this.convertWithGemini(text, platform);
208
+ case 'claude':
209
+ return await this.convertWithClaude(text, platform);
210
+ case 'copilot':
211
+ return await this.convertWithCopilot(text, platform);
212
+ case 'groq':
213
+ return await this.convertWithGroq(text, platform);
214
+ case 'cohere':
215
+ return await this.convertWithCohere(text, platform);
216
+ case 'mistral':
217
+ return await this.convertWithMistral(text, platform);
218
+ default:
219
+ throw new Error(`Unknown AI provider: ${provider}`);
220
+ }
221
+ } catch (error) {
222
+ console.error(`Error with ${provider}:`, error.message);
223
+ throw new Error(`AI conversion failed (${provider}): ${error.message}`);
224
+ }
225
+ }
226
+
227
+ // Get model to use (from env or default)
228
+ getModelForProvider(provider) {
229
+ // If specific model is set, use it
230
+ if (this.model) return this.model;
231
+
232
+ // Otherwise use defaults (optimized for speed)
233
+ const defaults = {
234
+ openai: 'gpt-4o-mini', // Faster than gpt-4o
235
+ gemini: 'gemini-2.5-flash', // Fast and reliable
236
+ claude: 'claude-3-5-sonnet-20241022',
237
+ copilot: 'gpt-4o',
238
+ groq: 'llama-3.3-70b-versatile',
239
+ cohere: 'command-r-plus',
240
+ mistral: 'mistral-large-latest',
241
+ };
242
+
243
+ return defaults[provider] || 'gpt-4';
244
+ }
245
+
246
+ async convertWithOpenAI(text, platform = null) {
247
+ if (!this.openai) {
248
+ throw new Error('OpenAI not configured');
249
+ }
250
+
251
+ const model = this.getModelForProvider('openai');
252
+
253
+ const response = await this.openai.chat.completions.create({
254
+ model: model,
255
+ messages: [
256
+ { role: 'system', content: this.getSystemPrompt(platform) },
257
+ { role: 'user', content: text }
258
+ ],
259
+ temperature: 0.3,
260
+ max_tokens: 500,
261
+ });
262
+
263
+ let convertedText = response.choices[0].message.content.trim();
264
+ convertedText = this.cleanAIResponse(convertedText);
265
+
266
+ return convertedText;
267
+ }
268
+
269
+ async convertWithGemini(text, platform = null) {
270
+ if (!this.gemini) {
271
+ throw new Error('Gemini not configured');
272
+ }
273
+
274
+ let modelName = this.getModelForProvider('gemini');
275
+
276
+ try {
277
+ // Use default Gemini config for best accuracy
278
+ const model = this.gemini.getGenerativeModel({ model: modelName });
279
+
280
+ // Build prompt and sanitize for Gemini API (requires ASCII-safe ByteString)
281
+ const rawPrompt = this.getSystemPrompt(platform).replace('{INPUT}', text);
282
+
283
+ // Remove ALL non-ASCII characters that could cause ByteString errors
284
+ const sanitizedPrompt = rawPrompt
285
+ .replace(/[^\x00-\x7F]/g, ' ') // Replace non-ASCII with spaces
286
+ .replace(/\s+/g, ' ') // Normalize multiple spaces
287
+ .trim();
288
+
289
+ const result = await model.generateContent(sanitizedPrompt);
290
+ const response = await result.response;
291
+ let convertedText = response.text().trim();
292
+
293
+ return this.cleanAIResponse(convertedText);
294
+ } catch (error) {
295
+ // If the model fails, try with a faster fallback
296
+ if (modelName !== 'gemini-2.5-flash') {
297
+ console.warn(`Gemini model ${modelName} failed, falling back to gemini-2.5-flash`);
298
+ const fallbackModel = this.gemini.getGenerativeModel({ model: 'gemini-2.5-flash' });
299
+ const prompt = this.getSystemPrompt(platform).replace('{INPUT}', text);
300
+ const sanitizedPrompt = prompt.replace(/[^\x00-\x7F]/g, " ").replace(/\s+/g, ' ').trim();
301
+ const result = await fallbackModel.generateContent(sanitizedPrompt);
302
+ const response = await result.response;
303
+ return this.cleanAIResponse(response.text().trim());
304
+ }
305
+ throw error;
306
+ }
307
+ }
308
+
309
+ async convertWithCopilot(text, platform = null) {
310
+ if (!this.copilot) {
311
+ throw new Error('GitHub Copilot not configured');
312
+ }
313
+
314
+ const model = this.getModelForProvider('copilot');
315
+
316
+ const response = await this.copilot.chat.completions.create({
317
+ model: model,
318
+ messages: [
319
+ { role: 'system', content: this.getSystemPrompt(platform) },
320
+ { role: 'user', content: text }
321
+ ],
322
+ temperature: 0.3,
323
+ max_tokens: 500,
324
+ });
325
+
326
+ let convertedText = response.choices[0].message.content.trim();
327
+ convertedText = this.cleanAIResponse(convertedText);
328
+
329
+ return convertedText;
330
+ }
331
+
332
+ async convertWithClaude(text, platform = null) {
333
+ if (!this.claude) {
334
+ throw new Error('Claude not configured');
335
+ }
336
+
337
+ const model = this.getModelForProvider('claude');
338
+ const systemPrompt = this.getSystemPrompt(platform);
339
+
340
+ const response = await this.claude.messages.create({
341
+ model: model,
342
+ max_tokens: 500,
343
+ system: systemPrompt,
344
+ messages: [
345
+ { role: 'user', content: text }
346
+ ],
347
+ temperature: 0.3,
348
+ });
349
+
350
+ let convertedText = response.content[0].text.trim();
351
+ convertedText = this.cleanAIResponse(convertedText);
352
+
353
+ return convertedText;
354
+ }
355
+
356
+ async convertWithGroq(text, platform = null) {
357
+ if (!this.groq) {
358
+ throw new Error('Groq not configured');
359
+ }
360
+
361
+ const model = this.getModelForProvider('groq');
362
+
363
+ const response = await this.groq.chat.completions.create({
364
+ model: model,
365
+ messages: [
366
+ { role: 'system', content: this.getSystemPrompt(platform) },
367
+ { role: 'user', content: text }
368
+ ],
369
+ temperature: 0.3,
370
+ max_tokens: 500,
371
+ });
372
+
373
+ let convertedText = response.choices[0].message.content.trim();
374
+ convertedText = this.cleanAIResponse(convertedText);
375
+
376
+ return convertedText;
377
+ }
378
+
379
+ async convertWithCohere(text, platform = null) {
380
+ if (!this.cohere) {
381
+ throw new Error('Cohere not configured');
382
+ }
383
+
384
+ const model = this.getModelForProvider('cohere');
385
+
386
+ const response = await this.cohere.chat.completions.create({
387
+ model: model,
388
+ messages: [
389
+ { role: 'system', content: this.getSystemPrompt(platform) },
390
+ { role: 'user', content: text }
391
+ ],
392
+ temperature: 0.3,
393
+ max_tokens: 500,
394
+ });
395
+
396
+ let convertedText = response.choices[0].message.content.trim();
397
+ convertedText = this.cleanAIResponse(convertedText);
398
+
399
+ return convertedText;
400
+ }
401
+
402
+ async convertWithMistral(text, platform = null) {
403
+ if (!this.mistral) {
404
+ throw new Error('Mistral AI not configured');
405
+ }
406
+
407
+ const model = this.getModelForProvider('mistral');
408
+
409
+ const response = await this.mistral.chat.completions.create({
410
+ model: model,
411
+ messages: [
412
+ { role: 'system', content: this.getSystemPrompt(platform) },
413
+ { role: 'user', content: text }
414
+ ],
415
+ temperature: 0.3,
416
+ max_tokens: 500,
417
+ });
418
+
419
+ let convertedText = response.choices[0].message.content.trim();
420
+ convertedText = this.cleanAIResponse(convertedText);
421
+
422
+ return convertedText;
423
+ }
424
+
425
+ // Clean up AI responses - remove markdown, explanations, etc.
426
+ cleanAIResponse(text) {
427
+ // Remove markdown code blocks
428
+ text = text.replace(/```[\s\S]*?```/g, '').trim();
429
+ text = text.replace(/```/g, '').trim();
430
+
431
+ // Remove any lines that look like explanations (starting with explanatory text)
432
+ const lines = text.split('\n');
433
+ const cleanedLines = lines.filter(line => {
434
+ const trimmed = line.trim();
435
+ if (!trimmed) return false;
436
+
437
+ // Keep lines that are commands or WAIT
438
+ if (trimmed.startsWith('launch ')) return true;
439
+ if (trimmed.startsWith('kill ')) return true;
440
+ if (trimmed === 'home') return true;
441
+ if (trimmed === 'back') return true;
442
+ if (trimmed.startsWith('url ')) return true;
443
+ if (trimmed.startsWith('click ')) return true;
444
+ if (trimmed.startsWith('tap ')) return true;
445
+ if (trimmed.startsWith('longpress ')) return true;
446
+ if (trimmed.startsWith('swipe ')) return true;
447
+ if (trimmed.startsWith('WAIT ')) return true;
448
+ if (trimmed.startsWith('screenshot')) return true;
449
+ if (trimmed.startsWith('restart')) return true;
450
+ if (trimmed.startsWith('rotate ')) return true;
451
+ if (trimmed.startsWith('darkmode')) return true;
452
+ if (trimmed.startsWith('lightmode')) return true;
453
+ if (trimmed.startsWith('airplane ')) return true;
454
+ if (trimmed.startsWith('wifi ')) return true;
455
+ if (trimmed.startsWith('volume ')) return true;
456
+ if (trimmed === 'getLocators') return true;
457
+ if (trimmed === 'recent') return true;
458
+ if (trimmed === 'notifications') return true;
459
+ if (trimmed === 'quicksettings') return true;
460
+
461
+ // If it doesn't start with a known command, it might be text to type
462
+ // Check if previous line was a command that expects text input
463
+ return true; // For now, include it (could be text to type)
464
+ });
465
+
466
+ return cleanedLines.join('\n').trim();
467
+ }
468
+
469
+ getAvailableProviders() {
470
+ const available = [];
471
+
472
+ if (this.openai) available.push({ id: 'openai', name: 'OpenAI', icon: '🤖' });
473
+ if (this.gemini) available.push({ id: 'gemini', name: 'Google Gemini', icon: '✨' });
474
+ if (this.claude) available.push({ id: 'claude', name: 'Anthropic Claude', icon: '🧠' });
475
+ if (this.copilot) available.push({ id: 'copilot', name: 'GitHub Copilot', icon: '🐙' });
476
+ if (this.groq) available.push({ id: 'groq', name: 'Groq', icon: '⚡' });
477
+ if (this.cohere) available.push({ id: 'cohere', name: 'Cohere', icon: '🌊' });
478
+ if (this.mistral) available.push({ id: 'mistral', name: 'Mistral AI', icon: '🌬️' });
479
+
480
+ return available;
481
+ }
482
+
483
+ setProvider(provider, model = null) {
484
+ const available = this.getAvailableProviders().map(p => p.id);
485
+ if (available.includes(provider)) {
486
+ this.provider = provider;
487
+ if (model) {
488
+ this.model = model;
489
+ }
490
+ return true;
491
+ }
492
+ return false;
493
+ }
494
+
495
+ getCurrentProvider() {
496
+ return {
497
+ id: this.provider,
498
+ name: this.getProviderName(this.provider),
499
+ model: this.model || this.getModelForProvider(this.provider),
500
+ available: this.getAvailableProviders(),
501
+ };
502
+ }
503
+
504
+ getProviderName(providerId) {
505
+ const names = {
506
+ openai: 'OpenAI',
507
+ gemini: 'Google Gemini',
508
+ claude: 'Anthropic Claude',
509
+ copilot: 'GitHub Copilot',
510
+ groq: 'Groq',
511
+ cohere: 'Cohere',
512
+ mistral: 'Mistral AI',
513
+ };
514
+ return names[providerId] || 'Unknown';
515
+ }
516
+ }
517
+
518
+ module.exports = AIProviderManager;