@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 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 `build/ai_app_bridge_artifacts` instead of reusing a stable filename
125
- or creating files in the project root.
126
- It keeps the newest 20 generated screenshots for each command prefix. Use
127
- `--artifact-dir` to choose that directory, or `--out-file` when a fixed path is
128
- intentional.
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
@@ -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
+ };
@@ -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 path.join(process.cwd(), 'build', 'ai_app_bridge_artifacts');
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.13",
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. 只在需要稳定动态画面时使用 `freeze-app`/`thaw-app`;如果本轮冻结过 app,最终回复前必须解冻。
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