appduct 0.10.0 → 0.11.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.
- package/README.md +122 -52
- package/dist/artifact-inspect.d.ts +0 -1
- package/dist/bin.d.ts +0 -1
- package/dist/bin.js +1250 -2
- package/dist/cli/command-options.d.ts +8 -1
- package/dist/cli/create-cli.d.ts +0 -1
- package/dist/cli/dispatch.d.ts +10 -1
- package/dist/cli/envelope.d.ts +19 -0
- package/dist/cli/global-flags.d.ts +30 -0
- package/dist/cli/open-target.d.ts +25 -8
- package/dist/cli/result-types.d.ts +31 -5
- package/dist/cli/router.d.ts +78 -0
- package/dist/cli/routes/daemon/index.d.ts +9 -0
- package/dist/cli/routes/daemon/index.js +848 -0
- package/dist/cli/routes/daemon/run.d.ts +3 -0
- package/dist/cli/routes/daemon/run.js +3246 -0
- package/dist/cli/routes/daemon/start.d.ts +3 -0
- package/dist/cli/routes/daemon/start.js +1677 -0
- package/dist/cli/routes/daemon/status.d.ts +4 -0
- package/dist/cli/routes/daemon/status.js +1685 -0
- package/dist/cli/routes/daemon/stop.d.ts +4 -0
- package/dist/cli/routes/daemon/stop.js +1655 -0
- package/dist/cli/routes/doctor.d.ts +3 -0
- package/dist/cli/routes/doctor.js +1075 -0
- package/dist/cli/routes/events.d.ts +3 -0
- package/dist/cli/routes/events.js +1915 -0
- package/dist/cli/routes/init.d.ts +3 -0
- package/dist/cli/routes/init.js +1486 -0
- package/dist/cli/routes/invoke.d.ts +3 -0
- package/dist/cli/routes/invoke.js +1893 -0
- package/dist/cli/routes/keygen.d.ts +3 -0
- package/dist/cli/routes/keygen.js +928 -0
- package/dist/cli/routes/link.d.ts +3 -0
- package/dist/cli/routes/link.js +2714 -0
- package/dist/cli/routes/ls.d.ts +3 -0
- package/dist/cli/routes/ls.js +1668 -0
- package/dist/cli/routes/mcp.d.ts +5 -0
- package/dist/cli/routes/mcp.js +3929 -0
- package/dist/cli/routes/revoke.d.ts +3 -0
- package/dist/cli/routes/revoke.js +1684 -0
- package/dist/cli/routes/tools.d.ts +3 -0
- package/dist/cli/routes/tools.js +1860 -0
- package/dist/cli/runner.d.ts +3 -11
- package/dist/cli/types.d.ts +9 -6
- package/dist/cli/version-guard.d.ts +20 -0
- package/dist/cli.d.ts +0 -1
- package/dist/client/app-client.d.ts +4 -4
- package/dist/client/bootstrap.d.ts +4 -4
- package/dist/client/connect.d.ts +0 -1
- package/dist/client/errors.d.ts +0 -1
- package/dist/client/index.d.ts +1 -2
- package/dist/client/index.js +2389 -16
- package/dist/commands/daemon/context.d.ts +14 -0
- package/dist/commands/daemon/run.d.ts +11 -0
- package/dist/commands/daemon/start.d.ts +5 -0
- package/dist/commands/daemon/status.d.ts +5 -0
- package/dist/commands/daemon/stop.d.ts +5 -0
- package/dist/commands/doctor.d.ts +0 -1
- package/dist/commands/events.d.ts +0 -1
- package/dist/commands/init.d.ts +17 -4
- package/dist/commands/invoke.d.ts +0 -1
- package/dist/commands/keygen.d.ts +0 -1
- package/dist/commands/link.d.ts +1 -2
- package/dist/commands/ls.d.ts +0 -1
- package/dist/commands/mcp.d.ts +2 -4
- package/dist/commands/revoke.d.ts +0 -1
- package/dist/commands/tools.d.ts +19 -3
- package/dist/daemon/address.d.ts +0 -1
- package/dist/daemon/audit.d.ts +6 -11
- package/dist/daemon/calls.d.ts +0 -1
- package/dist/daemon/config.d.ts +13 -16
- package/dist/daemon/daemon.d.ts +0 -1
- package/dist/daemon/event-bus.d.ts +0 -1
- package/dist/daemon/links.d.ts +0 -1
- package/dist/daemon/listener.d.ts +0 -1
- package/dist/daemon/log-rotation.d.ts +0 -1
- package/dist/daemon/pidfile.d.ts +14 -8
- package/dist/daemon/policy.d.ts +0 -1
- package/dist/daemon/registry.d.ts +0 -1
- package/dist/daemon/rpc-errors.d.ts +16 -0
- package/dist/daemon/rpc-server.d.ts +0 -11
- package/dist/daemon/sessions.d.ts +0 -1
- package/dist/daemon/socket-probe.d.ts +0 -1
- package/dist/daemon/state-dir.d.ts +0 -1
- package/dist/daemon/timers.d.ts +0 -1
- package/dist/daemon/tls.d.ts +0 -1
- package/dist/errors.d.ts +0 -1
- package/dist/host-certificate.d.ts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1307 -3
- package/dist/key-material.d.ts +0 -1
- package/dist/link.d.ts +8 -7
- package/dist/mcp/app-tools.d.ts +153 -0
- package/dist/mcp/connect-tool.d.ts +14 -8
- package/dist/mcp/daemon-tools.d.ts +2 -11
- package/dist/mcp/events-tool.d.ts +0 -1
- package/dist/mcp/server.d.ts +33 -14
- package/dist/native-scheme.d.ts +0 -1
- package/dist/output.d.ts +8 -11
- package/dist/package-root.d.ts +11 -0
- package/dist/package-version.d.ts +0 -1
- package/dist/qr-terminal.d.ts +0 -1
- package/dist/rpc/client.d.ts +3 -3
- package/dist/scheme.d.ts +60 -2
- package/dist/spki-pin.d.ts +0 -1
- package/package.json +6 -3
- package/dist/artifact-inspect.d.ts.map +0 -1
- package/dist/artifact-inspect.js +0 -334
- package/dist/bin.d.ts.map +0 -1
- package/dist/cli/command-options.d.ts.map +0 -1
- package/dist/cli/command-options.js +0 -95
- package/dist/cli/create-cli.d.ts.map +0 -1
- package/dist/cli/create-cli.js +0 -56
- package/dist/cli/dispatch.d.ts.map +0 -1
- package/dist/cli/dispatch.js +0 -275
- package/dist/cli/open-target.d.ts.map +0 -1
- package/dist/cli/open-target.js +0 -438
- package/dist/cli/result-types.d.ts.map +0 -1
- package/dist/cli/result-types.js +0 -6
- package/dist/cli/runner.d.ts.map +0 -1
- package/dist/cli/runner.js +0 -111
- package/dist/cli/types.d.ts.map +0 -1
- package/dist/cli/types.js +0 -10
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -2
- package/dist/client/app-client.d.ts.map +0 -1
- package/dist/client/app-client.js +0 -223
- package/dist/client/bootstrap.d.ts.map +0 -1
- package/dist/client/bootstrap.js +0 -108
- package/dist/client/connect.d.ts.map +0 -1
- package/dist/client/connect.js +0 -38
- package/dist/client/errors.d.ts.map +0 -1
- package/dist/client/errors.js +0 -40
- package/dist/client/index.d.ts.map +0 -1
- package/dist/commands/daemon.d.ts +0 -22
- package/dist/commands/daemon.d.ts.map +0 -1
- package/dist/commands/daemon.js +0 -128
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js +0 -42
- package/dist/commands/events.d.ts.map +0 -1
- package/dist/commands/events.js +0 -76
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -231
- package/dist/commands/invoke.d.ts.map +0 -1
- package/dist/commands/invoke.js +0 -54
- package/dist/commands/keygen.d.ts.map +0 -1
- package/dist/commands/keygen.js +0 -46
- package/dist/commands/link.d.ts.map +0 -1
- package/dist/commands/link.js +0 -43
- package/dist/commands/ls.d.ts.map +0 -1
- package/dist/commands/ls.js +0 -7
- package/dist/commands/mcp.d.ts.map +0 -1
- package/dist/commands/mcp.js +0 -98
- package/dist/commands/revoke.d.ts.map +0 -1
- package/dist/commands/revoke.js +0 -7
- package/dist/commands/tools.d.ts.map +0 -1
- package/dist/commands/tools.js +0 -52
- package/dist/daemon/address.d.ts.map +0 -1
- package/dist/daemon/address.js +0 -71
- package/dist/daemon/audit.d.ts.map +0 -1
- package/dist/daemon/audit.js +0 -237
- package/dist/daemon/calls.d.ts.map +0 -1
- package/dist/daemon/calls.js +0 -158
- package/dist/daemon/config.d.ts.map +0 -1
- package/dist/daemon/config.js +0 -170
- package/dist/daemon/daemon.d.ts.map +0 -1
- package/dist/daemon/daemon.js +0 -560
- package/dist/daemon/event-bus.d.ts.map +0 -1
- package/dist/daemon/event-bus.js +0 -111
- package/dist/daemon/links.d.ts.map +0 -1
- package/dist/daemon/links.js +0 -118
- package/dist/daemon/listener.d.ts.map +0 -1
- package/dist/daemon/listener.js +0 -132
- package/dist/daemon/log-rotation.d.ts.map +0 -1
- package/dist/daemon/log-rotation.js +0 -136
- package/dist/daemon/pidfile.d.ts.map +0 -1
- package/dist/daemon/pidfile.js +0 -90
- package/dist/daemon/policy.d.ts.map +0 -1
- package/dist/daemon/policy.js +0 -24
- package/dist/daemon/registry.d.ts.map +0 -1
- package/dist/daemon/registry.js +0 -31
- package/dist/daemon/rpc-server.d.ts.map +0 -1
- package/dist/daemon/rpc-server.js +0 -203
- package/dist/daemon/sessions.d.ts.map +0 -1
- package/dist/daemon/sessions.js +0 -429
- package/dist/daemon/socket-probe.d.ts.map +0 -1
- package/dist/daemon/socket-probe.js +0 -28
- package/dist/daemon/state-dir.d.ts.map +0 -1
- package/dist/daemon/state-dir.js +0 -43
- package/dist/daemon/timers.d.ts.map +0 -1
- package/dist/daemon/timers.js +0 -12
- package/dist/daemon/tls.d.ts.map +0 -1
- package/dist/daemon/tls.js +0 -79
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -162
- package/dist/host-certificate.d.ts.map +0 -1
- package/dist/host-certificate.js +0 -111
- package/dist/index.d.ts.map +0 -1
- package/dist/key-material.d.ts.map +0 -1
- package/dist/key-material.js +0 -42
- package/dist/link.d.ts.map +0 -1
- package/dist/link.js +0 -117
- package/dist/mcp/connect-tool.d.ts.map +0 -1
- package/dist/mcp/connect-tool.js +0 -396
- package/dist/mcp/daemon-tools.d.ts.map +0 -1
- package/dist/mcp/daemon-tools.js +0 -29
- package/dist/mcp/events-tool.d.ts.map +0 -1
- package/dist/mcp/events-tool.js +0 -322
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/mcp/server.js +0 -538
- package/dist/mcp/tool-mapping.d.ts +0 -58
- package/dist/mcp/tool-mapping.d.ts.map +0 -1
- package/dist/mcp/tool-mapping.js +0 -143
- package/dist/mcp/tool-namespace.d.ts +0 -40
- package/dist/mcp/tool-namespace.d.ts.map +0 -1
- package/dist/mcp/tool-namespace.js +0 -65
- package/dist/native-scheme.d.ts.map +0 -1
- package/dist/native-scheme.js +0 -465
- package/dist/output.d.ts.map +0 -1
- package/dist/output.js +0 -342
- package/dist/package-version.d.ts.map +0 -1
- package/dist/package-version.js +0 -47
- package/dist/qr-terminal.d.ts.map +0 -1
- package/dist/qr-terminal.js +0 -45
- package/dist/rpc/client.d.ts.map +0 -1
- package/dist/rpc/client.js +0 -819
- package/dist/scheme.d.ts.map +0 -1
- package/dist/scheme.js +0 -321
- package/dist/spki-pin.d.ts.map +0 -1
- 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
|
|
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
|
|
12
|
-
- **Same surface for humans and agents
|
|
13
|
-
- **Hardened control plane
|
|
14
|
-
- **Fits production-minded apps
|
|
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
|
|
|
@@ -29,33 +29,91 @@ appduct tools
|
|
|
29
29
|
appduct invoke sum --input '{"a":2,"b":3}'
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
That
|
|
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>] [--
|
|
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
|
|
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]` |
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
132
|
+
It's idempotent and safe to re-run:
|
|
77
133
|
|
|
78
|
-
- A plain re-run keeps whatever scheme is already recorded. If discovery
|
|
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
|
-
|
|
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
|
-
|
|
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>'
|
|
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> <
|
|
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
|
-
|
|
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
|
-
|
|
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`
|
|
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
|
|
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
|
|
108
|
-
- The app's **bundle id
|
|
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 --
|
|
171
|
+
appduct link --scheme myapp --open ios-device --app-id com.example.myapp
|
|
112
172
|
```
|
|
113
173
|
|
|
114
|
-
Two
|
|
174
|
+
Two things worth knowing before you rely on this:
|
|
115
175
|
|
|
116
|
-
- **What happens when the app is already running
|
|
117
|
-
- **A physical iPhone is never auto-detected.** `appduct_connect` called without a `target` only
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
|
@@ -160,19 +232,19 @@ import { connect } from "appduct/client";
|
|
|
160
232
|
// auto-spawns the daemon and picks the single session, or pass { selector: "pixel-8" } to target one explicitly
|
|
161
233
|
const app = await connect();
|
|
162
234
|
|
|
163
|
-
await app.tools(); //
|
|
235
|
+
await app.tools(); // ListedToolDescriptor[]; an ungrouped tool's group is null
|
|
164
236
|
const { total } = await app.call("sum", { a: 2, b: 3 });
|
|
165
237
|
const { payload } = await app.waitForEvent("checkout_done", { timeoutMs: 5_000 });
|
|
166
238
|
app.close();
|
|
167
239
|
```
|
|
168
240
|
|
|
169
|
-
Errors surface as
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
@@ -109,4 +109,3 @@ export type InspectArtifactOptions = {
|
|
|
109
109
|
* prevents a truthful presence/absence answer (missing tool, unreadable/corrupt artifact) — it
|
|
110
110
|
* never resolves successfully with a guessed or default answer. */
|
|
111
111
|
export declare const inspectArtifact: (artifactPath: string, options?: InspectArtifactOptions) => Promise<ArtifactInspection>;
|
|
112
|
-
//# sourceMappingURL=artifact-inspect.d.ts.map
|
package/dist/bin.d.ts
CHANGED