appduct 0.9.0 → 0.11.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.
Files changed (230) hide show
  1. package/README.md +122 -52
  2. package/dist/artifact-inspect.d.ts +5 -4
  3. package/dist/bin.d.ts +0 -1
  4. package/dist/bin.js +1250 -2
  5. package/dist/cli/command-options.d.ts +8 -1
  6. package/dist/cli/create-cli.d.ts +0 -1
  7. package/dist/cli/dispatch.d.ts +10 -1
  8. package/dist/cli/envelope.d.ts +19 -0
  9. package/dist/cli/global-flags.d.ts +30 -0
  10. package/dist/cli/open-target.d.ts +25 -8
  11. package/dist/cli/result-types.d.ts +30 -5
  12. package/dist/cli/router.d.ts +78 -0
  13. package/dist/cli/routes/daemon/index.d.ts +9 -0
  14. package/dist/cli/routes/daemon/index.js +848 -0
  15. package/dist/cli/routes/daemon/run.d.ts +3 -0
  16. package/dist/cli/routes/daemon/run.js +3246 -0
  17. package/dist/cli/routes/daemon/start.d.ts +3 -0
  18. package/dist/cli/routes/daemon/start.js +1668 -0
  19. package/dist/cli/routes/daemon/status.d.ts +4 -0
  20. package/dist/cli/routes/daemon/status.js +1676 -0
  21. package/dist/cli/routes/daemon/stop.d.ts +4 -0
  22. package/dist/cli/routes/daemon/stop.js +1646 -0
  23. package/dist/cli/routes/doctor.d.ts +3 -0
  24. package/dist/cli/routes/doctor.js +1075 -0
  25. package/dist/cli/routes/events.d.ts +3 -0
  26. package/dist/cli/routes/events.js +1906 -0
  27. package/dist/cli/routes/init.d.ts +3 -0
  28. package/dist/cli/routes/init.js +1486 -0
  29. package/dist/cli/routes/invoke.d.ts +3 -0
  30. package/dist/cli/routes/invoke.js +1884 -0
  31. package/dist/cli/routes/keygen.d.ts +3 -0
  32. package/dist/cli/routes/keygen.js +928 -0
  33. package/dist/cli/routes/link.d.ts +3 -0
  34. package/dist/cli/routes/link.js +2705 -0
  35. package/dist/cli/routes/ls.d.ts +3 -0
  36. package/dist/cli/routes/ls.js +1659 -0
  37. package/dist/cli/routes/mcp.d.ts +5 -0
  38. package/dist/cli/routes/mcp.js +3917 -0
  39. package/dist/cli/routes/revoke.d.ts +3 -0
  40. package/dist/cli/routes/revoke.js +1675 -0
  41. package/dist/cli/routes/tools.d.ts +3 -0
  42. package/dist/cli/routes/tools.js +1851 -0
  43. package/dist/cli/runner.d.ts +3 -11
  44. package/dist/cli/types.d.ts +9 -6
  45. package/dist/cli/version-guard.d.ts +20 -0
  46. package/dist/cli.d.ts +0 -1
  47. package/dist/client/app-client.d.ts +0 -1
  48. package/dist/client/bootstrap.d.ts +4 -4
  49. package/dist/client/connect.d.ts +0 -1
  50. package/dist/client/errors.d.ts +0 -1
  51. package/dist/client/index.d.ts +0 -1
  52. package/dist/client/index.js +2379 -16
  53. package/dist/commands/daemon/context.d.ts +14 -0
  54. package/dist/commands/daemon/run.d.ts +11 -0
  55. package/dist/commands/daemon/start.d.ts +5 -0
  56. package/dist/commands/daemon/status.d.ts +5 -0
  57. package/dist/commands/daemon/stop.d.ts +5 -0
  58. package/dist/commands/doctor.d.ts +0 -1
  59. package/dist/commands/events.d.ts +0 -1
  60. package/dist/commands/init.d.ts +17 -4
  61. package/dist/commands/invoke.d.ts +0 -1
  62. package/dist/commands/keygen.d.ts +0 -1
  63. package/dist/commands/link.d.ts +1 -2
  64. package/dist/commands/ls.d.ts +0 -1
  65. package/dist/commands/mcp.d.ts +2 -4
  66. package/dist/commands/revoke.d.ts +0 -1
  67. package/dist/commands/tools.d.ts +19 -3
  68. package/dist/daemon/address.d.ts +0 -1
  69. package/dist/daemon/audit.d.ts +6 -11
  70. package/dist/daemon/calls.d.ts +0 -1
  71. package/dist/daemon/config.d.ts +13 -16
  72. package/dist/daemon/daemon.d.ts +0 -1
  73. package/dist/daemon/event-bus.d.ts +0 -1
  74. package/dist/daemon/links.d.ts +0 -1
  75. package/dist/daemon/listener.d.ts +0 -1
  76. package/dist/daemon/log-rotation.d.ts +0 -1
  77. package/dist/daemon/pidfile.d.ts +14 -8
  78. package/dist/daemon/policy.d.ts +0 -1
  79. package/dist/daemon/registry.d.ts +0 -1
  80. package/dist/daemon/rpc-errors.d.ts +16 -0
  81. package/dist/daemon/rpc-server.d.ts +0 -11
  82. package/dist/daemon/sessions.d.ts +0 -1
  83. package/dist/daemon/socket-probe.d.ts +0 -1
  84. package/dist/daemon/state-dir.d.ts +0 -1
  85. package/dist/daemon/timers.d.ts +0 -1
  86. package/dist/daemon/tls.d.ts +0 -1
  87. package/dist/errors.d.ts +0 -1
  88. package/dist/host-certificate.d.ts +0 -1
  89. package/dist/index.d.ts +0 -1
  90. package/dist/index.js +1307 -3
  91. package/dist/key-material.d.ts +0 -1
  92. package/dist/link.d.ts +8 -7
  93. package/dist/mcp/app-tools.d.ts +153 -0
  94. package/dist/mcp/connect-tool.d.ts +14 -8
  95. package/dist/mcp/daemon-tools.d.ts +2 -11
  96. package/dist/mcp/events-tool.d.ts +0 -1
  97. package/dist/mcp/server.d.ts +33 -14
  98. package/dist/native-scheme.d.ts +0 -1
  99. package/dist/output.d.ts +8 -11
  100. package/dist/package-root.d.ts +11 -0
  101. package/dist/package-version.d.ts +0 -1
  102. package/dist/qr-terminal.d.ts +0 -1
  103. package/dist/rpc/client.d.ts +0 -1
  104. package/dist/scheme.d.ts +60 -2
  105. package/dist/spki-pin.d.ts +0 -1
  106. package/package.json +4 -3
  107. package/dist/artifact-inspect.d.ts.map +0 -1
  108. package/dist/artifact-inspect.js +0 -315
  109. package/dist/bin.d.ts.map +0 -1
  110. package/dist/cli/command-options.d.ts.map +0 -1
  111. package/dist/cli/command-options.js +0 -95
  112. package/dist/cli/create-cli.d.ts.map +0 -1
  113. package/dist/cli/create-cli.js +0 -56
  114. package/dist/cli/dispatch.d.ts.map +0 -1
  115. package/dist/cli/dispatch.js +0 -275
  116. package/dist/cli/open-target.d.ts.map +0 -1
  117. package/dist/cli/open-target.js +0 -438
  118. package/dist/cli/result-types.d.ts.map +0 -1
  119. package/dist/cli/result-types.js +0 -6
  120. package/dist/cli/runner.d.ts.map +0 -1
  121. package/dist/cli/runner.js +0 -111
  122. package/dist/cli/types.d.ts.map +0 -1
  123. package/dist/cli/types.js +0 -10
  124. package/dist/cli.d.ts.map +0 -1
  125. package/dist/cli.js +0 -2
  126. package/dist/client/app-client.d.ts.map +0 -1
  127. package/dist/client/app-client.js +0 -223
  128. package/dist/client/bootstrap.d.ts.map +0 -1
  129. package/dist/client/bootstrap.js +0 -108
  130. package/dist/client/connect.d.ts.map +0 -1
  131. package/dist/client/connect.js +0 -38
  132. package/dist/client/errors.d.ts.map +0 -1
  133. package/dist/client/errors.js +0 -40
  134. package/dist/client/index.d.ts.map +0 -1
  135. package/dist/commands/daemon.d.ts +0 -22
  136. package/dist/commands/daemon.d.ts.map +0 -1
  137. package/dist/commands/daemon.js +0 -128
  138. package/dist/commands/doctor.d.ts.map +0 -1
  139. package/dist/commands/doctor.js +0 -42
  140. package/dist/commands/events.d.ts.map +0 -1
  141. package/dist/commands/events.js +0 -76
  142. package/dist/commands/init.d.ts.map +0 -1
  143. package/dist/commands/init.js +0 -231
  144. package/dist/commands/invoke.d.ts.map +0 -1
  145. package/dist/commands/invoke.js +0 -54
  146. package/dist/commands/keygen.d.ts.map +0 -1
  147. package/dist/commands/keygen.js +0 -46
  148. package/dist/commands/link.d.ts.map +0 -1
  149. package/dist/commands/link.js +0 -43
  150. package/dist/commands/ls.d.ts.map +0 -1
  151. package/dist/commands/ls.js +0 -7
  152. package/dist/commands/mcp.d.ts.map +0 -1
  153. package/dist/commands/mcp.js +0 -98
  154. package/dist/commands/revoke.d.ts.map +0 -1
  155. package/dist/commands/revoke.js +0 -7
  156. package/dist/commands/tools.d.ts.map +0 -1
  157. package/dist/commands/tools.js +0 -52
  158. package/dist/daemon/address.d.ts.map +0 -1
  159. package/dist/daemon/address.js +0 -71
  160. package/dist/daemon/audit.d.ts.map +0 -1
  161. package/dist/daemon/audit.js +0 -237
  162. package/dist/daemon/calls.d.ts.map +0 -1
  163. package/dist/daemon/calls.js +0 -158
  164. package/dist/daemon/config.d.ts.map +0 -1
  165. package/dist/daemon/config.js +0 -170
  166. package/dist/daemon/daemon.d.ts.map +0 -1
  167. package/dist/daemon/daemon.js +0 -560
  168. package/dist/daemon/event-bus.d.ts.map +0 -1
  169. package/dist/daemon/event-bus.js +0 -111
  170. package/dist/daemon/links.d.ts.map +0 -1
  171. package/dist/daemon/links.js +0 -118
  172. package/dist/daemon/listener.d.ts.map +0 -1
  173. package/dist/daemon/listener.js +0 -132
  174. package/dist/daemon/log-rotation.d.ts.map +0 -1
  175. package/dist/daemon/log-rotation.js +0 -136
  176. package/dist/daemon/pidfile.d.ts.map +0 -1
  177. package/dist/daemon/pidfile.js +0 -90
  178. package/dist/daemon/policy.d.ts.map +0 -1
  179. package/dist/daemon/policy.js +0 -24
  180. package/dist/daemon/registry.d.ts.map +0 -1
  181. package/dist/daemon/registry.js +0 -31
  182. package/dist/daemon/rpc-server.d.ts.map +0 -1
  183. package/dist/daemon/rpc-server.js +0 -203
  184. package/dist/daemon/sessions.d.ts.map +0 -1
  185. package/dist/daemon/sessions.js +0 -429
  186. package/dist/daemon/socket-probe.d.ts.map +0 -1
  187. package/dist/daemon/socket-probe.js +0 -28
  188. package/dist/daemon/state-dir.d.ts.map +0 -1
  189. package/dist/daemon/state-dir.js +0 -43
  190. package/dist/daemon/timers.d.ts.map +0 -1
  191. package/dist/daemon/timers.js +0 -12
  192. package/dist/daemon/tls.d.ts.map +0 -1
  193. package/dist/daemon/tls.js +0 -79
  194. package/dist/errors.d.ts.map +0 -1
  195. package/dist/errors.js +0 -162
  196. package/dist/host-certificate.d.ts.map +0 -1
  197. package/dist/host-certificate.js +0 -111
  198. package/dist/index.d.ts.map +0 -1
  199. package/dist/key-material.d.ts.map +0 -1
  200. package/dist/key-material.js +0 -42
  201. package/dist/link.d.ts.map +0 -1
  202. package/dist/link.js +0 -117
  203. package/dist/mcp/connect-tool.d.ts.map +0 -1
  204. package/dist/mcp/connect-tool.js +0 -396
  205. package/dist/mcp/daemon-tools.d.ts.map +0 -1
  206. package/dist/mcp/daemon-tools.js +0 -29
  207. package/dist/mcp/events-tool.d.ts.map +0 -1
  208. package/dist/mcp/events-tool.js +0 -322
  209. package/dist/mcp/server.d.ts.map +0 -1
  210. package/dist/mcp/server.js +0 -538
  211. package/dist/mcp/tool-mapping.d.ts +0 -58
  212. package/dist/mcp/tool-mapping.d.ts.map +0 -1
  213. package/dist/mcp/tool-mapping.js +0 -143
  214. package/dist/mcp/tool-namespace.d.ts +0 -40
  215. package/dist/mcp/tool-namespace.d.ts.map +0 -1
  216. package/dist/mcp/tool-namespace.js +0 -65
  217. package/dist/native-scheme.d.ts.map +0 -1
  218. package/dist/native-scheme.js +0 -465
  219. package/dist/output.d.ts.map +0 -1
  220. package/dist/output.js +0 -342
  221. package/dist/package-version.d.ts.map +0 -1
  222. package/dist/package-version.js +0 -47
  223. package/dist/qr-terminal.d.ts.map +0 -1
  224. package/dist/qr-terminal.js +0 -45
  225. package/dist/rpc/client.d.ts.map +0 -1
  226. package/dist/rpc/client.js +0 -819
  227. package/dist/scheme.d.ts.map +0 -1
  228. package/dist/scheme.js +0 -321
  229. package/dist/spki-pin.d.ts.map +0 -1
  230. package/dist/spki-pin.js +0 -14
