android-agent-bridge 0.1.2 → 0.1.4

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
@@ -16,7 +16,13 @@ One `npx` command on the computer, one QR scan on the phone.
16
16
  [![MCP](https://img.shields.io/badge/MCP-27%20tools%20%C2%B7%206%20prompts-3ddc97)](#for-agents)
17
17
  [![License: MIT](https://img.shields.io/badge/license-MIT-3ddc97)](LICENSE)
18
18
 
19
- [**Website**](https://rudrasamadhiya.github.io/android-agent-bridge/) · [**Get started**](https://rudrasamadhiya.github.io/android-agent-bridge/start/) · [**Docs**](https://rudrasamadhiya.github.io/android-agent-bridge/docs/) · [**Download the app**](https://github.com/Rudrasamadhiya/android-agent-bridge/releases/latest/download/agent-bridge.apk)
19
+ [**Website**](https://rudrasamadhiya.github.io/android-agent-bridge/) · [**Get started**](https://rudrasamadhiya.github.io/android-agent-bridge/start/) · [**Docs**](https://rudrasamadhiya.github.io/android-agent-bridge/docs/) · [**Install the app**](https://rudrasamadhiya.github.io/android-agent-bridge/download/#install)
20
+
21
+ <br/>
22
+
23
+ <a href="https://rudrasamadhiya.github.io/android-agent-bridge/video/"><img src="https://raw.githubusercontent.com/Rudrasamadhiya/android-agent-bridge/main/docs/video/preview.gif" width="720" alt="Agent Bridge in 6 seconds: an agent taps, types and scrolls a real phone. Click to watch the full film." /></a>
24
+
25
+ **[▶ Watch the film (1:30)](https://rudrasamadhiya.github.io/android-agent-bridge/video/)**
20
26
 
21
27
  <br/>
22
28
 
@@ -45,7 +51,9 @@ One `npx` command on the computer, one QR scan on the phone.
45
51
  npx android-agent-bridge usb
46
52
  ```
47
53
 
48
- It installs the app, switches on its accessibility service, pairs and starts the bridge.
54
+ It installs the app, switches on its accessibility service, pairs and starts the bridge. If you don't have Android's adb tool, it downloads it from Google the first time.
55
+
56
+ > **No cable?** On Android 11+, turn on *Wireless debugging* in Developer options and run `npx android-agent-bridge wifi`, then scan the QR code from *Pair device with QR code*.
49
57
 
50
58
  **2. Connect your agent.** For Claude Code:
51
59
 
@@ -72,7 +80,7 @@ npx android-agent-bridge start
72
80
 
73
81
  In the app: **Scan QR code** → point it at the code in your terminal (or the bigger one at `localhost:8350/pair`) → turn on the **accessibility service**. Phone and computer must be on the same Wi-Fi.
74
82
 
75
- If Play Protect says *"App blocked to protect your device"*, install over USB with `usb` instead. Installs over USB aren't blocked.
83
+ If Play Protect says *"App blocked to protect your device"*, install from your computer instead with `usb` or `wifi`. Play Protect blocks sideloaded accessibility apps opened from a browser or chat app; installs through adb aren't affected.
76
84
 
77
85
  </details>
78
86
 
@@ -206,7 +214,7 @@ Apps that block screen capture (banking, streaming) show up black in screenshots
206
214
 
207
215
  <br/>
208
216
 
209
- - **"App blocked to protect your device":** install over USB with `npx android-agent-bridge usb`.
217
+ - **"App blocked to protect your device":** install from your computer with `npx android-agent-bridge usb`, or `npx android-agent-bridge wifi` (Android 11+, no cable).
210
218
  - **No phone connected:** is `start` running? Same Wi-Fi? On Windows, allow Node.js through the firewall. Over USB, run `usb` again.
211
219
  - **Accessibility switch greyed out** (Android 13+): App info → ⋮ → Allow restricted settings.
212
220
  - **A tap opened nothing right after Home:** Android holds app starts for about 5 seconds after Home; run it again.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "android-agent-bridge",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Give AI agents (Claude Code, Cursor, Antigravity) your whole Android phone: screenshots with numbered elements and a tap grid, tap, type, scroll, open apps. Pair once by QR over Wi-Fi or plug in USB. Android 5.0+.",
5
5
  "keywords": [
6
6
  "android",
package/server/adb.mjs CHANGED
@@ -2,9 +2,13 @@
2
2
  // fallback for what the app cannot do on old Android (taps by coordinate
3
3
  // before Android 7, Enter key before Android 11, screenshots without capture).
4
4
  import { execFile } from 'node:child_process';
5
- import { existsSync } from 'node:fs';
5
+ import { chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
6
+ import os from 'node:os';
6
7
  import path from 'node:path';
7
8
 
9
+ // Where platform-tools is downloaded when the computer has no Android SDK.
10
+ const OWN_TOOLS = path.join(os.homedir(), '.android-agent-bridge', 'platform-tools');
11
+
8
12
  export const KEYCODES = {
9
13
  back: 4, home: 3, recents: 187, enter: 66, tab: 61, delete: 67, escape: 111,
10
14
  volume_up: 24, volume_down: 25, mute: 164, power: 26, menu: 82, search: 84,
@@ -18,13 +22,42 @@ export function adbPath() {
18
22
  process.env.HOME && path.join(process.env.HOME, 'Android', 'Sdk')]) {
19
23
  if (root && existsSync(path.join(root, 'platform-tools', exe))) return path.join(root, 'platform-tools', exe);
20
24
  }
25
+ if (existsSync(path.join(OWN_TOOLS, exe))) return path.join(OWN_TOOLS, exe);
21
26
  return 'adb';
22
27
  }
23
28
 
29
+ /**
30
+ * Makes sure adb exists. Without an Android SDK or adb on PATH, downloads Google's
31
+ * platform-tools (about 7 MB) into ~/.android-agent-bridge once.
32
+ */
33
+ export async function ensureAdb(log = console.log) {
34
+ try { await adb(['version'], { serial: null }); return; } catch (e) { if (!/adb not found/.test(e.message)) return; }
35
+ const plat = process.platform === 'win32' ? 'windows' : process.platform === 'darwin' ? 'darwin' : 'linux';
36
+ const url = `https://dl.google.com/android/repository/platform-tools-latest-${plat}.zip`;
37
+ log('adb (Android platform-tools) not found; downloading it from Google, one time only…');
38
+ const res = await fetch(url);
39
+ if (!res.ok) throw new Error(`Couldn't download platform-tools (${res.status}). Install it yourself: https://developer.android.com/tools/releases/platform-tools`);
40
+ const base = path.dirname(OWN_TOOLS);
41
+ mkdirSync(base, { recursive: true });
42
+ const zip = path.join(base, 'platform-tools.zip');
43
+ writeFileSync(zip, Buffer.from(await res.arrayBuffer()));
44
+ rmSync(OWN_TOOLS, { recursive: true, force: true });
45
+ await new Promise((resolve, reject) => {
46
+ const [bin, args] = process.platform === 'win32'
47
+ ? ['powershell', ['-NoProfile', '-Command', `Expand-Archive -LiteralPath '${zip}' -DestinationPath '${base}' -Force`]]
48
+ : ['unzip', ['-q', '-o', zip, '-d', base]];
49
+ execFile(bin, args, { windowsHide: true, timeout: 120000 }, (err) => (err ? reject(new Error('Couldn\'t unpack platform-tools: ' + err.message)) : resolve()));
50
+ });
51
+ rmSync(zip, { force: true });
52
+ if (process.platform !== 'win32') chmodSync(path.join(OWN_TOOLS, 'adb'), 0o755);
53
+ log('✔ adb ready');
54
+ }
55
+
24
56
  export function adb(args, { timeout = 15000, binary = false, serial = process.env.ANDROID_SERIAL } = {}) {
25
57
  const full = serial ? ['-s', serial, ...args] : args;
26
58
  return new Promise((resolve, reject) => {
27
- execFile(adbPath(), full, { timeout, windowsHide: true, encoding: binary ? 'buffer' : 'utf8', maxBuffer: 64 * 1024 * 1024 }, (err, stdout, stderr) => {
59
+ // ADB_MDNS_OPENSCREEN lets adb find phones over Wi-Fi (wireless debugging) on every platform
60
+ execFile(adbPath(), full, { timeout, windowsHide: true, encoding: binary ? 'buffer' : 'utf8', maxBuffer: 64 * 1024 * 1024, env: { ...process.env, ADB_MDNS_OPENSCREEN: '1' } }, (err, stdout, stderr) => {
28
61
  if (err) {
29
62
  reject(new Error(err.code === 'ENOENT'
30
63
  ? 'adb not found. Install Android platform-tools (https://developer.android.com/tools/releases/platform-tools) or set ANDROID_HOME.'
package/server/cli.mjs CHANGED
@@ -3,6 +3,7 @@
3
3
  // (no command) MCP server, launched by AI agents
4
4
  // start run the bridge and show the pairing QR code
5
5
  // usb set up a USB-connected phone (install, permissions, pair)
6
+ // wifi same, without a cable (Android 11+ wireless debugging)
6
7
  // init add the MCP server + agent guide to the current project
7
8
  // status | screen | elements | tap | type | scroll | swipe | key | open | apps | wait
8
9
  import fs from 'node:fs';
@@ -11,7 +12,8 @@ import { fileURLToPath } from 'node:url';
11
12
  import { Bridge } from './bridge.mjs';
12
13
  import { loadConfig, resetToken, APP_ID } from './config.mjs';
13
14
  import { lanAddresses, pairLink, qrTerminal } from './hub.mjs';
14
- import { adb, devices, pickSerial } from './adb.mjs';
15
+ import { adb, devices, ensureAdb, pickSerial } from './adb.mjs';
16
+ import readline from 'node:readline/promises';
15
17
  import { fmtSize } from './files.mjs';
16
18
 
17
19
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -37,6 +39,7 @@ const HELP = `android-agent-bridge — let AI agents use your Android phone
37
39
  Setup
38
40
  start run the bridge and show the QR code to pair over Wi-Fi
39
41
  usb set up a phone plugged in over USB (install app, permissions, pair)
42
+ wifi [--code] same without a cable: Android 11+ wireless debugging, pair by QR (--code: pairing code)
40
43
  init add the MCP server and agent guide to this project
41
44
  pair [--reset] show the QR code again (--reset: new secret, phones must re-scan)
42
45
  (no command) MCP server (what Claude Code / Cursor / Antigravity launch)
@@ -256,12 +259,92 @@ function findApk() {
256
259
  }
257
260
 
258
261
  async function usb() {
259
- const { flags } = parseArgs(rest);
260
- const list = await devices().catch((e) => die(e.message));
262
+ await ensureAdb().catch((e) => die(e.message));
263
+ // a freshly started adb takes a moment to see a phone, so look a few times before giving up
264
+ let list = [];
265
+ for (let i = 0; i < 8 && !list.length; i++) {
266
+ list = await devices().catch((e) => die(e.message));
267
+ if (list.length) break;
268
+ const raw = await adb(['devices'], { serial: null }).catch(() => '');
269
+ if (/\tunauthorized/.test(raw)) {
270
+ if (i === 0) console.log('Phone found. On the phone, tap "Allow" on the "Allow USB debugging?" prompt (tick "Always allow").');
271
+ } else if (i >= 3) break;
272
+ await wait(1500);
273
+ }
261
274
  if (!list.length) {
262
- die('No phone found over USB.\n1. Settings → About phone → tap "Build number" 7 times (developer mode).\n2. Settings → Developer options → turn on USB debugging.\n3. Plug the phone in and accept the "Allow USB debugging?" prompt.');
275
+ die('No phone found over USB.\n1. Settings → About phone → tap "Build number" 7 times (developer mode).\n2. Settings → Developer options → turn on USB debugging.\n3. Plug the phone in and accept the "Allow USB debugging?" prompt.\n\nNo cable? Android 11+ can do this over Wi-Fi: npx android-agent-bridge wifi');
263
276
  }
264
- const serial = await pickSerial();
277
+ await setUp(await pickSerial());
278
+ }
279
+
280
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
281
+
282
+ /** Wireless-debugging services the phone announces on the network: [{ name, type, addr }]. */
283
+ async function mdnsServices() {
284
+ const out = await adb(['mdns', 'services'], { serial: null }).catch(() => '');
285
+ return out.split('\n').map((l) => l.trim().split(/\s+/)).filter((p) => p.length >= 3 && p[1].startsWith('_adb-tls'))
286
+ .map(([name, type, addr]) => ({ name, type, addr }));
287
+ }
288
+
289
+ /**
290
+ * Same as `usb`, but without a cable: Android 11+ "Wireless debugging". The phone scans a QR code
291
+ * from this terminal (or types a pairing code), then adb installs and sets up the app over Wi-Fi.
292
+ * Installs through adb are not blocked by Play Protect.
293
+ */
294
+ async function wifi() {
295
+ const { flags } = parseArgs(rest);
296
+ await ensureAdb().catch((e) => die(e.message));
297
+ console.log('\nInstall Agent Bridge over Wi-Fi (Android 11 or newer, phone on the same Wi-Fi as this computer)\n');
298
+ console.log('On the phone:');
299
+ console.log(' 1. Settings → About phone → tap "Build number" 7 times (once, turns on developer options)');
300
+ console.log(' 2. Settings → Developer options → Wireless debugging → turn it on');
301
+
302
+ let paired = false;
303
+ if (!flags.code) {
304
+ const name = 'agentbridge-' + Math.random().toString(36).slice(2, 7);
305
+ const pass = String(Math.floor(100000 + Math.random() * 900000));
306
+ console.log(' 3. Tap "Pair device with QR code" and scan this:\n');
307
+ console.log(await qrTerminal(`WIFI:T:ADB;S:${name};P:${pass};;`));
308
+ console.log('Waiting for the phone… (no QR option? Ctrl+C and run: npx android-agent-bridge wifi --code)');
309
+ for (let i = 0; i < 180 && !paired; i++) {
310
+ const svc = (await mdnsServices()).find((s) => s.name === name && s.type.startsWith('_adb-tls-pairing'));
311
+ if (svc) {
312
+ const out = await adb(['pair', svc.addr, pass], { serial: null, timeout: 30000 }).catch((e) => e.message);
313
+ if (!/Successfully paired/i.test(out)) die('Pairing failed: ' + out);
314
+ paired = true;
315
+ } else await wait(1000);
316
+ }
317
+ if (!paired) die('The phone did not show up. Check both are on the same Wi-Fi, or try: npx android-agent-bridge wifi --code');
318
+ } else {
319
+ console.log(' 3. Tap "Pair device with pairing code"\n');
320
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
321
+ const addr = (await rl.question('IP address & port shown in that dialog (e.g. 192.168.1.5:37000): ')).trim();
322
+ const code = (await rl.question('Wi-Fi pairing code: ')).trim();
323
+ rl.close();
324
+ const out = await adb(['pair', addr, code], { serial: null, timeout: 30000 }).catch((e) => e.message);
325
+ if (!/Successfully paired/i.test(out)) die('Pairing failed: ' + out);
326
+ }
327
+ console.log('✔ Paired with the phone for wireless debugging');
328
+
329
+ // after pairing the phone announces a connect service; adb usually connects to it by itself
330
+ let serial = null;
331
+ for (let i = 0; i < 30 && !serial; i++) {
332
+ const list = await devices().catch(() => []);
333
+ const wireless = list.find((d) => d.serial.includes('_adb-tls-connect') || /:\d+$/.test(d.serial));
334
+ if (wireless) { serial = wireless.serial; break; }
335
+ for (const s of (await mdnsServices()).filter((x) => x.type.startsWith('_adb-tls-connect'))) {
336
+ await adb(['connect', s.addr], { serial: null }).catch(() => {});
337
+ }
338
+ await wait(1000);
339
+ }
340
+ if (!serial) die('Paired, but could not connect. On the phone, check Wireless debugging is still on and run this again.');
341
+ console.log('✔ Connected over Wi-Fi');
342
+ await setUp(serial);
343
+ }
344
+
345
+ /** Installs the app if needed, switches on its accessibility service, pairs it and runs the bridge. */
346
+ async function setUp(serial) {
347
+ const { flags } = parseArgs(rest);
265
348
  const sh = (...c) => adb(['shell', ...c], { serial });
266
349
  const cfg = loadConfig();
267
350
  const sdk = Number(await sh('getprop', 'ro.build.version.sdk'));
@@ -348,6 +431,7 @@ try {
348
431
  } else if (cmd === 'help' || cmd === '--help' || cmd === '-h') console.log(HELP);
349
432
  else if (cmd === 'start') await start();
350
433
  else if (cmd === 'usb') await usb();
434
+ else if (cmd === 'wifi') await wifi();
351
435
  else if (cmd === 'init') init();
352
436
  else if (cmd === 'pair') {
353
437
  const cfg = parseArgs(rest).flags.reset ? resetToken() : loadConfig();