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.
- package/README.md +28 -2
- package/dist/adapters/adb.d.ts +2 -0
- package/dist/adapters/adb.js +12 -1
- package/dist/adapters/emulator.d.ts +2 -0
- package/dist/adapters/emulator.js +35 -5
- package/dist/adapters/ui-fallback-find.js +26 -1
- package/dist/cli/doctor.d.ts +10 -0
- package/dist/cli/doctor.js +154 -0
- package/dist/cli/emulator.js +2 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli.js +5 -3
- package/dist/index.js +22 -5
- package/dist/parsers/adb-output.js +5 -1
- package/dist/parsers/gradle-output.d.ts +1 -0
- package/dist/parsers/gradle-output.js +7 -0
- package/dist/server.js +47 -37
- package/dist/services/config.js +3 -2
- package/dist/services/device-state.js +18 -9
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/process-runner.d.ts +1 -0
- package/dist/services/process-runner.js +52 -1
- package/dist/services/test-baseline.d.ts +18 -0
- package/dist/services/test-baseline.js +51 -0
- package/dist/tools/adb-app.d.ts +0 -1
- package/dist/tools/adb-app.js +25 -20
- package/dist/tools/adb-device.d.ts +0 -1
- package/dist/tools/adb-device.js +17 -16
- package/dist/tools/adb-logcat.d.ts +3 -6
- package/dist/tools/adb-logcat.js +29 -31
- package/dist/tools/adb-shell.d.ts +15 -1
- package/dist/tools/adb-shell.js +37 -7
- package/dist/tools/cache.js +4 -3
- package/dist/tools/emulator-device.d.ts +0 -4
- package/dist/tools/emulator-device.js +24 -19
- package/dist/tools/gradle-build.d.ts +0 -1
- package/dist/tools/gradle-build.js +3 -4
- package/dist/tools/gradle-get-details.d.ts +14 -1
- package/dist/tools/gradle-get-details.js +132 -42
- package/dist/tools/gradle-list.js +4 -6
- package/dist/tools/gradle-test.d.ts +14 -1
- package/dist/tools/gradle-test.js +70 -9
- package/dist/tools/index.d.ts +3 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/rtfm.js +26 -30
- package/dist/tools/ui-action.d.ts +63 -0
- package/dist/tools/ui-action.js +83 -0
- package/dist/tools/ui-capture.d.ts +43 -0
- package/dist/tools/ui-capture.js +59 -0
- package/dist/tools/ui-find.d.ts +14 -2
- package/dist/tools/ui-find.js +87 -51
- package/dist/tools/{ui.d.ts → ui-query.d.ts} +9 -75
- package/dist/tools/ui-query.js +138 -0
- package/dist/types/device.d.ts +1 -1
- package/dist/types/errors.d.ts +3 -0
- package/dist/types/errors.js +4 -0
- package/dist/types/icon-recognition.d.ts +5 -0
- package/dist/types/schemas/adb-app-output.d.ts +83 -0
- package/dist/types/schemas/adb-app-output.js +60 -0
- package/dist/types/schemas/adb-device-output.d.ts +152 -0
- package/dist/types/schemas/adb-device-output.js +72 -0
- package/dist/types/schemas/adb-logcat-output.d.ts +15 -0
- package/dist/types/schemas/adb-logcat-output.js +14 -0
- package/dist/types/schemas/adb-shell-output.d.ts +17 -0
- package/dist/types/schemas/adb-shell-output.js +16 -0
- package/dist/types/schemas/cache-output.d.ts +88 -0
- package/dist/types/schemas/cache-output.js +51 -0
- package/dist/types/schemas/emulator-device-output.d.ts +100 -0
- package/dist/types/schemas/emulator-device-output.js +76 -0
- package/dist/types/schemas/gradle-build-output.d.ts +16 -0
- package/dist/types/schemas/gradle-build-output.js +15 -0
- package/dist/types/schemas/gradle-get-details-output.d.ts +129 -0
- package/dist/types/schemas/gradle-get-details-output.js +86 -0
- package/dist/types/schemas/gradle-list-output.d.ts +56 -0
- package/dist/types/schemas/gradle-list-output.js +39 -0
- package/dist/types/schemas/gradle-test-output.d.ts +82 -0
- package/dist/types/schemas/gradle-test-output.js +54 -0
- package/dist/types/schemas/index.d.ts +12 -0
- package/dist/types/schemas/index.js +12 -0
- package/dist/types/schemas/rtfm-output.d.ts +8 -0
- package/dist/types/schemas/rtfm-output.js +7 -0
- package/dist/types/schemas/ui-output.d.ts +361 -0
- package/dist/types/schemas/ui-output.js +188 -0
- package/dist/utils/logger.d.ts +6 -0
- package/dist/utils/logger.js +34 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/docs/contracts/replicant-mcp.contract.json +2371 -0
- package/docs/rtfm/adb.md +11 -1
- package/docs/rtfm/build.md +7 -1
- package/docs/rtfm/cache.md +21 -0
- package/docs/rtfm/ui.md +67 -35
- package/package.json +13 -5
- 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 {
|
|
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
|
-
|
|
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(
|
|
64
|
+
return handleCacheTool(parseOrThrow("cache", cacheInputSchema), context.cache);
|
|
43
65
|
case "rtfm":
|
|
44
|
-
return handleRtfmTool(
|
|
66
|
+
return handleRtfmTool(parseOrThrow("rtfm", rtfmInputSchema));
|
|
45
67
|
case "adb-device":
|
|
46
|
-
return handleAdbDeviceTool(
|
|
68
|
+
return handleAdbDeviceTool(parseOrThrow("adb-device", adbDeviceInputSchema), context);
|
|
47
69
|
case "adb-app":
|
|
48
|
-
return handleAdbAppTool(
|
|
70
|
+
return handleAdbAppTool(parseOrThrow("adb-app", adbAppInputSchema), context);
|
|
49
71
|
case "adb-logcat":
|
|
50
|
-
return handleAdbLogcatTool(
|
|
72
|
+
return handleAdbLogcatTool(parseOrThrow("adb-logcat", adbLogcatInputSchema), context);
|
|
51
73
|
case "adb-shell":
|
|
52
|
-
return handleAdbShellTool(
|
|
74
|
+
return handleAdbShellTool(parseOrThrow("adb-shell", adbShellInputSchema), context);
|
|
53
75
|
case "emulator-device":
|
|
54
|
-
return handleEmulatorDeviceTool(
|
|
76
|
+
return handleEmulatorDeviceTool(parseOrThrow("emulator-device", emulatorDeviceInputSchema), context);
|
|
55
77
|
case "gradle-build":
|
|
56
|
-
return handleGradleBuildTool(
|
|
78
|
+
return handleGradleBuildTool(parseOrThrow("gradle-build", gradleBuildInputSchema), context);
|
|
57
79
|
case "gradle-test":
|
|
58
|
-
return handleGradleTestTool(
|
|
80
|
+
return handleGradleTestTool(parseOrThrow("gradle-test", gradleTestInputSchema), context);
|
|
59
81
|
case "gradle-list":
|
|
60
|
-
return handleGradleListTool(
|
|
82
|
+
return handleGradleListTool(parseOrThrow("gradle-list", gradleListInputSchema), context);
|
|
61
83
|
case "gradle-get-details":
|
|
62
|
-
return handleGradleGetDetailsTool(
|
|
63
|
-
case "ui":
|
|
64
|
-
return
|
|
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:
|
|
98
|
+
version: VERSION,
|
|
73
99
|
}, {
|
|
74
100
|
capabilities: {
|
|
75
101
|
tools: {},
|
|
76
102
|
},
|
|
77
|
-
instructions: `
|
|
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
|
|
117
|
+
{ type: "text", text: JSON.stringify(metadata) },
|
|
106
118
|
],
|
|
107
119
|
};
|
|
108
120
|
}
|
|
109
121
|
return {
|
|
110
|
-
content: [{ type: "text", text: JSON.stringify(result
|
|
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()
|
|
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);
|
package/dist/services/config.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
26
|
-
|
|
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 (
|
|
30
|
-
|
|
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 =
|
|
35
|
-
throw new ReplicantError(ErrorCode.MULTIPLE_DEVICES, `${
|
|
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
|
-
|
|
39
|
-
|
|
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;
|
package/dist/services/index.d.ts
CHANGED
package/dist/services/index.js
CHANGED
|
@@ -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 =
|
|
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
|
+
}
|
package/dist/tools/adb-app.d.ts
CHANGED
package/dist/tools/adb-app.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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: "
|
|
91
|
-
packageName: { type: "string"
|
|
95
|
+
apkPath: { type: "string", description: "APK path" },
|
|
96
|
+
packageName: { type: "string" },
|
|
92
97
|
limit: {
|
|
93
98
|
type: "number",
|
|
94
|
-
description: "
|
|
99
|
+
description: "Default: 20, max: 100",
|
|
95
100
|
},
|
|
96
101
|
filter: {
|
|
97
102
|
type: "string",
|
|
98
|
-
description: "Filter
|
|
103
|
+
description: "Filter by name (case-insensitive)",
|
|
99
104
|
},
|
|
100
105
|
offset: {
|
|
101
106
|
type: "number",
|
|
102
|
-
description: "
|
|
107
|
+
description: "Pagination offset",
|
|
103
108
|
},
|
|
104
109
|
},
|
|
105
110
|
required: ["operation"],
|
package/dist/tools/adb-device.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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"
|
|
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
|
-
|
|
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;
|