@mobileaidev/ai-app-bridge 0.2.13 → 0.2.14
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 +8 -5
- package/bin/ai-app-bridge.js +7 -39
- package/bin/artifact-paths.js +129 -0
- package/bin/ios-provider.js +2 -10
- package/bin/mcp-server.js +3 -1
- package/package.json +2 -1
- package/skills/ai-app-bridge-use/SKILL.md +2 -1
package/README.md
CHANGED
|
@@ -121,11 +121,14 @@ on many Android 16 devices; ASCII text can still fall back to ADB when an older
|
|
|
121
121
|
bridge runtime is running.
|
|
122
122
|
|
|
123
123
|
When `screenshot` or `smoke` runs without `--out-file`, the CLI writes a unique
|
|
124
|
-
PNG under
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
PNG under a git-ignored project artifact directory. Gradle, Android, and Flutter
|
|
125
|
+
projects normally use `build/ai_app_bridge_artifacts`; Node projects can use
|
|
126
|
+
`node_modules/.cache/ai_app_bridge_artifacts`; Swift projects can use
|
|
127
|
+
`.build/ai_app_bridge_artifacts`. If the current git worktree has no ignored
|
|
128
|
+
artifact candidate, generated defaults go under `.git/ai_app_bridge_artifacts`
|
|
129
|
+
so they cannot dirty the repository root. It keeps the newest 20 generated
|
|
130
|
+
screenshots for each command prefix. Use `--artifact-dir` to choose an ignored
|
|
131
|
+
directory, or `--out-file` when a fixed path is intentional.
|
|
129
132
|
|
|
130
133
|
`launch-app` queries Android LAUNCHER activities before starting the app. If a
|
|
131
134
|
debug dependency exposes multiple launcher entries, it returns
|
package/bin/ai-app-bridge.js
CHANGED
|
@@ -7,6 +7,13 @@ const net = require('net');
|
|
|
7
7
|
const os = require('os');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const { IOSBridgeProvider } = require('./ios-provider');
|
|
10
|
+
const {
|
|
11
|
+
artifactTimestamp,
|
|
12
|
+
defaultArtifactDirectory,
|
|
13
|
+
defaultArtifactPath,
|
|
14
|
+
sanitizeArtifactExtension,
|
|
15
|
+
sanitizeArtifactName,
|
|
16
|
+
} = require('./artifact-paths');
|
|
10
17
|
|
|
11
18
|
const generatedArtifactRetention = 20;
|
|
12
19
|
|
|
@@ -2392,17 +2399,6 @@ function screenshotOutputPath(options = {}, prefix = 'ai_app_bridge_screenshot')
|
|
|
2392
2399
|
return defaultArtifactPath(prefix, 'png', { artifactDir: options.artifactDir });
|
|
2393
2400
|
}
|
|
2394
2401
|
|
|
2395
|
-
function defaultArtifactPath(prefix, extension, options = {}) {
|
|
2396
|
-
const directory = path.resolve(options.artifactDir || defaultArtifactDirectory());
|
|
2397
|
-
const suffix = [
|
|
2398
|
-
artifactTimestamp(options.now || new Date()),
|
|
2399
|
-
String(options.pid || process.pid),
|
|
2400
|
-
options.randomSuffix || Math.random().toString(36).slice(2, 8),
|
|
2401
|
-
].join('-');
|
|
2402
|
-
const name = `${sanitizeArtifactName(prefix)}-${suffix}.${sanitizeArtifactExtension(extension)}`;
|
|
2403
|
-
return path.join(directory, name);
|
|
2404
|
-
}
|
|
2405
|
-
|
|
2406
2402
|
async function pruneGeneratedArtifacts(options = {}) {
|
|
2407
2403
|
const keep = generatedArtifactRetention;
|
|
2408
2404
|
const result = {
|
|
@@ -2475,34 +2471,6 @@ async function pruneGeneratedArtifacts(options = {}) {
|
|
|
2475
2471
|
return result;
|
|
2476
2472
|
}
|
|
2477
2473
|
|
|
2478
|
-
function artifactTimestamp(date) {
|
|
2479
|
-
const value = date instanceof Date ? date : new Date(date);
|
|
2480
|
-
const pad = (number, size = 2) => String(number).padStart(size, '0');
|
|
2481
|
-
return [
|
|
2482
|
-
value.getUTCFullYear(),
|
|
2483
|
-
pad(value.getUTCMonth() + 1),
|
|
2484
|
-
pad(value.getUTCDate()),
|
|
2485
|
-
'-',
|
|
2486
|
-
pad(value.getUTCHours()),
|
|
2487
|
-
pad(value.getUTCMinutes()),
|
|
2488
|
-
pad(value.getUTCSeconds()),
|
|
2489
|
-
'-',
|
|
2490
|
-
pad(value.getUTCMilliseconds(), 3),
|
|
2491
|
-
].join('');
|
|
2492
|
-
}
|
|
2493
|
-
|
|
2494
|
-
function sanitizeArtifactName(value) {
|
|
2495
|
-
return String(value || 'artifact').replace(/[^a-zA-Z0-9_.-]+/g, '_').replace(/^_+|_+$/g, '') || 'artifact';
|
|
2496
|
-
}
|
|
2497
|
-
|
|
2498
|
-
function sanitizeArtifactExtension(value) {
|
|
2499
|
-
return sanitizeArtifactName(String(value || 'bin').replace(/^\.+/, '')) || 'bin';
|
|
2500
|
-
}
|
|
2501
|
-
|
|
2502
|
-
function defaultArtifactDirectory() {
|
|
2503
|
-
return path.join(process.cwd(), 'build', 'ai_app_bridge_artifacts');
|
|
2504
|
-
}
|
|
2505
|
-
|
|
2506
2474
|
function escapeRegExp(value) {
|
|
2507
2475
|
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2508
2476
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const { execFileSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const artifactDirectoryName = 'ai_app_bridge_artifacts';
|
|
6
|
+
|
|
7
|
+
function defaultArtifactDirectory(options = {}) {
|
|
8
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
9
|
+
const gitRoot = gitOutput(cwd, ['rev-parse', '--show-toplevel']);
|
|
10
|
+
if (!gitRoot) {
|
|
11
|
+
return path.join(cwd, 'build', artifactDirectoryName);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
for (const directory of artifactDirectoryCandidates(gitRoot)) {
|
|
15
|
+
if (isGitIgnored(gitRoot, directory)) {
|
|
16
|
+
return directory;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const gitDirectory = gitOutput(cwd, ['rev-parse', '--absolute-git-dir']);
|
|
21
|
+
return path.join(gitDirectory || path.join(gitRoot, '.git'), artifactDirectoryName);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function defaultArtifactPath(prefix, extension, options = {}) {
|
|
25
|
+
const directory = path.resolve(options.artifactDir || defaultArtifactDirectory({ cwd: options.cwd }));
|
|
26
|
+
const suffix = [
|
|
27
|
+
artifactTimestamp(options.now || new Date()),
|
|
28
|
+
String(options.pid || process.pid),
|
|
29
|
+
options.randomSuffix || Math.random().toString(36).slice(2, 8),
|
|
30
|
+
].join('-');
|
|
31
|
+
const name = `${sanitizeArtifactName(prefix)}-${suffix}.${sanitizeArtifactExtension(extension)}`;
|
|
32
|
+
return path.join(directory, name);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function artifactDirectoryCandidates(gitRoot) {
|
|
36
|
+
const root = path.resolve(gitRoot);
|
|
37
|
+
const candidates = [];
|
|
38
|
+
|
|
39
|
+
if (hasAny(root, ['settings.gradle', 'settings.gradle.kts', 'build.gradle', 'build.gradle.kts', 'gradlew', 'pubspec.yaml'])) {
|
|
40
|
+
candidates.push(path.join(root, 'build', artifactDirectoryName));
|
|
41
|
+
}
|
|
42
|
+
if (hasAny(root, ['Package.swift'])) {
|
|
43
|
+
candidates.push(path.join(root, '.build', artifactDirectoryName));
|
|
44
|
+
}
|
|
45
|
+
if (hasAny(root, ['pubspec.yaml'])) {
|
|
46
|
+
candidates.push(path.join(root, '.dart_tool', artifactDirectoryName));
|
|
47
|
+
}
|
|
48
|
+
if (hasAny(root, ['package.json'])) {
|
|
49
|
+
candidates.push(path.join(root, 'node_modules', '.cache', artifactDirectoryName));
|
|
50
|
+
}
|
|
51
|
+
if (hasAny(root, ['Cargo.toml'])) {
|
|
52
|
+
candidates.push(path.join(root, 'target', artifactDirectoryName));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
candidates.push(
|
|
56
|
+
path.join(root, 'build', artifactDirectoryName),
|
|
57
|
+
path.join(root, '.build', artifactDirectoryName),
|
|
58
|
+
path.join(root, '.dart_tool', artifactDirectoryName),
|
|
59
|
+
path.join(root, 'node_modules', '.cache', artifactDirectoryName),
|
|
60
|
+
path.join(root, 'target', artifactDirectoryName),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return [...new Set(candidates)];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function hasAny(root, fileNames) {
|
|
67
|
+
return fileNames.some((fileName) => fs.existsSync(path.join(root, fileName)));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isGitIgnored(gitRoot, directory) {
|
|
71
|
+
const relative = path.relative(gitRoot, directory);
|
|
72
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
execFileSync('git', ['-C', gitRoot, 'check-ignore', '-q', '--', relative], {
|
|
77
|
+
stdio: 'ignore',
|
|
78
|
+
timeout: 2000,
|
|
79
|
+
});
|
|
80
|
+
return true;
|
|
81
|
+
} catch (_) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function gitOutput(cwd, args) {
|
|
87
|
+
try {
|
|
88
|
+
return execFileSync('git', ['-C', cwd, ...args], {
|
|
89
|
+
encoding: 'utf8',
|
|
90
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
91
|
+
timeout: 2000,
|
|
92
|
+
}).trim();
|
|
93
|
+
} catch (_) {
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function artifactTimestamp(date) {
|
|
99
|
+
const value = date instanceof Date ? date : new Date(date);
|
|
100
|
+
const pad = (number, size = 2) => String(number).padStart(size, '0');
|
|
101
|
+
return [
|
|
102
|
+
value.getUTCFullYear(),
|
|
103
|
+
pad(value.getUTCMonth() + 1),
|
|
104
|
+
pad(value.getUTCDate()),
|
|
105
|
+
'-',
|
|
106
|
+
pad(value.getUTCHours()),
|
|
107
|
+
pad(value.getUTCMinutes()),
|
|
108
|
+
pad(value.getUTCSeconds()),
|
|
109
|
+
'-',
|
|
110
|
+
pad(value.getUTCMilliseconds(), 3),
|
|
111
|
+
].join('');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function sanitizeArtifactName(value) {
|
|
115
|
+
return String(value || 'artifact').replace(/[^a-zA-Z0-9_.-]+/g, '_').replace(/^_+|_+$/g, '') || 'artifact';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function sanitizeArtifactExtension(value) {
|
|
119
|
+
return sanitizeArtifactName(String(value || 'bin').replace(/^\.+/, '')) || 'bin';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
artifactDirectoryCandidates,
|
|
124
|
+
artifactTimestamp,
|
|
125
|
+
defaultArtifactDirectory,
|
|
126
|
+
defaultArtifactPath,
|
|
127
|
+
sanitizeArtifactExtension,
|
|
128
|
+
sanitizeArtifactName,
|
|
129
|
+
};
|
package/bin/ios-provider.js
CHANGED
|
@@ -4,6 +4,7 @@ const http = require('http');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { URL } = require('url');
|
|
7
|
+
const { defaultArtifactPath } = require('./artifact-paths');
|
|
7
8
|
|
|
8
9
|
const defaultRuntimePort = 18080;
|
|
9
10
|
const runtimePortSearchCount = 50;
|
|
@@ -304,7 +305,7 @@ class IOSBridgeProvider {
|
|
|
304
305
|
const device = await this.requireDevice(args);
|
|
305
306
|
const outFile = args.outFile
|
|
306
307
|
? path.resolve(args.outFile)
|
|
307
|
-
: defaultArtifactPath('ios-screenshot', 'png', args.artifactDir);
|
|
308
|
+
: defaultArtifactPath('ios-screenshot', 'png', { artifactDir: args.artifactDir });
|
|
308
309
|
await fs.promises.mkdir(path.dirname(outFile), { recursive: true });
|
|
309
310
|
const command = [
|
|
310
311
|
'device',
|
|
@@ -1164,15 +1165,6 @@ function sleep(ms) {
|
|
|
1164
1165
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1165
1166
|
}
|
|
1166
1167
|
|
|
1167
|
-
function defaultArtifactPath(prefix, extension, artifactDir) {
|
|
1168
|
-
const dir = artifactDir ? path.resolve(artifactDir) : path.join(process.cwd(), 'build', 'ai_app_bridge_artifacts');
|
|
1169
|
-
return path.join(dir, `${prefix}-${artifactTimestamp()}.${extension}`);
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
function artifactTimestamp(date = new Date()) {
|
|
1173
|
-
return date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
1168
|
module.exports = {
|
|
1177
1169
|
IOSBridgeProvider,
|
|
1178
1170
|
formatHostForUrl,
|
package/bin/mcp-server.js
CHANGED
|
@@ -6,6 +6,7 @@ const path = require('path');
|
|
|
6
6
|
const packageInfo = require('../package.json');
|
|
7
7
|
const { IOSBridgeProvider } = require('./ios-provider');
|
|
8
8
|
const { WebBridgeProvider } = require('./web-provider');
|
|
9
|
+
const { defaultArtifactDirectory } = require('./artifact-paths');
|
|
9
10
|
const bridgeDir = __dirname;
|
|
10
11
|
const cliScript = path.join(bridgeDir, 'ai-app-bridge.js');
|
|
11
12
|
const nodeBinary = process.env.AI_APP_BRIDGE_NODE || process.execPath;
|
|
@@ -1146,7 +1147,7 @@ async function runSmoke(args) {
|
|
|
1146
1147
|
function defaultArtifactDirFor(command, args) {
|
|
1147
1148
|
if (args.outFile) return '';
|
|
1148
1149
|
if (command !== 'screenshot' && command !== 'smoke') return '';
|
|
1149
|
-
return
|
|
1150
|
+
return defaultArtifactDirectory();
|
|
1150
1151
|
}
|
|
1151
1152
|
|
|
1152
1153
|
function addCommonArgs(cliArgs, args) {
|
|
@@ -1289,6 +1290,7 @@ if (require.main === module) {
|
|
|
1289
1290
|
module.exports = {
|
|
1290
1291
|
buildBridgeCliArgs,
|
|
1291
1292
|
commandDomains,
|
|
1293
|
+
defaultArtifactDirFor,
|
|
1292
1294
|
mcpHelpText,
|
|
1293
1295
|
readNextMessage,
|
|
1294
1296
|
runBatch,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mobileaidev/ai-app-bridge",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.14",
|
|
4
4
|
"description": "Desktop CLI and MCP server for AI App Bridge across Android, iOS, Flutter, WebView, and Web targets.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"bin/ai-app-bridge.js",
|
|
20
|
+
"bin/artifact-paths.js",
|
|
20
21
|
"bin/mcp-server.js",
|
|
21
22
|
"bin/ios-provider.js",
|
|
22
23
|
"bin/web-provider.js",
|
|
@@ -30,7 +30,8 @@ AI App Bridge 支持 Android native apps、Android WebView/H5/CDP、Flutter apps
|
|
|
30
30
|
3. 选择命令路径:按任务类型选 `core`、`app`、`action`、`flutter`、`webview`、`ios`、`web`、`diagnostics` 或 `advanced` 域;不要先退回原始 `adb`、浏览器脚本或坐标猜测。
|
|
31
31
|
4. 用 `batch` 串联相关步骤:观察、操作、等待、截图、tree 验证尽量放进一次 MCP 调用。
|
|
32
32
|
5. 验证可见结果:界面变化必须用 `screenshot` 加 `tree`/`uia-tree` 交叉确认。
|
|
33
|
-
6.
|
|
33
|
+
6. 生成截图等默认产物时,让 AI Bridge 自动选择会被当前项目 git 忽略的目录;只有确实需要固定路径时才传 `outFile`/`artifactDir`,且路径必须在 `build`、`.build`、`.dart_tool`、`node_modules/.cache`、`target` 或其他已忽略目录内。
|
|
34
|
+
7. 只在需要稳定动态画面时使用 `freeze-app`/`thaw-app`;如果本轮冻结过 app,最终回复前必须解冻。
|
|
34
35
|
|
|
35
36
|
## 能力发现和调用
|
|
36
37
|
|