appduct 0.11.1 → 0.13.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 +43 -38
- package/dist/bin.js +50 -36
- package/dist/cli/command-options.d.ts +7 -17
- package/dist/cli/result-types.d.ts +11 -8
- package/dist/cli/routes/daemon/index.js +12 -16
- package/dist/cli/routes/daemon/run.js +216 -46
- package/dist/cli/routes/daemon/start.js +18 -16
- package/dist/cli/routes/daemon/status.js +18 -16
- package/dist/cli/routes/daemon/stop.js +18 -16
- package/dist/cli/routes/doctor.js +12 -16
- package/dist/cli/routes/events/index.d.ts +9 -0
- package/dist/cli/routes/events/index.js +842 -0
- package/dist/cli/routes/events/shared-options.d.ts +12 -0
- package/dist/cli/routes/events/shared-options.js +102 -0
- package/dist/cli/routes/events/since.d.ts +6 -0
- package/dist/cli/routes/events/since.js +1977 -0
- package/dist/cli/routes/events/tail.d.ts +6 -0
- package/dist/cli/routes/{events.js → events/tail.js} +184 -162
- package/dist/cli/routes/init.js +14 -18
- package/dist/cli/routes/keygen.js +13 -16
- package/dist/cli/routes/mcp.js +193 -175
- package/dist/cli/routes/sessions/index.d.ts +7 -0
- package/dist/cli/routes/sessions/index.js +843 -0
- package/dist/cli/routes/sessions/link.d.ts +4 -0
- package/dist/cli/routes/{link.js → sessions/link.js} +19 -17
- package/dist/cli/routes/sessions/ls.d.ts +4 -0
- package/dist/cli/routes/{ls.js → sessions/ls.js} +19 -17
- package/dist/cli/routes/sessions/revoke.d.ts +4 -0
- package/dist/cli/routes/{revoke.js → sessions/revoke.js} +20 -18
- package/dist/cli/routes/tools/call.d.ts +4 -0
- package/dist/cli/routes/{invoke.js → tools/call.js} +21 -19
- package/dist/cli/routes/tools/describe.d.ts +6 -0
- package/dist/cli/routes/tools/describe.js +1837 -0
- package/dist/cli/routes/tools/index.d.ts +6 -0
- package/dist/cli/routes/tools/index.js +843 -0
- package/dist/cli/routes/tools/ls.d.ts +4 -0
- package/dist/cli/routes/{tools.js → tools/ls.js} +46 -73
- package/dist/cli/routes/tools/shared-options.d.ts +16 -0
- package/dist/cli/routes/tools/shared-options.js +122 -0
- package/dist/client/app-client.d.ts +96 -37
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.js +183 -139
- package/dist/commands/events.d.ts +31 -13
- package/dist/commands/invoke.d.ts +1 -1
- package/dist/commands/link.d.ts +1 -1
- package/dist/commands/ls.d.ts +1 -1
- package/dist/commands/revoke.d.ts +1 -1
- package/dist/commands/tools.d.ts +12 -13
- package/dist/daemon/config.d.ts +5 -2
- package/dist/daemon/daemon.d.ts +3 -0
- package/dist/daemon/event-bus.d.ts +36 -4
- package/dist/daemon/events-log.d.ts +31 -0
- package/dist/daemon/memory-append-only-file.d.ts +17 -0
- package/dist/daemon/node-append-only-file.d.ts +9 -0
- package/dist/daemon/rpc-errors.d.ts +1 -1
- package/dist/daemon/state-dir.d.ts +2 -0
- package/dist/errors.d.ts +1 -1
- package/dist/events/index.d.ts +60 -0
- package/dist/index.js +62 -39
- package/dist/link.d.ts +1 -1
- package/dist/mcp/app-tools.d.ts +3 -2
- package/dist/mcp/connect-tool.d.ts +1 -1
- package/dist/mcp/events-tool.d.ts +28 -34
- package/dist/native-scheme.d.ts +1 -1
- package/dist/output.d.ts +29 -6
- package/dist/rpc/client.d.ts +1 -1
- package/dist/scheme.d.ts +4 -4
- package/package.json +2 -2
- package/dist/cli/routes/events.d.ts +0 -3
- package/dist/cli/routes/invoke.d.ts +0 -3
- package/dist/cli/routes/link.d.ts +0 -3
- package/dist/cli/routes/ls.d.ts +0 -3
- package/dist/cli/routes/revoke.d.ts +0 -3
- package/dist/cli/routes/tools.d.ts +0 -3
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ The `appduct` package is the operator/agent side of Appduct: a CLI and an MCP se
|
|
|
9
9
|
## Why use this package
|
|
10
10
|
|
|
11
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 (`
|
|
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
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
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
|
|
|
@@ -17,44 +17,48 @@ The `appduct` package is the operator/agent side of Appduct: a CLI and an MCP se
|
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
npm install -g appduct
|
|
20
|
-
appduct link --scheme myapp --qr
|
|
20
|
+
appduct sessions link --scheme myapp --qr
|
|
21
21
|
```
|
|
22
22
|
|
|
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.
|
|
23
|
+
`sessions 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
25
|
Scan the QR (or open the deep link) in an Appduct-enabled app, then:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
appduct tools
|
|
29
|
-
appduct
|
|
28
|
+
appduct tools ls
|
|
29
|
+
appduct tools call sum --input '{"a":2,"b":3}'
|
|
30
30
|
```
|
|
31
31
|
|
|
32
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
|
+
Every command is `appduct <noun> <verb> [selector] [args]`, the same shape as `appduct daemon run|start|stop|status`:
|
|
37
|
+
|
|
36
38
|
| Command | Role |
|
|
37
39
|
| --- | --- |
|
|
38
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 |
|
|
39
41
|
| `appduct keygen [--out <path>] [--force]` | generate a daemon private key, print its app pin |
|
|
40
|
-
| `appduct
|
|
41
|
-
| `appduct
|
|
42
|
-
| `appduct
|
|
43
|
-
| `appduct tools [selector] --
|
|
44
|
-
| `appduct
|
|
45
|
-
| `appduct
|
|
46
|
-
| `appduct
|
|
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>] [--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 tail [selector] [--follow]` | stream the events the app posts with `postEvent`; `--json` emits NDJSON |
|
|
50
|
+
| `appduct events since [selector] <cursor>` | one-shot pull the events retained since `<cursor>`; `--json` emits NDJSON |
|
|
47
51
|
| `appduct daemon run\|start\|stop\|status` | daemon lifecycle |
|
|
48
52
|
| `appduct mcp [--scheme <s>]` | start a stdio MCP server proxying connected apps' tools to MCP clients |
|
|
49
53
|
| `appduct doctor <artifact> [--assert-present\|--assert-absent]` | release-gate step: report or assert whether a built `.app`/`.ipa`/`.apk`/`.aab` contains Appduct |
|
|
50
54
|
|
|
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 <
|
|
55
|
+
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.
|
|
52
56
|
|
|
53
|
-
`--timeout` on `
|
|
57
|
+
`--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.
|
|
54
58
|
|
|
55
|
-
### `appduct tools`: a signature per tool
|
|
59
|
+
### `appduct tools ls`: a signature per tool
|
|
56
60
|
|
|
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:
|
|
61
|
+
`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:
|
|
58
62
|
|
|
59
63
|
```
|
|
60
64
|
Tools
|
|
@@ -63,16 +67,16 @@ Tools
|
|
|
63
67
|
set_flag(name: "dark_mode" | "new_checkout", enabled: bool) [prompt]
|
|
64
68
|
Toggle a feature flag.
|
|
65
69
|
|
|
66
|
-
Run `appduct tools <name>` for a tool's full schema.
|
|
70
|
+
Run `appduct tools describe <name>` for a tool's full schema.
|
|
67
71
|
```
|
|
68
72
|
|
|
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"`.
|
|
73
|
+
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"`.
|
|
70
74
|
|
|
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
|
|
75
|
+
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.
|
|
72
76
|
|
|
73
77
|
### Tool groups
|
|
74
78
|
|
|
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:
|
|
79
|
+
When the app puts its tools in groups ([`docs/TOOLS.md`](../../docs/TOOLS.md#group-tools-in-a-large-app)), `appduct tools ls` lists them under group headings, with subgroups indented under their parent and ungrouped tools last:
|
|
76
80
|
|
|
77
81
|
```
|
|
78
82
|
Tools
|
|
@@ -93,23 +97,23 @@ Tools
|
|
|
93
97
|
On a large app, start with `--groups` to see what there is, then list one group:
|
|
94
98
|
|
|
95
99
|
```
|
|
96
|
-
$ appduct tools --groups
|
|
100
|
+
$ appduct tools ls --groups
|
|
97
101
|
Groups
|
|
98
102
|
cart 12
|
|
99
103
|
checkout 8
|
|
100
104
|
checkout/payment 3
|
|
101
105
|
(ungrouped) 2
|
|
102
106
|
|
|
103
|
-
22 tools in total. Run `appduct tools --group <name>` to list one group's tools.
|
|
107
|
+
22 tools in total. Run `appduct tools ls --group <name>` to list one group's tools.
|
|
104
108
|
|
|
105
|
-
$ appduct tools --group checkout
|
|
109
|
+
$ appduct tools ls --group checkout
|
|
106
110
|
```
|
|
107
111
|
|
|
108
112
|
`--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.
|
|
109
113
|
|
|
110
114
|
### The deep-link scheme
|
|
111
115
|
|
|
112
|
-
`appduct link`, `appduct mcp`, and the MCP `appduct_connect` tool all resolve the scheme the same way, first match wins:
|
|
116
|
+
`appduct sessions link`, `appduct mcp`, and the MCP `appduct_connect` tool all resolve the scheme the same way, first match wins:
|
|
113
117
|
|
|
114
118
|
1. `--scheme <s>`
|
|
115
119
|
2. the `APPDUCT_SCHEME` environment variable
|
|
@@ -164,17 +168,17 @@ It needs all of:
|
|
|
164
168
|
- **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.
|
|
165
169
|
- The device **paired and trusted** by this Mac, with **Developer Mode** enabled on it (Settings → Privacy & Security → Developer Mode).
|
|
166
170
|
- 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`.
|
|
171
|
+
- 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`.
|
|
168
172
|
- The app's **bundle id** — see [Delivering the link to a device](#delivering-the-link-to-a-device) for how to supply it.
|
|
169
173
|
|
|
170
174
|
```bash
|
|
171
|
-
appduct link --scheme myapp --open ios-device --app-id com.example.myapp
|
|
175
|
+
appduct sessions link --scheme myapp --open ios-device --app-id com.example.myapp
|
|
172
176
|
```
|
|
173
177
|
|
|
174
178
|
Two things worth knowing before you rely on this:
|
|
175
179
|
|
|
176
180
|
- **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.
|
|
181
|
+
- **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.
|
|
178
182
|
|
|
179
183
|
## MCP setup (Claude Code, Cursor, and similar)
|
|
180
184
|
|
|
@@ -208,11 +212,11 @@ Once configured, an agent reaches the connected app's tools through three built-
|
|
|
208
212
|
|
|
209
213
|
| Tool | Does what | CLI equivalent |
|
|
210
214
|
| --- | --- | --- |
|
|
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
|
|
215
|
+
| `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` |
|
|
216
|
+
| `appduct_describe_tool` | Shows one tool's full input and output schema. | `appduct tools describe <name>` |
|
|
217
|
+
| `appduct_call_tool` | Calls a tool by `name` with `args`, with progress and errors preserved. | `appduct tools call` |
|
|
214
218
|
|
|
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`.
|
|
219
|
+
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`.
|
|
216
220
|
|
|
217
221
|
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
222
|
|
|
@@ -220,11 +224,11 @@ Set this before you connect: the daemon reads `config.json` only when it starts,
|
|
|
220
224
|
|
|
221
225
|
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.
|
|
222
226
|
|
|
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
|
|
227
|
+
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 the next event whose name matches `name`, a whole-name glob (`*` waits for any name) — checking what's already retained before waiting live — rejecting with `tool_timeout` if none arrives in time.
|
|
224
228
|
|
|
225
229
|
## Test runners: `appduct/client`
|
|
226
230
|
|
|
227
|
-
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
|
|
231
|
+
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:
|
|
228
232
|
|
|
229
233
|
```ts
|
|
230
234
|
import { connect } from "appduct/client";
|
|
@@ -264,18 +268,19 @@ const app = await connect<Tools>();
|
|
|
264
268
|
const { total } = await app.call("sum", { a: 2, b: 3 }); // typed
|
|
265
269
|
```
|
|
266
270
|
|
|
267
|
-
`waitForEvent` 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:
|
|
271
|
+
`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:
|
|
268
272
|
|
|
269
273
|
```ts
|
|
270
274
|
const { events, cursor } = await app.events(); // pull: what already happened
|
|
271
275
|
const next = await app.waitForEvent("checkout_done", { since: cursor });
|
|
276
|
+
const capped = await app.waitForEvent("checkout_done", { payloadMaxBytes: 1024 }); // check `truncated` before reading `payload`
|
|
272
277
|
```
|
|
273
278
|
|
|
274
279
|
For everything else, the package exports `runCli` and the command handlers from [`src/index.ts`](https://github.com/callstackincubator/appduct/blob/main/packages/appduct/src/index.ts), so you can embed the same behavior in Node or Bun scripts without shelling out.
|
|
275
280
|
|
|
276
281
|
## Keys and pins
|
|
277
282
|
|
|
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.
|
|
283
|
+
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 [`docs/SECURITY.md`][security]'s "Trust modes" for what that does and doesn't protect.
|
|
279
284
|
|
|
280
285
|
For a build that should trust only a key you embedded ahead of time, generate one explicitly:
|
|
281
286
|
|
|
@@ -285,17 +290,17 @@ appduct keygen
|
|
|
285
290
|
|
|
286
291
|
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.
|
|
287
292
|
|
|
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.
|
|
293
|
+
`appduct sessions 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.
|
|
289
294
|
|
|
290
295
|
## Daemon lifecycle
|
|
291
296
|
|
|
292
|
-
The daemon auto-starts the first time any CLI or MCP command needs it — you don't normally run `appduct daemon start` yourself. It writes state to `<state-dir>/` (`daemon.sock`, `daemon.pid`, `daemon.log`, `daemon.log.1`, `key.pem`, `config.json`, `audit/`), holds a single-instance lock via the pidfile, and runs independently of any one device's connection, so a reload or crash on the device side never costs you the daemon process.
|
|
297
|
+
The daemon auto-starts the first time any CLI or MCP command needs it — you don't normally run `appduct daemon start` yourself. It writes state to `<state-dir>/` (`daemon.sock`, `daemon.pid`, `daemon.log`, `daemon.log.1`, `events.log`, `events.log.1`, `key.pem`, `config.json`, `audit/`), holds a single-instance lock via the pidfile, and runs independently of any one device's connection, so a reload or crash on the device side never costs you the daemon process.
|
|
293
298
|
|
|
294
299
|
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.
|
|
295
300
|
|
|
296
301
|
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.
|
|
297
302
|
|
|
298
|
-
|
|
303
|
+
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 [`ARCHITECTURE.md` §3][architecture].
|
|
299
304
|
|
|
300
305
|
## Release gate: `appduct doctor`
|
|
301
306
|
|
package/dist/bin.js
CHANGED
|
@@ -123,27 +123,30 @@ var createCli = () => {
|
|
|
123
123
|
'Android package name to write as "appId.android" (needed to deliver a link with --open android).'
|
|
124
124
|
);
|
|
125
125
|
cli.command("keygen", "Generate an Appduct host private key and print its app fingerprint.").option("--out <path>", "Destination path (default: <state-dir>/key.pem).").option("--force", "Overwrite an existing key at the destination path.");
|
|
126
|
-
cli.command("
|
|
126
|
+
cli.command("sessions [...args]", "Manage Appduct sessions: ls, revoke, or link.").usage("sessions <ls|revoke|link> [selector] [args]").option("--ttl <seconds>", "link: time-to-live in seconds (default: from config.json).").option("--qr", "link: also render the deep link as a terminal QR code.").option(
|
|
127
127
|
"--scheme <scheme>",
|
|
128
|
-
`
|
|
128
|
+
`link: deep-link URI scheme (also: APPDUCT_SCHEME; default: .appduct/config.json, then app.json's "expo.scheme", then the Android/iOS project files in <cwd>).`
|
|
129
129
|
).option(
|
|
130
130
|
"--open <target>",
|
|
131
|
-
"
|
|
132
|
-
).option("--device <id>", "adb serial, simulator udid or paired-device udid to target when --open is ambiguous.").option(
|
|
131
|
+
"link: deliver the link automatically via adb/simctl/devicectl (android|ios-sim|ios-device; ios-device is experimental)."
|
|
132
|
+
).option("--device <id>", "link: adb serial, simulator udid or paired-device udid to target when --open is ambiguous.").option(
|
|
133
133
|
"--app-id <id>",
|
|
134
|
-
`
|
|
134
|
+
`link: installed app id for --open android/ios-device (default: .appduct/config.json's "appId.<platform>").`
|
|
135
135
|
).option(
|
|
136
136
|
"--relaunch",
|
|
137
|
-
"
|
|
137
|
+
"link: with --open ios-device, terminate a running instance first (try this if delivery to an already-running app does nothing)."
|
|
138
138
|
);
|
|
139
|
-
cli.command("
|
|
140
|
-
cli.command("tools [selector] [name]", "List a session's tools, or show one tool's full schema.").option("--full", "Render full schemas/annotations for every listed tool.").option("--group <name>", 'Only tools in this group ("checkout" includes "checkout/payment").').option("--groups", "List the session's groups with tool counts instead of its tools.").option("--filter <text>", "Only tools whose name or description contains this text (case-insensitive).").option("--limit <n>", "Show at most n tools.").option("--offset <n>", "Skip the first n tools of the sorted list.");
|
|
141
|
-
cli.command("invoke [selector] [tool]", "Call a tool on a session.").option("--input <json>", "Tool input arguments as a JSON object.").option(
|
|
139
|
+
cli.command("tools [...args]", "List a session's tools, describe one, or call one: ls, describe, or call.").usage("tools <ls|describe|call> [selector] [args]").option("--full", "ls: render full schemas/annotations for every listed tool.").option("--group <name>", 'ls: only tools in this group ("checkout" includes "checkout/payment").').option("--groups", "ls: list the session's groups with tool counts instead of its tools.").option("--filter <text>", "ls: only tools whose name or description contains this text (case-insensitive).").option("--limit <n>", "ls: show at most n tools.").option("--offset <n>", "ls: skip the first n tools of the sorted list.").option("--input <json>", "call: tool input arguments as a JSON object.").option(
|
|
142
140
|
"--timeout <ms>",
|
|
143
|
-
"
|
|
141
|
+
"call: call timeout in milliseconds. Shortens the deadline; it cannot extend one past the app's own timer, which is the tool's declared timeoutMs (else 10000)."
|
|
142
|
+
);
|
|
143
|
+
cli.command("events [...args]", "Stream the app's events, or replay them since a cursor: tail or since.").usage("events <tail|since> [selector] [args]").option("--follow", "tail: accepted for script readability; the default behavior already follows.").option(
|
|
144
|
+
"--name <glob>",
|
|
145
|
+
'Only events whose name matches this whole-name, case-sensitive glob ("*" matches any run of characters).'
|
|
146
|
+
).option(
|
|
147
|
+
"--payload-max-bytes <n>",
|
|
148
|
+
"Cap each event's payload at this many UTF-8 bytes of its JSON; over the cap, the event carries payloadPreview, payloadBytes and truncated: true instead of payload."
|
|
144
149
|
);
|
|
145
|
-
cli.command("events [selector]", "Stream session/tool events until interrupted.").option("--follow", "Accepted for script readability; the default behavior already follows.").option("--since <cursor>", "One-shot: print events retained since this cursor instead of streaming live.");
|
|
146
|
-
cli.command("revoke [selector]", "Revoke a session.");
|
|
147
150
|
cli.command("mcp", "Start a stdio MCP server that gives MCP clients access to connected apps' tools.").option(
|
|
148
151
|
"--scheme <scheme>",
|
|
149
152
|
"Deep-link URI scheme for appduct_connect (also: APPDUCT_SCHEME)."
|
|
@@ -166,6 +169,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
166
169
|
"eventBufferSize",
|
|
167
170
|
"auditRetentionDays",
|
|
168
171
|
"daemonLogMaxBytes",
|
|
172
|
+
"eventsLogMaxBytes",
|
|
169
173
|
"policy",
|
|
170
174
|
"advertisedIp",
|
|
171
175
|
"scheme",
|
|
@@ -225,6 +229,7 @@ var defaultConfig = (paths) => {
|
|
|
225
229
|
eventBufferSize: 256,
|
|
226
230
|
auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
|
|
227
231
|
daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
|
|
232
|
+
eventsLogMaxBytes: 10 * 1024 * 1024,
|
|
228
233
|
restartDaemonOnVersionMismatch: false,
|
|
229
234
|
policy: {
|
|
230
235
|
default: "allow",
|
|
@@ -287,6 +292,9 @@ var loadConfig = async (paths, options = {}) => {
|
|
|
287
292
|
if (parsed.daemonLogMaxBytes !== void 0) {
|
|
288
293
|
config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
|
|
289
294
|
}
|
|
295
|
+
if (parsed.eventsLogMaxBytes !== void 0) {
|
|
296
|
+
config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
|
|
297
|
+
}
|
|
290
298
|
if (parsed.restartDaemonOnVersionMismatch !== void 0) {
|
|
291
299
|
config.restartDaemonOnVersionMismatch = requireBoolean(
|
|
292
300
|
parsed.restartDaemonOnVersionMismatch,
|
|
@@ -335,6 +343,7 @@ var getStateDirPaths = (stateDir) => {
|
|
|
335
343
|
socketPath: join3(stateDir, "daemon.sock"),
|
|
336
344
|
pidFilePath: join3(stateDir, "daemon.pid"),
|
|
337
345
|
logFilePath: join3(stateDir, "daemon.log"),
|
|
346
|
+
eventsLogPath: join3(stateDir, "events.log"),
|
|
338
347
|
keyPath: join3(stateDir, "key.pem"),
|
|
339
348
|
configPath: join3(stateDir, "config.json"),
|
|
340
349
|
auditDir: join3(stateDir, "audit"),
|
|
@@ -632,9 +641,6 @@ var renderLsData = (colors, data, now) => {
|
|
|
632
641
|
...rows.map((row) => ` ${row.map((cell, index) => cell.padEnd(widths[index])).join(" ")}`)
|
|
633
642
|
];
|
|
634
643
|
};
|
|
635
|
-
var isToolsListing = (data) => {
|
|
636
|
-
return typeof data === "object" && data !== null && Array.isArray(data.tools);
|
|
637
|
-
};
|
|
638
644
|
var isToolGroupsListing = (data) => {
|
|
639
645
|
return typeof data === "object" && data !== null && Array.isArray(data.groups) && !Array.isArray(data.tools);
|
|
640
646
|
};
|
|
@@ -652,7 +658,7 @@ var renderEmptyToolsLine = (data) => {
|
|
|
652
658
|
}
|
|
653
659
|
if (data.group !== void 0) {
|
|
654
660
|
const match = data.filter === void 0 ? "" : ` match ${JSON.stringify(data.filter)}`;
|
|
655
|
-
return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools --groups\` to see the session's groups.`;
|
|
661
|
+
return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools ls --groups\` to see the session's groups.`;
|
|
656
662
|
}
|
|
657
663
|
return data.filter === void 0 ? " No tools registered." : ` No tools match ${JSON.stringify(data.filter)}.`;
|
|
658
664
|
};
|
|
@@ -734,7 +740,7 @@ var renderToolSummaryTable = (colors, data) => {
|
|
|
734
740
|
...grouped ? renderGroupedToolLines(colors, data.tools) : data.tools.flatMap((tool) => renderToolSummaryLines(tool, " ")),
|
|
735
741
|
...renderTruncationLine(data),
|
|
736
742
|
"",
|
|
737
|
-
"Run `appduct tools <name>` for a tool's full schema."
|
|
743
|
+
"Run `appduct tools describe <name>` for a tool's full schema."
|
|
738
744
|
];
|
|
739
745
|
};
|
|
740
746
|
var renderToolDetail = (colors, tool, flags) => {
|
|
@@ -786,16 +792,13 @@ var renderToolGroups = (colors, data) => {
|
|
|
786
792
|
colors.green("Groups"),
|
|
787
793
|
...data.groups.map((entry) => ` ${label(entry).padEnd(width)} ${String(entry.total).padStart(countWidth)}`),
|
|
788
794
|
"",
|
|
789
|
-
`${data.total} tool${data.total === 1 ? "" : "s"} in total. ` + (hasAnyGroup(data.groups) ? "Run `appduct tools --group <name>` to list one group's tools." : "No tool declares a group.")
|
|
795
|
+
`${data.total} tool${data.total === 1 ? "" : "s"} in total. ` + (hasAnyGroup(data.groups) ? "Run `appduct tools ls --group <name>` to list one group's tools." : "No tool declares a group.")
|
|
790
796
|
];
|
|
791
797
|
};
|
|
792
|
-
var
|
|
798
|
+
var renderToolsListData = (colors, data, flags, full) => {
|
|
793
799
|
if (isToolGroupsListing(data)) {
|
|
794
800
|
return renderToolGroups(colors, data);
|
|
795
801
|
}
|
|
796
|
-
if (!isToolsListing(data)) {
|
|
797
|
-
return renderToolDetail(colors, data, flags);
|
|
798
|
-
}
|
|
799
802
|
return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
|
|
800
803
|
};
|
|
801
804
|
var renderInvokeData = (colors, data, flags) => {
|
|
@@ -981,15 +984,17 @@ var renderSuccessData = (colors, command, data, options) => {
|
|
|
981
984
|
return renderInitData(colors, data, flags);
|
|
982
985
|
case "keygen":
|
|
983
986
|
return renderKeygenData(colors, data, flags);
|
|
984
|
-
case "link":
|
|
987
|
+
case "sessions link":
|
|
985
988
|
return renderLinkData(colors, data, flags, options.qr);
|
|
986
|
-
case "ls":
|
|
989
|
+
case "sessions ls":
|
|
987
990
|
return renderLsData(colors, data, options.now ?? /* @__PURE__ */ new Date());
|
|
988
|
-
case "tools":
|
|
989
|
-
return
|
|
990
|
-
case "
|
|
991
|
+
case "tools ls":
|
|
992
|
+
return renderToolsListData(colors, data, flags, options.full);
|
|
993
|
+
case "tools describe":
|
|
994
|
+
return renderToolDetail(colors, data, flags);
|
|
995
|
+
case "tools call":
|
|
991
996
|
return renderInvokeData(colors, data, flags);
|
|
992
|
-
case "revoke":
|
|
997
|
+
case "sessions revoke":
|
|
993
998
|
return renderRevokeData(colors, data);
|
|
994
999
|
case "daemon run":
|
|
995
1000
|
return renderDaemonRunData(colors, data, flags);
|
|
@@ -1144,21 +1149,30 @@ var DAEMON_RESTART_ENV = "APPDUCT_DAEMON_RESTART";
|
|
|
1144
1149
|
var isEnvTruthy = (value) => {
|
|
1145
1150
|
return value === "1" || value?.toLowerCase() === "true";
|
|
1146
1151
|
};
|
|
1152
|
+
var REMOVED_COMMANDS = {
|
|
1153
|
+
ls: "sessions ls",
|
|
1154
|
+
revoke: "sessions revoke",
|
|
1155
|
+
link: "sessions link",
|
|
1156
|
+
invoke: "tools call"
|
|
1157
|
+
};
|
|
1158
|
+
var rootUnknownCommandError = (word) => {
|
|
1159
|
+
if (word !== void 0 && Object.hasOwn(REMOVED_COMMANDS, word)) {
|
|
1160
|
+
return usageError(`Unknown command "${word}"; use "appduct ${REMOVED_COMMANDS[word]}".`);
|
|
1161
|
+
}
|
|
1162
|
+
return unknownCommandError(word);
|
|
1163
|
+
};
|
|
1147
1164
|
var rootRouter = createRouter(
|
|
1148
1165
|
{
|
|
1149
1166
|
init: () => import("./cli/routes/init.js"),
|
|
1150
1167
|
keygen: () => import("./cli/routes/keygen.js"),
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
invoke: () => import("./cli/routes/invoke.js"),
|
|
1155
|
-
revoke: () => import("./cli/routes/revoke.js"),
|
|
1156
|
-
events: () => import("./cli/routes/events.js"),
|
|
1168
|
+
sessions: () => import("./cli/routes/sessions/index.js"),
|
|
1169
|
+
tools: () => import("./cli/routes/tools/index.js"),
|
|
1170
|
+
events: () => import("./cli/routes/events/index.js"),
|
|
1157
1171
|
mcp: () => import("./cli/routes/mcp.js"),
|
|
1158
1172
|
doctor: () => import("./cli/routes/doctor.js"),
|
|
1159
1173
|
daemon: () => import("./cli/routes/daemon/index.js")
|
|
1160
1174
|
},
|
|
1161
|
-
{ unknown:
|
|
1175
|
+
{ unknown: rootUnknownCommandError }
|
|
1162
1176
|
);
|
|
1163
1177
|
var runCli = async (argv, options = {}) => {
|
|
1164
1178
|
const writers = {
|
|
@@ -1197,7 +1211,7 @@ var runCli = async (argv, options = {}) => {
|
|
|
1197
1211
|
return executeCommand(
|
|
1198
1212
|
"cli",
|
|
1199
1213
|
() => {
|
|
1200
|
-
throw
|
|
1214
|
+
throw rootUnknownCommandError(parsedArgs[0]);
|
|
1201
1215
|
},
|
|
1202
1216
|
{
|
|
1203
1217
|
flags,
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* positive integer, or throws a clear `usage_error`. */
|
|
8
8
|
export declare const parsePositiveIntegerOption: (value: unknown, flagName: string) => number | undefined;
|
|
9
9
|
/** Parses a `cac`-provided option value into a non-negative integer (0 allowed, unlike
|
|
10
|
-
* {@link parsePositiveIntegerOption}), or throws a clear `usage_error`. Used by `events
|
|
11
|
-
* whose
|
|
10
|
+
* {@link parsePositiveIntegerOption}), or throws a clear `usage_error`. Used by `events since`'s
|
|
11
|
+
* `<cursor>` positional, whose value `0` is a meaningful "everything retained" value, not an
|
|
12
|
+
* omission. */
|
|
12
13
|
export declare const parseNonNegativeIntegerOption: (value: unknown, flagName: string) => number | undefined;
|
|
13
14
|
/**
|
|
14
15
|
* Reads a free-text flag's value exactly as typed. `cac` coerces any numeric-looking value to a
|
|
@@ -19,9 +20,9 @@ export declare const parseNonNegativeIntegerOption: (value: unknown, flagName: s
|
|
|
19
20
|
*/
|
|
20
21
|
export declare const readTextOption: (argv: readonly string[], value: unknown, flagName: string) => string | undefined;
|
|
21
22
|
/**
|
|
22
|
-
* Splits the positional args of a command shaped `<command> [selector] <target>` (e.g. `
|
|
23
|
-
* [selector] <tool>`, `tools [selector] <name>`): the last positional is always the
|
|
24
|
-
* target, everything before it (zero or one args) is the optional selector.
|
|
23
|
+
* Splits the positional args of a command shaped `<command> [selector] <target>` (e.g. `tools
|
|
24
|
+
* call [selector] <tool>`, `tools describe [selector] <name>`): the last positional is always the
|
|
25
|
+
* required target, everything before it (zero or one args) is the optional selector.
|
|
25
26
|
*/
|
|
26
27
|
export declare const splitSelectorAndRequiredTarget: (args: readonly string[], commandUsage: string) => {
|
|
27
28
|
selector?: string;
|
|
@@ -33,18 +34,7 @@ export declare const splitOptionalSelector: (args: readonly string[], commandUsa
|
|
|
33
34
|
selector?: string;
|
|
34
35
|
};
|
|
35
36
|
/**
|
|
36
|
-
*
|
|
37
|
-
* [name]`): with 2 args, `(selector, target)`; with 1, the single arg is ambiguous between "the
|
|
38
|
-
* selector" and "the target with the selector omitted" — the caller resolves that (see
|
|
39
|
-
* `commands/tools.ts`), so it comes back unlabeled here as `selectorOrTarget`.
|
|
40
|
-
*/
|
|
41
|
-
export declare const splitOptionalSelectorAndTarget: (args: readonly string[], commandUsage: string) => {
|
|
42
|
-
selector?: string;
|
|
43
|
-
target?: string;
|
|
44
|
-
selectorOrTarget?: string;
|
|
45
|
-
};
|
|
46
|
-
/**
|
|
47
|
-
* Parses the JSON payload for `invoke --input`; never throws a raw `SyntaxError` at the CLI
|
|
37
|
+
* Parses the JSON payload for `tools call --input`; never throws a raw `SyntaxError` at the CLI
|
|
48
38
|
* boundary. A missing flag is a usage error (nothing was given); a present-but-unparseable or
|
|
49
39
|
* wrong-shaped value is a validation error (something was given, and it's invalid).
|
|
50
40
|
*/
|
|
@@ -111,9 +111,10 @@ export type LinkCommandData = {
|
|
|
111
111
|
/** The `--open` target the link was delivered to, alongside `delivered`. */
|
|
112
112
|
target?: OpenTarget;
|
|
113
113
|
};
|
|
114
|
-
/** `appduct ls`: `sessions.list` passthrough, verbatim (ARCHITECTURE.md §10: "--json
|
|
114
|
+
/** `appduct sessions ls`: `sessions.list` passthrough, verbatim (ARCHITECTURE.md §10: "--json
|
|
115
|
+
* passthrough"). */
|
|
115
116
|
export type LsCommandData = SessionSummary[];
|
|
116
|
-
/** `appduct tools`'s listing form: the daemon's `tools.list` result, plus the `--group`/`--filter`/
|
|
117
|
+
/** `appduct tools ls`'s listing form: the daemon's `tools.list` result, plus the `--group`/`--filter`/
|
|
117
118
|
* `--limit`/`--offset` inputs that produced it (only the ones actually given — echoed so `--json` and the
|
|
118
119
|
* human renderer's "Showing n of total" line can report what was asked for without threading the
|
|
119
120
|
* CLI options through separately). */
|
|
@@ -123,22 +124,24 @@ export type ToolsListing = ToolsListResult & {
|
|
|
123
124
|
group?: string;
|
|
124
125
|
filter?: string;
|
|
125
126
|
};
|
|
126
|
-
/** `appduct tools --groups`: the daemon's `groups` summary alone, plus the registry's size. */
|
|
127
|
+
/** `appduct tools ls --groups`: the daemon's `groups` summary alone, plus the registry's size. */
|
|
127
128
|
export type ToolGroupsListing = {
|
|
128
129
|
groups: ToolGroupSummary[];
|
|
129
130
|
/** Every tool in the session, grouped or not. */
|
|
130
131
|
total: number;
|
|
131
132
|
};
|
|
132
|
-
/** `appduct tools`: a listing, a groups summary (`--groups`), or a
|
|
133
|
-
* resolved to a
|
|
134
|
-
* `tools.list` — not a registration, so its `group` is `null` for an
|
|
133
|
+
/** `appduct tools ls`/`appduct tools describe`: a listing, a groups summary (`--groups`), or a
|
|
134
|
+
* single entry when `describe`'s `<name>` resolved to a tool. The detail form is the daemon's own
|
|
135
|
+
* listing entry, picked out of `tools.list` — not a registration, so its `group` is `null` for an
|
|
136
|
+
* ungrouped tool. */
|
|
135
137
|
export type ToolsCommandData = ToolsListing | ToolGroupsListing | ToolsListEntry;
|
|
136
|
-
/** `appduct
|
|
138
|
+
/** `appduct tools call`: the tool's raw result payload, printed as-is. */
|
|
137
139
|
export type InvokeCommandData = unknown;
|
|
138
140
|
export type RevokeCommandData = {
|
|
139
141
|
ok: true;
|
|
140
142
|
};
|
|
141
|
-
/** A single `appduct events` line, in both human and NDJSON
|
|
143
|
+
/** A single `appduct events tail`/`appduct events since` line, in both human and NDJSON
|
|
144
|
+
* (`--json`) rendering. */
|
|
142
145
|
export type EventsCommandLine = EventNotification;
|
|
143
146
|
export type DaemonRunCommandData = {
|
|
144
147
|
daemon: {
|
|
@@ -330,9 +330,6 @@ var renderLsData = (colors, data, now) => {
|
|
|
330
330
|
...rows.map((row) => ` ${row.map((cell, index) => cell.padEnd(widths[index])).join(" ")}`)
|
|
331
331
|
];
|
|
332
332
|
};
|
|
333
|
-
var isToolsListing = (data) => {
|
|
334
|
-
return typeof data === "object" && data !== null && Array.isArray(data.tools);
|
|
335
|
-
};
|
|
336
333
|
var isToolGroupsListing = (data) => {
|
|
337
334
|
return typeof data === "object" && data !== null && Array.isArray(data.groups) && !Array.isArray(data.tools);
|
|
338
335
|
};
|
|
@@ -350,7 +347,7 @@ var renderEmptyToolsLine = (data) => {
|
|
|
350
347
|
}
|
|
351
348
|
if (data.group !== void 0) {
|
|
352
349
|
const match = data.filter === void 0 ? "" : ` match ${JSON.stringify(data.filter)}`;
|
|
353
|
-
return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools --groups\` to see the session's groups.`;
|
|
350
|
+
return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools ls --groups\` to see the session's groups.`;
|
|
354
351
|
}
|
|
355
352
|
return data.filter === void 0 ? " No tools registered." : ` No tools match ${JSON.stringify(data.filter)}.`;
|
|
356
353
|
};
|
|
@@ -432,7 +429,7 @@ var renderToolSummaryTable = (colors, data) => {
|
|
|
432
429
|
...grouped ? renderGroupedToolLines(colors, data.tools) : data.tools.flatMap((tool) => renderToolSummaryLines(tool, " ")),
|
|
433
430
|
...renderTruncationLine(data),
|
|
434
431
|
"",
|
|
435
|
-
"Run `appduct tools <name>` for a tool's full schema."
|
|
432
|
+
"Run `appduct tools describe <name>` for a tool's full schema."
|
|
436
433
|
];
|
|
437
434
|
};
|
|
438
435
|
var renderToolDetail = (colors, tool, flags) => {
|
|
@@ -484,16 +481,13 @@ var renderToolGroups = (colors, data) => {
|
|
|
484
481
|
colors.green("Groups"),
|
|
485
482
|
...data.groups.map((entry) => ` ${label(entry).padEnd(width)} ${String(entry.total).padStart(countWidth)}`),
|
|
486
483
|
"",
|
|
487
|
-
`${data.total} tool${data.total === 1 ? "" : "s"} in total. ` + (hasAnyGroup(data.groups) ? "Run `appduct tools --group <name>` to list one group's tools." : "No tool declares a group.")
|
|
484
|
+
`${data.total} tool${data.total === 1 ? "" : "s"} in total. ` + (hasAnyGroup(data.groups) ? "Run `appduct tools ls --group <name>` to list one group's tools." : "No tool declares a group.")
|
|
488
485
|
];
|
|
489
486
|
};
|
|
490
|
-
var
|
|
487
|
+
var renderToolsListData = (colors, data, flags, full) => {
|
|
491
488
|
if (isToolGroupsListing(data)) {
|
|
492
489
|
return renderToolGroups(colors, data);
|
|
493
490
|
}
|
|
494
|
-
if (!isToolsListing(data)) {
|
|
495
|
-
return renderToolDetail(colors, data, flags);
|
|
496
|
-
}
|
|
497
491
|
return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
|
|
498
492
|
};
|
|
499
493
|
var renderInvokeData = (colors, data, flags) => {
|
|
@@ -679,15 +673,17 @@ var renderSuccessData = (colors, command, data, options) => {
|
|
|
679
673
|
return renderInitData(colors, data, flags);
|
|
680
674
|
case "keygen":
|
|
681
675
|
return renderKeygenData(colors, data, flags);
|
|
682
|
-
case "link":
|
|
676
|
+
case "sessions link":
|
|
683
677
|
return renderLinkData(colors, data, flags, options.qr);
|
|
684
|
-
case "ls":
|
|
678
|
+
case "sessions ls":
|
|
685
679
|
return renderLsData(colors, data, options.now ?? /* @__PURE__ */ new Date());
|
|
686
|
-
case "tools":
|
|
687
|
-
return
|
|
688
|
-
case "
|
|
680
|
+
case "tools ls":
|
|
681
|
+
return renderToolsListData(colors, data, flags, options.full);
|
|
682
|
+
case "tools describe":
|
|
683
|
+
return renderToolDetail(colors, data, flags);
|
|
684
|
+
case "tools call":
|
|
689
685
|
return renderInvokeData(colors, data, flags);
|
|
690
|
-
case "revoke":
|
|
686
|
+
case "sessions revoke":
|
|
691
687
|
return renderRevokeData(colors, data);
|
|
692
688
|
case "daemon run":
|
|
693
689
|
return renderDaemonRunData(colors, data, flags);
|