package/README.md CHANGED
@@ -4,14 +4,14 @@
4
4
 
5
5
  [![MIT license][license-badge]][license] [![npm downloads][npm-downloads-badge]][npm-downloads] [![PRs Welcome][prs-welcome-badge]][prs-welcome]
6
6
 
7
- The **`appduct`** package is the operator/agent side of Appduct: a long-lived **daemon** owning a TLS-terminated `wss://` listener and any number of device sessions, a **CLI** and an **MCP server** that are thin RPC clients of it, and the **key management** tying it together — so **developers, QA, and agents** steer app state from a terminal or an MCP client **instead of a hidden debug menu**.
7
+ The `appduct` package is the operator/agent side of Appduct: a CLI and an MCP server, backed by a long-lived background daemon that holds your device sessions. You use it to list and call the tools a connected app has registered — from a terminal, from an agent, or from a test suite — instead of driving the app through a hidden debug menu.
8
8
 
9
9
  ## Why use this package
10
10
 
11
- - **One daemon, many devices**: it auto-spawns on first use, serves every device on one `wss://` port, and survives Metro reloads, backgrounding, and network flaps by suspending and resuming sessions rather than dying with them.
12
- - **Same surface for humans and agents**: the CLI (`tools`, `invoke`, `events`, ...) and `appduct mcp` use the same RPC methods — an agent sees the tools a human operator does.
13
- - **Hardened control plane**: the CLI/MCP never touch sockets, keys, or state files directly; everything goes through a Unix-domain-socket RPC surface gated by filesystem permissions ([docs/SECURITY.md][security]).
14
- - **Fits production-minded apps**: the app only exposes what you register; trust boundaries are pins + TLS, and production deployments can gate tools with policy and audit every call ([docs/ARCHITECTURE.md][architecture] §12).
11
+ - **One daemon, many devices.** It auto-spawns on first use, serves every device on one `wss://` port, and survives Metro reloads, backgrounding, and network flaps by suspending and resuming sessions instead of dying with them.
12
+ - **Same surface for humans and agents.** The CLI (`tools`, `invoke`, `events`, ...) and `appduct mcp` use the same RPC methods, so an agent sees the tools a human operator does.
13
+ - **Hardened control plane.** The CLI and MCP server never touch sockets, keys, or state files directly — everything goes through a Unix-domain-socket RPC surface gated by filesystem permissions (see [`docs/SECURITY.md`][security]).
14
+ - **Fits production-minded apps.** The app only exposes what you register; trust boundaries are pins and TLS, and production deployments can gate tools with policy and audit every call.
15
15
 
