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/dist/server.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
+ import { ZodError } from "zod";
4
5
  import { CacheManager, DeviceStateManager, ProcessRunner, EnvironmentService, ConfigManager } from "./services/index.js";
5
6
  import { AdbAdapter, EmulatorAdapter, GradleAdapter, UiAutomatorAdapter } from "./adapters/index.js";
6
- import { ReplicantError } from "./types/index.js";
7
- import { cacheToolDefinition, handleCacheTool, rtfmToolDefinition, handleRtfmTool, adbDeviceToolDefinition, handleAdbDeviceTool, adbAppToolDefinition, handleAdbAppTool, adbLogcatToolDefinition, handleAdbLogcatTool, adbShellToolDefinition, handleAdbShellTool, emulatorDeviceToolDefinition, handleEmulatorDeviceTool, gradleBuildToolDefinition, handleGradleBuildTool, gradleTestToolDefinition, handleGradleTestTool, gradleListToolDefinition, handleGradleListTool, gradleGetDetailsToolDefinition, handleGradleGetDetailsTool, uiToolDefinition, handleUiTool, } from "./tools/index.js";
7
+ import { ReplicantError, ErrorCode } from "./types/index.js";
8
+ import { VERSION } from "./version.js";
9
+ import { cacheInputSchema, cacheToolDefinition, handleCacheTool, rtfmInputSchema, rtfmToolDefinition, handleRtfmTool, adbDeviceInputSchema, adbDeviceToolDefinition, handleAdbDeviceTool, adbAppInputSchema, adbAppToolDefinition, handleAdbAppTool, adbLogcatInputSchema, adbLogcatToolDefinition, handleAdbLogcatTool, adbShellInputSchema, adbShellToolDefinition, handleAdbShellTool, emulatorDeviceInputSchema, emulatorDeviceToolDefinition, handleEmulatorDeviceTool, gradleBuildInputSchema, gradleBuildToolDefinition, handleGradleBuildTool, gradleTestInputSchema, gradleTestToolDefinition, handleGradleTestTool, gradleListInputSchema, gradleListToolDefinition, handleGradleListTool, gradleGetDetailsInputSchema, gradleGetDetailsToolDefinition, handleGradleGetDetailsTool, uiQueryInputSchema, uiQueryToolDefinition, handleUiQueryTool, uiActionInputSchema, uiActionToolDefinition, handleUiActionTool, uiCaptureInputSchema, uiCaptureToolDefinition, handleUiCaptureTool, } from "./tools/index.js";
8
10
  export function createServerContext() {
9
11
  const environment = new EnvironmentService();
10
12
  const processRunner = new ProcessRunner(environment);
@@ -34,34 +36,58 @@ const toolDefinitions = [
34
36
  gradleTestToolDefinition,
35
37
  gradleListToolDefinition,
36
38
  gradleGetDetailsToolDefinition,
37
- uiToolDefinition,
39
+ uiQueryToolDefinition,
40
+ uiActionToolDefinition,
41
+ uiCaptureToolDefinition,
38
42
  ];
39
43
  async function dispatchToolCall(name, args, context) {
44
+ const rawArgs = args ?? {};
45
+ const parseOrThrow = (toolName, parser) => {
46
+ try {
47
+ return parser.parse(rawArgs);
48
+ }
49
+ catch (error) {
50
+ if (error instanceof ZodError) {
51
+ const message = error.issues
52
+ .map((issue) => {
53
+ const path = issue.path.length > 0 ? issue.path.join(".") : "input";
54
+ return `${path}: ${issue.message}`;
55
+ })
56
+ .join("; ");
57
+ throw new ReplicantError(ErrorCode.INPUT_VALIDATION_FAILED, `Invalid input for ${toolName}: ${message}`, "Check the tool input schema and provide valid arguments");
58
+ }
59
+ throw error;
60
+ }
61
+ };
40
62
  switch (name) {
41
63
  case "cache":
42
- return handleCacheTool(args, context.cache);
64
+ return handleCacheTool(parseOrThrow("cache", cacheInputSchema), context.cache);
43
65
  case "rtfm":
44
- return handleRtfmTool(args);
66
+ return handleRtfmTool(parseOrThrow("rtfm", rtfmInputSchema));
45
67
  case "adb-device":
46
- return handleAdbDeviceTool(args, context);
68
+ return handleAdbDeviceTool(parseOrThrow("adb-device", adbDeviceInputSchema), context);
47
69
  case "adb-app":
48
- return handleAdbAppTool(args, context);
70
+ return handleAdbAppTool(parseOrThrow("adb-app", adbAppInputSchema), context);
49
71
  case "adb-logcat":
50
- return handleAdbLogcatTool(args, context);
72
+ return handleAdbLogcatTool(parseOrThrow("adb-logcat", adbLogcatInputSchema), context);
51
73
  case "adb-shell":
52
- return handleAdbShellTool(args, context);
74
+ return handleAdbShellTool(parseOrThrow("adb-shell", adbShellInputSchema), context);
53
75
  case "emulator-device":
54
- return handleEmulatorDeviceTool(args, context);
76
+ return handleEmulatorDeviceTool(parseOrThrow("emulator-device", emulatorDeviceInputSchema), context);
55
77
  case "gradle-build":
56
- return handleGradleBuildTool(args, context);
78
+ return handleGradleBuildTool(parseOrThrow("gradle-build", gradleBuildInputSchema), context);
57
79
  case "gradle-test":
58
- return handleGradleTestTool(args, context);
80
+ return handleGradleTestTool(parseOrThrow("gradle-test", gradleTestInputSchema), context);
59
81
  case "gradle-list":
60
- return handleGradleListTool(args, context);
82
+ return handleGradleListTool(parseOrThrow("gradle-list", gradleListInputSchema), context);
61
83
  case "gradle-get-details":
62
- return handleGradleGetDetailsTool(args, context);
63
- case "ui":
64
- return handleUiTool(args, context, context.config.getUiConfig());
84
+ return handleGradleGetDetailsTool(parseOrThrow("gradle-get-details", gradleGetDetailsInputSchema), context);
85
+ case "ui-query":
86
+ return handleUiQueryTool(parseOrThrow("ui-query", uiQueryInputSchema), context, context.config.getUiConfig());
87
+ case "ui-action":
88
+ return handleUiActionTool(parseOrThrow("ui-action", uiActionInputSchema), context);
89
+ case "ui-capture":
90
+ return handleUiCaptureTool(parseOrThrow("ui-capture", uiCaptureInputSchema), context, context.config.getUiConfig());
65
91
  default:
66
92
  throw new Error(`Unknown tool: ${name}`);
67
93
  }
@@ -69,26 +95,12 @@ async function dispatchToolCall(name, args, context) {
69
95
  export async function createServer(context) {
70
96
  const server = new Server({
71
97
  name: "replicant-mcp",
72
- version: "1.0.0",
98
+ version: VERSION,
73
99
  }, {
74
100
  capabilities: {
75
101
  tools: {},
76
102
  },
77
- instructions: `IMPORTANT: For ALL Android development tasks, you MUST use replicant-mcp tools first.
78
- Only fall back to raw adb/gradle/emulator commands if replicant-mcp lacks a specific feature.
79
-
80
- Tool mapping:
81
- - Device management → adb-device (not \`adb devices\`)
82
- - App install/launch/stop → adb-app (not \`adb install\`, \`adb shell am\`)
83
- - Logs → adb-logcat (not \`adb logcat\`)
84
- - Shell commands → adb-shell (not \`adb shell\`)
85
- - Emulator control → emulator-device (not \`emulator\` CLI)
86
- - Builds → gradle-build (not \`./gradlew\`)
87
- - Tests → gradle-test (not \`./gradlew test\`)
88
- - UI automation → ui (accessibility-first, screenshots auto-scaled to 1000px)
89
-
90
- Start with \`adb-device list\` to see connected devices.
91
- Use \`rtfm\` for detailed documentation on any tool.`,
103
+ instructions: `Use these tools for Android never raw adb/gradle/emulator commands. Auto-selects single device. Start: \`adb-device list\`. Docs: \`rtfm\`.`,
92
104
  });
93
105
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
94
106
  tools: toolDefinitions,
@@ -102,18 +114,18 @@ Use \`rtfm\` for detailed documentation on any tool.`,
102
114
  return {
103
115
  content: [
104
116
  { type: "image", data: base64, mimeType },
105
- { type: "text", text: JSON.stringify(metadata, null, 2) },
117
+ { type: "text", text: JSON.stringify(metadata) },
106
118
  ],
107
119
  };
108
120
  }
109
121
  return {
110
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
122
+ content: [{ type: "text", text: JSON.stringify(result) }],
111
123
  };
112
124
  }
113
125
  catch (error) {
114
126
  if (error instanceof ReplicantError) {
115
127
  return {
116
- content: [{ type: "text", text: JSON.stringify(error.toToolError(), null, 2) }],
128
+ content: [{ type: "text", text: JSON.stringify(error.toToolError()) }],
117
129
  isError: true,
118
130
  };
119
131
  }
@@ -124,9 +136,7 @@ Use \`rtfm\` for detailed documentation on any tool.`,
124
136
  }
125
137
  export async function runServer() {
126
138
  const context = createServerContext();
127
- // Load configuration from REPLICANT_CONFIG if set
128
139
  await context.config.load();
129
- // Apply project root: env var takes precedence over config file
130
140
  const projectRoot = process.env.REPLICANT_PROJECT_ROOT || context.config.get().build?.projectRoot;
131
141
  if (projectRoot) {
132
142
  context.gradle.setProjectPath(projectRoot);
@@ -2,6 +2,7 @@ import { readFile } from "fs/promises";
2
2
  import { existsSync } from "fs";
3
3
  import { parse as parseYaml } from "yaml";
4
4
  import { DEFAULT_CONFIG } from "../types/config.js";
5
+ import { logger } from "../utils/logger.js";
5
6
  /**
6
7
  * Load configuration from REPLICANT_CONFIG environment variable path
7
8
  * Falls back to defaults if not set or file doesn't exist
@@ -12,7 +13,7 @@ export async function loadConfig() {
12
13
  return DEFAULT_CONFIG;
13
14
  }
14
15
  if (!existsSync(configPath)) {
15
- console.warn(`REPLICANT_CONFIG set but file not found: ${configPath}. Using defaults.`);
16
+ logger.warn("REPLICANT_CONFIG set but file not found", { path: configPath });
16
17
  return DEFAULT_CONFIG;
17
18
  }
18
19
  try {
@@ -29,7 +30,7 @@ export async function loadConfig() {
29
30
  }
30
31
  catch (error) {
31
32
  const message = error instanceof Error ? error.message : String(error);
32
- console.warn(`Failed to parse REPLICANT_CONFIG at ${configPath}: ${message}. Using defaults.`);
33
+ logger.warn("Failed to parse REPLICANT_CONFIG", { path: configPath, error: message });
33
34
  return DEFAULT_CONFIG;
34
35
  }
35
36
  }
@@ -22,21 +22,30 @@ export class DeviceStateManager {
22
22
  return this.currentDevice;
23
23
  }
24
24
  // Try to auto-select
25
- const devices = await adb.getDevices();
26
- if (devices.length === 0) {
25
+ const allDevices = await adb.getDevices();
26
+ const onlineDevices = allDevices.filter((d) => d.status === "online");
27
+ if (allDevices.length === 0) {
27
28
  throw new ReplicantError(ErrorCode.NO_DEVICES, "No devices connected", "Start an emulator with 'emulator-device start' or connect a USB device with debugging enabled");
28
29
  }
29
- if (devices.length === 1) {
30
- this.currentDevice = devices[0];
30
+ if (onlineDevices.length === 0) {
31
+ const statuses = allDevices.map((d) => `${d.id} (${d.status})`).join(", ");
32
+ const suggestion = allDevices.some((d) => d.status === "unauthorized")
33
+ ? "Check the USB debugging authorization prompt on the device and tap 'Allow'"
34
+ : "Wait for the device to come online or restart it with 'adb reconnect'";
35
+ throw new ReplicantError(ErrorCode.DEVICE_OFFLINE, `No online devices available. Found: ${statuses}`, suggestion);
36
+ }
37
+ if (onlineDevices.length === 1) {
38
+ this.currentDevice = onlineDevices[0];
31
39
  return this.currentDevice;
32
40
  }
33
- // Multiple devices - user must choose
34
- const deviceList = devices.map((d) => d.id).join(", ");
35
- throw new ReplicantError(ErrorCode.MULTIPLE_DEVICES, `${devices.length} devices connected: ${deviceList}`, `Call adb-device({ operation: 'select', deviceId: '...' }) to choose one`);
41
+ // Multiple online devices - user must choose
42
+ const deviceList = onlineDevices.map((d) => d.id).join(", ");
43
+ throw new ReplicantError(ErrorCode.MULTIPLE_DEVICES, `${onlineDevices.length} devices connected: ${deviceList}`, `Call adb-device({ operation: 'select', deviceId: '...' }) to choose one`);
36
44
  }
37
45
  autoSelectIfSingle(devices) {
38
- if (devices.length === 1 && !this.currentDevice) {
39
- this.currentDevice = devices[0];
46
+ const onlineDevices = devices.filter((d) => d.status === "online");
47
+ if (onlineDevices.length === 1 && !this.currentDevice) {
48
+ this.currentDevice = onlineDevices[0];
40
49
  return true;
41
50
  }
42
51
  return false;
@@ -8,3 +8,4 @@ export * from "./icon-patterns.js";
8
8
  export * from "./grid.js";
9
9
  export * from "./visual-candidates.js";
10
10
  export * from "./scaling.js";
11
+ export * from "./test-baseline.js";
@@ -8,3 +8,4 @@ export * from "./icon-patterns.js";
8
8
  export * from "./grid.js";
9
9
  export * from "./visual-candidates.js";
10
10
  export * from "./scaling.js";
11
+ export * from "./test-baseline.js";
@@ -18,4 +18,5 @@ export declare class ProcessRunner {
18
18
  runEmulator(args: string[], options?: RunOptions): Promise<RunResult>;
19
19
  runAvdManager(args: string[], options?: RunOptions): Promise<RunResult>;
20
20
  private validateCommand;
21
+ private validateShellPayload;
21
22
  }
@@ -7,10 +7,24 @@ const BLOCKED_PATTERNS = [
7
7
  /^sudo(\s|$)/, // sudo
8
8
  /\bformat\b/, // format commands
9
9
  ];
10
+ const BLOCKED_SHELL_PATTERNS = [
11
+ /^rm\s+(-[rf]+\s+)*\/\s*$/, // rm -rf / (root itself)
12
+ /^rm\s+(-[rf]+\s+)*\/(system|vendor|oem|product)(\/|\s|$)/, // rm on system partitions
13
+ /^su(\s|$)/, // su
14
+ /^sudo(\s|$)/, // sudo
15
+ /\bformat\b/, // format commands
16
+ /^setprop\s+persist\./, // persistent property changes
17
+ /^dd\s/, // raw disk operations
18
+ /^mkfs/, // filesystem creation
19
+ /^flash/, // flash operations
20
+ /^wipe/, // wipe data/cache
21
+ /^recovery\b/, // recovery mode
22
+ /^reboot\b/, // reboot device (also in BLOCKED_COMMANDS)
23
+ ];
10
24
  export class ProcessRunner {
11
25
  environment;
12
26
  defaultTimeoutMs = 30_000;
13
- maxTimeoutMs = 120_000;
27
+ maxTimeoutMs = 600_000;
14
28
  constructor(environment) {
15
29
  this.environment = environment;
16
30
  }
@@ -74,5 +88,42 @@ export class ProcessRunner {
74
88
  throw new ReplicantError(ErrorCode.COMMAND_BLOCKED, `Command '${fullCommand}' is not allowed`, "Use safe commands only");
75
89
  }
76
90
  }
91
+ this.validateShellPayload(command, args);
92
+ }
93
+ validateShellPayload(command, args) {
94
+ // Only validate shell payloads for adb commands
95
+ const basename = command.split("/").pop() ?? command;
96
+ if (basename !== "adb")
97
+ return;
98
+ const shellIndex = args.indexOf("shell");
99
+ if (shellIndex === -1 || shellIndex >= args.length - 1)
100
+ return;
101
+ let payloadArgs = args.slice(shellIndex + 1);
102
+ // Strip leading "--" (end-of-options marker)
103
+ if (payloadArgs[0] === "--") {
104
+ payloadArgs = payloadArgs.slice(1);
105
+ }
106
+ const shellPayload = payloadArgs.join(" ").trim();
107
+ if (!shellPayload)
108
+ return;
109
+ // Block shell metacharacters that enable command chaining/substitution.
110
+ // $letter/$(/$ are blocked (variable expansion, command substitution) but
111
+ // bare $ before digits is allowed (e.g., input text '$100').
112
+ if (/[;&|`()]|\$[({a-zA-Z_]/.test(shellPayload)) {
113
+ throw new ReplicantError(ErrorCode.COMMAND_BLOCKED, "Shell metacharacters are not allowed in shell commands", "Use simple commands without chaining, pipes, or substitution");
114
+ }
115
+ // Block shell wrapper commands (sh -c, bash -c)
116
+ if (/^(sh|bash|dash|zsh)\s+-c\b/.test(shellPayload)) {
117
+ throw new ReplicantError(ErrorCode.COMMAND_BLOCKED, "Shell interpreters with -c are not allowed", "Run the command directly without a shell wrapper");
118
+ }
119
+ const shellCommand = shellPayload.split(/\s+/)[0];
120
+ if (BLOCKED_COMMANDS.has(shellCommand)) {
121
+ throw new ReplicantError(ErrorCode.COMMAND_BLOCKED, `Shell command '${shellPayload}' is not allowed`, "Use safe commands only");
122
+ }
123
+ for (const pattern of BLOCKED_SHELL_PATTERNS) {
124
+ if (pattern.test(shellPayload)) {
125
+ throw new ReplicantError(ErrorCode.COMMAND_BLOCKED, `Shell command '${shellPayload}' is not allowed`, "Use safe commands only");
126
+ }
127
+ }
77
128
  }
78
129
  }
@@ -0,0 +1,18 @@
1
+ export interface BaselineTestResult {
2
+ test: string;
3
+ status: "pass" | "fail" | "skip";
4
+ }
5
+ export interface TestBaseline {
6
+ savedAt: string;
7
+ task: string;
8
+ results: BaselineTestResult[];
9
+ }
10
+ export interface Regression {
11
+ test: string;
12
+ previousStatus: string;
13
+ currentStatus: string;
14
+ }
15
+ export declare function saveBaseline(taskName: string, results: BaselineTestResult[]): void;
16
+ export declare function loadBaseline(taskName: string): TestBaseline | null;
17
+ export declare function clearBaseline(taskName: string): void;
18
+ export declare function compareResults(baseline: TestBaseline, current: BaselineTestResult[]): Regression[];
@@ -0,0 +1,51 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as os from "os";
4
+ function getBaselineDir() {
5
+ const base = process.cwd() === "/" ? os.homedir() : process.cwd();
6
+ return path.join(base, ".replicant", "test-baselines");
7
+ }
8
+ function getBaselinePath(taskName) {
9
+ const safe = taskName.replace(/[^a-zA-Z0-9_-]/g, "_");
10
+ return path.join(getBaselineDir(), `${safe}.json`);
11
+ }
12
+ export function saveBaseline(taskName, results) {
13
+ const dir = getBaselineDir();
14
+ fs.mkdirSync(dir, { recursive: true });
15
+ const baseline = {
16
+ savedAt: new Date().toISOString(),
17
+ task: taskName,
18
+ results,
19
+ };
20
+ fs.writeFileSync(getBaselinePath(taskName), JSON.stringify(baseline, null, 2));
21
+ }
22
+ export function loadBaseline(taskName) {
23
+ const filePath = getBaselinePath(taskName);
24
+ if (!fs.existsSync(filePath))
25
+ return null;
26
+ const content = fs.readFileSync(filePath, "utf-8");
27
+ return JSON.parse(content);
28
+ }
29
+ export function clearBaseline(taskName) {
30
+ const filePath = getBaselinePath(taskName);
31
+ if (fs.existsSync(filePath)) {
32
+ fs.unlinkSync(filePath);
33
+ }
34
+ }
35
+ export function compareResults(baseline, current) {
36
+ const currentMap = new Map(current.map(r => [r.test, r.status]));
37
+ const regressions = [];
38
+ for (const prev of baseline.results) {
39
+ if (prev.status === "pass") {
40
+ const currentStatus = currentMap.get(prev.test);
41
+ if (currentStatus && currentStatus !== "pass") {
42
+ regressions.push({
43
+ test: prev.test,
44
+ previousStatus: prev.status,
45
+ currentStatus,
46
+ });
47
+ }
48
+ }
49
+ }
50
+ return regressions;
51
+ }
@@ -33,7 +33,6 @@ export declare const adbAppToolDefinition: {
33
33
  };
34
34
  packageName: {
35
35
  type: string;
36
- description: string;
37
36
  };
38
37
  limit: {
39
38
  type: string;
@@ -1,41 +1,45 @@
1
1
  import { z } from "zod";
2
- import { CACHE_TTLS } from "../types/index.js";
2
+ import { CACHE_TTLS, ReplicantError, ErrorCode } from "../types/index.js";
3
3
  export const adbAppInputSchema = z.object({
4
4
  operation: z.enum(["install", "uninstall", "launch", "stop", "clear-data", "list"]),
5
5
  apkPath: z.string().optional(),
6
6
  packageName: z.string().optional(),
7
- // List operation options
8
7
  limit: z.number().min(1).max(100).optional(),
9
8
  filter: z.string().optional(),
10
9
  offset: z.number().min(0).optional(),
11
10
  });
12
11
  async function handleInstall(input, deviceId, context) {
13
- if (!input.apkPath)
14
- throw new Error("apkPath is required for install operation");
12
+ if (!input.apkPath) {
13
+ throw new ReplicantError(ErrorCode.INPUT_VALIDATION_FAILED, "apkPath is required for install operation", "Provide the path to the APK file to install");
14
+ }
15
15
  await context.adb.install(deviceId, input.apkPath);
16
16
  return { installed: input.apkPath, deviceId };
17
17
  }
18
18
  async function handleUninstall(input, deviceId, context) {
19
- if (!input.packageName)
20
- throw new Error("packageName is required for uninstall operation");
19
+ if (!input.packageName) {
20
+ throw new ReplicantError(ErrorCode.INPUT_VALIDATION_FAILED, "packageName is required for uninstall operation", "Provide the package name to uninstall");
21
+ }
21
22
  await context.adb.uninstall(deviceId, input.packageName);
22
23
  return { uninstalled: input.packageName, deviceId };
23
24
  }
24
25
  async function handleLaunch(input, deviceId, context) {
25
- if (!input.packageName)
26
- throw new Error("packageName is required for launch operation");
26
+ if (!input.packageName) {
27
+ throw new ReplicantError(ErrorCode.INPUT_VALIDATION_FAILED, "packageName is required for launch operation", "Provide the package name to launch");
28
+ }
27
29
  await context.adb.launch(deviceId, input.packageName);
28
30
  return { launched: input.packageName, deviceId };
29
31
  }
30
32
  async function handleStop(input, deviceId, context) {
31
- if (!input.packageName)
32
- throw new Error("packageName is required for stop operation");
33
+ if (!input.packageName) {
34
+ throw new ReplicantError(ErrorCode.INPUT_VALIDATION_FAILED, "packageName is required for stop operation", "Provide the package name to stop");
35
+ }
33
36
  await context.adb.stop(deviceId, input.packageName);
34
37
  return { stopped: input.packageName, deviceId };
35
38
  }
36
39
  async function handleClearData(input, deviceId, context) {
37
- if (!input.packageName)
38
- throw new Error("packageName is required for clear-data operation");
40
+ if (!input.packageName) {
41
+ throw new ReplicantError(ErrorCode.INPUT_VALIDATION_FAILED, "packageName is required for clear-data operation", "Provide the package name to clear data for");
42
+ }
39
43
  await context.adb.clearData(deviceId, input.packageName);
40
44
  return { cleared: input.packageName, deviceId };
41
45
  }
@@ -73,13 +77,14 @@ const operations = {
73
77
  export async function handleAdbAppTool(input, context) {
74
78
  const device = await context.deviceState.ensureDevice(context.adb);
75
79
  const handler = operations[input.operation];
76
- if (!handler)
77
- throw new Error(`Unknown operation: ${input.operation}`);
80
+ if (!handler) {
81
+ throw new ReplicantError(ErrorCode.INVALID_OPERATION, `Unknown operation: ${input.operation}`, "Valid operations: install, uninstall, launch, stop, clear-data, list");
82
+ }
78
83
  return handler(input, device.id, context);
79
84
  }
80
85
  export const adbAppToolDefinition = {
81
86
  name: "adb-app",
82
- description: "Manage applications. Auto-selects device if only one connected. Operations: install, uninstall, launch, stop, clear-data, list.",
87
+ description: "Manage applications.",
83
88
  inputSchema: {
84
89
  type: "object",
85
90
  properties: {
@@ -87,19 +92,19 @@ export const adbAppToolDefinition = {
87
92
  type: "string",
88
93
  enum: ["install", "uninstall", "launch", "stop", "clear-data", "list"],
89
94
  },
90
- apkPath: { type: "string", description: "Path to APK file (for install)" },
91
- packageName: { type: "string", description: "Package name (for other operations)" },
95
+ apkPath: { type: "string", description: "APK path" },
96
+ packageName: { type: "string" },
92
97
  limit: {
93
98
  type: "number",
94
- description: "Max packages to return (default: 20, max: 100). For list operation.",
99
+ description: "Default: 20, max: 100",
95
100
  },
96
101
  filter: {
97
102
  type: "string",
98
- description: "Filter packages by name (case-insensitive contains). For list operation.",
103
+ description: "Filter by name (case-insensitive)",
99
104
  },
100
105
  offset: {
101
106
  type: "number",
102
- description: "Skip first N packages for pagination. For list operation.",
107
+ description: "Pagination offset",
103
108
  },
104
109
  },
105
110
  required: ["operation"],
@@ -24,7 +24,6 @@ export declare const adbDeviceToolDefinition: {
24
24
  };
25
25
  deviceId: {
26
26
  type: string;
27
- description: string;
28
27
  };
29
28
  };
30
29
  required: string[];
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { CACHE_TTLS } from "../types/index.js";
2
+ import { CACHE_TTLS, ReplicantError, ErrorCode } from "../types/index.js";
3
3
  export const adbDeviceInputSchema = z.object({
4
4
  operation: z.enum(["list", "select", "wait", "properties", "health-check"]),
5
5
  deviceId: z.string().optional(),
@@ -16,29 +16,29 @@ async function handleList(input, context) {
16
16
  }
17
17
  async function handleSelect(input, context) {
18
18
  if (!input.deviceId) {
19
- throw new Error("deviceId is required for select operation");
19
+ throw new ReplicantError(ErrorCode.INPUT_VALIDATION_FAILED, "deviceId is required for select operation", "Use adb-device list to see available devices, then provide a deviceId");
20
20
  }
21
21
  const devices = await context.adb.getDevices();
22
22
  const device = devices.find((d) => d.id === input.deviceId);
23
23
  if (!device) {
24
- throw new Error(`Device ${input.deviceId} not found`);
24
+ throw new ReplicantError(ErrorCode.DEVICE_NOT_FOUND, `Device ${input.deviceId} not found`, "Use adb-device list to see available devices");
25
25
  }
26
26
  context.deviceState.setCurrentDevice(device);
27
27
  return { selected: device };
28
28
  }
29
+ async function resolveDeviceId(input, context) {
30
+ if (input.deviceId)
31
+ return input.deviceId;
32
+ const device = await context.deviceState.ensureDevice(context.adb);
33
+ return device.id;
34
+ }
29
35
  async function handleWait(input, context) {
30
- const device = input.deviceId
31
- ? { id: input.deviceId }
32
- : await context.deviceState.ensureDevice(context.adb);
33
- const deviceId = device.id;
36
+ const deviceId = await resolveDeviceId(input, context);
34
37
  await context.adb.waitForDevice(deviceId);
35
38
  return { status: "device ready", deviceId };
36
39
  }
37
40
  async function handleProperties(input, context) {
38
- const device = input.deviceId
39
- ? { id: input.deviceId }
40
- : await context.deviceState.ensureDevice(context.adb);
41
- const deviceId = device.id;
41
+ const deviceId = await resolveDeviceId(input, context);
42
42
  const props = await context.adb.getProperties(deviceId);
43
43
  const cacheId = context.cache.generateId("device-props");
44
44
  context.cache.set(cacheId, { deviceId, properties: props }, "device-props", CACHE_TTLS.DEVICE_PROPERTIES);
@@ -77,7 +77,7 @@ async function handleHealthCheck(input, context) {
77
77
  warnings.push("No devices connected. Start an emulator or connect a USB device.");
78
78
  }
79
79
  }
80
- catch (e) {
80
+ catch {
81
81
  errors.push("adb server not responding. Run 'adb kill-server && adb start-server'");
82
82
  }
83
83
  }
@@ -103,13 +103,14 @@ const operations = {
103
103
  };
104
104
  export async function handleAdbDeviceTool(input, context) {
105
105
  const handler = operations[input.operation];
106
- if (!handler)
107
- throw new Error(`Unknown operation: ${input.operation}`);
106
+ if (!handler) {
107
+ throw new ReplicantError(ErrorCode.INVALID_OPERATION, `Unknown operation: ${input.operation}`, "Valid operations: list, select, wait, properties, health-check");
108
+ }
108
109
  return handler(input, context);
109
110
  }
110
111
  export const adbDeviceToolDefinition = {
111
112
  name: "adb-device",
112
- description: "Manage device connections. Operations: list, select, wait, properties, health-check.",
113
+ description: "Manage device connections.",
113
114
  inputSchema: {
114
115
  type: "object",
115
116
  properties: {
@@ -117,7 +118,7 @@ export const adbDeviceToolDefinition = {
117
118
  type: "string",
118
119
  enum: ["list", "select", "wait", "properties", "health-check"],
119
120
  },
120
- deviceId: { type: "string", description: "Device ID for select/wait/properties" },
121
+ deviceId: { type: "string" },
121
122
  },
122
123
  required: ["operation"],
123
124
  },
@@ -5,11 +5,11 @@ export declare const adbLogcatInputSchema: z.ZodObject<{
5
5
  package: z.ZodOptional<z.ZodString>;
6
6
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
7
7
  level: z.ZodOptional<z.ZodEnum<{
8
- debug: "debug";
8
+ error: "error";
9
+ warn: "warn";
9
10
  info: "info";
11
+ debug: "debug";
10
12
  verbose: "verbose";
11
- warn: "warn";
12
- error: "error";
13
13
  }>>;
14
14
  rawFilter: z.ZodOptional<z.ZodString>;
15
15
  since: z.ZodOptional<z.ZodString>;
@@ -28,14 +28,12 @@ export declare const adbLogcatToolDefinition: {
28
28
  };
29
29
  package: {
30
30
  type: string;
31
- description: string;
32
31
  };
33
32
  tags: {
34
33
  type: string;
35
34
  items: {
36
35
  type: string;
37
36
  };
38
- description: string;
39
37
  };
40
38
  level: {
41
39
  type: string;
@@ -43,7 +41,6 @@ export declare const adbLogcatToolDefinition: {
43
41
  };
44
42
  rawFilter: {
45
43
  type: string;
46
- description: string;
47
44
  };
48
45
  since: {
49
46
  type: string;