replicant-mcp 1.4.8 → 1.6.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 (95) hide show
  1. package/README.md +28 -2
  2. package/dist/adapters/adb.d.ts +2 -0
  3. package/dist/adapters/adb.js +12 -1
  4. package/dist/adapters/emulator.d.ts +2 -0
  5. package/dist/adapters/emulator.js +35 -5
  6. package/dist/adapters/ui-fallback-find.js +26 -1
  7. package/dist/cli/doctor.d.ts +10 -0
  8. package/dist/cli/doctor.js +154 -0
  9. package/dist/cli/emulator.js +2 -1
  10. package/dist/cli/index.d.ts +1 -0
  11. package/dist/cli/index.js +1 -0
  12. package/dist/cli.js +5 -3
  13. package/dist/index.js +22 -5
  14. package/dist/parsers/adb-output.js +5 -1
  15. package/dist/parsers/gradle-output.d.ts +1 -0
  16. package/dist/parsers/gradle-output.js +7 -0
  17. package/dist/server.js +47 -37
  18. package/dist/services/config.js +3 -2
  19. package/dist/services/device-state.js +18 -9
  20. package/dist/services/index.d.ts +1 -0
  21. package/dist/services/index.js +1 -0
  22. package/dist/services/process-runner.d.ts +1 -0
  23. package/dist/services/process-runner.js +52 -1
  24. package/dist/services/test-baseline.d.ts +18 -0
  25. package/dist/services/test-baseline.js +51 -0
  26. package/dist/tools/adb-app.d.ts +0 -1
  27. package/dist/tools/adb-app.js +25 -20
  28. package/dist/tools/adb-device.d.ts +0 -1
  29. package/dist/tools/adb-device.js +17 -16
  30. package/dist/tools/adb-logcat.d.ts +3 -6
  31. package/dist/tools/adb-logcat.js +29 -31
  32. package/dist/tools/adb-shell.d.ts +15 -1
  33. package/dist/tools/adb-shell.js +37 -7
  34. package/dist/tools/cache.js +4 -3
  35. package/dist/tools/emulator-device.d.ts +0 -4
  36. package/dist/tools/emulator-device.js +24 -19
  37. package/dist/tools/gradle-build.d.ts +0 -1
  38. package/dist/tools/gradle-build.js +3 -4
  39. package/dist/tools/gradle-get-details.d.ts +14 -1
  40. package/dist/tools/gradle-get-details.js +132 -42
  41. package/dist/tools/gradle-list.js +4 -6
  42. package/dist/tools/gradle-test.d.ts +14 -1
  43. package/dist/tools/gradle-test.js +70 -9
  44. package/dist/tools/index.d.ts +3 -1
  45. package/dist/tools/index.js +3 -1
  46. package/dist/tools/rtfm.js +26 -30
  47. package/dist/tools/ui-action.d.ts +63 -0
  48. package/dist/tools/ui-action.js +83 -0
  49. package/dist/tools/ui-capture.d.ts +43 -0
  50. package/dist/tools/ui-capture.js +59 -0
  51. package/dist/tools/ui-find.d.ts +14 -2
  52. package/dist/tools/ui-find.js +87 -51
  53. package/dist/tools/{ui.d.ts → ui-query.d.ts} +9 -75
  54. package/dist/tools/ui-query.js +138 -0
  55. package/dist/types/device.d.ts +1 -1
  56. package/dist/types/errors.d.ts +3 -0
  57. package/dist/types/errors.js +4 -0
  58. package/dist/types/icon-recognition.d.ts +5 -0
  59. package/dist/types/schemas/adb-app-output.d.ts +83 -0
  60. package/dist/types/schemas/adb-app-output.js +60 -0
  61. package/dist/types/schemas/adb-device-output.d.ts +152 -0
  62. package/dist/types/schemas/adb-device-output.js +72 -0
  63. package/dist/types/schemas/adb-logcat-output.d.ts +15 -0
  64. package/dist/types/schemas/adb-logcat-output.js +14 -0
  65. package/dist/types/schemas/adb-shell-output.d.ts +17 -0
  66. package/dist/types/schemas/adb-shell-output.js +16 -0
  67. package/dist/types/schemas/cache-output.d.ts +88 -0
  68. package/dist/types/schemas/cache-output.js +51 -0
  69. package/dist/types/schemas/emulator-device-output.d.ts +100 -0
  70. package/dist/types/schemas/emulator-device-output.js +76 -0
  71. package/dist/types/schemas/gradle-build-output.d.ts +16 -0
  72. package/dist/types/schemas/gradle-build-output.js +15 -0
  73. package/dist/types/schemas/gradle-get-details-output.d.ts +129 -0
  74. package/dist/types/schemas/gradle-get-details-output.js +86 -0
  75. package/dist/types/schemas/gradle-list-output.d.ts +56 -0
  76. package/dist/types/schemas/gradle-list-output.js +39 -0
  77. package/dist/types/schemas/gradle-test-output.d.ts +82 -0
  78. package/dist/types/schemas/gradle-test-output.js +54 -0
  79. package/dist/types/schemas/index.d.ts +12 -0
  80. package/dist/types/schemas/index.js +12 -0
  81. package/dist/types/schemas/rtfm-output.d.ts +8 -0
  82. package/dist/types/schemas/rtfm-output.js +7 -0
  83. package/dist/types/schemas/ui-output.d.ts +361 -0
  84. package/dist/types/schemas/ui-output.js +188 -0
  85. package/dist/utils/logger.d.ts +6 -0
  86. package/dist/utils/logger.js +34 -0
  87. package/dist/version.d.ts +1 -0
  88. package/dist/version.js +4 -0
  89. package/docs/contracts/replicant-mcp.contract.json +2371 -0
  90. package/docs/rtfm/adb.md +11 -1
  91. package/docs/rtfm/build.md +7 -1
  92. package/docs/rtfm/cache.md +21 -0
  93. package/docs/rtfm/ui.md +67 -35
  94. package/package.json +13 -5
  95. package/dist/tools/ui.js +0 -244
