mobile-ai-agents 1.0.35 → 1.0.36

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/README.md CHANGED
@@ -11,7 +11,7 @@ Android · iOS · Flutter · React Native · Kotlin Multiplatform · Unity · Un
11
11
  [![GitHub Stars](https://img.shields.io/github/stars/salmanashraf/mobile-agency?style=social)](https://github.com/salmanashraf/mobile-agency/stargazers)
12
12
  [![Platform](https://img.shields.io/badge/platforms-Android%20%7C%20iOS%20%7C%20Flutter%20%7C%20RN%20%7C%20KMP%20%7C%20Unity%20%7C%20Unreal-brightgreen)](#core-loops-and-platform-plugins)
13
13
 
14
- [Installation Guide](docs/installation.md) · [Core Loops](docs/core-loops.md) · [mobile-karpathy](mobile-karpathy.md) · [Wiki](https://github.com/salmanashraf/mobile-agency/wiki) · [Getting Started](docs/getting-started.md) · [Contributing](CONTRIBUTING.md)
14
+ [Installation Guide](docs/installation.md) · [Android Device Setup](docs/android-device-setup.md) · [Core Loops](docs/core-loops.md) · [mobile-karpathy](mobile-karpathy.md) · [Wiki](https://github.com/salmanashraf/mobile-ai-agents/wiki) · [Getting Started](docs/getting-started.md) · [Contributing](CONTRIBUTING.md)
15
15
 
16
16
  > AI agents, skills, and Loop Engineering workflows for building, testing, reviewing, and shipping Android, iOS, Flutter, React Native, Unity, and Unreal apps.
17
17
 
@@ -398,6 +398,14 @@ Mobile MCP fits the QA stage next: use it for emulator, simulator, or real-devic
398
398
 
399
399
  Mobile MCP gives Mobile AI Agents a device automation layer for iOS and Android simulators, emulators, and real devices.
400
400
 
401
+ Prepare an Android project with environment diagnostics and settings-preserving MCP setup:
402
+
403
+ ```bash
404
+ npx mobile-ai-agents doctor --platform android
405
+ npx mobile-ai-agents mcp setup --client claude
406
+ npx mobile-ai-agents mcp config init --app-id com.example.app
407
+ ```
408
+
401
409
  ```
402
410
  /mobile-mcp-qa
403
411
  1. List devices
@@ -408,7 +416,7 @@ Mobile MCP gives Mobile AI Agents a device automation layer for iOS and Android
408
416
  6. Produce MOBILE_MCP_QA_REPORT.md
409
417
  ```
410
418
 
411
- Use it inside APPFORGE Stage 7 Full QA, UI match review, launch readiness checks, and screenshot validation. Full guide: [docs/mobile-mcp.md](docs/mobile-mcp.md)
419
+ Use it inside APPFORGE Stage 7 Full QA, UI match review, launch readiness checks, and screenshot validation. Guides: [Android Device QA Setup](docs/android-device-setup.md), [wiki guide](https://github.com/salmanashraf/mobile-ai-agents/wiki/Android-Device-QA-Setup), and [Mobile MCP Integration](docs/mobile-mcp.md).
412
420
 
413
421
  ### Device Proof Reports
414
422
 
@@ -0,0 +1,182 @@
1
+ 'use strict';
2
+
3
+ const childProcess = require('child_process');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const { parseOptions } = require('../lib/options');
8
+ const { inspectMcpConfiguration, loadProjectConfig } = require('./mcp');
9
+
10
+ function executableOnPath(name, env, fsImpl) {
11
+ const entries = String(env.PATH || '').split(path.delimiter).filter(Boolean);
12
+ for (const entry of entries) {
13
+ const candidate = path.join(entry, name);
14
+ if (fsImpl.existsSync(candidate)) return candidate;
15
+ }
16
+ return null;
17
+ }
18
+
19
+ function defaultRunner(command, args, env) {
20
+ const result = childProcess.spawnSync(command, args, {
21
+ encoding: 'utf8',
22
+ env,
23
+ timeout: 10000,
24
+ });
25
+ return {
26
+ status: result.status,
27
+ stdout: result.stdout || '',
28
+ stderr: result.stderr || '',
29
+ error: result.error ? result.error.message : '',
30
+ };
31
+ }
32
+
33
+ function resultStatus(checks) {
34
+ if (checks.some(check => check.status === 'FAIL')) return 'FAIL';
35
+ if (checks.some(check => check.status === 'WARN')) return 'WARN';
36
+ return 'PASS';
37
+ }
38
+
39
+ function parseAdbDevices(output) {
40
+ return String(output)
41
+ .split(/\r?\n/)
42
+ .slice(1)
43
+ .map(line => line.trim())
44
+ .filter(Boolean)
45
+ .map(line => {
46
+ const [serial, state] = line.split(/\s+/);
47
+ return { serial, state };
48
+ });
49
+ }
50
+
51
+ function collectAndroidDoctor(options = {}) {
52
+ const cwd = options.cwd || process.cwd();
53
+ const env = options.env || process.env;
54
+ const homedir = options.homedir || os.homedir();
55
+ const fsImpl = options.fsImpl || fs;
56
+ const runner = options.runner || defaultRunner;
57
+ const checks = [];
58
+ const sdkCandidates = [
59
+ env.ANDROID_HOME,
60
+ env.ANDROID_SDK_ROOT,
61
+ path.join(homedir, 'Library', 'Android', 'sdk'),
62
+ path.join(homedir, 'Android', 'Sdk'),
63
+ ].filter(Boolean);
64
+ const sdkRoot = sdkCandidates.find(candidate => fsImpl.existsSync(candidate));
65
+
66
+ checks.push(sdkRoot
67
+ ? { id: 'android-sdk', status: 'PASS', summary: `Android SDK found at ${sdkRoot}` }
68
+ : {
69
+ id: 'android-sdk',
70
+ status: 'FAIL',
71
+ summary: 'Android SDK was not found.',
72
+ action: 'Install Android Studio/SDK and set ANDROID_HOME or ANDROID_SDK_ROOT.',
73
+ });
74
+
75
+ const adb = executableOnPath('adb', env, fsImpl) ||
76
+ (sdkRoot && fsImpl.existsSync(path.join(sdkRoot, 'platform-tools', 'adb')) ? path.join(sdkRoot, 'platform-tools', 'adb') : null);
77
+ if (!adb) {
78
+ checks.push({
79
+ id: 'adb',
80
+ status: 'FAIL',
81
+ summary: 'adb was not found.',
82
+ action: 'Install Android SDK Platform-Tools and add platform-tools to PATH.',
83
+ });
84
+ } else {
85
+ const version = runner(adb, ['version'], env);
86
+ checks.push(version.status === 0
87
+ ? { id: 'adb', status: 'PASS', summary: `adb available at ${adb}`, details: version.stdout.split(/\r?\n/)[0] }
88
+ : { id: 'adb', status: 'FAIL', summary: 'adb exists but could not run.', details: version.stderr || version.error, action: 'Repair Platform-Tools or PATH permissions.' });
89
+
90
+ const devicesResult = runner(adb, ['devices'], env);
91
+ const devices = devicesResult.status === 0 ? parseAdbDevices(devicesResult.stdout) : [];
92
+ checks.push(devicesResult.status !== 0
93
+ ? { id: 'android-devices', status: 'FAIL', summary: 'adb could not list devices.', details: devicesResult.stderr || devicesResult.error, action: 'Restart adb and reconnect or authorize the target device.' }
94
+ : devices.some(device => device.state === 'device')
95
+ ? { id: 'android-devices', status: 'PASS', summary: `${devices.filter(device => device.state === 'device').length} ready Android device(s).`, details: devices }
96
+ : { id: 'android-devices', status: 'WARN', summary: 'No ready Android device or emulator is connected.', details: devices, action: 'Start an emulator or connect and authorize a device.' });
97
+ }
98
+
99
+ const emulator = executableOnPath('emulator', env, fsImpl) ||
100
+ (sdkRoot && fsImpl.existsSync(path.join(sdkRoot, 'emulator', 'emulator')) ? path.join(sdkRoot, 'emulator', 'emulator') : null);
101
+ if (!emulator) {
102
+ checks.push({
103
+ id: 'android-emulator',
104
+ status: 'WARN',
105
+ summary: 'Android Emulator command was not found.',
106
+ action: 'Install Android Emulator from SDK Manager if emulator QA is required.',
107
+ });
108
+ } else {
109
+ const avdResult = runner(emulator, ['-list-avds'], env);
110
+ const avds = avdResult.status === 0 ? avdResult.stdout.split(/\r?\n/).map(v => v.trim()).filter(Boolean) : [];
111
+ checks.push(avdResult.status !== 0
112
+ ? { id: 'android-emulator', status: 'WARN', summary: 'Emulator exists but AVDs could not be listed.', details: avdResult.stderr || avdResult.error, action: 'Open Android Studio Device Manager and repair the emulator installation.' }
113
+ : avds.length
114
+ ? { id: 'android-emulator', status: 'PASS', summary: `${avds.length} Android Virtual Device image(s) available.`, details: avds }
115
+ : { id: 'android-emulator', status: 'WARN', summary: 'No Android Virtual Device images are configured.', action: 'Create an AVD in Android Studio Device Manager.' });
116
+ }
117
+
118
+ const mcpConfigs = inspectMcpConfiguration({ cwd, homedir, fsImpl });
119
+ checks.push(mcpConfigs.length
120
+ ? { id: 'mobile-mcp', status: 'PASS', summary: `Mobile MCP configured for ${mcpConfigs.map(item => item.client).join(', ')}.`, details: mcpConfigs.map(item => item.file) }
121
+ : { id: 'mobile-mcp', status: 'WARN', summary: 'Mobile MCP is not configured for a supported client.', action: 'Run mobile-ai-agents mcp setup --client <client>.' });
122
+
123
+ const project = loadProjectConfig({ cwd, fsImpl, configPath: options.configPath });
124
+ if (!project.config) {
125
+ checks.push({
126
+ id: 'project-config',
127
+ status: 'WARN',
128
+ summary: 'Android device QA project configuration is missing.',
129
+ action: 'Run mobile-ai-agents mcp config init --app-id com.example.app.',
130
+ });
131
+ } else if (project.errors.length) {
132
+ checks.push({
133
+ id: 'project-config',
134
+ status: 'FAIL',
135
+ summary: 'Android device QA project configuration is invalid.',
136
+ details: project.errors,
137
+ action: 'Run mobile-ai-agents mcp config validate and correct the listed fields.',
138
+ });
139
+ } else {
140
+ const apk = path.resolve(cwd, project.config.apkPath);
141
+ checks.push(fsImpl.existsSync(apk)
142
+ ? { id: 'project-config', status: 'PASS', summary: 'Android device QA project configuration is valid.', details: project.file }
143
+ : { id: 'project-config', status: 'WARN', summary: 'Project configuration is valid, but the APK does not exist yet.', details: project.config.apkPath, action: 'Build the configured APK before device QA.' });
144
+ }
145
+
146
+ return {
147
+ platform: 'android',
148
+ status: resultStatus(checks),
149
+ checkedAt: new Date().toISOString(),
150
+ checks,
151
+ };
152
+ }
153
+
154
+ function printDoctorReport(report) {
155
+ console.log('');
156
+ console.log(' Android Environment Doctor');
157
+ console.log(` Result: ${report.status}`);
158
+ console.log('');
159
+ for (const check of report.checks) {
160
+ const icon = check.status === 'PASS' ? '✓' : check.status === 'WARN' ? '!' : '✗';
161
+ console.log(` ${icon} [${check.status}] ${check.summary}`);
162
+ if (check.action) console.log(` Action: ${check.action}`);
163
+ }
164
+ console.log('');
165
+ }
166
+
167
+ async function cmdDoctor(args) {
168
+ const opts = parseOptions(args);
169
+ const platform = opts.platform === true ? '' : opts.platform;
170
+ if (platform !== 'android') throw new Error('doctor currently requires --platform android.');
171
+ const report = collectAndroidDoctor({ configPath: opts.config === true ? undefined : opts.config });
172
+ if (opts.json) console.log(JSON.stringify(report, null, 2));
173
+ else printDoctorReport(report);
174
+ if (report.status === 'FAIL') process.exitCode = 1;
175
+ return report;
176
+ }
177
+
178
+ module.exports = {
179
+ cmdDoctor,
180
+ collectAndroidDoctor,
181
+ parseAdbDevices,
182
+ };
@@ -0,0 +1,330 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { parseOptions } = require('../lib/options');
7
+
8
+ const SERVER_NAME = 'mobile-mcp';
9
+ const SERVER_COMMAND = 'npx';
10
+ const SERVER_ARGS = ['-y', '@mobilenext/mobile-mcp@latest'];
11
+ const PROJECT_CONFIG_PATH = path.join('.mobile-ai-agents', 'android-device.json');
12
+
13
+ const JSON_CLIENTS = {
14
+ claude: ({ cwd }) => path.join(cwd, '.mcp.json'),
15
+ cursor: ({ cwd }) => path.join(cwd, '.cursor', 'mcp.json'),
16
+ windsurf: ({ homedir }) => path.join(homedir, '.codeium', 'windsurf', 'mcp_config.json'),
17
+ };
18
+
19
+ function ensureParent(file, fsImpl = fs) {
20
+ fsImpl.mkdirSync(path.dirname(file), { recursive: true });
21
+ }
22
+
23
+ function readJsonObject(file, fsImpl = fs) {
24
+ if (!fsImpl.existsSync(file)) return {};
25
+ try {
26
+ const value = JSON.parse(fsImpl.readFileSync(file, 'utf8'));
27
+ if (!value || Array.isArray(value) || typeof value !== 'object') {
28
+ throw new Error('top-level value must be an object');
29
+ }
30
+ return value;
31
+ } catch (error) {
32
+ throw new Error(`Invalid JSON in ${file}: ${error.message}`);
33
+ }
34
+ }
35
+
36
+ function resolveClientConfig(client, options = {}) {
37
+ const cwd = options.cwd || process.cwd();
38
+ const homedir = options.homedir || os.homedir();
39
+ if (options.configPath) return path.resolve(cwd, options.configPath);
40
+ if (client === 'codex') return path.join(homedir, '.codex', 'config.toml');
41
+ const resolver = JSON_CLIENTS[client];
42
+ if (!resolver) {
43
+ throw new Error('Unsupported MCP client. Use claude, cursor, windsurf, or codex.');
44
+ }
45
+ return resolver({ cwd, homedir });
46
+ }
47
+
48
+ function setupJsonClient(file, fsImpl = fs) {
49
+ const config = readJsonObject(file, fsImpl);
50
+ const servers = config.mcpServers && typeof config.mcpServers === 'object' && !Array.isArray(config.mcpServers)
51
+ ? { ...config.mcpServers }
52
+ : {};
53
+ const desired = { command: SERVER_COMMAND, args: SERVER_ARGS };
54
+ const current = servers[SERVER_NAME];
55
+ const unchanged = current &&
56
+ current.command === desired.command &&
57
+ JSON.stringify(current.args) === JSON.stringify(desired.args);
58
+
59
+ if (unchanged) return { changed: false, file, clientFormat: 'json' };
60
+
61
+ config.mcpServers = { ...servers, [SERVER_NAME]: desired };
62
+ ensureParent(file, fsImpl);
63
+ fsImpl.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
64
+ return { changed: true, file, clientFormat: 'json' };
65
+ }
66
+
67
+ function setupCodexClient(file, fsImpl = fs) {
68
+ const existing = fsImpl.existsSync(file) ? fsImpl.readFileSync(file, 'utf8') : '';
69
+ const block = [
70
+ '[mcp_servers.mobile-mcp]',
71
+ `command = "${SERVER_COMMAND}"`,
72
+ `args = ["${SERVER_ARGS.join('", "')}"]`,
73
+ ].join('\n');
74
+ const sectionPattern = /^\[mcp_servers\.(?:mobile-mcp|"mobile-mcp")\]\s*$/m;
75
+ const match = sectionPattern.exec(existing);
76
+ let next;
77
+
78
+ if (!match) {
79
+ const prefix = existing.trimEnd();
80
+ next = `${prefix}${prefix ? '\n\n' : ''}${block}\n`;
81
+ } else {
82
+ const sectionStart = match.index;
83
+ const afterHeader = sectionStart + match[0].length;
84
+ const remainder = existing.slice(afterHeader);
85
+ const nextSection = /^\s*\[[^\]]+\]/m.exec(remainder);
86
+ const sectionEnd = nextSection ? afterHeader + nextSection.index : existing.length;
87
+ next = `${existing.slice(0, sectionStart)}${block}\n${existing.slice(sectionEnd).replace(/^\s+/, '')}`;
88
+ }
89
+
90
+ if (next === existing) return { changed: false, file, clientFormat: 'toml' };
91
+ ensureParent(file, fsImpl);
92
+ fsImpl.writeFileSync(file, next);
93
+ return { changed: true, file, clientFormat: 'toml' };
94
+ }
95
+
96
+ function setupMcpClient(client, options = {}) {
97
+ const fsImpl = options.fsImpl || fs;
98
+ const file = resolveClientConfig(client, options);
99
+ const result = client === 'codex'
100
+ ? setupCodexClient(file, fsImpl)
101
+ : setupJsonClient(file, fsImpl);
102
+ return { ...result, client, server: SERVER_NAME };
103
+ }
104
+
105
+ function defaultProjectConfig(values = {}) {
106
+ return {
107
+ version: 1,
108
+ platform: 'android',
109
+ appId: values.appId || '',
110
+ apkPath: values.apkPath || 'app/build/outputs/apk/debug/app-debug.apk',
111
+ device: values.device || 'auto',
112
+ flow: values.flow || ['Launch app', 'Verify the initial screen'],
113
+ timeouts: {
114
+ bootMs: values.bootMs || 120000,
115
+ actionMs: values.actionMs || 15000,
116
+ },
117
+ evidenceDir: values.evidenceDir || '.mobile-ai-agents/evidence',
118
+ };
119
+ }
120
+
121
+ function validateProjectConfig(config) {
122
+ const errors = [];
123
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
124
+ return ['Configuration must be a JSON object.'];
125
+ }
126
+ if (config.version !== 1) errors.push('version must be 1.');
127
+ if (config.platform !== 'android') errors.push('platform must be "android".');
128
+ if (typeof config.appId !== 'string' || !/^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+$/.test(config.appId)) {
129
+ errors.push('appId must be a valid Android application ID such as com.example.app.');
130
+ }
131
+ if (typeof config.apkPath !== 'string' || !config.apkPath.endsWith('.apk')) {
132
+ errors.push('apkPath must point to an .apk file.');
133
+ }
134
+ if (typeof config.device !== 'string' || !config.device.trim()) {
135
+ errors.push('device must be "auto", an emulator name, or a device serial.');
136
+ }
137
+ if (!Array.isArray(config.flow) || !config.flow.length || config.flow.some(step => typeof step !== 'string' || !step.trim())) {
138
+ errors.push('flow must be a non-empty array of test steps.');
139
+ }
140
+ if (!config.timeouts || !Number.isInteger(config.timeouts.bootMs) || config.timeouts.bootMs <= 0) {
141
+ errors.push('timeouts.bootMs must be a positive integer.');
142
+ }
143
+ if (!config.timeouts || !Number.isInteger(config.timeouts.actionMs) || config.timeouts.actionMs <= 0) {
144
+ errors.push('timeouts.actionMs must be a positive integer.');
145
+ }
146
+ if (typeof config.evidenceDir !== 'string' || !config.evidenceDir.trim()) {
147
+ errors.push('evidenceDir must be a non-empty path.');
148
+ }
149
+ return errors;
150
+ }
151
+
152
+ function projectConfigFile(options = {}) {
153
+ const cwd = options.cwd || process.cwd();
154
+ return path.resolve(cwd, options.configPath || PROJECT_CONFIG_PATH);
155
+ }
156
+
157
+ function initProjectConfig(values = {}, options = {}) {
158
+ const fsImpl = options.fsImpl || fs;
159
+ const file = projectConfigFile(options);
160
+ const existing = readJsonObject(file, fsImpl);
161
+ const base = Object.keys(existing).length ? existing : defaultProjectConfig();
162
+ const next = {
163
+ ...base,
164
+ version: 1,
165
+ platform: 'android',
166
+ appId: values.appId !== undefined ? values.appId : base.appId,
167
+ apkPath: values.apkPath !== undefined ? values.apkPath : base.apkPath,
168
+ device: values.device !== undefined ? values.device : base.device,
169
+ flow: values.flow !== undefined ? values.flow : base.flow,
170
+ timeouts: {
171
+ ...(base.timeouts || {}),
172
+ bootMs: values.bootMs !== undefined ? values.bootMs : base.timeouts?.bootMs || 120000,
173
+ actionMs: values.actionMs !== undefined ? values.actionMs : base.timeouts?.actionMs || 15000,
174
+ },
175
+ evidenceDir: values.evidenceDir !== undefined ? values.evidenceDir : base.evidenceDir,
176
+ };
177
+ const errors = validateProjectConfig(next);
178
+ if (errors.length) throw new Error(`Invalid Android device configuration:\n- ${errors.join('\n- ')}`);
179
+
180
+ const serialized = `${JSON.stringify(next, null, 2)}\n`;
181
+ const current = fsImpl.existsSync(file) ? fsImpl.readFileSync(file, 'utf8') : '';
182
+ if (current !== serialized) {
183
+ ensureParent(file, fsImpl);
184
+ fsImpl.writeFileSync(file, serialized);
185
+ }
186
+ return { changed: current !== serialized, file, config: next };
187
+ }
188
+
189
+ function loadProjectConfig(options = {}) {
190
+ const fsImpl = options.fsImpl || fs;
191
+ const file = projectConfigFile(options);
192
+ if (!fsImpl.existsSync(file)) return { file, config: null, errors: ['Project configuration does not exist.'] };
193
+ const config = readJsonObject(file, fsImpl);
194
+ return { file, config, errors: validateProjectConfig(config) };
195
+ }
196
+
197
+ function knownMcpConfigFiles(options = {}) {
198
+ const cwd = options.cwd || process.cwd();
199
+ const homedir = options.homedir || os.homedir();
200
+ return [
201
+ { client: 'claude', format: 'json', file: path.join(cwd, '.mcp.json') },
202
+ { client: 'cursor', format: 'json', file: path.join(cwd, '.cursor', 'mcp.json') },
203
+ { client: 'windsurf', format: 'json', file: path.join(homedir, '.codeium', 'windsurf', 'mcp_config.json') },
204
+ { client: 'codex', format: 'toml', file: path.join(homedir, '.codex', 'config.toml') },
205
+ ];
206
+ }
207
+
208
+ function inspectMcpConfiguration(options = {}) {
209
+ const fsImpl = options.fsImpl || fs;
210
+ const found = [];
211
+ for (const candidate of knownMcpConfigFiles(options)) {
212
+ if (!fsImpl.existsSync(candidate.file)) continue;
213
+ try {
214
+ let configured;
215
+ if (candidate.format === 'json') {
216
+ const server = readJsonObject(candidate.file, fsImpl).mcpServers?.[SERVER_NAME];
217
+ configured = server?.command === SERVER_COMMAND && JSON.stringify(server.args) === JSON.stringify(SERVER_ARGS);
218
+ } else {
219
+ const content = fsImpl.readFileSync(candidate.file, 'utf8');
220
+ const header = /^\[mcp_servers\.(?:mobile-mcp|"mobile-mcp")\]\s*$/m.exec(content);
221
+ if (!header) {
222
+ configured = false;
223
+ } else {
224
+ const remainder = content.slice(header.index + header[0].length);
225
+ const nextSection = /^\s*\[[^\]]+\]/m.exec(remainder);
226
+ const section = nextSection ? remainder.slice(0, nextSection.index) : remainder;
227
+ configured = /^\s*command\s*=\s*"npx"\s*$/m.test(section) &&
228
+ /^\s*args\s*=\s*\[\s*"-y"\s*,\s*"@mobilenext\/mobile-mcp@latest"\s*\]\s*$/m.test(section);
229
+ }
230
+ }
231
+ if (configured) found.push(candidate);
232
+ } catch {
233
+ // Invalid files are reported by setup/validation rather than hidden as configured.
234
+ }
235
+ }
236
+ return found;
237
+ }
238
+
239
+ function printMcpHelp() {
240
+ console.log('');
241
+ console.log(' Mobile MCP setup');
242
+ console.log('');
243
+ console.log(' Commands:');
244
+ console.log(' mobile-ai-agents mcp setup --client <claude|cursor|windsurf|codex>');
245
+ console.log(' mobile-ai-agents mcp config init --app-id com.example.app [options]');
246
+ console.log(' mobile-ai-agents mcp config validate [--json]');
247
+ console.log('');
248
+ console.log(' Config options:');
249
+ console.log(' --apk <path> APK path');
250
+ console.log(' --device <auto|name|serial> Device selection');
251
+ console.log(' --flow "step 1|step 2" Ordered test flow');
252
+ console.log(' --boot-timeout-ms <number> Emulator boot timeout');
253
+ console.log(' --action-timeout-ms <number> Per-action timeout');
254
+ console.log(' --evidence-dir <path> Screenshots and reports');
255
+ console.log(' --config <path> Override target config path');
256
+ console.log('');
257
+ }
258
+
259
+ async function cmdMcp(args) {
260
+ const opts = parseOptions(args);
261
+ const [sub, action] = opts._;
262
+
263
+ if (!sub || sub === 'help') {
264
+ printMcpHelp();
265
+ return;
266
+ }
267
+
268
+ if (sub === 'setup') {
269
+ if (!opts.client || opts.client === true) throw new Error('mcp setup requires --client.');
270
+ const result = setupMcpClient(String(opts.client), { configPath: opts.config === true ? undefined : opts.config });
271
+ if (opts.json) {
272
+ console.log(JSON.stringify(result, null, 2));
273
+ } else {
274
+ console.log('');
275
+ console.log(` ${result.changed ? '✓ Configured' : '✓ Already configured'} mobile-mcp for ${result.client}`);
276
+ console.log(` File: ${result.file}`);
277
+ console.log('');
278
+ }
279
+ return result;
280
+ }
281
+
282
+ if (sub === 'config' && action === 'init') {
283
+ const flow = typeof opts.flow === 'string' ? opts.flow.split('|').map(v => v.trim()).filter(Boolean) : undefined;
284
+ const result = initProjectConfig({
285
+ appId: opts['app-id'] === true ? undefined : opts['app-id'],
286
+ apkPath: opts.apk === true ? undefined : opts.apk,
287
+ device: opts.device === true ? undefined : opts.device,
288
+ flow,
289
+ bootMs: opts['boot-timeout-ms'] ? Number(opts['boot-timeout-ms']) : undefined,
290
+ actionMs: opts['action-timeout-ms'] ? Number(opts['action-timeout-ms']) : undefined,
291
+ evidenceDir: opts['evidence-dir'] === true ? undefined : opts['evidence-dir'],
292
+ }, { configPath: opts.config === true ? undefined : opts.config });
293
+ console.log('');
294
+ console.log(` ${result.changed ? '✓ Wrote' : '✓ Unchanged'} ${result.file}`);
295
+ console.log('');
296
+ return result;
297
+ }
298
+
299
+ if (sub === 'config' && action === 'validate') {
300
+ const result = loadProjectConfig({ configPath: opts.config === true ? undefined : opts.config });
301
+ if (opts.json) {
302
+ console.log(JSON.stringify({ valid: result.errors.length === 0, ...result }, null, 2));
303
+ } else if (result.errors.length) {
304
+ console.log('');
305
+ console.log(` ✗ Invalid ${result.file}`);
306
+ result.errors.forEach(error => console.log(` - ${error}`));
307
+ console.log('');
308
+ } else {
309
+ console.log('');
310
+ console.log(` ✓ Valid ${result.file}`);
311
+ console.log('');
312
+ }
313
+ if (result.errors.length) process.exitCode = 1;
314
+ return result;
315
+ }
316
+
317
+ throw new Error('Unknown mcp command. Use mcp help.');
318
+ }
319
+
320
+ module.exports = {
321
+ PROJECT_CONFIG_PATH,
322
+ cmdMcp,
323
+ defaultProjectConfig,
324
+ initProjectConfig,
325
+ inspectMcpConfiguration,
326
+ loadProjectConfig,
327
+ resolveClientConfig,
328
+ setupMcpClient,
329
+ validateProjectConfig,
330
+ };
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ const { cmdDoctor } = require('./doctor');
4
+ const { cmdMcp } = require('./mcp');
5
+
6
+ function createCommandRouter(legacyHandlers) {
7
+ const routes = new Map([
8
+ ['start', legacyHandlers.start],
9
+ ['install', legacyHandlers.install],
10
+ ['add', legacyHandlers.add],
11
+ ['memory', legacyHandlers.memory],
12
+ ['list', legacyHandlers.list],
13
+ ['doctor', cmdDoctor],
14
+ ['mcp', cmdMcp],
15
+ ['help', legacyHandlers.help],
16
+ ['--help', legacyHandlers.help],
17
+ ['-h', legacyHandlers.help],
18
+ ]);
19
+
20
+ return async function routeCommand(command, args = []) {
21
+ if (command === undefined) return legacyHandlers.help(args);
22
+ const handler = routes.get(command);
23
+ if (!handler) {
24
+ const error = new Error(`Unknown command: ${command}`);
25
+ error.code = 'UNKNOWN_COMMAND';
26
+ throw error;
27
+ }
28
+ return handler(args);
29
+ };
30
+ }
31
+
32
+ module.exports = { createCommandRouter };
package/cli/index.js CHANGED
@@ -6,6 +6,8 @@ const fs = require('fs');
6
6
  const path = require('path');
7
7
  const os = require('os');
8
8
  const readline = require('readline');
9
+ const { createCommandRouter } = require('./commands/router');
10
+ const { parseOptions } = require('./lib/options');
9
11
 
10
12
  const REPO = 'salmanashraf/mobile-agency';
11
13
  const BRANCH = 'main';
@@ -283,26 +285,6 @@ function slugify(s) {
283
285
  .slice(0, 80) || 'memory';
284
286
  }
285
287
 
286
- function parseOptions(args) {
287
- const out = { _: [] };
288
- for (let i = 0; i < args.length; i++) {
289
- const arg = args[i];
290
- if (!arg.startsWith('--')) {
291
- out._.push(arg);
292
- continue;
293
- }
294
- const key = arg.slice(2);
295
- const next = args[i + 1];
296
- if (!next || next.startsWith('--')) {
297
- out[key] = true;
298
- } else {
299
- out[key] = next;
300
- i++;
301
- }
302
- }
303
- return out;
304
- }
305
-
306
288
  function readStdinIfPiped() {
307
289
  if (process.stdin.isTTY) return '';
308
290
  try {
@@ -1401,6 +1383,8 @@ function cmdList() {
1401
1383
  console.log(dim(' npx mobile-ai-agents add skill grill-mobile # one skill'));
1402
1384
  console.log(dim(' npx mobile-ai-agents add workflow feature-ship # one workflow'));
1403
1385
  console.log(dim(' npx mobile-ai-agents memory init # local Mobile Memory'));
1386
+ console.log(dim(' npx mobile-ai-agents doctor --platform android # check Android setup'));
1387
+ console.log(dim(' npx mobile-ai-agents mcp setup --client claude # configure Mobile MCP'));
1404
1388
  console.log('');
1405
1389
  }
1406
1390
 
@@ -1413,6 +1397,8 @@ function cmdHelp() {
1413
1397
  console.log(` ${bold('install')} Install agents, skills, and workflows`);
1414
1398
  console.log(` ${bold('add')} agent|skill|workflow <name> Install a single item`);
1415
1399
  console.log(` ${bold('memory')} init|capture|search|... Local Mobile Memory store`);
1400
+ console.log(` ${bold('doctor')} --platform android Check SDK, adb, emulator, MCP, and project config`);
1401
+ console.log(` ${bold('mcp')} setup|config Configure Mobile MCP and Android device QA`);
1416
1402
  console.log(` ${bold('list')} List all available agents, skills, and workflows`);
1417
1403
  console.log(` ${bold('help')} Show this help`);
1418
1404
  console.log('');
@@ -1447,6 +1433,11 @@ function cmdHelp() {
1447
1433
  console.log(dim(' npx mobile-ai-agents add workflow feature-ship'));
1448
1434
  console.log(dim(' npx mobile-ai-agents memory init'));
1449
1435
  console.log(dim(' npx mobile-ai-agents memory capture --type decision --text "Use Room for offline persistence"'));
1436
+ console.log(dim(' npx mobile-ai-agents doctor --platform android'));
1437
+ console.log(dim(' npx mobile-ai-agents doctor --platform android --json'));
1438
+ console.log(dim(' npx mobile-ai-agents mcp setup --client codex'));
1439
+ console.log(dim(' npx mobile-ai-agents mcp config init --app-id com.example.app'));
1440
+ console.log(dim(' npx mobile-ai-agents mcp config validate'));
1450
1441
  console.log('');
1451
1442
  console.log(dim(` github.com/${REPO}`));
1452
1443
  console.log('');
@@ -1454,28 +1445,24 @@ function cmdHelp() {
1454
1445
 
1455
1446
  // ─── Main ────────────────────────────────────────────────────────────────────
1456
1447
 
1448
+ const routeCommand = createCommandRouter({
1449
+ start: cmdStart,
1450
+ install: cmdInstall,
1451
+ add: cmdAdd,
1452
+ memory: cmdMemory,
1453
+ list: cmdList,
1454
+ help: cmdHelp,
1455
+ });
1456
+
1457
1457
  async function main() {
1458
1458
  const [,, cmd, ...args] = process.argv;
1459
1459
 
1460
1460
  try {
1461
- switch (cmd) {
1462
- case 'start': await cmdStart(args); break;
1463
- case 'install': await cmdInstall(args); break;
1464
- case 'add': await cmdAdd(args); break;
1465
- case 'memory': cmdMemory(args); break;
1466
- case 'list': cmdList(); break;
1467
- case 'help':
1468
- case '--help':
1469
- case '-h':
1470
- case undefined: cmdHelp(); break;
1471
- default:
1472
- console.error(`\n Unknown command: ${cmd}\n`);
1473
- cmdHelp();
1474
- process.exit(1);
1475
- }
1461
+ await routeCommand(cmd, args);
1476
1462
  } catch (e) {
1477
- console.error(`\n Error: ${e.message}\n`);
1478
- process.exit(1);
1463
+ console.error(`\n ${e.code === 'UNKNOWN_COMMAND' ? e.message : `Error: ${e.message}`}\n`);
1464
+ if (e.code === 'UNKNOWN_COMMAND') cmdHelp();
1465
+ process.exitCode = 1;
1479
1466
  }
1480
1467
  }
1481
1468
 
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ function parseOptions(args) {
4
+ const out = { _: [] };
5
+ for (let i = 0; i < args.length; i++) {
6
+ const arg = args[i];
7
+ if (!arg.startsWith('--')) {
8
+ out._.push(arg);
9
+ continue;
10
+ }
11
+
12
+ const key = arg.slice(2);
13
+ const next = args[i + 1];
14
+ if (!next || next.startsWith('--')) {
15
+ out[key] = true;
16
+ } else {
17
+ out[key] = next;
18
+ i++;
19
+ }
20
+ }
21
+ return out;
22
+ }
23
+
24
+ module.exports = { parseOptions };
@@ -0,0 +1,138 @@
1
+ # Android Device QA Setup
2
+
3
+ Prepare an Android project for the `/mobile-mcp-qa` skill with repeatable environment checks, MCP configuration, and a project-level test definition.
4
+
5
+ This setup layer verifies prerequisites and records the intended QA flow. Device-flow execution is tracked separately in [issue #54](https://github.com/salmanashraf/mobile-ai-agents/issues/54).
6
+
7
+ ## Prerequisites
8
+
9
+ - Node.js 18 or newer
10
+ - Android Studio with Android SDK Platform-Tools
11
+ - Android Emulator plus at least one AVD, or an authorized physical device
12
+ - Claude Code, Cursor, Windsurf, or Codex
13
+
14
+ ## 1. Check the Android Environment
15
+
16
+ Run from the mobile app project:
17
+
18
+ ```bash
19
+ npx mobile-ai-agents doctor --platform android
20
+ ```
21
+
22
+ The doctor checks:
23
+
24
+ - `ANDROID_HOME`, `ANDROID_SDK_ROOT`, and common SDK locations
25
+ - `adb` availability and version
26
+ - connected and authorized devices
27
+ - Android Emulator installation and configured AVDs
28
+ - Mobile MCP configuration for supported AI clients
29
+ - `.mobile-ai-agents/android-device.json` validity and APK availability
30
+
31
+ Use machine-readable output in CI or scripts:
32
+
33
+ ```bash
34
+ npx mobile-ai-agents doctor --platform android --json
35
+ ```
36
+
37
+ Exit code `1` means at least one required check failed. Warnings identify optional or not-yet-ready items, such as a missing APK before the first build.
38
+
39
+ ## 2. Configure Mobile MCP
40
+
41
+ Choose the client you use:
42
+
43
+ ```bash
44
+ npx mobile-ai-agents mcp setup --client claude
45
+ npx mobile-ai-agents mcp setup --client cursor
46
+ npx mobile-ai-agents mcp setup --client windsurf
47
+ npx mobile-ai-agents mcp setup --client codex
48
+ ```
49
+
50
+ The command adds this server without deleting existing settings or MCP servers:
51
+
52
+ ```json
53
+ {
54
+ "command": "npx",
55
+ "args": ["-y", "@mobilenext/mobile-mcp@latest"]
56
+ }
57
+ ```
58
+
59
+ Running the same setup command again is safe and leaves an already-correct file unchanged.
60
+
61
+ Default configuration files:
62
+
63
+ | Client | File |
64
+ |---|---|
65
+ | Claude Code | `<project>/.mcp.json` |
66
+ | Cursor | `<project>/.cursor/mcp.json` |
67
+ | Windsurf | `~/.codeium/windsurf/mcp_config.json` |
68
+ | Codex | `~/.codex/config.toml` |
69
+
70
+ Use `--config <path>` to target a different client config file.
71
+
72
+ ## 3. Define the Project QA Flow
73
+
74
+ Create `.mobile-ai-agents/android-device.json`:
75
+
76
+ ```bash
77
+ npx mobile-ai-agents mcp config init \
78
+ --app-id com.example.app \
79
+ --apk app/build/outputs/apk/debug/app-debug.apk \
80
+ --device auto \
81
+ --flow "Launch app|Open settings|Enable notifications|Restart app"
82
+ ```
83
+
84
+ Generated shape:
85
+
86
+ ```json
87
+ {
88
+ "version": 1,
89
+ "platform": "android",
90
+ "appId": "com.example.app",
91
+ "apkPath": "app/build/outputs/apk/debug/app-debug.apk",
92
+ "device": "auto",
93
+ "flow": [
94
+ "Launch app",
95
+ "Open settings",
96
+ "Enable notifications",
97
+ "Restart app"
98
+ ],
99
+ "timeouts": {
100
+ "bootMs": 120000,
101
+ "actionMs": 15000
102
+ },
103
+ "evidenceDir": ".mobile-ai-agents/evidence"
104
+ }
105
+ ```
106
+
107
+ You can also copy [the configuration template](../templates/android-device-config.json).
108
+
109
+ Validate after editing:
110
+
111
+ ```bash
112
+ npx mobile-ai-agents mcp config validate
113
+ npx mobile-ai-agents mcp config validate --json
114
+ ```
115
+
116
+ ## 4. Run the Skill
117
+
118
+ After the doctor has no failures, restart the AI client so it loads Mobile MCP, then invoke:
119
+
120
+ ```text
121
+ /mobile-mcp-qa
122
+ ```
123
+
124
+ The skill uses the app ID, build, target device, expected flow, and evidence requirements to guide device QA. See [Mobile MCP Integration](mobile-mcp.md) for the full prompt format and report output.
125
+
126
+ ## Troubleshooting
127
+
128
+ | Result | Fix |
129
+ |---|---|
130
+ | Android SDK not found | Install the SDK and set `ANDROID_HOME` or `ANDROID_SDK_ROOT`. |
131
+ | `adb` not found | Install Platform-Tools and add `<sdk>/platform-tools` to `PATH`. |
132
+ | No ready device | Start an AVD or connect a device, enable USB debugging, and authorize the computer. |
133
+ | No AVD configured | Create one in Android Studio Device Manager. |
134
+ | MCP not configured | Run `mcp setup --client <client>`, then restart the client. |
135
+ | Invalid project config | Run `mcp config validate` and correct every listed field. |
136
+ | APK missing | Build the configured APK before device QA. |
137
+
138
+ Do not use production accounts or irreversible actions during automated QA. Prefer test builds, sandbox credentials, and disposable data.
package/docs/index.html CHANGED
@@ -679,7 +679,12 @@
679
679
  <span class="comment"># Or just one Android agent</span><br>
680
680
  <span class="cmd2">npx mobile-ai-agents add agent axiom</span><br>
681
681
  <span class="cmd2">npx mobile-ai-agents add agent anr-investigation</span><br>
682
- <span class="cmd2">npx mobile-ai-agents add agent memory-leak-analyzer</span>
682
+ <span class="cmd2">npx mobile-ai-agents add agent memory-leak-analyzer</span><br><br>
683
+ <span class="comment"># Prepare Android device QA with Mobile MCP</span><br>
684
+ <span class="cmd2">npx mobile-ai-agents doctor</span> <span class="flag">--platform</span> <span class="val">android</span><br>
685
+ <span class="cmd2">npx mobile-ai-agents mcp setup</span> <span class="flag">--client</span> <span class="val">claude</span><br>
686
+ <span class="cmd2">npx mobile-ai-agents mcp config init</span> <span class="flag">--app-id</span> <span class="val">com.example.app</span><br><br>
687
+ <a href="android-device-setup.md">Read the Android Device QA Setup guide →</a>
683
688
  </div>
684
689
  <div class="code-block" id="tab-cursor" style="display:none">
685
690
  <span class="comment"># Install for Cursor (.cursorrules in current project)</span><br>
@@ -60,6 +60,14 @@ node --version
60
60
 
61
61
  If this prints a version like `v18.x`, `v20.x`, or newer, you are ready.
62
62
 
63
+ If you plan to use `/mobile-mcp-qa` with Android, also install Android Studio, Platform-Tools, and an emulator image or connect an authorized device. Verify the setup with:
64
+
65
+ ```bash
66
+ npx mobile-ai-agents doctor --platform android
67
+ ```
68
+
69
+ Then follow the [Android Device QA Setup guide](android-device-setup.md) to configure Mobile MCP and the project test flow.
70
+
63
71
  ---
64
72
 
65
73
  ## Step 1: Pick Your AI Tool
@@ -33,6 +33,20 @@ It exposes tools for:
33
33
 
34
34
  ## Install
35
35
 
36
+ ### Automated setup
37
+
38
+ From an Android app project, run the environment doctor and configure your AI client:
39
+
40
+ ```bash
41
+ npx mobile-ai-agents doctor --platform android
42
+ npx mobile-ai-agents mcp setup --client <claude|cursor|windsurf|codex>
43
+ npx mobile-ai-agents mcp config init --app-id com.example.app
44
+ ```
45
+
46
+ The setup command preserves existing settings and is idempotent. The project config records the app ID, APK, target device, flow, timeouts, and evidence directory. See [Android Device QA Setup](android-device-setup.md) for the full command reference and troubleshooting.
47
+
48
+ ### Manual setup
49
+
36
50
  Standard MCP config:
37
51
 
38
52
  ```json
@@ -51,6 +51,7 @@ test -f "$tmp_home/.claude/commands/<name>.md"
51
51
 
52
52
  ```bash
53
53
  git diff --check
54
+ npm test
54
55
  node --check cli/index.js
55
56
  bash -n install.sh
56
57
  node cli/index.js list
package/docs/wiki.md CHANGED
@@ -41,12 +41,22 @@
41
41
 
42
42
  For first-time setup, use the dedicated [Installation Guide](installation.md). It explains which command to run for Claude Code, Cursor, Windsurf, GitHub Copilot, and Codex.
43
43
 
44
+ For Android device or emulator QA, use the [Android Device QA Setup](android-device-setup.md). It covers the environment doctor, Mobile MCP client setup, and `.mobile-ai-agents/android-device.json` project configuration used by `/mobile-mcp-qa`.
45
+
44
46
  Fast path:
45
47
 
46
48
  ```bash
47
49
  npx mobile-ai-agents install
48
50
  ```
49
51
 
52
+ Android QA setup:
53
+
54
+ ```bash
55
+ npx mobile-ai-agents doctor --platform android
56
+ npx mobile-ai-agents mcp setup --client claude
57
+ npx mobile-ai-agents mcp config init --app-id com.example.app
58
+ ```
59
+
50
60
  Common installs:
51
61
 
52
62
  | I use... | Run this |
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "mobile-ai-agents",
3
- "version": "1.0.35",
3
+ "version": "1.0.36",
4
4
  "description": "AI agents, skills, and Loop Engineering workflows for building, testing, auditing, and releasing mobile apps.",
5
5
  "bin": {
6
6
  "mobile-ai-agents": "cli/index.js",
7
7
  "mobile-agency": "cli/index.js"
8
8
  },
9
+ "scripts": {
10
+ "test": "node --test"
11
+ },
9
12
  "files": [
10
13
  "AGENTS.md",
11
14
  "CLAUDE.md",
@@ -16,6 +16,20 @@ Mobile MCP is best used after implementation, during APPFORGE Stage 7 Full QA, a
16
16
 
17
17
  ## Prerequisites
18
18
 
19
+ For Android, use the Mobile AI Agents setup commands from the app project:
20
+
21
+ ```bash
22
+ npx mobile-ai-agents doctor --platform android
23
+ npx mobile-ai-agents mcp setup --client <claude|cursor|windsurf|codex>
24
+ npx mobile-ai-agents mcp config init \
25
+ --app-id com.example.app \
26
+ --apk app/build/outputs/apk/debug/app-debug.apk \
27
+ --flow "Launch app|Complete the primary flow|Verify success"
28
+ npx mobile-ai-agents mcp config validate
29
+ ```
30
+
31
+ The setup command preserves existing client settings and is safe to repeat. See `docs/android-device-setup.md` for configuration fields, JSON output, and troubleshooting.
32
+
19
33
  Install Mobile MCP in your AI tool:
20
34
 
21
35
  ```json
@@ -0,0 +1,16 @@
1
+ {
2
+ "version": 1,
3
+ "platform": "android",
4
+ "appId": "com.example.app",
5
+ "apkPath": "app/build/outputs/apk/debug/app-debug.apk",
6
+ "device": "auto",
7
+ "flow": [
8
+ "Launch app",
9
+ "Verify the initial screen"
10
+ ],
11
+ "timeouts": {
12
+ "bootMs": 120000,
13
+ "actionMs": 15000
14
+ },
15
+ "evidenceDir": ".mobile-ai-agents/evidence"
16
+ }