mobile-automator 0.25.0 → 0.27.1

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.
Files changed (46) hide show
  1. package/README.md +13 -2
  2. package/bin/mauto-session-daemon.js +85 -16
  3. package/package.json +3 -3
  4. package/src/cli.js +754 -65
  5. package/src/device/action-catalog.js +2 -2
  6. package/src/device/bridge.js +66 -9
  7. package/src/device/connection.js +23 -4
  8. package/src/device/crash-model.js +110 -0
  9. package/src/device/device-call.js +145 -0
  10. package/src/device/device-resolver.js +2 -1
  11. package/src/device/element-model.js +37 -15
  12. package/src/device/engine-hints.js +83 -0
  13. package/src/device/failure-probe.js +276 -0
  14. package/src/device/mobile-mcp-client.js +3 -1
  15. package/src/device/mobile-mcp-tools.js +64 -0
  16. package/src/device/resolve-connection.js +8 -10
  17. package/src/device/session-daemon.js +265 -51
  18. package/src/device/session-handle.js +53 -0
  19. package/src/device/session-log.js +5 -16
  20. package/src/device/swipe-geometry.js +39 -0
  21. package/src/device/tool-args.js +3 -1
  22. package/src/guide/content/execute.agnostic.md +48 -5
  23. package/src/guide/content/execute.aware.md +42 -1
  24. package/src/guide/content/execute.invariants.md +1 -0
  25. package/src/observe/accepts.js +108 -0
  26. package/src/observe/event.js +241 -0
  27. package/src/observe/failure-capture.js +166 -0
  28. package/src/observe/flush.js +177 -0
  29. package/src/observe/paths.js +125 -0
  30. package/src/observe/recorder.js +178 -0
  31. package/src/observe/settings.js +51 -0
  32. package/src/observe/sinks/file.js +101 -0
  33. package/src/observe/sinks/stderr.js +20 -0
  34. package/src/observe/spool.js +199 -0
  35. package/src/observe/telemetry.js +143 -0
  36. package/src/observe/trace.js +174 -0
  37. package/src/observe/transport.js +169 -0
  38. package/src/observe/vocab.js +96 -0
  39. package/src/output/envelope.js +6 -0
  40. package/src/result/capability-catalog.js +14 -0
  41. package/src/result/store.js +98 -1
  42. package/src/schemas/config_schema.json +11 -0
  43. package/src/schemas/result_schema.json +55 -0
  44. package/src/setup/scaffold.js +27 -0
  45. package/src/util/log-rotate.js +34 -0
  46. package/src/workspace/discover.js +69 -0
package/README.md CHANGED
@@ -8,13 +8,13 @@ Claude Code, Cursor, Gemini CLI, GitHub Copilot, and OpenAI Agents all have one-
8
8
 
9
9
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
10
10
  [![Platform](https://img.shields.io/badge/Platform-Android%20%7C%20iOS%20%7C%20Flutter%20%7C%20RN%20%7C%20KMP%20%7C%20CMP-green.svg)](#)
11
- [![Node](https://img.shields.io/badge/Node-%E2%89%A520-brightgreen.svg)](#)
11
+ [![Node](https://img.shields.io/badge/Node-%E2%89%A522.12-brightgreen.svg)](#)
12
12
 
13
13
  ---
14
14
 
15
15
  ## ⏱️ Quick start (5 minutes)
16
16
 
17
- **You need:** Node ≥ 20 · an AI coding agent · a running emulator/simulator or a connected device.
17
+ **You need:** Node ≥ 22.12 · 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
 
@@ -114,6 +114,17 @@ In agnostic mode, OS-shaped gestures become four semantic actions — `press_bac
114
114
 
115
115
  ---
116
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
+
117
128
  ## 📋 Command reference
118
129
 
119
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
- process.on('uncaughtException', (err) => {
78
- process.stderr.write(`mauto-session-daemon: uncaught ${err && err.stack ? err.stack : err}\n`);
79
- // Tear the daemon down (closes the mobile-mcp child) then let 'exit' clean
80
- // the files.
81
- if (daemon && typeof daemon.stop === 'function') {
82
- daemon.stop().catch(() => {});
83
- }
84
- process.exit(1);
85
- });
86
- process.on('unhandledRejection', (err) => {
87
- process.stderr.write(`mauto-session-daemon: unhandled rejection ${err && err.stack ? err.stack : err}\n`);
88
- // Same teardown as uncaughtException: a rejected promise must not leave a
89
- // leaked mobile-mcp child / stale files wedging the next spawn.
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({ projectRoot, device, idleMs });
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.25.0",
3
+ "version": "0.27.1",
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": {
@@ -33,7 +33,7 @@
33
33
  "test:all": "jest"
34
34
  },
35
35
  "dependencies": {
36
- "@mobilenext/mobile-mcp": "0.0.55",
36
+ "@mobilenext/mobile-mcp": "1.0.5",
37
37
  "@modelcontextprotocol/sdk": "^1.29.0",
38
38
  "ajv": "^8.20.0",
39
39
  "commander": "^12.1.0"
@@ -45,6 +45,6 @@
45
45
  "toml": "^3.0.0"
46
46
  },
47
47
  "engines": {
48
- "node": ">=20.0.0"
48
+ "node": ">=22.12.0"
49
49
  }
50
50
  }