16
16
  ## Getting started
17
17
 
@@ -22,40 +22,98 @@ appduct link --scheme myapp --qr
22
22
 
23
23
  `link` needs your app's deep-link scheme: pass `--scheme` (the app's `expo.scheme`, or its bare-RN equivalent), or set `"scheme"` once in `~/.appduct/config.json` and omit the flag. Without either, the command exits with a usage error.
24
24
 
25
- Scan the QR (or open the deep link) in a Appduct-enabled app, then:
25
+ Scan the QR (or open the deep link) in an Appduct-enabled app, then:
26
26
 
27
27
  ```bash
28
28
  appduct tools
29
29
  appduct invoke sum --input '{"a":2,"b":3}'
30
30
  ```
31
31
 
32
- That is the whole loop. There is no host process to start: `appduct` auto-spawns its daemon the first time any command needs it.
32
+ That's the whole loop. There is no host process to start — `appduct` auto-spawns its daemon the first time any command needs it.
33
33
 
34
34
  ## Commands
35
35
 
36
36
  | Command | Role |
37
37
  | --- | --- |
38
- | `appduct init [--scheme <s>] [--force]` | set up an app directory: write `.appduct/config.json`, print the MCP snippet |
38
+ | `appduct init [--scheme <s>] [--ios-app-id <id>] [--android-app-id <id>] [--force]` | set up an app directory: write `.appduct/config.json`, print the MCP snippet |
39
39
  | `appduct keygen [--out <path>] [--force]` | generate a daemon private key, print its app pin |
40
- | `appduct link [--ttl <s>] [--qr] [--open android\|ios-sim\|ios-device] [--device <id>] [--bundle-id <id>] [--scheme <s>]` | mint a pending session and print its deep link |
40
+ | `appduct link [--ttl <s>] [--qr] [--open android\|ios-sim\|ios-device] [--device <id>] [--app-id <id>] [--scheme <s>]` | mint a pending session and print its deep link |
41
41
  | `appduct ls` | list sessions: alias, state, device, tool count |
42
- | `appduct tools [selector] [name] [--full]` | list a session's tools, or show one tool's full schema |
43
- | `appduct invoke [selector] <tool> --input '<json>' [--timeout <ms>]` | call a tool. `--timeout` (clamped to 1 000–600 000 ms) can **shorten** the deadline but cannot extend it past the app's own timer: the app aborts the handler at the tool's declared `timeoutMs`, or 10 s for a tool that declares none, whatever the caller asks for. Give a slow tool more room by declaring `timeoutMs` on its registration |
42
+ | `appduct tools [selector] [name] [--full] [--group <name>] [--filter <text>] [--limit <n>] [--offset <n>]` | list a session's tools (one call signature + description per line), or show one tool's full schema |
43
+ | `appduct tools [selector] --groups` | list a session's tool groups with their tool counts |
44
+ | `appduct invoke [selector] <tool> --input '<json>' [--timeout <ms>]` | call a tool |
44
45
  | `appduct events [selector] [--follow] [--since <cursor>]` | stream session/tool events (default), or one-shot pull everything retained since `<cursor>` (`--since`); `--json` emits NDJSON |
45
46
  | `appduct revoke [selector]` | revoke a session |
46
47
  | `appduct daemon run\|start\|stop\|status` | daemon lifecycle |
47
48
  | `appduct mcp [--scheme <s>]` | start a stdio MCP server proxying connected apps' tools to MCP clients |
48
- | `appduct doctor <artifact> [--assert-present\|--assert-absent]` | **release-gate step**: report or assert whether a built `.app`/`.ipa`/`.apk`/`.aab` contains Appduct |
49
+ | `appduct doctor <artifact> [--assert-present\|--assert-absent]` | release-gate step: report or assert whether a built `.app`/`.ipa`/`.apk`/`.aab` contains Appduct |
49
50
 
