mobile-automator 0.24.0 → 0.26.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 +27 -4
- package/bin/mauto-session-daemon.js +85 -16
- package/package.json +4 -3
- package/src/cli.js +724 -59
- package/src/device/bridge.js +30 -0
- package/src/device/connection.js +23 -4
- package/src/device/crash-model.js +110 -0
- package/src/device/device-call.js +145 -0
- package/src/device/failure-probe.js +276 -0
- package/src/device/mobile-mcp-tools.js +64 -0
- package/src/device/resolve-connection.js +8 -10
- package/src/device/session-daemon.js +265 -51
- package/src/device/session-handle.js +53 -0
- package/src/device/session-log.js +5 -16
- package/src/guide/content/execute.agnostic.md +48 -5
- package/src/guide/content/execute.aware.md +42 -1
- package/src/guide/content/execute.invariants.md +1 -0
- package/src/observe/accepts.js +108 -0
- package/src/observe/event.js +241 -0
- package/src/observe/failure-capture.js +166 -0
- package/src/observe/flush.js +177 -0
- package/src/observe/paths.js +125 -0
- package/src/observe/recorder.js +178 -0
- package/src/observe/settings.js +51 -0
- package/src/observe/sinks/file.js +101 -0
- package/src/observe/sinks/stderr.js +20 -0
- package/src/observe/spool.js +199 -0
- package/src/observe/telemetry.js +143 -0
- package/src/observe/trace.js +174 -0
- package/src/observe/transport.js +169 -0
- package/src/observe/vocab.js +96 -0
- package/src/output/envelope.js +6 -0
- package/src/result/capability-catalog.js +14 -0
- package/src/result/store.js +98 -1
- package/src/schemas/config_schema.json +11 -0
- package/src/schemas/result_schema.json +55 -0
- package/src/setup/scaffold.js +27 -0
- package/src/util/log-rotate.js +34 -0
- package/src/workspace/discover.js +69 -0
package/README.md
CHANGED
|
@@ -8,24 +8,36 @@ Claude Code, Cursor, Gemini CLI, GitHub Copilot, and OpenAI Agents all have one-
|
|
|
8
8
|
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
[](#)
|
|
11
|
-
[](#)
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
## ⏱️ Quick start (5 minutes)
|
|
16
16
|
|
|
17
|
-
**You need:** Node ≥
|
|
17
|
+
**You need:** Node ≥ 20 · an AI coding agent · a running emulator/simulator or a connected device.
|
|
18
18
|
|
|
19
19
|
> This quick start uses **Claude Code** as the example agent. For Cursor, swap `--agent claude` → `--agent cursor`. For any other agent, see [Using another agent](#-using-another-agent).
|
|
20
20
|
|
|
21
|
-
**1. Install `mauto
|
|
21
|
+
**1. Install `mauto`:**
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
+
npm i -g mobile-automator # exposes `mauto` globally
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
<details>
|
|
28
|
+
<summary>Or run it without installing / build from source</summary>
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx mobile-automator <verb> # no global install
|
|
32
|
+
|
|
33
|
+
# from source, to run unreleased changes:
|
|
24
34
|
git clone https://github.com/sh3lan93/mobile-automator
|
|
25
35
|
cd mobile-automator
|
|
26
|
-
npm install && npm link
|
|
36
|
+
npm install && npm link
|
|
27
37
|
```
|
|
28
38
|
|
|
39
|
+
</details>
|
|
40
|
+
|
|
29
41
|
**2. Wire it into your project and agent:**
|
|
30
42
|
|
|
31
43
|
```bash
|
|
@@ -102,6 +114,17 @@ In agnostic mode, OS-shaped gestures become four semantic actions — `press_bac
|
|
|
102
114
|
|
|
103
115
|
---
|
|
104
116
|
|
|
117
|
+
### Telemetry
|
|
118
|
+
|
|
119
|
+
`mauto` collects nothing by default. Anonymous usage telemetry is opt-in via
|
|
120
|
+
`mauto telemetry enable`, sends no free text of any kind, and carries no
|
|
121
|
+
per-machine identifier.
|
|
122
|
+
`MAUTO_TELEMETRY=0` and `DO_NOT_TRACK=1` force it off. Run `mauto telemetry
|
|
123
|
+
status` for the exact field list, or read
|
|
124
|
+
[Telemetry & Privacy](https://sh3lan93.github.io/mobile-automator/reference/telemetry/).
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
105
128
|
## 📋 Command reference
|
|
106
129
|
|
|
107
130
|
Most low-level verbs are called **by the agent**, not by you. The ones you'll run directly are at the top.
|
|
@@ -12,6 +12,9 @@ const fs = require('fs');
|
|
|
12
12
|
|
|
13
13
|
const { startDaemon } = require('../src/device/session-daemon');
|
|
14
14
|
const paths = require('../src/device/session-paths');
|
|
15
|
+
const { boundRecorder } = require('../src/observe/recorder');
|
|
16
|
+
const { daemonEventLogPath } = require('../src/observe/paths');
|
|
17
|
+
const { makeFlusher } = require('../src/observe/flush');
|
|
15
18
|
|
|
16
19
|
// Best-effort synchronous unlink — never throws. Used by the crash guards so a
|
|
17
20
|
// hard failure can't leave a stale lock/socket/pidfile wedging the next spawn.
|
|
@@ -64,6 +67,39 @@ async function main() {
|
|
|
64
67
|
const idleRaw = process.env.MAUTO_SESSION_IDLE_MS;
|
|
65
68
|
const idleMs = idleRaw ? Number(idleRaw) : undefined;
|
|
66
69
|
|
|
70
|
+
// Where this process's events go: its own log file (daemon.ndjson, not the
|
|
71
|
+
// CLI's mauto.ndjson — see observe/paths.js). `env` is read once and the log
|
|
72
|
+
// path derived from that same object, so the sinks and the path can never
|
|
73
|
+
// disagree about MAUTO_LOG_DIR. See boundRecorder for why record()'s defaults
|
|
74
|
+
// are wrong for a detached process, and for why a construction failure
|
|
75
|
+
// degrades rather than throwing.
|
|
76
|
+
//
|
|
77
|
+
// The stderr sink is deliberately left in place: the daemon's stderr IS
|
|
78
|
+
// mobile-automator/.session/daemon.log (PR #176), not a terminal, which
|
|
79
|
+
// inverts cli.js finish()'s calculus — a warn line here costs a human no
|
|
80
|
+
// terminal noise and lands next to the adb/simctl output that explains it.
|
|
81
|
+
//
|
|
82
|
+
// A FACTORY, not a recorder. session_id belongs to startDaemon — it mints the
|
|
83
|
+
// id, writes it to the handle and binds it — so this file cannot build a
|
|
84
|
+
// session-bound recorder, and must not try: an id minted here and one written
|
|
85
|
+
// there are two owners that nothing forces to agree. This file owns WHERE the
|
|
86
|
+
// events go; startDaemon owns WHOSE they are.
|
|
87
|
+
const env = process.env;
|
|
88
|
+
const logPath = daemonEventLogPath(projectRoot, env);
|
|
89
|
+
const recorderFor = (fields) => boundRecorder({ projectRoot, env, logPath, fields });
|
|
90
|
+
|
|
91
|
+
// Two bindings of that one factory, because the crash guards outlive the
|
|
92
|
+
// question "is there a session yet?".
|
|
93
|
+
//
|
|
94
|
+
// Until startDaemon resolves there is no session to name, so this one carries
|
|
95
|
+
// the process identity alone. Nothing is lost by that: a crash before
|
|
96
|
+
// startDaemon resolves is by definition a daemon that never wrote a handle,
|
|
97
|
+
// so a session id would name nothing a reader could join against, and `pid`
|
|
98
|
+
// already groups those events. The moment the daemon exists we adopt ITS
|
|
99
|
+
// recorder (below), so every later crash — the common case — is stamped with
|
|
100
|
+
// the session whose handle is on disk.
|
|
101
|
+
let observe = recorderFor({ src: 'daemon', pid: process.pid });
|
|
102
|
+
|
|
67
103
|
let daemon = null;
|
|
68
104
|
|
|
69
105
|
// Crash guards: this is the real, single-daemon process, so a best-effort
|
|
@@ -74,26 +110,59 @@ async function main() {
|
|
|
74
110
|
// the next spawn isn't wedged by leftovers from this process.
|
|
75
111
|
const onExit = () => cleanupWorkspaceIfOwned(projectRoot);
|
|
76
112
|
process.on('exit', onExit);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
113
|
+
// One fatal handler, two registrations. The two crashes differ by exactly two
|
|
114
|
+
// strings; everything else about them is load-bearing and identical, so they
|
|
115
|
+
// are built from one function rather than kept in sync by hand.
|
|
116
|
+
//
|
|
117
|
+
// Recorded FIRST, before the stderr write and before teardown: these are the
|
|
118
|
+
// invisible deaths #156 is about, and process.exit(1) below is immediate. The
|
|
119
|
+
// file sink appends synchronously, so the line is on disk by the time the
|
|
120
|
+
// process is gone — a buffered sink would lose exactly this event.
|
|
121
|
+
//
|
|
122
|
+
// Two writes on purpose, to two different readers. The structured event
|
|
123
|
+
// carries the classification (and stays one line); the raw stderr write is
|
|
124
|
+
// #176's contract — the full stack, unchanged, in .session/daemon.log.
|
|
125
|
+
//
|
|
126
|
+
// Then tear the daemon down (closing the mobile-mcp child) and let 'exit'
|
|
127
|
+
// clean the files. Recording must not delay or displace the teardown that
|
|
128
|
+
// frees the lock/socket/pidfile, and `daemon` may still be null — a crash can
|
|
129
|
+
// beat startDaemon to it.
|
|
130
|
+
const onFatal = (kind, label) => (err) => {
|
|
131
|
+
observe({
|
|
132
|
+
level: 'error',
|
|
133
|
+
event: 'daemon.crash',
|
|
134
|
+
error_code: err && err.code,
|
|
135
|
+
message: `${kind}: ${err && err.message ? err.message : err}`,
|
|
136
|
+
});
|
|
137
|
+
process.stderr.write(`mauto-session-daemon: ${label} ${err && err.stack ? err.stack : err}\n`);
|
|
90
138
|
if (daemon && typeof daemon.stop === 'function') {
|
|
91
|
-
daemon.stop().catch(() => {});
|
|
139
|
+
daemon.stop('crash').catch(() => {});
|
|
92
140
|
}
|
|
93
141
|
process.exit(1);
|
|
94
|
-
}
|
|
142
|
+
};
|
|
143
|
+
process.on('uncaughtException', onFatal('uncaughtException', 'uncaught'));
|
|
144
|
+
process.on('unhandledRejection', onFatal('unhandledRejection', 'unhandled rejection'));
|
|
95
145
|
|
|
96
|
-
daemon = await startDaemon({
|
|
146
|
+
daemon = await startDaemon({
|
|
147
|
+
projectRoot,
|
|
148
|
+
device,
|
|
149
|
+
idleMs,
|
|
150
|
+
recorderFor,
|
|
151
|
+
// Same defensive shape as recorderFor's own construction guarantee: a
|
|
152
|
+
// telemetry fault must never be the reason a daemon fails to start.
|
|
153
|
+
flushFor: ({ observe: sessionObserve }) => {
|
|
154
|
+
try {
|
|
155
|
+
return makeFlusher({ projectRoot, env: process.env, observe: sessionObserve });
|
|
156
|
+
} catch (_) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
// Adopt the daemon's own recorder: same log file, now bound to the session id
|
|
162
|
+
// it minted and wrote into the handle. onFatal reads `observe` when it fires,
|
|
163
|
+
// not when it was built, so every crash from here on is joinable to that
|
|
164
|
+
// handle. This is a READ of startDaemon's id, never a second mint of one.
|
|
165
|
+
observe = daemon.observe;
|
|
97
166
|
// Keep the event loop alive until the daemon stops (idle reap / signal /
|
|
98
167
|
// shutdown frame), then exit cleanly.
|
|
99
168
|
await daemon.whenStopped;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobile-automator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Host-agnostic mauto CLI for AI-driven mobile QA automation — analyzes a mobile project and drives devices through mobile-mcp.",
|
|
5
5
|
"homepage": "https://github.com/sh3lan93/mobile-automator#readme",
|
|
6
6
|
"repository": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"prepublishOnly": "npm test",
|
|
27
27
|
"test:watch": "jest --watch",
|
|
28
28
|
"test:coverage": "jest --coverage",
|
|
29
|
-
"lint:guides": "jest tests/lint
|
|
29
|
+
"lint:guides": "jest tests/lint",
|
|
30
30
|
"lint:schema-additive": "jest tests/lint/schema-additive.test.js",
|
|
31
31
|
"test:unit": "jest tests/unit",
|
|
32
32
|
"test:integration": "jest tests/integration",
|
|
@@ -41,9 +41,10 @@
|
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"jest": "^29.7.0",
|
|
43
43
|
"jest-environment-jsdom": "^29.7.0",
|
|
44
|
+
"js-yaml": "^4.3.2",
|
|
44
45
|
"toml": "^3.0.0"
|
|
45
46
|
},
|
|
46
47
|
"engines": {
|
|
47
|
-
"node": ">=
|
|
48
|
+
"node": ">=20.0.0"
|
|
48
49
|
}
|
|
49
50
|
}
|