appduct 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -317
- package/dist/cli/routes/daemon/run.js +4 -3
- package/dist/client/app-client.d.ts +8 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +28 -14
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,204 +1,68 @@
|
|
|
1
|
-
|
|
1
|
+
<a href="https://callstackincubator.github.io/appduct/"><img src="https://raw.githubusercontent.com/callstackincubator/appduct/main/.github/assets/readme-banner.png" alt="Appduct by Callstack. Your app is an MCP server now: your agent calls its functions over MCP, on iOS, Android, React Native, Flutter and web." width="100%" /></a>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Appduct CLI, MCP server and test client
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Call tools inside a running iOS, Android, React Native, Flutter or Web app from your terminal, an AI agent or a test suite. The `appduct` package connects to apps that have an Appduct SDK installed and tools registered.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Requirements
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
- **Same surface for humans and agents.** The CLI (`sessions`, `tools`, `events`, ...) and `appduct mcp` use the same RPC methods, so an agent sees the tools a human operator does.
|
|
13
|
-
- **Local to your user account.** The CLI and MCP server reach the daemon through a local socket only your user account can open, and the daemon's key and logs are readable only by you (see [Security][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.
|
|
9
|
+
- Node 20 or newer.
|
|
10
|
+
- An Appduct-enabled app. Choose an [SDK setup guide](https://callstackincubator.github.io/appduct/start/introduction/#packages), or [try a playground](https://callstackincubator.github.io/appduct/start/playground/).
|
|
11
|
+
- For automatic device delivery: Xcode for an iOS Simulator, or `adb` for Android. Physical devices can use a QR code; Web pages use their URL.
|
|
15
12
|
|
|
16
13
|
## Getting started
|
|
17
14
|
|
|
15
|
+
Install the CLI:
|
|
16
|
+
|
|
18
17
|
```bash
|
|
19
18
|
npm install -g appduct
|
|
20
|
-
appduct sessions link --qr
|
|
21
19
|
```
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
Scan the QR (or open the deep link) in an Appduct-enabled app, then:
|
|
21
|
+
With your app running, connect from its project directory:
|
|
26
22
|
|
|
27
23
|
```bash
|
|
28
|
-
appduct
|
|
29
|
-
appduct tools call sum --input '{"a":2,"b":3}'
|
|
30
|
-
```
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
## Commands
|
|
35
|
-
|
|
36
|
-
Every command is `appduct <noun> <verb> [selector] [args]`, the same shape as `appduct daemon run|start|stop|status`:
|
|
37
|
-
|
|
38
|
-
| Command | Role |
|
|
39
|
-
| --- | --- |
|
|
40
|
-
| `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 |
|
|
41
|
-
| `appduct keygen [--out <path>] [--force]` | generate a daemon private key, print its app pin |
|
|
42
|
-
| `appduct sessions ls` | list sessions: alias, state, device, tool count |
|
|
43
|
-
| `appduct sessions link [--ttl <s>] [--qr] [--open android\|ios-sim\|ios-device] [--device <id>] [--app-id <id>] [--relaunch] [--scheme <s>]` | mint a pending session and print its deep link |
|
|
44
|
-
| `appduct sessions revoke [selector]` | revoke a session |
|
|
45
|
-
| `appduct tools ls [selector] [--full] [--group <name>] [--filter <text>] [--limit <n>] [--offset <n>]` | list a session's tools (one call signature + description per line) |
|
|
46
|
-
| `appduct tools ls [selector] --groups` | list a session's tool groups with their tool counts |
|
|
47
|
-
| `appduct tools describe [selector] <name>` | show one tool's full schema |
|
|
48
|
-
| `appduct tools call [selector] <name> --input '<json>' [--timeout <ms>]` | call a tool |
|
|
49
|
-
| `appduct events ls [selector] [--name <glob>] [--limit <n>] [--offset <n>]` | list the events the app declares, one signature line each; an exact `--name` prints the full payload schema; `--limit`/`--offset` page a long list, ending with a `Showing n of total events (offset o).` line when events were left out; `--json` carries the full descriptors, with `total` counting matches before paging |
|
|
50
|
-
| `appduct events tail [selector] [--follow] [--name <glob>] [--payload-max-bytes <n>]` | stream the events the app posts with `postEvent`; `--name` keeps only matching names, `--payload-max-bytes` cuts each payload down to a preview; `--json` emits NDJSON |
|
|
51
|
-
| `appduct events since [selector] <cursor> [--name <glob>] [--payload-max-bytes <n>]` | one-shot pull the events retained since `<cursor>`, with the same filters as `tail`; `--json` emits NDJSON |
|
|
52
|
-
| `appduct daemon run\|start\|stop\|status` | daemon lifecycle |
|
|
53
|
-
| `appduct mcp [--scheme <s>]` | start a stdio MCP server proxying connected apps' tools to MCP clients |
|
|
54
|
-
| `appduct doctor <artifact> [--assert-present\|--assert-absent]` | release-gate step: report or assert whether a built `.app`/`.ipa`/`.apk`/`.aab` contains Appduct |
|
|
55
|
-
|
|
56
|
-
Every command that targets a session accepts an optional `selector` (a session id or an alias from `appduct sessions 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 <noun> --help` for the exact flags any of its verbs takes.
|
|
57
|
-
|
|
58
|
-
`--timeout` on `tools call` 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.
|
|
59
|
-
|
|
60
|
-
### `appduct tools ls`: a signature per tool
|
|
61
|
-
|
|
62
|
-
`appduct tools ls` 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:
|
|
63
|
-
|
|
64
|
-
```
|
|
65
|
-
Tools
|
|
66
|
-
seed_cart(items: int, sku?: string, clear?: bool = true) -> { added: int, cartId: string }
|
|
67
|
-
Fill the cart with test items for the current user.
|
|
68
|
-
set_flag(name: "dark_mode" | "new_checkout", enabled: bool) [prompt]
|
|
69
|
-
Toggle a feature flag.
|
|
70
|
-
|
|
71
|
-
Run `appduct tools describe <name>` for a tool's full schema.
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
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 describe <name>`) still has it. A `[prompt]`/`[deny]` tag follows a tool whose effective policy isn't `"allow"`.
|
|
75
|
-
|
|
76
|
-
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 ls --json` returns `{ tools, total, groups }` — `total` is the count after `--group`/`--filter` but before `--limit`/`--offset`. `appduct tools describe <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 `describe` is a usage error.
|
|
77
|
-
|
|
78
|
-
### Tool groups
|
|
79
|
-
|
|
80
|
-
When the app puts its tools in groups ([Write tools](https://callstackincubator.github.io/appduct/guides/writing-tools/#group-tools-in-a-large-app)), `appduct tools ls` lists them under group headings, with subgroups indented under their parent and ungrouped tools last:
|
|
81
|
-
|
|
82
|
-
```
|
|
83
|
-
Tools
|
|
84
|
-
cart
|
|
85
|
-
add_item(sku: string, quantity: number)
|
|
86
|
-
Add a product to the cart
|
|
87
|
-
checkout
|
|
88
|
-
begin_checkout()
|
|
89
|
-
Start checkout with the current cart
|
|
90
|
-
checkout/payment
|
|
91
|
-
pay_with_card(card: string)
|
|
92
|
-
Pay for the order with a test card
|
|
93
|
-
(ungrouped)
|
|
94
|
-
reset_app()
|
|
95
|
-
Clear all local data
|
|
24
|
+
appduct sessions link --qr
|
|
96
25
|
```
|
|
97
26
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
```
|
|
101
|
-
$ appduct tools ls --groups
|
|
102
|
-
Groups
|
|
103
|
-
cart 12
|
|
104
|
-
checkout 8
|
|
105
|
-
checkout/payment 3
|
|
106
|
-
(ungrouped) 2
|
|
27
|
+
Scan the QR code with the device's camera. The app needs a URL scheme, which the CLI can discover from static project files; use `--scheme myapp` if needed. A physical device must be on the same local network as your computer.
|
|
107
28
|
|
|
108
|
-
|
|
29
|
+
For a simulator, Android device or Web page, choose one of these paths instead:
|
|
109
30
|
|
|
110
|
-
|
|
31
|
+
```bash
|
|
32
|
+
appduct sessions link --open ios-sim
|
|
33
|
+
appduct sessions link --open android --app-id com.example.shop
|
|
34
|
+
appduct sessions link --open web http://localhost:5173/
|
|
111
35
|
```
|
|
112
36
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
### The deep-link scheme
|
|
116
|
-
|
|
117
|
-
`appduct sessions link`, `appduct mcp`, and the MCP `appduct_connect` tool all resolve the scheme the same way, first match wins:
|
|
118
|
-
|
|
119
|
-
1. `--scheme <s>`
|
|
120
|
-
2. the `APPDUCT_SCHEME` environment variable
|
|
121
|
-
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)
|
|
122
|
-
4. `scheme` in `<state-dir>/config.json`
|
|
123
|
-
5. a static-file project probe, tried in this order (no walk-up — an app root is where you run these commands):
|
|
124
|
-
1. `<cwd>/app.json`'s `expo.scheme` — a string, or the first entry of an array
|
|
125
|
-
2. **Android**: `app/build.gradle.kts` or `app/build.gradle`'s `appductScheme` manifest placeholder (`manifestPlaceholders["appductScheme"] = "myapp"` or `manifestPlaceholders.appductScheme = "myapp"`), then `app/src/main/AndroidManifest.xml`'s first `<data android:scheme>` inside an intent filter that also declares `android.intent.action.VIEW`
|
|
126
|
-
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`
|
|
127
|
-
6. otherwise an error naming every location above
|
|
128
|
-
|
|
129
|
-
In a Flutter project root (a directory with a `pubspec.yaml`), the Android probes read `android/app/` instead of `app/`, and the iOS and macOS ones find `ios/Runner/Info.plist` and `macos/Runner/Info.plist`. `appduct init` there prints the Flutter setup steps instead of the React Native reminder.
|
|
130
|
-
|
|
131
|
-
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.
|
|
132
|
-
|
|
133
|
-
**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.
|
|
134
|
-
|
|
135
|
-
`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 wiring your app still needs: the `import "@appduct/react-native/auto"` reminder for a React Native or Expo app, or the step a plain iOS or Android app needs (`Appduct.shared.handle(url)`, or the `appductScheme` placeholder). A `react-native` or `expo` dependency in your `package.json` is what says this is a React Native app; running `init` from the app's `ios/` or `android/` directory finds that manifest where the native toolchain puts you. It never generates keys or touches daemon state.
|
|
136
|
-
|
|
137
|
-
`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"`. `platforms` lists every native platform discovery saw (`["android", "ios"]` for a root that spans both), on every run including `"already-recorded"` and `--scheme`, and is left out when it saw none; `init` prints the wiring step for each platform in it.
|
|
138
|
-
|
|
139
|
-
It's idempotent and safe to re-run:
|
|
140
|
-
|
|
141
|
-
- 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.
|
|
142
|
-
- `--scheme <different>` needs `--force` to replace a recorded scheme; `--force` on its own re-adopts `app.json`'s value.
|
|
143
|
-
- `--force` merges into the existing JSON rather than truncating it.
|
|
144
|
-
|
|
145
|
-
`--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`.
|
|
146
|
-
|
|
147
|
-
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.
|
|
148
|
-
|
|
149
|
-
`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.
|
|
150
|
-
|
|
151
|
-
### Delivering the link to a device
|
|
152
|
-
|
|
153
|
-
`--open` hands the freshly minted deep link straight to a device, so nothing has to scan a QR code:
|
|
154
|
-
|
|
155
|
-
| `--open` | Mechanism | Address in the link |
|
|
156
|
-
| --- | --- | --- |
|
|
157
|
-
| `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) |
|
|
158
|
-
| `ios-sim` | `xcrun simctl openurl <udid> <link>` | `127.0.0.1` (the simulator shares the host's network) |
|
|
159
|
-
| `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 |
|
|
160
|
-
|
|
161
|
-
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.
|
|
162
|
-
|
|
163
|
-
**`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.
|
|
164
|
-
|
|
165
|
-
#### `--open ios-device` (experimental)
|
|
166
|
-
|
|
167
|
-
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.
|
|
168
|
-
|
|
169
|
-
It needs all of:
|
|
170
|
-
|
|
171
|
-
- **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.
|
|
172
|
-
- The device **paired and trusted** by this Mac, with **Developer Mode** enabled on it (Settings → Privacy & Security → Developer Mode).
|
|
173
|
-
- A **development-signed build of your app already installed** — `devicectl` launches an installed app, it doesn't install one.
|
|
174
|
-
- The phone and this machine **on the same network**, reachable at the address the link advertises. `appduct sessions 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`.
|
|
175
|
-
- The app's **bundle id** — see [Delivering the link to a device](#delivering-the-link-to-a-device) for how to supply it.
|
|
37
|
+
The Web command prints a URL to open. Once connected, list tools and call one your app registered:
|
|
176
38
|
|
|
177
39
|
```bash
|
|
178
|
-
appduct
|
|
40
|
+
appduct tools ls
|
|
41
|
+
appduct tools call seed_cart --input '{"items":3}'
|
|
179
42
|
```
|
|
180
43
|
|
|
181
|
-
|
|
44
|
+
The SDK quick starts register this example tool, which returns `{"added":3}`. Appduct starts its background service automatically.
|
|
182
45
|
|
|
183
|
-
|
|
184
|
-
- **A physical iPhone is never auto-detected.** `appduct_connect` called without a `target` only considers booted simulators and attached Android devices, and `appduct sessions 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.
|
|
46
|
+
## Common commands
|
|
185
47
|
|
|
186
|
-
|
|
48
|
+
| Command | What you can do |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `appduct init` | Record your project's scheme and app ids, and print MCP configuration. |
|
|
51
|
+
| `appduct sessions ls` | Find connected sessions and their aliases. |
|
|
52
|
+
| `appduct tools ls` | Find registered tools; use `--groups` or `--group cart` for larger registries. |
|
|
53
|
+
| `appduct tools describe seed_cart` | Read a tool's full schema. |
|
|
54
|
+
| `appduct tools call seed_cart --input '{"items":3}'` | Call a tool. |
|
|
55
|
+
| `appduct events ls` | Find the events the app declares. |
|
|
56
|
+
| `appduct events tail --follow` | Watch events the app posts. |
|
|
57
|
+
| `appduct sessions revoke` | Disconnect a session. |
|
|
58
|
+
| `appduct daemon status` | Inspect the background service. |
|
|
59
|
+
| `appduct doctor build/Shop.ipa --assert-absent` | Check that a built app excludes Appduct. |
|
|
187
60
|
|
|
188
|
-
|
|
61
|
+
Run `appduct <command> --help` for flags, or read the [CLI reference](https://callstackincubator.github.io/appduct/reference/cli/). Use `--json` for scripts.
|
|
189
62
|
|
|
190
|
-
|
|
191
|
-
{
|
|
192
|
-
"mcpServers": {
|
|
193
|
-
"appduct": {
|
|
194
|
-
"command": "appduct",
|
|
195
|
-
"args": ["mcp"]
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
```
|
|
63
|
+
## Connect an agent
|
|
200
64
|
|
|
201
|
-
|
|
65
|
+
Add this entry to your agent's MCP configuration, replacing `myapp` with your app's scheme:
|
|
202
66
|
|
|
203
67
|
```json
|
|
204
68
|
{
|
|
@@ -211,151 +75,44 @@ An MCP server is usually launched with a working directory you don't control, so
|
|
|
211
75
|
}
|
|
212
76
|
```
|
|
213
77
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
| Tool | Does what | CLI equivalent |
|
|
217
|
-
| --- | --- | --- |
|
|
218
|
-
| `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 ls` |
|
|
219
|
-
| `appduct_describe_tool` | Shows one tool's full input and output schema. | `appduct tools describe <name>` |
|
|
220
|
-
| `appduct_call_tool` | Calls a tool by `name` with `args`, with progress and errors preserved. | `appduct tools call` |
|
|
221
|
-
|
|
222
|
-
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 sessions ls`.
|
|
223
|
-
|
|
224
|
-
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.
|
|
78
|
+
The agent can connect a device, list your app's tools, inspect their schemas and call them. App tools are reached through `appduct_list_tools`, `appduct_describe_tool` and `appduct_call_tool`; event and connection tools are available too. See [Use Appduct with an agent](https://callstackincubator.github.io/appduct/guides/agents/) for client setup and approval policy.
|
|
225
79
|
|
|
226
|
-
|
|
80
|
+
## Call tools from tests
|
|
227
81
|
|
|
228
|
-
|
|
82
|
+
<a id="test-runners-appductclient"></a>
|
|
229
83
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
## Test runners: `appduct/client`
|
|
233
|
-
|
|
234
|
-
A thin typed wrapper over the same daemon RPC the CLI and MCP server use — for a Jest/Vitest/Detox spec that drives a running app without spawning `appduct tools call ... --json` and parsing stdout:
|
|
84
|
+
Install `appduct` as a dev dependency of your test project. With the app connected:
|
|
235
85
|
|
|
236
86
|
```ts
|
|
237
87
|
import { connect } from "appduct/client";
|
|
238
88
|
|
|
239
|
-
// auto-spawns the daemon and picks the single session, or pass { selector: "pixel-8" } to target one explicitly
|
|
240
89
|
const app = await connect();
|
|
241
|
-
|
|
242
|
-
await app.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
app.close();
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
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:
|
|
249
|
-
|
|
250
|
-
```ts
|
|
251
|
-
await expect(app.call("checkout", {})).rejects.toMatchObject({ type: "policy_denied" });
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
Pair a simulator/emulator in a `globalSetup` without shelling out, via `link`/`waitForSession`:
|
|
255
|
-
|
|
256
|
-
```ts
|
|
257
|
-
import { link, waitForSession } from "appduct/client";
|
|
258
|
-
|
|
259
|
-
const { sessionId } = await link({ target: "ios-sim" });
|
|
260
|
-
const app = await waitForSession(sessionId, { timeoutMs: 60_000 });
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
`app.call`'s tool name and args/result types can't be inferred automatically — tools are registered by the connected app at runtime — so declare your own tool map once for typed calls throughout a suite:
|
|
264
|
-
|
|
265
|
-
```ts
|
|
266
|
-
type Tools = {
|
|
267
|
-
sum: { args: { a: number; b: number }; result: { total: number } };
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
const app = await connect<Tools>();
|
|
271
|
-
const { total } = await app.call("sum", { a: 2, b: 3 }); // typed
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
`waitForEvent`'s first argument is a whole-name glob (`*` waits for any name, `"cart.*"` for any name starting with `cart.`). It first drains the daemon's per-session retained buffer for an already-arrived match before falling back to a live wait, so it's safe to call after the action that emits the event. Pass `since` (the `cursor` from a previous `app.events()`/`waitForEvent()` call) to skip events already handled, and `payloadMaxBytes` to cap the resolved event's payload the same way `app.events()` does — without it the payload always comes back whole:
|
|
275
|
-
|
|
276
|
-
```ts
|
|
277
|
-
const { events, cursor } = await app.events(); // pull: what already happened
|
|
278
|
-
const next = await app.waitForEvent("checkout_done", { since: cursor });
|
|
279
|
-
const capped = await app.waitForEvent("checkout_done", { payloadMaxBytes: 1024 }); // check `truncated` before reading `payload`
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
For anything the client doesn't cover, the package's main entry exports `runCli(argv, { stdout, stderr })`, which runs any `appduct` command in your Node or Bun process and resolves to its exit code.
|
|
283
|
-
|
|
284
|
-
## Keys and pins
|
|
285
|
-
|
|
286
|
-
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 sessions link` carries that fingerprint on the deep link for the app to pick up. See [Security][security]'s "Trust modes" for what that does and doesn't protect.
|
|
287
|
-
|
|
288
|
-
For a build that should trust only a key you embedded ahead of time, generate one explicitly:
|
|
289
|
-
|
|
290
|
-
```bash
|
|
291
|
-
appduct keygen
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
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.
|
|
295
|
-
|
|
296
|
-
The deep link `appduct sessions link` prints carries the daemon's current fingerprint in its `pin` parameter. A build with embedded `cliPins` ignores it: embedded pins always win, in every build type. A build with no embedded pins trusts it for that one session.
|
|
297
|
-
|
|
298
|
-
## Daemon lifecycle
|
|
299
|
-
|
|
300
|
-
The daemon auto-starts the first time any CLI or MCP command needs it — you don't normally run `appduct daemon start` yourself. It keeps its key, config, logs and audit log in the state directory (`~/.appduct` by default), runs one instance per state directory, and keeps running whatever happens to a device, so an app reload or crash never takes the daemon down.
|
|
301
|
-
|
|
302
|
-
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.
|
|
303
|
-
|
|
304
|
-
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.
|
|
305
|
-
|
|
306
|
-
No 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, a `daemon.log` over `daemonLogMaxBytes` (default 10 MiB) is rotated to `daemon.log.1` when a daemon is next spawned, and `events.log` is rotated to `events.log.1` as soon as it passes `eventsLogMaxBytes` (default 10 MiB). `events.log` records the daemon's own events (links, sessions, tool registries and tool calls), one JSON line each, for debugging Appduct itself; your app's events are never written to it. All three are `config.json` keys; see below.
|
|
307
|
-
|
|
308
|
-
### Daemon settings: `config.json`
|
|
309
|
-
|
|
310
|
-
`<state-dir>/config.json` (`~/.appduct/config.json` by default) is optional, and so is every key in it. The daemon reads it when it starts, so after editing it run `appduct daemon stop`; the next command starts a fresh daemon with the new values. Stopping the daemon disconnects every device.
|
|
311
|
-
|
|
312
|
-
| Key | Default | What it sets |
|
|
313
|
-
| --- | --- | --- |
|
|
314
|
-
| `wssPort` | `8443` | Port devices connect to. `0` picks a free port. |
|
|
315
|
-
| `graceSeconds` | `600` | How long a disconnected session can still resume. |
|
|
316
|
-
| `linkTtlSeconds` | `300` | How long a new link stays valid. Override per link with `--ttl`. |
|
|
317
|
-
| `eventBufferSize` | `256` | App events kept per session for `events since` and `waitForEvent`. |
|
|
318
|
-
| `advertisedIp` | detected | Address put in links for physical devices. |
|
|
319
|
-
| `scheme` | none | Fallback deep-link scheme (step 4 of [The deep-link scheme](#the-deep-link-scheme)). |
|
|
320
|
-
| `restartDaemonOnVersionMismatch` | `false` | Always replace an outdated daemon, even with devices connected. |
|
|
321
|
-
| `policy` | `{ "default": "allow", "destructive": "allow" }` | Which tools callers may run without approval; see [Security][security]. |
|
|
322
|
-
| `auditRetentionDays` | `30` | Days of audit log to keep. |
|
|
323
|
-
| `daemonLogMaxBytes`, `eventsLogMaxBytes` | 10 MiB | Size at which `daemon.log` and `events.log` are rotated. |
|
|
324
|
-
|
|
325
|
-
`keyPath` and `keepaliveIntervalSeconds` exist too; you rarely need them. An unknown key prints a warning and is ignored.
|
|
326
|
-
|
|
327
|
-
## Release gate: `appduct doctor`
|
|
328
|
-
|
|
329
|
-
Appduct's inclusion in a build is controlled entirely by autolinking exclusion (see [Build variants][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:
|
|
330
|
-
|
|
331
|
-
```bash
|
|
332
|
-
appduct doctor ./build/MyApp.ipa --assert-absent
|
|
333
|
-
appduct doctor ./build/app-release.apk --assert-absent
|
|
90
|
+
try {
|
|
91
|
+
const result = await app.call("seed_cart", { items: 3 });
|
|
92
|
+
// result: { added: 3 }
|
|
93
|
+
} finally {
|
|
94
|
+
app.close();
|
|
95
|
+
}
|
|
334
96
|
```
|
|
335
97
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
| Code | Meaning |
|
|
339
|
-
| --- | --- |
|
|
340
|
-
| `0` | The artifact was inspected, and any `--assert-present`/`--assert-absent` held |
|
|
341
|
-
| `3` | The artifact was inspected and the assertion failed |
|
|
342
|
-
| `66` | The artifact couldn't be inspected: it's missing, unreadable or corrupt, or a tool `doctor` needs (such as `unzip`) isn't installed |
|
|
98
|
+
The client also pairs devices, waits for sessions, reads events and supports a typed tool map. Failures reject with an `AppductError` whose `type` names the error. See [Test setup](https://callstackincubator.github.io/appduct/guides/cli/#drive-the-app-from-a-test-suite) and the [client reference](https://callstackincubator.github.io/appduct/reference/cli/#appductclient).
|
|
343
99
|
|
|
344
|
-
|
|
100
|
+
<a id="the-deep-link-scheme"></a>
|
|
345
101
|
|
|
346
|
-
##
|
|
102
|
+
## Things to know
|
|
347
103
|
|
|
348
|
-
- **
|
|
349
|
-
- **[
|
|
350
|
-
- **
|
|
104
|
+
- **Select a session when several are connected.** Pass an alias or session id from `appduct sessions ls`; the CLI won't guess.
|
|
105
|
+
- **Declare longer timeouts in the app.** `tools call --timeout` can shorten a deadline, but it can't extend the app's timer. See [Long-running tools](https://callstackincubator.github.io/appduct/guides/writing-tools/#long-running-tools).
|
|
106
|
+
- **Keep project settings and private state separate.** `appduct init` writes project settings to `.appduct/config.json`. The daemon keeps its key and logs in `~/.appduct`; don't commit that directory.
|
|
107
|
+
- **Pin builds you distribute.** `appduct keygen` prints a pin to embed in the app. See [Security](https://callstackincubator.github.io/appduct/guides/security/) for trust, policy and key rotation.
|
|
108
|
+
- **Reconnect after stopping the daemon.** `appduct daemon stop` disconnects apps. Config changes require a restart; see [Service configuration](https://callstackincubator.github.io/appduct/reference/cli/#background-service-state-dirconfigjson).
|
|
351
109
|
|
|
352
|
-
##
|
|
110
|
+
## Learn more
|
|
353
111
|
|
|
354
|
-
- [
|
|
355
|
-
- [
|
|
356
|
-
- [
|
|
357
|
-
- [Build variants]
|
|
358
|
-
- [Monorepo README](https://github.com/callstackincubator/appduct/blob/main/README.md)
|
|
112
|
+
- [CLI guide](https://callstackincubator.github.io/appduct/guides/cli/): devices, events and scripting
|
|
113
|
+
- [CLI reference](https://callstackincubator.github.io/appduct/reference/cli/): commands, configuration, MCP tools and client API
|
|
114
|
+
- SDKs: [iOS](https://callstackincubator.github.io/appduct/install/ios/), [Android](https://callstackincubator.github.io/appduct/install/android/), [React Native](https://callstackincubator.github.io/appduct/install/react-native/), [Flutter](https://callstackincubator.github.io/appduct/install/flutter/), [Web](https://callstackincubator.github.io/appduct/install/web/)
|
|
115
|
+
- [Build variants](https://callstackincubator.github.io/appduct/guides/build-variants/): inclusion and artifact checks
|
|
359
116
|
|
|
360
117
|
## Made with ❤️ at Callstack
|
|
361
118
|
|
|
@@ -363,18 +120,4 @@ A pipeline should fail on anything other than `0`. `66` never means "absent", so
|
|
|
363
120
|
|
|
364
121
|
Like the project? ⚛️ [Join the team](https://callstack.com/careers/?utm_campaign=Senior_RN&utm_source=github&utm_medium=readme) who does amazing stuff for clients and drives React Native Open Source! 🔥
|
|
365
122
|
|
|
366
|
-
[appduct-banner]: https://img.shields.io/badge/Appduct-callstack%2Fincubator-111827?style=for-the-badge&logo=github&logoColor=white
|
|
367
|
-
[repo]: https://github.com/callstackincubator/appduct
|
|
368
123
|
[callstack-readme-with-love]: https://callstack.com/?utm_source=github.com&utm_medium=referral&utm_campaign=appduct&utm_term=readme-with-love
|
|
369
|
-
[architecture]: https://github.com/callstackincubator/appduct/blob/main/docs/ARCHITECTURE.md
|
|
370
|
-
[protocol]: https://github.com/callstackincubator/appduct/blob/main/docs/PROTOCOL.md
|
|
371
|
-
[security]: https://callstackincubator.github.io/appduct/guides/security/
|
|
372
|
-
[build-variants]: https://callstackincubator.github.io/appduct/guides/build-variants/
|
|
373
|
-
[license-badge]: https://img.shields.io/npm/l/appduct?style=for-the-badge
|
|
374
|
-
[license]: https://github.com/callstackincubator/appduct/blob/main/LICENSE
|
|
375
|
-
[npm-downloads-badge]: https://img.shields.io/npm/dm/appduct?style=for-the-badge
|
|
376
|
-
[npm-downloads]: https://www.npmjs.com/package/appduct
|
|
377
|
-
[prs-welcome-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge
|
|
378
|
-
[prs-welcome]: https://github.com/callstackincubator/appduct/pulls
|
|
379
|
-
[chat-badge]: https://img.shields.io/discord/426714625279524876.svg?style=for-the-badge
|
|
380
|
-
[chat]: https://discord.gg/xgGt7KAjxv
|
|
@@ -39,13 +39,14 @@ var relayPage = (page, link, openDaemonSocket) => {
|
|
|
39
39
|
const message = JSON.parse(json);
|
|
40
40
|
if (message.kind === "open") {
|
|
41
41
|
closeCurrent(GOING_AWAY, "superseded");
|
|
42
|
+
const id = message.id;
|
|
42
43
|
const socket = openDaemonSocket(url, {
|
|
43
|
-
open: () => current === socket && tellPage({ kind: "open" }),
|
|
44
|
-
message: (text) => current === socket && tellPage({ kind: "message", text }),
|
|
44
|
+
open: () => current === socket && tellPage({ kind: "open", id }),
|
|
45
|
+
message: (text) => current === socket && tellPage({ kind: "message", id, text }),
|
|
45
46
|
close: (code, reason) => {
|
|
46
47
|
if (current !== socket) return;
|
|
47
48
|
current = void 0;
|
|
48
|
-
tellPage({ kind: "close", code, reason });
|
|
49
|
+
tellPage({ kind: "close", id, code, reason });
|
|
49
50
|
}
|
|
50
51
|
});
|
|
51
52
|
current = socket;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* (`mcp/events-tool.ts`) uses — called on a fresh stream opened for that one wait, so concurrent
|
|
9
9
|
* waits on the same `AppClient` never share (and can't clobber) one connection's subscription.
|
|
10
10
|
*/
|
|
11
|
-
import { type AppEvent, type FullAppEvent, type ListedToolDescriptor } from "@appduct/shared";
|
|
11
|
+
import { type AppEvent, type FullAppEvent, type ListedToolDescriptor, type SessionState } from "@appduct/shared";
|
|
12
12
|
import type { DaemonStream } from "../rpc/client.js";
|
|
13
13
|
/** A project's own tool map — declared once, e.g. `type Tools = { sum: { args: { a: number; b:
|
|
14
14
|
* number }; result: { total: number } } }` — to get typed {@link AppClient.call} throughout a test
|
|
@@ -93,6 +93,13 @@ export type EventsResult = EventsCounts & {
|
|
|
93
93
|
export type { AppEvent, FullAppEvent } from "@appduct/shared";
|
|
94
94
|
export type AppClient<TTools = ToolMap> = {
|
|
95
95
|
readonly sessionId: string;
|
|
96
|
+
/** The session's state now: `active` while the app is connected, `suspended` while it is away
|
|
97
|
+
* (its process died, it was backgrounded or a page closed) and may still come back. Rejects with
|
|
98
|
+
* `"unknown_session"` once the session is gone. */
|
|
99
|
+
state(): Promise<SessionState>;
|
|
100
|
+
/** Ends the session: the daemon disconnects the app and forgets the session, so it can never
|
|
101
|
+
* resume. Works in any state. */
|
|
102
|
+
revoke(): Promise<void>;
|
|
96
103
|
/** `tools.list` for this session. An entry spells an ungrouped tool's `group` as `null`,
|
|
97
104
|
* where a registration omits it. */
|
|
98
105
|
tools(): Promise<ListedToolDescriptor[]>;
|
package/dist/client/index.d.ts
CHANGED
|
@@ -17,4 +17,4 @@ export { link, waitForSession, type LinkOptions, type LinkResult, type WaitForSe
|
|
|
17
17
|
export { AppductError, type AppductErrorType } from "./errors.js";
|
|
18
18
|
export type { AppClient, AppEvent, CallOptions, EventsOptions, EventsResult, FullAppEvent, FullEventsResult, ToolMap, WaitForEventOptions, } from "./app-client.js";
|
|
19
19
|
export type { TruncatedAppEvent } from "@appduct/shared";
|
|
20
|
-
export type { AgentEndpoint, ErrorType, ListedToolDescriptor, ToolDescriptor } from "@appduct/shared";
|
|
20
|
+
export type { AgentEndpoint, ErrorType, ListedToolDescriptor, SessionState, ToolDescriptor } from "@appduct/shared";
|
package/dist/client/index.js
CHANGED
|
@@ -28,13 +28,14 @@ var relayPage = (page, link2, openDaemonSocket) => {
|
|
|
28
28
|
const message = JSON.parse(json);
|
|
29
29
|
if (message.kind === "open") {
|
|
30
30
|
closeCurrent(GOING_AWAY, "superseded");
|
|
31
|
+
const id = message.id;
|
|
31
32
|
const socket = openDaemonSocket(url, {
|
|
32
|
-
open: () => current === socket && tellPage({ kind: "open" }),
|
|
33
|
-
message: (text) => current === socket && tellPage({ kind: "message", text }),
|
|
33
|
+
open: () => current === socket && tellPage({ kind: "open", id }),
|
|
34
|
+
message: (text) => current === socket && tellPage({ kind: "message", id, text }),
|
|
34
35
|
close: (code, reason) => {
|
|
35
36
|
if (current !== socket) return;
|
|
36
37
|
current = void 0;
|
|
37
|
-
tellPage({ kind: "close", code, reason });
|
|
38
|
+
tellPage({ kind: "close", id, code, reason });
|
|
38
39
|
}
|
|
39
40
|
});
|
|
40
41
|
current = socket;
|
|
@@ -1331,6 +1332,20 @@ var makeAppClient = (stream, sessionId, openStream) => {
|
|
|
1331
1332
|
const openWaitStreams = /* @__PURE__ */ new Set();
|
|
1332
1333
|
const client = {
|
|
1333
1334
|
sessionId,
|
|
1335
|
+
state: async () => {
|
|
1336
|
+
try {
|
|
1337
|
+
return (await stream.call(RPC_METHODS3.sessionsDescribe, { selector: sessionId })).state;
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
throw toAppductError(error);
|
|
1340
|
+
}
|
|
1341
|
+
},
|
|
1342
|
+
revoke: async () => {
|
|
1343
|
+
try {
|
|
1344
|
+
await stream.call(RPC_METHODS3.sessionsRevoke, { selector: sessionId });
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
throw toAppductError(error);
|
|
1347
|
+
}
|
|
1348
|
+
},
|
|
1334
1349
|
tools: async () => {
|
|
1335
1350
|
try {
|
|
1336
1351
|
const result = await stream.call(RPC_METHODS3.toolsList, {
|
|
@@ -2539,14 +2554,6 @@ var waitForSession = async (sessionId, options = {}) => {
|
|
|
2539
2554
|
}
|
|
2540
2555
|
const openStream = () => openDaemonStream({ stateDir, spawn: options.spawn, autoSpawn: options.autoSpawn });
|
|
2541
2556
|
try {
|
|
2542
|
-
try {
|
|
2543
|
-
await stream.call(RPC_METHODS6.sessionsDescribe, { selector: sessionId });
|
|
2544
|
-
return makeAppClient(stream, sessionId, openStream);
|
|
2545
|
-
} catch (error) {
|
|
2546
|
-
if (!(error instanceof DaemonRpcError) || error.data?.type !== "unknown_session") {
|
|
2547
|
-
throw error;
|
|
2548
|
-
}
|
|
2549
|
-
}
|
|
2550
2557
|
await new Promise((resolve2, reject) => {
|
|
2551
2558
|
let settled = false;
|
|
2552
2559
|
const settle = (fn) => {
|
|
@@ -2588,9 +2595,16 @@ var waitForSession = async (sessionId, options = {}) => {
|
|
|
2588
2595
|
settle(resolve2);
|
|
2589
2596
|
}
|
|
2590
2597
|
});
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2598
|
+
const fail = (error) => settle(() => reject(toAppductError(error)));
|
|
2599
|
+
stream.call(RPC_METHODS6.eventsSubscribe, { sessionSelector: sessionId, kinds: ["session_claimed"] }).then(() => {
|
|
2600
|
+
stream.call(RPC_METHODS6.sessionsDescribe, { selector: sessionId }).then(
|
|
2601
|
+
() => settle(resolve2),
|
|
2602
|
+
// Not claimed yet: the subscription reports the claim when it comes.
|
|
2603
|
+
(error) => {
|
|
2604
|
+
if (!(error instanceof DaemonRpcError) || error.data?.type !== "unknown_session") fail(error);
|
|
2605
|
+
}
|
|
2606
|
+
);
|
|
2607
|
+
}, fail);
|
|
2594
2608
|
});
|
|
2595
2609
|
return makeAppClient(stream, sessionId, openStream);
|
|
2596
2610
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appduct",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Call functions inside your running iOS, Android or React Native app from a terminal, test or AI agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"reflect-metadata": "^0.2.2",
|
|
48
48
|
"toqr": "^0.1.1",
|
|
49
49
|
"ws": "^8.18.3",
|
|
50
|
-
"@appduct/shared": "0.
|
|
50
|
+
"@appduct/shared": "0.16.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/node": "^22.10.0",
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"typescript": "~5.9.2",
|
|
62
62
|
"vite": "^8.1.4",
|
|
63
63
|
"webpack": "^5.111.1",
|
|
64
|
-
"@appduct/
|
|
65
|
-
"@appduct/
|
|
64
|
+
"@appduct/web": "0.16.0",
|
|
65
|
+
"@appduct/react-native": "0.16.0"
|
|
66
66
|
},
|
|
67
67
|
"files": [
|
|
68
68
|
"bin.js",
|