package/README.md CHANGED
@@ -31,12 +31,13 @@ replicant-mcp is a [Model Context Protocol](https://modelcontextprotocol.io/) se
31
31
 
32
32
  | Category | Capabilities |
33
33
  |----------|-------------|
34
- | **Build & Test** | Build APKs/bundles, run unit and instrumented tests, list modules/variants/tasks |
34
+ | **Build & Test** | Build APKs/bundles, run unit and instrumented tests, list modules/variants/tasks, test regression detection with baseline comparison |
35
35
  | **Emulator** | Create, start, stop, wipe emulators; save/load/delete snapshots |
36
36
  | **Device Control** | List connected devices, select active device, query device properties |
37
37
  | **App Management** | Install, uninstall, launch, stop apps; clear app data |
38
38
  | **Log Analysis** | Filter logcat by package, tag, level, time |
39
39
  | **UI Automation** | Accessibility-first element finding, spatial proximity search, tap, text input, screenshots |
40
+ | **Diagnostics** | Environment health checks via `replicant doctor`; structured logging with configurable level and format |
40
41
 
41
42
  ---
42
43
 
@@ -44,7 +45,6 @@ replicant-mcp is a [Model Context Protocol](https://modelcontextprotocol.io/) se
44
45
 
45
46
  - Custom build commands (project-specific overrides, auto-detect gradlew)
46
47
  - Video capture (start/stop recording, duration-based capture)
47
- - Raw screenshot mode for external context management
48
48
 
49
49
  ---
50
50
 
@@ -68,6 +68,12 @@ emulator -version # Should show Android emulator version
68
68
  npm install -g replicant-mcp
69
69
  ```
70
70
 
71
+ After installation, run the built-in diagnostics to verify your environment:
72
+
73
+ ```bash
74
+ replicant doctor
75
+ ```
76
+
71
77
  ### Updating
72
78
 
73
79
  ```bash
@@ -181,11 +187,31 @@ replicant-mcp uses progressive disclosure (summaries first, details on demand) t
181
187
  ## More Info
182
188
 
183
189
  - **Configuration:** Set `REPLICANT_CONFIG` for advanced options. See [docs/configuration.md](docs/configuration.md).
190
+ - **Logging:** Set `REPLICANT_LOG_LEVEL` (`error`, `warn`, `info`, `debug`) and `REPLICANT_LOG_FORMAT` (`json` for structured output) to control server logging. Logs are written to stderr.
184
191
  - **Troubleshooting:** Common issues and solutions in [docs/troubleshooting.md](docs/troubleshooting.md).
185
192
  - **Tool documentation:** Ask Claude to call `rtfm` with a category like "build", "adb", "emulator", or "ui".
186
193
 
187
194
  ---
188
195
 
196
+ ## Documentation
197
+
198
+ | Document | Description |
199
+ |----------|-------------|
200
+ | [Architecture](docs/architecture.md) | Design overview and progressive disclosure pattern |
201
+ | [Configuration](docs/configuration.md) | Config file reference, environment variables, Gradle setup |
202
+ | [API Stability](docs/api-stability.md) | Tool API versioning policy and deprecation process |
203
+ | [Security Model](docs/security.md) | adb-shell safety model, command denylist, threat boundaries |
204
+ | [Support Matrix](docs/support-matrix.md) | Tested OS, Node.js, Android SDK, and emulator versions |
205
+ | [Known Limitations](docs/known-limitations.md) | Accessibility gaps, timeouts, single-device focus, and more |
206
+ | [Artifacts](docs/artifacts.md) | `.replicant/` directory contents and privacy considerations |
207
+ | [Troubleshooting](docs/troubleshooting.md) | Common issues and solutions |
208
+ | [Changelog](CHANGELOG.md) | Version history |
209
+ | [Security Policy](SECURITY.md) | Vulnerability reporting process |
210
+ | [Support / Getting Help](SUPPORT.md) | How to report bugs and ask questions |
211
+ | [Contributing](CONTRIBUTING.md) | Development setup and guidelines |
212
+
213
+ ---
214
+
189
215
  ## Contributing
190
216
 
191
217
  See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
@@ -15,6 +15,8 @@ export declare class AdbAdapter {
15
15
  logcat(deviceId: string, options: {
16
16
  lines?: number;
17
17
  filter?: string;
18
+ since?: string;
19
+ package?: string;
18
20
  }): Promise<string>;
19
21
  waitForDevice(deviceId: string, timeoutMs?: number): Promise<void>;
20
22
  getProperties(deviceId: string): Promise<Record<string, string>>;
@@ -53,6 +53,9 @@ export class AdbAdapter {
53
53
  }
54
54
  async logcat(deviceId, options) {
55
55
  const args = ["-s", deviceId, "logcat", "-d"];
56
+ if (options.since) {
57
+ args.push("-T", options.since);
58
+ }
56
59
  if (options.lines) {
57
60
  args.push("-t", options.lines.toString());
58
61
  }
@@ -60,7 +63,15 @@ export class AdbAdapter {
60
63
  args.push(...options.filter.split(" "));
61
64
  }
62
65
  const result = await this.adb(args);
63
- return result.stdout;
66
+ let output = result.stdout;
67
+ // Package filtering: filter output lines containing the package name
68
+ // We use string matching on output lines rather than --pid (requires pidof)
69
+ // or -e regex (varies across adb versions)
70
+ if (options.package) {
71
+ const lines = output.split("\n");
72
+ output = lines.filter((line) => line.includes(options.package)).join("\n");
73
+ }
74
+ return output;
64
75
  }
65
76
  async waitForDevice(deviceId, timeoutMs = 30000) {
66
77
  await this.adb(["-s", deviceId, "wait-for-device"], timeoutMs);
@@ -10,6 +10,8 @@ export declare class EmulatorAdapter {
10
10
  list(): Promise<EmulatorListResult>;
11
11
  create(name: string, device: string, systemImage: string): Promise<void>;
12
12
  start(avdName: string): Promise<string>;
13
+ private getRunningEmulatorIds;
14
+ private getAvdName;
13
15
  kill(emulatorId: string): Promise<void>;
14
16
  wipe(avdName: string): Promise<void>;
15
17
  snapshotSave(emulatorId: string, name: string): Promise<void>;
@@ -29,6 +29,8 @@ export class EmulatorAdapter {
29
29
  }
30
30
  }
31
31
  async start(avdName) {
32
+ // Snapshot existing emulators before starting a new one
33
+ const existingIds = await this.getRunningEmulatorIds();
32
34
  // Start emulator in background - don't wait for it
33
35
  // Returns immediately, emulator boots in background
34
36
  this.runner.runEmulator([
@@ -40,13 +42,41 @@ export class EmulatorAdapter {
40
42
  });
41
43
  // Give it a moment to register
42
44
  await new Promise((r) => setTimeout(r, 2000));
43
- // Find the new emulator ID
45
+ // Find the NEW emulator by diffing against pre-existing ones
46
+ const currentIds = await this.getRunningEmulatorIds();
47
+ const newIds = currentIds.filter((id) => !existingIds.includes(id));
48
+ if (newIds.length === 1) {
49
+ return newIds[0];
50
+ }
51
+ // Ambiguous or no new emulator — fall back to matching by AVD name
52
+ for (const id of currentIds) {
53
+ if (existingIds.includes(id))
54
+ continue;
55
+ const name = await this.getAvdName(id);
56
+ if (name === avdName)
57
+ return id;
58
+ }
59
+ // Last resort: check all current emulators by AVD name
60
+ for (const id of currentIds) {
61
+ const name = await this.getAvdName(id);
62
+ if (name === avdName)
63
+ return id;
64
+ }
65
+ throw new ReplicantError(ErrorCode.EMULATOR_START_FAILED, `Emulator ${avdName} failed to start`, "Check the AVD name and try again");
66
+ }
67
+ async getRunningEmulatorIds() {
44
68
  const result = await this.runner.runAdb(["devices"]);
45
- const match = result.stdout.match(/emulator-\d+/);
46
- if (!match) {
47
- throw new ReplicantError(ErrorCode.EMULATOR_START_FAILED, `Emulator ${avdName} failed to start`, "Check the AVD name and try again");
69
+ const matches = result.stdout.match(/emulator-\d+/g);
70
+ return matches ?? [];
71
+ }
72
+ async getAvdName(emulatorId) {
73
+ try {
74
+ const result = await this.runner.runAdb(["-s", emulatorId, "emu", "avd", "name"]);
75
+ return result.stdout.trim().split("\n")[0].trim();
76
+ }
77
+ catch {
78
+ return "";
48
79
  }
49
- return match[0];
50
80
  }
51
81
  async kill(emulatorId) {
52
82
  await this.runner.runAdb(["-s", emulatorId, "emu", "kill"]);
@@ -3,7 +3,20 @@ import { extractText, searchText } from "../services/ocr.js";
3
3
  import { matchIconPattern, matchesResourceId } from "../services/icon-patterns.js";
4
4
  import { filterIconCandidates, formatBounds, cropCandidateImage } from "../services/visual-candidates.js";
5
5
  import { calculateGridCellBounds, calculatePositionCoordinates, createGridOverlay, POSITION_LABELS, } from "../services/grid.js";
6
+ function createEarlyStopResult(tier, source) {
7
+ return {
8
+ elements: [],
9
+ source,
10
+ tier,
11
+ confidence: "low",
12
+ stoppedEarly: true,
13
+ stoppedAtTier: tier,
14
+ nextTierAvailable: tier < 5 ? (tier + 1) : undefined,
15
+ stopReason: "maxTier limit reached",
16
+ };
17
+ }
6
18
  export async function findWithFallbacks(deps, deviceId, selector, options = {}) {
19
+ const maxTier = options.maxTier ?? 5;
7
20
  // Handle Tier 5 grid refinement FIRST (when gridCell and gridPosition are provided)
8
21
  if (options.gridCell !== undefined && options.gridPosition !== undefined) {
9
22
  let width, height;
@@ -42,6 +55,9 @@ export async function findWithFallbacks(deps, deviceId, selector, options = {})
42
55
  confidence: "high",
43
56
  };
44
57
  }
58
+ if (maxTier === 1) {
59
+ return createEarlyStopResult(1, "accessibility");
60
+ }
45
61
  // Tier 2: ResourceId pattern match (for text-based queries)
46
62
  if (selector.text || selector.textContains) {
47
63
  const query = selector.text || selector.textContains;
@@ -62,9 +78,12 @@ export async function findWithFallbacks(deps, deviceId, selector, options = {})
62
78
  };
63
79
  }
64
80
  }
81
+ if (maxTier === 2) {
82
+ return createEarlyStopResult(2, "accessibility");
83
+ }
65
84
  }
66
85
  // Tier 3: OCR
67
- if (selector.text || selector.textContains) {
86
+ if ((selector.text || selector.textContains) && maxTier >= 3) {
68
87
  const searchTerm = selector.text || selector.textContains;
69
88
  const screenshotResult = await deps.screenshot(deviceId, {});
70
89
  try {
@@ -81,6 +100,9 @@ export async function findWithFallbacks(deps, deviceId, selector, options = {})
81
100
  : undefined,
82
101
  };
83
102
  }
103
+ if (maxTier === 3) {
104
+ return createEarlyStopResult(3, "ocr");
105
+ }
84
106
  // Tier 4: Visual candidates (unlabeled clickables)
85
107
  const tree = await deps.dump(deviceId);
86
108
  const flat = flattenTree(tree);
@@ -106,6 +128,9 @@ export async function findWithFallbacks(deps, deviceId, selector, options = {})
106
128
  : undefined,
107
129
  };
108
130
  }
131
+ if (maxTier === 4) {
132
+ return createEarlyStopResult(4, "visual");
133
+ }
109
134
  // Tier 5: Grid fallback
110
135
  const gridImage = await createGridOverlay(screenshotResult.path);
111
136
  return {
@@ -0,0 +1,10 @@
1
+ import { Command } from "commander";
2
+ export interface CheckResult {
3
+ name: string;
4
+ status: "ok" | "warn" | "fail";
5
+ detail: string;
6
+ suggestion?: string;
7
+ }
8
+ export declare function runChecks(): CheckResult[];
9
+ export declare function formatJson(checks: CheckResult[]): string;
10
+ export declare function createDoctorCommand(): Command;
@@ -0,0 +1,154 @@
1
+ import { Command } from "commander";
2
+ import { execSync } from "child_process";
3
+ import { existsSync } from "fs";
4
+ // All commands below are hardcoded literals (no user input), so execSync is safe here.
5
+ function exec(cmd) {
6
+ return execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
7
+ }
8
+ function checkNode() {
9
+ const version = process.version;
10
+ const major = parseInt(version.slice(1).split(".")[0], 10);
11
+ if (major >= 18) {
12
+ return { name: "Node.js", status: "ok", detail: version };
13
+ }
14
+ return { name: "Node.js", status: "fail", detail: version, suggestion: "Upgrade to Node.js >= 18" };
15
+ }
16
+ function checkNpm() {
17
+ try {
18
+ return { name: "npm", status: "ok", detail: exec("npm --version") };
19
+ }
20
+ catch {
21
+ return { name: "npm", status: "fail", detail: "not found", suggestion: "Install npm (comes with Node.js)" };
22
+ }
23
+ }
24
+ function checkAndroidHome() {
25
+ const home = process.env.ANDROID_HOME;
26
+ if (!home) {
27
+ return { name: "ANDROID_HOME", status: "fail", detail: "not set", suggestion: "Set ANDROID_HOME to your Android SDK path" };
28
+ }
29
+ if (!existsSync(home)) {
30
+ return { name: "ANDROID_HOME", status: "fail", detail: `${home} (not found)`, suggestion: "Path does not exist. Check ANDROID_HOME value" };
31
+ }
32
+ return { name: "ANDROID_HOME", status: "ok", detail: home };
33
+ }
34
+ function checkAdb() {
35
+ try {
36
+ const out = exec("adb version");
37
+ const match = out.match(/Android Debug Bridge version ([\d.]+)/);
38
+ return { name: "adb", status: "ok", detail: match ? match[1] : "installed" };
39
+ }
40
+ catch {
41
+ return { name: "adb", status: "fail", detail: "not found", suggestion: "Install Android SDK platform-tools and add to PATH" };
42
+ }
43
+ }
44
+ function checkEmulator() {
45
+ try {
46
+ const out = exec("emulator -version");
47
+ const match = out.match(/version ([\d.]+)/);
48
+ return { name: "emulator", status: "ok", detail: match ? match[1] : "installed" };
49
+ }
50
+ catch {
51
+ return { name: "emulator", status: "fail", detail: "not found", suggestion: "Install Android SDK emulator and add to PATH" };
52
+ }
53
+ }
54
+ function checkAvdmanager() {
55
+ try {
56
+ exec("avdmanager list avd");
57
+ return { name: "avdmanager", status: "ok", detail: "installed" };
58
+ }
59
+ catch (e) {
60
+ const msg = e instanceof Error ? e.message : "";
61
+ const detail = msg.includes("ENOENT") || msg.includes("not found") ? "not found" : "command failed";
62
+ return { name: "avdmanager", status: "fail", detail, suggestion: "Install Android SDK cmdline-tools and add to PATH" };
63
+ }
64
+ }
65
+ function checkAvds(avdmanagerResult) {
66
+ if (avdmanagerResult.status === "fail") {
67
+ return { name: "AVDs", status: "fail", detail: "skipped (avdmanager unavailable)", suggestion: "Install avdmanager first" };
68
+ }
69
+ try {
70
+ const out = exec("avdmanager list avd");
71
+ const matches = out.match(/Name:/g);
72
+ const count = matches ? matches.length : 0;
73
+ if (count === 0) {
74
+ return { name: "AVDs", status: "warn", detail: "none found", suggestion: "Create an AVD: avdmanager create avd ..." };
75
+ }
76
+ return { name: "AVDs", status: "ok", detail: `${count} available` };
77
+ }
78
+ catch {
79
+ return { name: "AVDs", status: "fail", detail: "could not list", suggestion: "avdmanager command failed" };
80
+ }
81
+ }
82
+ function checkDevices(adbResult) {
83
+ if (adbResult.status === "fail") {
84
+ return { name: "Connected devices", status: "fail", detail: "skipped (adb unavailable)", suggestion: "Install adb first" };
85
+ }
86
+ try {
87
+ const out = exec("adb devices");
88
+ const lines = out.split("\n").filter((l) => l.includes("\tdevice"));
89
+ return { name: "Connected devices", status: lines.length > 0 ? "ok" : "warn", detail: `${lines.length} connected` };
90
+ }
91
+ catch {
92
+ return { name: "Connected devices", status: "fail", detail: "could not query", suggestion: "adb command failed" };
93
+ }
94
+ }
95
+ function checkGradle() {
96
+ try {
97
+ const out = exec("gradle --version");
98
+ const match = out.match(/Gradle ([\d.]+)/);
99
+ return { name: "System gradle", status: "ok", detail: match ? match[1] : "installed" };
100
+ }
101
+ catch {
102
+ return { name: "System gradle", status: "warn", detail: "not found (optional)", suggestion: "Most projects use the Gradle wrapper (gradlew) instead" };
103
+ }
104
+ }
105
+ export function runChecks() {
106
+ const adbResult = checkAdb();
107
+ const avdmanagerResult = checkAvdmanager();
108
+ return [
109
+ checkNode(), checkNpm(), checkAndroidHome(), adbResult,
110
+ checkEmulator(), avdmanagerResult, checkAvds(avdmanagerResult), checkDevices(adbResult), checkGradle(),
111
+ ];
112
+ }
113
+ function formatTty(checks) {
114
+ const symbols = { ok: "\u2713 OK", warn: "\u26A0 WARN", fail: "\u2717 FAIL" };
115
+ const colors = { ok: "\x1b[32m", warn: "\x1b[33m", fail: "\x1b[31m" };
116
+ const reset = "\x1b[0m";
117
+ const lines = ["\nReplicant Doctor\n================\n"];
118
+ for (const c of checks) {
119
+ const sym = `${colors[c.status]}${symbols[c.status]}${reset}`;
120
+ lines.push(` ${sym} ${c.name}: ${c.detail}`);
121
+ if (c.suggestion)
122
+ lines.push(` ${c.suggestion}`);
123
+ }
124
+ const summary = { ok: 0, warn: 0, fail: 0 };
125
+ for (const c of checks)
126
+ summary[c.status]++;
127
+ lines.push(`\n${summary.ok} passed, ${summary.warn} warnings, ${summary.fail} failures\n`);
128
+ return lines.join("\n");
129
+ }
130
+ export function formatJson(checks) {
131
+ const summary = { ok: 0, warn: 0, fail: 0 };
132
+ for (const c of checks)
133
+ summary[c.status]++;
134
+ const status = summary.fail > 0 ? "fail" : summary.warn > 0 ? "warn" : "ok";
135
+ return JSON.stringify({ status, checks, summary }, null, 2);
136
+ }
137
+ export function createDoctorCommand() {
138
+ return new Command("doctor")
139
+ .description("Check environment for Android development readiness")
140
+ .option("--json", "Output as JSON")
141
+ .action((options) => {
142
+ const checks = runChecks();
143
+ const useJson = options.json || !process.stdout.isTTY;
144
+ if (useJson) {
145
+ console.log(formatJson(checks));
146
+ }
147
+ else {
148
+ console.log(formatTty(checks));
149
+ }
150
+ const hasFail = checks.some((c) => c.status === "fail");
151
+ if (hasFail)
152
+ process.exit(1);
153
+ });
154
+ }
@@ -135,7 +135,7 @@ export function createEmulatorCommand() {
135
135
  console.log(`Snapshot loaded: ${snapshotName}`);
136
136
  }
137
137
  break;
138
- case "list":
138
+ case "list": {
139
139
  const snapshots = await adapter.snapshotList(deviceId);
140
140
  if (options.json) {
141
141
  console.log(JSON.stringify({ deviceId, snapshots }, null, 2));
@@ -152,6 +152,7 @@ export function createEmulatorCommand() {
152
152
  }
153
153
  }
154
154
  break;
155
+ }
155
156
  case "delete":
156
157
  if (!snapshotName) {
157
158
  console.error("Error: --name is required for delete action");
@@ -3,4 +3,5 @@ export { createAdbCommand } from "./adb.js";
3
3
  export { createEmulatorCommand } from "./emulator.js";
4
4
  export { createUiCommand } from "./ui.js";
5
5
  export { createCacheCommand } from "./cache.js";
6
+ export { createDoctorCommand } from "./doctor.js";
6
7
  export * from "./formatter.js";
package/dist/cli/index.js CHANGED
@@ -3,4 +3,5 @@ export { createAdbCommand } from "./adb.js";
3
3
  export { createEmulatorCommand } from "./emulator.js";
4
4
  export { createUiCommand } from "./ui.js";
5
5
  export { createCacheCommand } from "./cache.js";
6
+ export { createDoctorCommand } from "./doctor.js";
6
7
  export * from "./formatter.js";
package/dist/cli.js CHANGED
@@ -1,14 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
- import { createGradleCommand, createAdbCommand, createEmulatorCommand, createUiCommand, createCacheCommand, } from "./cli/index.js";
3
+ import { createGradleCommand, createAdbCommand, createEmulatorCommand, createUiCommand, createCacheCommand, createDoctorCommand, } from "./cli/index.js";
4
+ import { VERSION } from "./version.js";
4
5
  const program = new Command();
5
6
  program
6
- .name("replicant")
7
+ .name("replicant-mcp")
7
8
  .description("Android development CLI")
8
- .version("1.0.0");
9
+ .version(VERSION);
9
10
  program.addCommand(createGradleCommand());
10
11
  program.addCommand(createAdbCommand());
11
12
  program.addCommand(createEmulatorCommand());
12
13
  program.addCommand(createUiCommand());
13
14
  program.addCommand(createCacheCommand());
15
+ program.addCommand(createDoctorCommand());
14
16
  program.parse();
package/dist/index.js CHANGED
@@ -1,6 +1,23 @@
1
1
  #!/usr/bin/env node
2
- import { runServer } from "./server.js";
3
- runServer().catch((error) => {
4
- console.error("Server error:", error);
5
- process.exit(1);
6
- });
2
+ if (process.argv.length > 2) {
3
+ // CLI mode: has arguments (e.g., --version, doctor, adb, etc.)
4
+ import("./cli.js").catch((error) => {
5
+ console.error("Failed to load CLI:", error);
6
+ process.exit(1);
7
+ });
8
+ }
9
+ else {
10
+ // MCP server mode: no arguments, start the server
11
+ Promise.all([import("./server.js"), import("./utils/logger.js")])
12
+ .then(([{ runServer }, { logger }]) => {
13
+ runServer().catch((error) => {
14
+ logger.error("Server error", { error: String(error) });
15
+ process.exit(1);
16
+ });
17
+ })
18
+ .catch((error) => {
19
+ console.error("Failed to start server:", error);
20
+ process.exit(1);
21
+ });
22
+ }
23
+ export {};
@@ -9,7 +9,11 @@ export function parseDeviceList(output) {
9
9
  if (!id)
10
10
  continue;
11
11
  const type = id.startsWith("emulator") ? "emulator" : "physical";
12
- const status = statusStr === "device" ? "online" : "offline";
12
+ const status = statusStr === "device"
13
+ ? "online"
14
+ : statusStr === "unauthorized"
15
+ ? "unauthorized"
16
+ : "offline";
13
17
  devices.push({
14
18
  id,
15
19
  type,
@@ -17,6 +17,7 @@ export interface TestResult {
17
17
  test: string;
18
18
  message: string;
19
19
  }>;
20
+ passedTests: string[];
20
21
  }
21
22
  export declare function parseBuildOutput(output: string): BuildResult;
22
23
  export declare function parseTestOutput(output: string): TestResult;
@@ -31,6 +31,12 @@ export function parseTestOutput(output) {
31
31
  while ((match = failureRegex.exec(output)) !== null) {
32
32
  failures.push({ test: `${match[1]}.${match[2]}`, message: "" });
33
33
  }
34
+ // Extract passed test names
35
+ const passedTests = [];
36
+ const passedRegex = /(\S+) > (\S+) PASSED/g;
37
+ while ((match = passedRegex.exec(output)) !== null) {
38
+ passedTests.push(`${match[1]}.${match[2]}`);
39
+ }
34
40
  return {
35
41
  passed,
36
42
  failed,
@@ -38,6 +44,7 @@ export function parseTestOutput(output) {
38
44
  total,
39
45
  duration: durationMatch?.[1],
40
46
  failures,
47
+ passedTests,
41
48
  };
42
49
  }
43
50
  export function parseModuleList(output) {