50
- Every command that targets a session accepts an optional `selector` (a session id or an alias from `appduct ls`); omit it when exactly one session is active. Global flags: `--json` (machine-readable output), `--no-color`, `--state-dir <path>` (default `~/.appduct`), `--daemon-restart` (on a daemon/CLI version mismatch, restart the daemon even though that drops live sessions and unclaimed links; `APPDUCT_DAEMON_RESTART=1` and `config.json`'s `restartDaemonOnVersionMismatch` do the same for every command, and `--no-daemon-restart` overrules both for one). Run `appduct <command> --help` for the exact flags of any command, or `appduct --help` for the full list.
51
+ Every command that targets a session accepts an optional `selector` (a session id or an alias from `appduct ls`); omit it when exactly one session is active. Global flags: `--json` (machine-readable output; compact by default, one line), `--pretty` (indent `--json` output, and JSON values embedded in human output, 2 spaces — never NDJSON event lines), `--verbose` (include the `meta` block — `command`, `timestamp`, `duration_ms` — omitted by default in both human and `--json` output), `--no-color`, `--state-dir <path>` (default `~/.appduct`), `--daemon-restart` (on a daemon/CLI version mismatch, restart the daemon even though that drops live sessions and unclaimed links — `APPDUCT_DAEMON_RESTART=1` and `config.json`'s `restartDaemonOnVersionMismatch` do the same for every command, and `--no-daemon-restart` overrules both for one). Run `appduct <command> --help` for the exact flags of any command.
52
+
53
+ `--timeout` on `invoke` is clamped to 1,000–600,000 ms and can only **shorten** the deadline, never extend it past the app's own timer: the app aborts the handler at the tool's declared `timeoutMs`, or 10 seconds for a tool that declares none, regardless of what the caller asks for. If a tool needs more room, declare `timeoutMs` on its registration.
54
+
55
+ ### `appduct tools`: a signature per tool
56
+
57
+ `appduct tools` prints one call signature plus a one-line description per tool, not the full schema — cheap to read even against an app that registers hundreds of tools:
58
+
59
+ ```
60
+ Tools
61
+ seed_cart(items: int, sku?: string, clear?: bool = true) -> { added: int, cartId: string }
62
+ Fill the cart with test items for the current user.
63
+ set_flag(name: "dark_mode" | "new_checkout", enabled: bool) [prompt]
64
+ Toggle a feature flag.
65
+
66
+ Run `appduct tools <name>` for a tool's full schema.
67
+ ```
68
+
69
+ A signature is derived straight from the tool's JSON Schema: required params are `name: type`, optional ones `name?: type` (with `= <default>` when the schema declares a short one), and `-> type` is the result when the tool declares an `output_schema`. `...` anywhere means the schema shape wasn't one this renderer could summarize — the tool's full schema (`appduct tools <name>`) still has it. A `[prompt]`/`[deny]` tag follows a tool whose effective policy isn't `"allow"`.
70
+
71
+ Use `--filter <text>` to narrow the listing to tools whose name or description contains `<text>` (case-insensitive), and `--limit <n>`/`--offset <n>` to page through it; a truncated listing prints a trailing `Showing n of total tools (offset o). Narrow with --filter <text> or page with --offset <n>.` line so you know more were left out. `appduct tools --json` returns `{ tools, total, groups }` — `total` is the count after `--group`/`--filter` but before `--limit`/`--offset`. `appduct tools <name>` (a single tool) is unaffected by any of this and always returns the bare tool descriptor; passing `--group`, `--groups`, `--filter`, `--limit` or `--offset` with a `<name>` is a usage error.
72
+
73
+ ### Tool groups
74
+
75
+ When the app puts its tools in groups ([`docs/TOOLS.md`](../../docs/TOOLS.md#group-tools-in-a-large-app)), `appduct tools` lists them under group headings, with subgroups indented under their parent and ungrouped tools last:
76
+
77
+ ```
78
+ Tools
79
+ cart
80
+ add_item(sku: string, quantity: number)
81
+ Add a product to the cart
82
+ checkout
83
+ begin_checkout()
84
+ Start checkout with the current cart
85
+ checkout/payment
86
+ pay_with_card(card: string)
87
+ Pay for the order with a test card
88
+ (ungrouped)
89
+ reset_app()
90
+ Clear all local data
91
+ ```
92
+
93
+ On a large app, start with `--groups` to see what there is, then list one group:
94
+
95
+ ```
96
+ $ appduct tools --groups
97
+ Groups
98
+ cart 12
99
+ checkout 8
100
+ checkout/payment 3
101
+ (ungrouped) 2
102
+
103
+ 22 tools in total. Run `appduct tools --group <name>` to list one group's tools.
104
+
105
+ $ appduct tools --group checkout
106
+ ```
107
+
108
+ `--group checkout` lists `checkout` and all of its subgroups; `--group checkout/payment` lists only that subgroup. Matching is by whole name and case-sensitive, so `--group checkout` never matches a `checkoutx` group. `--group` combines with `--filter`, `--limit` and `--offset`. When a listing without `--group` is cut short, the footer names the top-level groups to narrow to: `Showing 5 of 22 tools (offset 0). Narrow with --group <name> (groups: cart 12, checkout 8) or --filter <text>, or page with --offset <n>.` A group that is not one or two `/`-separated names of letters, digits, `_` and `-` (for example `checkout/` or `a/b/c`) is a usage error. With `--json`, each tool carries its `group` — `null` for an ungrouped tool, exactly the value `groups` uses for its own ungrouped row — and `groups` lists every group with its count, whatever `--group` or `--filter` you passed.
51
109
 
52
110
  ### The deep-link scheme
53
111
 
54
- `appduct link`, `appduct mcp` and the MCP `appduct_connect` tool all resolve the scheme the same way, first match wins:
112
+ `appduct link`, `appduct mcp`, and the MCP `appduct_connect` tool all resolve the scheme the same way, first match wins:
55
113
 
56
114
  1. `--scheme <s>`
57
115
  2. the `APPDUCT_SCHEME` environment variable
58
- 3. the nearest `.appduct/config.json` that declares a `scheme`, walking up from the working directory — one without that key does not stop the walk, and the walk skips `~/.appduct` and the state dir in use so global config is never mistaken for a project's
116
+ 3. the nearest `.appduct/config.json` that declares a `scheme`, walking up from the working directory (a file without that key doesn't stop the walk; the walk skips `~/.appduct` and the active state dir, so global config is never mistaken for a project's)
59
117
  4. `scheme` in `<state-dir>/config.json`
60
118
  5. a static-file project probe, tried in this order (no walk-up — an app root is where you run these commands):
61
119
  1. `<cwd>/app.json`'s `expo.scheme` — a string, or the first entry of an array
@@ -63,25 +121,25 @@ Every command that targets a session accepts an optional `selector` (a session i
63
121
  3. **iOS**: any `Info.plist` up to two levels below the app root (excluding `Pods`, `build`, `node_modules`, `DerivedData`) for the first `CFBundleURLSchemes` entry, then xcodegen's `project.yml` for the same key under `info.properties.CFBundleURLTypes`
64
122
  6. otherwise an error naming every location above
65
123
 
66
- So in an Expo app root, none of it needs configuring; a plain Xcode or Gradle app root needs no configuring either, once its `Info.plist`/`AndroidManifest.xml`/`build.gradle` already declares a `CFBundleURLTypes`/intent-filter/`appductScheme` for deep links. `app.config.js` / `app.config.ts` are never executed to read a scheme, and neither is `xcodebuild`, `plutil` or a Gradle evaluation for the native probes — every one of them is a plain, defensively-parsed read of a static project file (a binary-encoded `Info.plist` is reported as unreadable rather than decoded). Dynamic-config projects, or anything these probes can't make sense of, should use `--scheme`, `APPDUCT_SCHEME`, or `appduct init --scheme <s>`.
67
-
68
- **Disagreement is never guessed away.** If more than one native probe resolves to a *different* scheme (say, an `android/` and an `ios/` tree in the same repo declaring different values), resolution fails with a usage error naming every conflicting source instead of picking one.
124
+ In an Expo app root, none of this needs configuring; a plain Xcode or Gradle app root doesn't either, once its `Info.plist`/`AndroidManifest.xml`/`build.gradle` already declares deep links. These probes never execute `app.config.js`/`app.config.ts`, `xcodebuild`, `plutil`, or a Gradle evaluation — each is a plain, defensively-parsed read of a static project file. If your project uses dynamic config, or the probes can't make sense of your setup, use `--scheme`, `APPDUCT_SCHEME`, or `appduct init --scheme <s>` instead.
69
125
 
70
- `appduct init`, run in an app root, writes `.appduct/config.json` (mode `0600`, in a `0700` directory) with the discovered scheme and prints the MCP server entry to paste plus the `import "@appduct/react-native/auto"` reminder. It never generates keys or touches daemon state.
126
+ **Disagreement is never guessed away.** If more than one native probe resolves to a different scheme (say, an `android/` and an `ios/` tree in the same repo declaring different values), resolution fails with a usage error naming every conflicting source instead of picking one.
71
127
 
72
- **`init` is not the resolver.** It consults exactly two *kinds* of source — `--scheme`, and whatever the same static-file discovery step 5 above runs (`<cwd>/app.json`, then the native Android/iOS probes) — plus whatever the file already records. It deliberately ignores `APPDUCT_SCHEME` and does not walk up to a parent `.appduct/config.json`, because it is deciding what to *write here*: inheriting either would bake an ambient value into a committed file (a shell variable that happened to be exported, or a parent project's scheme silently copied into a sub-package). The precedence list above is what *reads* the result. `--json`'s `source` field reports which input won: `"--scheme"`, `"app.json"`, one of the four native-probe names (`"android-gradle"`, `"android-manifest"`, `"ios-info-plist"`, `"ios-project-yml"`), or `"already-recorded"`. When it is a discovery tier, `origin` names the exact file (and key/placeholder) the value came from, and the printed hint says so too — "Scheme … was read from …" — rather than only naming the platform.
128
+ `appduct init`, run in an app root, writes `.appduct/config.json` (mode `0600`, in a `0700` directory) with the discovered scheme and prints the MCP server entry to paste, plus the `import "@appduct/react-native/auto"` reminder. It never generates keys or touches daemon state.
73
129
 
74
- It also refuses to run where `<cwd>/.appduct` would be the daemon's own state directory (`~/.appduct`, or a `--state-dir`/`APPDUCT_STATE_DIR` you point at it) — that directory holds `key.pem` and the audit log, and a config written there is not something to commit.
130
+ `init` only looks at `--scheme` and the static-file discovery in step 5 above (`app.json`, then the native probes), plus whatever the file already records — it deliberately ignores `APPDUCT_SCHEME` and does not walk up to a parent `.appduct/config.json`, since baking either into a committed file would be wrong (a shell variable that happened to be exported, or a parent project's scheme silently copied into a sub-package). `--json`'s `source` field reports which input won: `"--scheme"`, `"app.json"`, one of the four native-probe names (`"android-gradle"`, `"android-manifest"`, `"ios-info-plist"`, `"ios-project-yml"`), or `"already-recorded"`.
75
131
 
76
- It is idempotent, and safe to re-run:
132
+ It's idempotent and safe to re-run:
77
133
 
78
- - A plain re-run keeps whatever scheme is already recorded. If discovery (`app.json` or a native probe) has since come to declare a different value, the result carries a `note` saying so rather than failing — a command documented as safe to re-run must not start erroring because somebody renamed a scheme.
134
+ - A plain re-run keeps whatever scheme is already recorded. If discovery has since found a different value, the result carries a `note` rather than failing.
79
135
  - `--scheme <different>` needs `--force` to replace a recorded scheme; `--force` on its own re-adopts `app.json`'s value.
80
136
  - `--force` merges into the existing JSON rather than truncating it.
81
137
 
82
- A project `.appduct/config.json` holds client-side settings only — it can never redirect the state directory, key or policy (`--state-dir` / `APPDUCT_STATE_DIR` do that). The reverse is worth stating too: **do not point `--state-dir` at a project `.appduct/` directory you commit.** The state dir is where the daemon keeps `key.pem` and its audit log, and a committed one would publish a private key. The two directories share a name and nothing else; the walk-up that finds a project config deliberately skips `~/.appduct` and whatever state dir is in use, so neither can be mistaken for the other.
138
+ `--ios-app-id <id>` and `--android-app-id <id>` write `appId.ios` and `appId.android` into the same file — the installed app ids that `--open android`, `--open ios-device` and `appduct_connect` need (see [Delivering the link to a device](#delivering-the-link-to-a-device)). They're two flags rather than one because the platforms' ids don't always match, and `init` never guesses one from the other. Like `--scheme`, replacing an id that's already recorded needs `--force`.
83
139
 
84
- `appduct mcp` is the one exception to step 6: it starts even with no scheme, because it is still useful for proxying tools to a session paired another way. Only `appduct_connect` fails, and its error names every location tried.
140
+ A project `.appduct/config.json` holds client-side settings only — it can never redirect the state directory, key, or policy (`--state-dir` / `APPDUCT_STATE_DIR` do that). **Do not point `--state-dir` at a project `.appduct/` directory you commit** — the state dir is where the daemon keeps `key.pem` and its audit log, and a committed one would publish a private key. The two directories share a name and nothing else.
141
+
142
+ `appduct mcp` is the one exception to step 6: it starts even with no scheme, because it's still useful for proxying tools to a session paired another way. Only `appduct_connect` fails, and its error names every location tried.
85
143
 
86
144
  ### Delivering the link to a device
87
145
 
@@ -89,36 +147,38 @@ A project `.appduct/config.json` holds client-side settings only — it can neve
89
147
 
90
148
  | `--open` | Mechanism | Address in the link |
91
149
  | --- | --- | --- |
92
- | `android` | `adb reverse tcp:<port> tcp:<port>`, then `adb shell am start -a android.intent.action.VIEW -d '<link>'` | `127.0.0.1` (the port is forwarded onto the device) |
150
+ | `android` | `adb reverse tcp:<port> tcp:<port>`, then `adb shell am start -a android.intent.action.VIEW -d '<link>' -p <app-id>` | `127.0.0.1` (the port is forwarded onto the device) |
93
151
  | `ios-sim` | `xcrun simctl openurl <udid> <link>` | `127.0.0.1` (the simulator shares the host's network) |
94
- | `ios-device` **(experimental)** | `xcrun devicectl device process launch --device <udid> [--terminate-existing] --payload-url <link> <bundle-id>` | the machine's detected LAN address — there is no `adb reverse` equivalent on iOS |
152
+ | `ios-device` **(experimental)** | `xcrun devicectl device process launch --device <udid> [--terminate-existing] --payload-url <link> <app-id>` | the machine's detected LAN address — there's no `adb reverse` equivalent on iOS |
153
+
154
+ With no `--device`, every target requires exactly one device and errors naming each candidate rather than picking one arbitrarily: `android` counts attached devices (`ANDROID_SERIAL` disambiguates), `ios-sim` counts booted simulators, and `ios-device` counts connected iOS devices only — `devicectl` lists every device the Mac has ever paired, so disconnected phones and non-iOS ones are filtered out first.
95
155
 
96
- With no `--device`, every target requires exactly one device and errors naming each candidate rather than making an arbitrary pick: `android` counts attached devices (`ANDROID_SERIAL` disambiguates), `ios-sim` counts booted simulators, and `ios-device` counts *connected iOS* devices — `devicectl` lists every CoreDevice the Mac has ever paired, so disconnected phones and non-iOS ones (a paired Watch or Vision Pro) are filtered out before the count.
156
+ **`android` and `ios-device` need the installed app's id** — the Android package name or the iOS bundle id. Pass `--app-id <id>` (`appId` over MCP), or record it once in `.appduct/config.json` with `appduct init --android-app-id <id> --ios-app-id <id>`. Without one the command fails before minting anything. Naming the app is what keeps Android from showing an "Open with" chooser when more than one installed app declares your scheme — `adb` would report success, and the session would never be claimed. `ios-sim` needs no app id (`simctl openurl` has no equivalent), and passing one with it is an error. The id may contain only letters, digits, `.`, `_` and `-`, and must start with a letter or digit.
97
157
 
98
158
  #### `--open ios-device` (experimental)
99
159
 
100
- A wired iPhone or iPad used to have no automated path at all — only a human scanning the QR code. `--open ios-device` closes that gap through `xcrun devicectl`, but it is **experimental**: `devicectl`'s `--payload-url` flag is not documented by Apple, and it cannot be verified in CI, so treat a failure here as a reason to fall back to the QR flow rather than as a bug in your app.
160
+ This is the only automated path for a wired iPhone or iPad — otherwise you're scanning a QR code by hand. It relies on `xcrun devicectl`'s undocumented `--payload-url` flag, which can't be verified in CI, so treat a failure here as a reason to fall back to the QR flow rather than as a bug in your app.
101
161
 
102
162
  It needs all of:
103
163
 
104
- - **iOS 17 or newer** on the device, and **Xcode 15 or newer** on the host (`devicectl` does not exist before that). For iOS 16 and below, use the QR/deep-link flow.
164
+ - **iOS 17 or newer** on the device, and **Xcode 15 or newer** on the host (`devicectl` doesn't exist before that). For iOS 16 and below, use the QR/deep-link flow instead.
105
165
  - The device **paired and trusted** by this Mac, with **Developer Mode** enabled on it (Settings → Privacy & Security → Developer Mode).
106
- - A **development-signed build of your app already installed** — `devicectl` launches an installed app; it does not install one.
107
- - The phone and this machine **on the same network**, reachable at the address the link advertises. `appduct link` prints it on its `Endpoint` line (`--json`: `endpoint.address`); `advertisedIp` in `config.json` overrides the detection. If no routable address is found, detection falls back to `127.0.0.1` — which a phone cannot reach — so `ios-device` refuses to deliver such a link and tells you to set `advertisedIp` rather than leaving you with a session that is never claimed.
108
- - The app's **bundle id**, from `iosBundleId` in `config.json` or `--bundle-id <id>` on the command line. Without one the command fails with a usage error before minting anything. It must look like a bundle id — letters, digits, `.` and `-` — since it is passed to `devicectl` as a trailing positional, where a value starting with `-` would be read as an option instead.
166
+ - A **development-signed build of your app already installed** — `devicectl` launches an installed app, it doesn't install one.
167
+ - The phone and this machine **on the same network**, reachable at the address the link advertises. `appduct link` prints it on its `Endpoint` line (`--json`: `endpoint.address`); `advertisedIp` in `config.json` overrides detection. If no routable address is found, detection falls back to `127.0.0.1` — which a phone can't reach — so `ios-device` refuses to deliver such a link and tells you to set `advertisedIp`.
168
+ - The app's **bundle id** — see [Delivering the link to a device](#delivering-the-link-to-a-device) for how to supply it.
109
169
 
110
170
  ```bash
111
- appduct link --scheme myapp --open ios-device --bundle-id com.example.myapp
171
+ appduct link --scheme myapp --open ios-device --app-id com.example.myapp
112
172
  ```
113
173
 
114
- Two behaviours worth knowing:
174
+ Two things worth knowing before you rely on this:
115
175
 
116
- - **What happens when the app is already running is unverified.** The delivery is a plain `process launch`, which is what Expo's own `devicectl` integration does. Whether that foregrounds a running app with the URL, cold-launches it, or fails outright has not been observed on hardware. If delivery to an already-running app does nothing, pass **`--relaunch`** (`relaunch: true` over MCP): it adds `--terminate-existing`, killing the running instance first. Appduct copes with the restart either way — a delivered link supersedes a held session.
117
- - **A physical iPhone is never auto-detected.** `appduct_connect` called without a `target` only ever considers booted simulators and attached Android devices; `appduct link` without `--open` does not look for a device at all. A paired iPhone is often someone's personal phone, so it has to be asked for explicitly.
176
+ - **What happens when the app is already running hasn't been verified on hardware.** If delivery to an already-running app does nothing, pass **`--relaunch`** (`relaunch: true` over MCP) — it kills the running instance first. Appduct copes with the restart either way; a delivered link supersedes a held session.
177
+ - **A physical iPhone is never auto-detected.** `appduct_connect` called without a `target` only considers booted simulators and attached Android devices, and `appduct link` without `--open` doesn't look for a device at all — a paired iPhone is often someone's personal phone, so it has to be asked for explicitly.
118
178
 
119
179
  ## MCP setup (Claude Code, Cursor, and similar)
120
180
 
121
- Add `appduct mcp` to the agent's MCP server config — no separate server process to manage; it auto-spawns the daemon the same way the CLI does:
181
+ Add `appduct mcp` to the agent's MCP server config — no separate server process to manage, it auto-spawns the daemon the same way the CLI does:
122
182
 
123
183
  ```json
124
184
  {
@@ -131,7 +191,7 @@ Add `appduct mcp` to the agent's MCP server config — no separate server proces
131
191
  }
132
192
  ```
133
193
 
134
- An MCP server is usually launched with a working directory you don't control, so discovery from `app.json` may not apply. On a machine with more than one app, make the entry self-contained by naming the scheme in it — which is exactly what `appduct init` prints:
194
+ An MCP server is usually launched with a working directory you don't control, so `app.json` discovery may not apply. On a machine with more than one app, make the entry self-contained by naming the scheme in it — which is exactly what `appduct init` prints:
135
195
 
136
196
  ```json
137
197
  {
@@ -144,9 +204,21 @@ An MCP server is usually launched with a working directory you don't control, so
144
204
  }
145
205
  ```
146
206
 
147
- Once configured, the connected app's tools appear as MCP tools automatically: `tools/list` mirrors the live registry (namespaced `<alias>__<name>` when more than one session is active), and `tools/call` proxies straight to the app with progress and errors preserved.
207
+ Once configured, an agent reaches the connected app's tools through three built-in tools that work like the CLI:
208
+
209
+ | Tool | Does what | CLI equivalent |
210
+ | --- | --- | --- |
211
+ | `appduct_list_tools` | Lists the app's tools as one-line signatures, with each tool's group and policy, plus the app's groups with counts. Returns 50 at a time unless given `limit`; takes `group`, `filter` and `offset`. | `appduct tools` |
212
+ | `appduct_describe_tool` | Shows one tool's full input and output schema. | `appduct tools <name>` |
213
+ | `appduct_call_tool` | Calls a tool by `name` with `args`, with progress and errors preserved. | `appduct invoke` |
214
+
215
+ The app's tools don't show up as MCP tools of their own. An app with hundreds of tools still adds only these three to the agent's tool list, and that list doesn't change when tools register or a device connects. With more than one device connected, each of the three needs `selector`: the session alias or id from `appduct ls`.
216
+
217
+ Your MCP client asks permission for `appduct_call_tool` as a single tool, so choosing "always allow" there approves every tool the app registers, destructive ones included. To keep a person approving those calls, set `policy.destructive` to `"prompt"` in the state directory's `config.json`. Each call to a tool marked `destructiveHint` then shows an approval prompt in clients that support it, and is denied in clients that don't. A destructive tool without that annotation falls under `policy.default` instead.
218
+
219
+ Set this before you connect: the daemon reads `config.json` only when it starts, so changing it later takes `appduct daemon stop`. That disconnects every device, which then has to link again, and leaves a running `appduct mcp` without a daemon, so restart the MCP server in your client as well.
148
220
 
149
- Four built-in tools cover what an agent can't do through the app's own registry. `appduct_connect` mints a link and, by default, delivers it to whichever `android`/`ios-sim` device it detects — pass `target`/`device` to choose, or `target: "none"` to force the human flow — falling back to a QR code, plus instructions to show it, only when there is nothing to deliver to. `appduct_wait_for_session` then waits for that session to be claimed. `target: "ios-device"` reaches a paired physical iPhone or iPad, with `bundleId` (or `iosBundleId` in `config.json`) and the [prerequisites above](#--open-ios-device-experimental); it is experimental and never auto-detected, so an agent has to ask for it by name.
221
+ Four more built-in tools cover what the app's own tools can't. `appduct_connect` mints a link and, by default, delivers it to whichever `android`/`ios-sim` device it detects — pass `target`/`device` to choose, or `target: "none"` to force the human flow — falling back to a QR code, plus instructions to show it, only when there's nothing to deliver to. Delivering to `android` (chosen or detected) needs `appId`, resolved the same way as `--app-id` (see [Delivering the link to a device](#delivering-the-link-to-a-device)); passing it with `target: "ios-sim"` or `"none"` is an error. `appduct_wait_for_session` then waits for that session to be claimed. `target: "ios-device"` reaches a paired physical iPhone or iPad, with `appId` and the [prerequisites above](#--open-ios-device-experimental) — it's experimental and never auto-detected, so an agent has to ask for it by name.
150
222
 
151
223
  The other two give an agent a pull surface over `postEvent()`-pushed app events: `appduct_events` drains everything retained since a cursor, and `appduct_wait_for_event` blocks for a matching event (checking what's already retained before waiting live), rejecting with `tool_timeout` if none arrives in time.
152
224
 
@@ -166,13 +238,13 @@ const { payload } = await app.waitForEvent("checkout_done", { timeoutMs: 5_000 }
166
238
  app.close();
167
239
  ```
168
240
 
169
- Errors surface as a `AppductError` whose `type` preserves the daemon's wire error type verbatim, so a test can assert on it directly instead of string-matching stderr:
241
+ Errors surface as an `AppductError` whose `type` preserves the daemon's wire error type verbatim, so a test can assert on it directly instead of string-matching stderr:
170
242
 
171
243
  ```ts
172
244
  await expect(app.call("checkout", {})).rejects.toMatchObject({ type: "policy_denied" });
173
245
  ```
174
246
 
175
- Pair a simulator/emulator in a `globalSetup` without shelling out via `link`/`waitForSession`:
247
+ Pair a simulator/emulator in a `globalSetup` without shelling out, via `link`/`waitForSession`:
176
248
 
177
249
  ```ts
178
250
  import { link, waitForSession } from "appduct/client";
@@ -203,7 +275,7 @@ For everything else, the package exports `runCli` and the command handlers from
203
275
 
204
276
  ## Keys and pins
205
277
 
206
- You can skip this entirely while your app has no `cliPins` configured — the zero-config default, in **any** build type. The daemon auto-generates its own `key.pem` the first time it starts if one isn't already there (mode `0600`) and prints its `sha256/...` fingerprint on that first run; `appduct link` carries that fingerprint on the deep link for the app to pick up. See [`docs/SECURITY.md`][security]'s "Trust modes" for what that does and does not protect.
278
+ You can skip this entirely while your app has no `cliPins` configured — the zero-config default, in any build type. The daemon auto-generates its own `key.pem` the first time it starts if one isn't already there (mode `0600`) and prints its `sha256/...` fingerprint on that first run; `appduct link` carries that fingerprint on the deep link for the app to pick up. See [`docs/SECURITY.md`][security]'s "Trust modes" for what that does and doesn't protect.
207
279
 
208
280
  For a build that should trust only a key you embedded ahead of time, generate one explicitly:
209
281
 
@@ -211,9 +283,9 @@ For a build that should trust only a key you embedded ahead of time, generate on
211
283
  appduct keygen
212
284
  ```
213
285
 
214
- The command writes an unencrypted PEM private key (PKCS#8) to `<state-dir>/key.pem` by default (override with `--out`; add `--force` to overwrite) and prints the exact `sha256/...` SPKI fingerprint your app should place into `cliPins`. It runs non-interactively — safe to call from CI or a setup script.
286
+ This writes an unencrypted PEM private key (PKCS#8) to `<state-dir>/key.pem` by default (override with `--out`; add `--force` to overwrite) and prints the exact `sha256/...` SPKI fingerprint your app should place into `cliPins`. It runs non-interactively, so it's safe to call from CI or a setup script.
215
287
 
216
- `appduct link`'s deep link is `<scheme>:///?appduct=<payload>&pin=<sha256/...>`. The `appduct` param is the existing binary v2 bootstrap payload (address, session id, token, expiry — unchanged); `pin` is a separate, out-of-band query param carrying the daemon's current SPKI fingerprint for apps that want to pick it up. An app build with embedded `cliPins` ignores `pin` outright — embedded pins always win, in every build type. A build with no embedded pins trusts it for that one session.
288
+ `appduct link`'s deep link is `<scheme>:///?appduct=<payload>&pin=<sha256/...>`. The `appduct` param is the binary v2 bootstrap payload (address, session id, token, expiry); `pin` is a separate, out-of-band query param carrying the daemon's current SPKI fingerprint for apps that want to pick it up. An app build with embedded `cliPins` ignores `pin` outright — embedded pins always win, in every build type. A build with no embedded pins trusts it for that one session.
217
289
 
218
290
  ## Daemon lifecycle
219
291
 
@@ -221,20 +293,20 @@ The daemon auto-starts the first time any CLI or MCP command needs it — you do
221
293
 
222
294
  Use `appduct daemon status` to see what's running (version, pid, `wssPort`, pinned keys, live sessions, effective policy, and the audit log's retention window, file count, size, and failure counters) and `appduct daemon stop` to shut it down explicitly.
223
295
 
224
- Because the daemon outlives the CLI that started it, upgrading Appduct would otherwise leave the old daemon serving your commands. Every CLI/MCP process compares its version with the daemon's on its first command and replaces a stale daemon when nothing would be lost; when there is — connected sessions, or an unexpired link nobody has scanned yet — the command stops instead, naming both versions, until you run `appduct daemon stop` or pass `--daemon-restart`. A daemon *newer* than your CLI is left alone with a warning, so a project-local install never downgrades a global one's daemon.
296
+ Because the daemon outlives the CLI that started it, upgrading Appduct would otherwise leave the old daemon serving your commands. Every CLI/MCP process compares its version with the daemon's on its first command and replaces a stale daemon when nothing would be lost; when there is something at stake — connected sessions, or an unexpired link nobody has scanned yet — the command stops instead, naming both versions, until you run `appduct daemon stop` or pass `--daemon-restart`. A daemon newer than your CLI is left alone with a warning, so a project-local install never downgrades a global one's daemon.
225
297
 
226
- Neither log grows without bound: `audit/<YYYY-MM-DD>.jsonl` files older than `auditRetentionDays` (default 30) are pruned at daemon start and once a day after, and a `daemon.log` over `daemonLogMaxBytes` (default 10 MiB) is rotated to `daemon.log.1` when a daemon is next spawned. Both are `config.json` keys ([ARCHITECTURE.md §3][architecture]).
298
+ Neither log grows without bound: `audit/<YYYY-MM-DD>.jsonl` files older than `auditRetentionDays` (default 30) are pruned at daemon start and once a day after, and a `daemon.log` over `daemonLogMaxBytes` (default 10 MiB) is rotated to `daemon.log.1` when a daemon is next spawned. Both are `config.json` keys — see [`ARCHITECTURE.md` §3][architecture].
227
299
 
228
300
  ## Release gate: `appduct doctor`
229
301
 
230
- Appduct's inclusion in a build is controlled entirely by autolinking exclusion (see [docs/BUILD-VARIANTS.md][build-variants]) — there is no runtime `debuggable` check to catch a pipeline that forgot to exclude the package. `doctor` is the replacement: an artifact-level assertion you run against the thing you're about to ship, not the config you think produced it.
302
+ Appduct's inclusion in a build is controlled entirely by autolinking exclusion (see [`docs/BUILD-VARIANTS.md`][build-variants]) — there's no runtime `debuggable` check to catch a pipeline that forgot to exclude the package. `doctor` checks the built artifact itself, not the config you think produced it:
231
303
 
232
304
  ```bash
233
305
  appduct doctor ./build/MyApp.ipa --assert-absent
234
306
  appduct doctor ./build/app-release.apk --assert-absent
235
307
  ```
236
308
 
237
- Exit codes, what it inspects per platform, the marker-only detection rules on both platforms, and the CI wiring are in [docs/CI.md][ci].
309
+ Exit codes, what it inspects per platform, and the marker-only detection rules on both platforms are documented in `appduct doctor --help`; CI wiring lives in `.github/workflows/test.yaml`.
238
310
 
239
311
  ## Related packages
240
312
 
@@ -247,7 +319,6 @@ Exit codes, what it inspects per platform, the marker-only detection rules on bo
247
319
  - [Wire protocol][protocol]
248
320
  - [Security model & key rotation][security]
249
321
  - [Build variants][build-variants]
250
- - [CI and the release gate][ci]
251
322
  - [Monorepo README](https://github.com/callstackincubator/appduct/blob/main/README.md)
252
323
 
253
324
  ## Made with ❤️ at Callstack
@@ -263,7 +334,6 @@ Like the project? ⚛️ [Join the team](https://callstack.com/careers/?utm_camp
263
334
  [protocol]: https://github.com/callstackincubator/appduct/blob/main/docs/PROTOCOL.md
264
335
  [security]: https://github.com/callstackincubator/appduct/blob/main/docs/SECURITY.md
265
336
  [build-variants]: https://github.com/callstackincubator/appduct/blob/main/docs/BUILD-VARIANTS.md
266
- [ci]: https://github.com/callstackincubator/appduct/blob/main/docs/CI.md
267
337
  [license-badge]: https://img.shields.io/npm/l/appduct?style=for-the-badge
268
338
  [license]: https://github.com/callstackincubator/appduct/blob/main/LICENSE
269
339
  [npm-downloads-badge]: https://img.shields.io/npm/dm/appduct?style=for-the-badge
@@ -31,7 +31,7 @@
31
31
  * future stub could ship them) without the real implementation being present, exactly the reason
32
32
  * the Android signals below aren't all treated as equally authoritative either.
33
33
  * - Android: the primary signal is `AppductNativeMarker`
34
- * (`packages/native/android/core/src/main/java/com/callstackincubator/appduct/AppductNativeMarker.kt`,
34
+ * (`packages/native/android/core/src/main/java/com/callstack/appduct/AppductNativeMarker.kt`,
35
35
  * vendored into `@appduct/react-native` at `android/core/src/main/java/...`), a marker class with no
36
36
  * other purpose. Its fully-qualified name is kept unminified and unremoved by a `-keep` rule in
37
37
  * `consumer-rules.pro` (same vendoring path), shipped to every consuming app via `consumerProguardFiles`
@@ -41,10 +41,12 @@
41
41
  * minification in every supported consumer setup.
42
42
  *
43
43
  * Two more signals are kept as fallbacks for artifacts built before this marker existed: the
44
- * `com.callstackincubator.appduct` package string in the dex string pool (survives as long as
44
+ * `com.callstack.appduct` package string in the dex string pool (and its pre-rename spelling,
45
+ * `com.callstackincubator.appduct` — every Android marker below is matched against both, so an
46
+ * app built against Appduct 0.9.0 or earlier is still detected) (survives as long as
45
47
  * R8/ProGuard minification+obfuscation isn't applied to it — an aggressive release config with no keep
46
48
  * rule for this package can rename it away) and the plugin-authored meta-data key names in
47
- * `AndroidManifest.xml` (`com.callstackincubator.appduct.CLI_PINS`/`TRUST`/`ALLOW_PRIVATE_LAN_ONLY`):
49
+ * `AndroidManifest.xml` (`com.callstack.appduct.CLI_PINS`/`TRUST`/`ALLOW_PRIVATE_LAN_ONLY`):
48
50
  * those are XML attribute string values written by the config plugin at prebuild time, not compiled
49
51
  * identifiers, so R8 never touches them, but they only exist at all if the config plugin ran. Both
50
52
  * encodings AAPT2 can choose for the manifest string pool (UTF-8 or UTF-16LE) are checked.
@@ -107,4 +109,3 @@ export type InspectArtifactOptions = {
107
109
  * prevents a truthful presence/absence answer (missing tool, unreadable/corrupt artifact) — it
108
110
  * never resolves successfully with a guessed or default answer. */
109
111
  export declare const inspectArtifact: (artifactPath: string, options?: InspectArtifactOptions) => Promise<ArtifactInspection>;
110
- //# sourceMappingURL=artifact-inspect.d.ts.map
package/dist/bin.d.ts CHANGED
@@ -1,3 +1,2 @@
1
1
  #!/usr/bin/env node
2
2
  export {};
3
- //# sourceMappingURL=bin.d.ts.map