appduct 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +43 -37
  2. package/dist/bin.js +102 -37
  3. package/dist/cli/command-options.d.ts +7 -17
  4. package/dist/cli/result-types.d.ts +19 -9
  5. package/dist/cli/routes/daemon/index.js +70 -17
  6. package/dist/cli/routes/daemon/run.js +288 -50
  7. package/dist/cli/routes/daemon/start.js +70 -17
  8. package/dist/cli/routes/daemon/status.js +70 -17
  9. package/dist/cli/routes/daemon/stop.js +70 -17
  10. package/dist/cli/routes/doctor.js +70 -17
  11. package/dist/cli/routes/events/index.d.ts +6 -0
  12. package/dist/cli/routes/events/index.js +900 -0
  13. package/dist/cli/routes/events/ls.d.ts +5 -0
  14. package/dist/cli/routes/events/ls.js +1798 -0
  15. package/dist/cli/routes/events/shared-options.d.ts +12 -0
  16. package/dist/cli/routes/events/shared-options.js +102 -0
  17. package/dist/cli/routes/events/since.d.ts +6 -0
  18. package/dist/cli/routes/events/since.js +2034 -0
  19. package/dist/cli/routes/events/tail.d.ts +6 -0
  20. package/dist/cli/routes/{events.js → events/tail.js} +235 -163
  21. package/dist/cli/routes/init.js +72 -19
  22. package/dist/cli/routes/keygen.js +70 -17
  23. package/dist/cli/routes/mcp.js +289 -166
  24. package/dist/cli/routes/sessions/index.d.ts +7 -0
  25. package/dist/cli/routes/sessions/index.js +900 -0
  26. package/dist/cli/routes/sessions/link.d.ts +4 -0
  27. package/dist/cli/routes/{link.js → sessions/link.js} +71 -18
  28. package/dist/cli/routes/sessions/ls.d.ts +4 -0
  29. package/dist/cli/routes/{ls.js → sessions/ls.js} +71 -18
  30. package/dist/cli/routes/sessions/revoke.d.ts +4 -0
  31. package/dist/cli/routes/{revoke.js → sessions/revoke.js} +72 -19
  32. package/dist/cli/routes/tools/call.d.ts +4 -0
  33. package/dist/cli/routes/{invoke.js → tools/call.js} +73 -20
  34. package/dist/cli/routes/tools/describe.d.ts +6 -0
  35. package/dist/cli/routes/tools/describe.js +1894 -0
  36. package/dist/cli/routes/tools/index.d.ts +6 -0
  37. package/dist/cli/routes/tools/index.js +900 -0
  38. package/dist/cli/routes/tools/ls.d.ts +4 -0
  39. package/dist/cli/routes/{tools.js → tools/ls.js} +98 -74
  40. package/dist/cli/routes/tools/shared-options.d.ts +16 -0
  41. package/dist/cli/routes/tools/shared-options.js +122 -0
  42. package/dist/client/app-client.d.ts +96 -37
  43. package/dist/client/index.d.ts +3 -2
  44. package/dist/client/index.js +178 -138
  45. package/dist/commands/events-ls.d.ts +21 -0
  46. package/dist/commands/events.d.ts +31 -14
  47. package/dist/commands/invoke.d.ts +1 -1
  48. package/dist/commands/link.d.ts +1 -1
  49. package/dist/commands/ls.d.ts +1 -1
  50. package/dist/commands/revoke.d.ts +1 -1
  51. package/dist/commands/tools.d.ts +12 -13
  52. package/dist/daemon/calls.d.ts +3 -1
  53. package/dist/daemon/config.d.ts +1 -1
  54. package/dist/daemon/event-bus.d.ts +42 -0
  55. package/dist/daemon/registry.d.ts +20 -12
  56. package/dist/daemon/rpc-errors.d.ts +4 -1
  57. package/dist/daemon/sessions.d.ts +4 -2
  58. package/dist/errors.d.ts +1 -1
  59. package/dist/events/index.d.ts +60 -0
  60. package/dist/index.js +114 -40
  61. package/dist/link.d.ts +1 -1
  62. package/dist/mcp/app-tools.d.ts +22 -2
  63. package/dist/mcp/connect-tool.d.ts +1 -1
  64. package/dist/mcp/events-tool.d.ts +67 -27
  65. package/dist/native-scheme.d.ts +1 -1
  66. package/dist/output.d.ts +29 -6
  67. package/dist/rpc/client.d.ts +1 -1
  68. package/dist/scheme.d.ts +4 -4
  69. package/package.json +2 -2
  70. package/dist/cli/routes/events.d.ts +0 -3
  71. package/dist/cli/routes/invoke.d.ts +0 -3
  72. package/dist/cli/routes/link.d.ts +0 -3
  73. package/dist/cli/routes/ls.d.ts +0 -3
  74. package/dist/cli/routes/revoke.d.ts +0 -3
  75. 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 (`tools`, `invoke`, `events`, ...) and `appduct mcp` use the same RPC methods, so an agent sees the tools a human operator does.
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,49 @@ 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 invoke sum --input '{"a":2,"b":3}'
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 link [--ttl <s>] [--qr] [--open android\|ios-sim\|ios-device] [--device <id>] [--app-id <id>] [--scheme <s>]` | mint a pending session and print its deep link |
41
- | `appduct ls` | list sessions: alias, state, device, tool count |
42
- | `appduct tools [selector] [name] [--full] [--group <name>] [--filter <text>] [--limit <n>] [--offset <n>]` | list a session's tools (one call signature + description per line), or show one tool's full schema |
43
- | `appduct tools [selector] --groups` | list a session's tool groups with their tool counts |
44
- | `appduct invoke [selector] <tool> --input '<json>' [--timeout <ms>]` | call a tool |
45
- | `appduct events [selector] [--follow] [--since <cursor>]` | stream the events the app posts with `postEvent` (default), or one-shot pull the ones retained since `<cursor>` (`--since`); `--json` emits NDJSON |
46
- | `appduct revoke [selector]` | revoke a session |
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 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]` | stream the events the app posts with `postEvent`; `--json` emits NDJSON |
51
+ | `appduct events since [selector] <cursor>` | one-shot pull the events retained since `<cursor>`; `--json` emits NDJSON |
47
52
  | `appduct daemon run\|start\|stop\|status` | daemon lifecycle |
48
53
  | `appduct mcp [--scheme <s>]` | start a stdio MCP server proxying connected apps' tools to MCP clients |
49
54
  | `appduct doctor <artifact> [--assert-present\|--assert-absent]` | release-gate step: report or assert whether a built `.app`/`.ipa`/`.apk`/`.aab` contains Appduct |
50
55
 
51
- Every command that targets a session accepts an optional `selector` (a session id or an alias from `appduct ls`); omit it when exactly one session is active. Global flags: `--json` (machine-readable output; compact by default, one line), `--pretty` (indent `--json` output, and JSON values embedded in human output, 2 spaces — never NDJSON event lines), `--verbose` (include the `meta` block — `command`, `timestamp`, `duration_ms` — omitted by default in both human and `--json` output), `--no-color`, `--state-dir <path>` (default `~/.appduct`), `--daemon-restart` (on a daemon/CLI version mismatch, restart the daemon even though that drops live sessions and unclaimed links — `APPDUCT_DAEMON_RESTART=1` and `config.json`'s `restartDaemonOnVersionMismatch` do the same for every command, and `--no-daemon-restart` overrules both for one). Run `appduct <command> --help` for the exact flags of any command.
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.
52
57
 
53
- `--timeout` on `invoke` is clamped to 1,000–600,000 ms and can only **shorten** the deadline, never extend it past the app's own timer: the app aborts the handler at the tool's declared `timeoutMs`, or 10 seconds for a tool that declares none, regardless of what the caller asks for. If a tool needs more room, declare `timeoutMs` on its registration.
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.
54
59
 
55
- ### `appduct tools`: a signature per tool
60
+ ### `appduct tools ls`: a signature per tool
56
61
 
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:
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:
58
63
 
59
64
  ```
60
65
  Tools
@@ -63,16 +68,16 @@ Tools
63
68
  set_flag(name: "dark_mode" | "new_checkout", enabled: bool) [prompt]
64
69
  Toggle a feature flag.
65
70
 
66
- Run `appduct tools <name>` for a tool's full schema.
71
+ Run `appduct tools describe <name>` for a tool's full schema.
67
72
  ```
68
73
 
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"`.
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"`.
70
75
 
71
- Use `--filter <text>` to narrow the listing to tools whose name or description contains `<text>` (case-insensitive), and `--limit <n>`/`--offset <n>` to page through it; a truncated listing prints a trailing `Showing n of total tools (offset o). Narrow with --filter <text> or page with --offset <n>.` line so you know more were left out. `appduct tools --json` returns `{ tools, total, groups }` — `total` is the count after `--group`/`--filter` but before `--limit`/`--offset`. `appduct tools <name>` (a single tool) is unaffected by any of this and always returns the bare tool descriptor; passing `--group`, `--groups`, `--filter`, `--limit` or `--offset` with a `<name>` is a usage error.
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.
72
77
 
73
78
  ### Tool groups
74
79
 
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:
80
+ 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
81
 
77
82
  ```
78
83
  Tools
@@ -93,23 +98,23 @@ Tools
93
98
  On a large app, start with `--groups` to see what there is, then list one group:
94
99
 
95
100
  ```
96
- $ appduct tools --groups
101
+ $ appduct tools ls --groups
97
102
  Groups
98
103
  cart 12
99
104
  checkout 8
100
105
  checkout/payment 3
101
106
  (ungrouped) 2
102
107
 
103
- 22 tools in total. Run `appduct tools --group <name>` to list one group's tools.
108
+ 22 tools in total. Run `appduct tools ls --group <name>` to list one group's tools.
104
109
 
105
- $ appduct tools --group checkout
110
+ $ appduct tools ls --group checkout
106
111
  ```
107
112
 
108
113
  `--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
114
 
110
115
  ### The deep-link scheme
111
116
 
112
- `appduct link`, `appduct mcp`, and the MCP `appduct_connect` tool all resolve the scheme the same way, first match wins:
117
+ `appduct sessions link`, `appduct mcp`, and the MCP `appduct_connect` tool all resolve the scheme the same way, first match wins:
113
118
 
114
119
  1. `--scheme <s>`
115
120
  2. the `APPDUCT_SCHEME` environment variable
@@ -164,17 +169,17 @@ It needs all of:
164
169
  - **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
170
  - The device **paired and trusted** by this Mac, with **Developer Mode** enabled on it (Settings → Privacy & Security → Developer Mode).
166
171
  - 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`.
172
+ - 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
173
  - The app's **bundle id** — see [Delivering the link to a device](#delivering-the-link-to-a-device) for how to supply it.
169
174
 
170
175
  ```bash
171
- appduct link --scheme myapp --open ios-device --app-id com.example.myapp
176
+ appduct sessions link --scheme myapp --open ios-device --app-id com.example.myapp
172
177
  ```
173
178
 
174
179
  Two things worth knowing before you rely on this:
175
180
 
176
181
  - **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.
182
+ - **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
183
 
179
184
  ## MCP setup (Claude Code, Cursor, and similar)
180
185
 
@@ -208,23 +213,23 @@ Once configured, an agent reaches the connected app's tools through three built-
208
213
 
209
214
  | Tool | Does what | CLI equivalent |
210
215
  | --- | --- | --- |
211
- | `appduct_list_tools` | Lists the app's tools as one-line signatures, with each tool's group and policy, plus the app's groups with counts. Returns 50 at a time unless given `limit`; takes `group`, `filter` and `offset`. | `appduct tools` |
212
- | `appduct_describe_tool` | Shows one tool's full input and output schema. | `appduct tools <name>` |
213
- | `appduct_call_tool` | Calls a tool by `name` with `args`, with progress and errors preserved. | `appduct invoke` |
216
+ | `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` |
217
+ | `appduct_describe_tool` | Shows one tool's full input and output schema. | `appduct tools describe <name>` |
218
+ | `appduct_call_tool` | Calls a tool by `name` with `args`, with progress and errors preserved. | `appduct tools call` |
214
219
 
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`.
220
+ 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
221
 
217
222
  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
223
 
219
224
  Set this before you connect: the daemon reads `config.json` only when it starts, so changing it later takes `appduct daemon stop`. That disconnects every device, which then has to link again, and leaves a running `appduct mcp` without a daemon, so restart the MCP server in your client as well.
220
225
 
221
- Four more built-in tools cover what the app's own tools can't. `appduct_connect` mints a link and, by default, delivers it to whichever `android`/`ios-sim` device it detects — pass `target`/`device` to choose, or `target: "none"` to force the human flow — falling back to a QR code, plus instructions to show it, only when there's nothing to deliver to. Delivering to `android` (chosen or detected) needs `appId`, resolved the same way as `--app-id` (see [Delivering the link to a device](#delivering-the-link-to-a-device)); passing it with `target: "ios-sim"` or `"none"` is an error. `appduct_wait_for_session` then waits for that session to be claimed. `target: "ios-device"` reaches a paired physical iPhone or iPad, with `appId` and the [prerequisites above](#--open-ios-device-experimental) — it's experimental and never auto-detected, so an agent has to ask for it by name.
226
+ Five 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
227
 
223
- The other two give an agent a pull surface over `postEvent()`-pushed app events: `appduct_events` drains everything retained since a cursor, and `appduct_wait_for_event` blocks for a matching event (checking what's already retained before waiting live), rejecting with `tool_timeout` if none arrives in time.
228
+ The other three give an agent a pull surface over `postEvent()`-pushed app events. `appduct_list_events` lists the events the app declared (`appduct events ls`) as signatures with descriptions, plus the payload schema when `name` is an exact name, so an agent learns the names before it waits on one. `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
229
 
225
230
  ## Test runners: `appduct/client`
226
231
 
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 invoke ... --json` and parsing stdout:
232
+ 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
233
 
229
234
  ```ts
230
235
  import { connect } from "appduct/client";
@@ -264,18 +269,19 @@ const app = await connect<Tools>();
264
269
  const { total } = await app.call("sum", { a: 2, b: 3 }); // typed
265
270
  ```
266
271
 
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:
272
+ `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
273
 
269
274
  ```ts
270
275
  const { events, cursor } = await app.events(); // pull: what already happened
271
276
  const next = await app.waitForEvent("checkout_done", { since: cursor });
277
+ const capped = await app.waitForEvent("checkout_done", { payloadMaxBytes: 1024 }); // check `truncated` before reading `payload`
272
278
  ```
273
279
 
274
280
  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
281
 
276
282
  ## Keys and pins
277
283
 
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.
284
+ 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
285
 
280
286
  For a build that should trust only a key you embedded ahead of time, generate one explicitly:
281
287
 
@@ -285,7 +291,7 @@ appduct keygen
285
291
 
286
292
  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
293
 
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.
294
+ `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
295
 
290
296
  ## Daemon lifecycle
291
297
 
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("link", "Mint a pending session and print its deep link.").option("--ttl <seconds>", "Link time-to-live in seconds (default: from config.json).").option("--qr", "Also render the deep link as a terminal QR code.").option(
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
- `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>).`
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
- "Deliver the link automatically via adb/simctl/devicectl (android|ios-sim|ios-device; ios-device is experimental)."
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
- `Installed app id for --open android/ios-device (default: .appduct/config.json's "appId.<platform>").`
134
+ `link: installed app id for --open android/ios-device (default: .appduct/config.json's "appId.<platform>").`
135
135
  ).option(
136
136
  "--relaunch",
137
- "With --open ios-device, terminate a running instance first (try this if delivery to an already-running app does nothing)."
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("ls", "List Appduct sessions.");
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
- "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)."
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]", "List the events an app declares, stream them, or replay them since a cursor: ls, tail, or since.").usage("events <ls|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). ls: an exact name prints that event's full payload schema.`
146
+ ).option("--limit <n>", "ls: show at most n events.").option("--offset <n>", "ls: skip the first n events of the name-sorted list.").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 the events the app posts until interrupted.").option("--follow", "Accepted for script readability; the default behavior already follows.").option("--since <cursor>", "One-shot: print the app 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)."
@@ -521,6 +524,7 @@ var getExitCodeForError = (error) => {
521
524
  import pc from "picocolors";
522
525
  import {
523
526
  formatAgentWebSocketUrl,
527
+ renderEventSignature,
524
528
  renderToolSignature,
525
529
  summarizeToolDescription
526
530
  } from "@appduct/shared";
@@ -616,6 +620,13 @@ var formatDevice = (device) => {
616
620
  const label = parts.length > 0 ? parts.join(" ") : "unknown device";
617
621
  return device.os ? `${label} (${device.os})` : label;
618
622
  };
623
+ var SUSPEND_REASON_LABELS = {
624
+ app_backgrounded: "app in background",
625
+ connection_lost: "connection lost"
626
+ };
627
+ var formatSessionState = (session) => {
628
+ return session.suspendReason ? `${session.state} (${SUSPEND_REASON_LABELS[session.suspendReason]})` : session.state;
629
+ };
619
630
  var renderLsData = (colors, data, now) => {
620
631
  if (data.length === 0) {
621
632
  return [colors.dim("Sessions"), " No Appduct sessions are registered."];
@@ -624,7 +635,7 @@ var renderLsData = (colors, data, now) => {
624
635
  const headers = ["Alias", "State", "Device", "Tools", "Age"];
625
636
  const rows = data.map((session) => [
626
637
  session.alias,
627
- session.state,
638
+ formatSessionState(session),
628
639
  formatDevice(session.device),
629
640
  String(session.toolCount),
630
641
  formatAge(session.claimedAt ?? session.createdAt, nowIso)
@@ -638,9 +649,6 @@ var renderLsData = (colors, data, now) => {
638
649
  ...rows.map((row) => ` ${row.map((cell, index) => cell.padEnd(widths[index])).join(" ")}`)
639
650
  ];
640
651
  };
641
- var isToolsListing = (data) => {
642
- return typeof data === "object" && data !== null && Array.isArray(data.tools);
643
- };
644
652
  var isToolGroupsListing = (data) => {
645
653
  return typeof data === "object" && data !== null && Array.isArray(data.groups) && !Array.isArray(data.tools);
646
654
  };
@@ -658,7 +666,7 @@ var renderEmptyToolsLine = (data) => {
658
666
  }
659
667
  if (data.group !== void 0) {
660
668
  const match = data.filter === void 0 ? "" : ` match ${JSON.stringify(data.filter)}`;
661
- return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools --groups\` to see the session's groups.`;
669
+ return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools ls --groups\` to see the session's groups.`;
662
670
  }
663
671
  return data.filter === void 0 ? " No tools registered." : ` No tools match ${JSON.stringify(data.filter)}.`;
664
672
  };
@@ -740,7 +748,7 @@ var renderToolSummaryTable = (colors, data) => {
740
748
  ...grouped ? renderGroupedToolLines(colors, data.tools) : data.tools.flatMap((tool) => renderToolSummaryLines(tool, " ")),
741
749
  ...renderTruncationLine(data),
742
750
  "",
743
- "Run `appduct tools <name>` for a tool's full schema."
751
+ "Run `appduct tools describe <name>` for a tool's full schema."
744
752
  ];
745
753
  };
746
754
  var renderToolDetail = (colors, tool, flags) => {
@@ -792,18 +800,62 @@ var renderToolGroups = (colors, data) => {
792
800
  colors.green("Groups"),
793
801
  ...data.groups.map((entry) => ` ${label(entry).padEnd(width)} ${String(entry.total).padStart(countWidth)}`),
794
802
  "",
795
- `${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.")
803
+ `${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.")
796
804
  ];
797
805
  };
798
- var renderToolsData = (colors, data, flags, full) => {
806
+ var renderToolsListData = (colors, data, flags, full) => {
799
807
  if (isToolGroupsListing(data)) {
800
808
  return renderToolGroups(colors, data);
801
809
  }
802
- if (!isToolsListing(data)) {
803
- return renderToolDetail(colors, data, flags);
804
- }
805
810
  return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
806
811
  };
812
+ var renderEventsTruncationLine = (data) => {
813
+ const offset = data.offset ?? 0;
814
+ if (data.events.length >= data.total) {
815
+ return [];
816
+ }
817
+ return [
818
+ "",
819
+ `Showing ${data.events.length} of ${data.total} events (offset ${offset}). Narrow with --name <glob> or page with --offset <n>.`
820
+ ];
821
+ };
822
+ var renderEventsListData = (colors, data, flags) => {
823
+ const [only] = data.events;
824
+ if (data.name !== void 0 && !data.name.includes("*") && only !== void 0) {
825
+ return renderFields(
826
+ colors.green(`Event: ${only.name}`),
827
+ [
828
+ ["Signature", renderEventSignature(only)],
829
+ ["Description", only.description],
830
+ ["Payload schema", only.payload_schema]
831
+ ],
832
+ flags
833
+ );
834
+ }
835
+ if (data.events.length === 0 && data.total > 0) {
836
+ return [
837
+ colors.green("Events"),
838
+ ` No events at offset ${data.offset ?? 0}; ${data.total} matching event${data.total === 1 ? "" : "s"} in total.`
839
+ ];
840
+ }
841
+ if (data.events.length === 0) {
842
+ return [
843
+ colors.green("Events"),
844
+ data.name === void 0 ? " No events declared." : ` No events match ${JSON.stringify(data.name)}.`
845
+ ];
846
+ }
847
+ const lines = data.events.flatMap((event) => [
848
+ ` ${renderEventSignature(event)}`,
849
+ ` ${summarizeToolDescription(event.description)}`
850
+ ]);
851
+ return [
852
+ colors.green("Events"),
853
+ ...lines,
854
+ ...renderEventsTruncationLine(data),
855
+ "",
856
+ "Run `appduct events ls --name <name>` for an event's full payload schema."
857
+ ];
858
+ };
807
859
  var renderInvokeData = (colors, data, flags) => {
808
860
  return [colors.green("Result"), formatScalar(data, flags)];
809
861
  };
@@ -987,15 +1039,19 @@ var renderSuccessData = (colors, command, data, options) => {
987
1039
  return renderInitData(colors, data, flags);
988
1040
  case "keygen":
989
1041
  return renderKeygenData(colors, data, flags);
990
- case "link":
1042
+ case "sessions link":
991
1043
  return renderLinkData(colors, data, flags, options.qr);
992
- case "ls":
1044
+ case "sessions ls":
993
1045
  return renderLsData(colors, data, options.now ?? /* @__PURE__ */ new Date());
994
- case "tools":
995
- return renderToolsData(colors, data, flags, options.full);
996
- case "invoke":
1046
+ case "tools ls":
1047
+ return renderToolsListData(colors, data, flags, options.full);
1048
+ case "tools describe":
1049
+ return renderToolDetail(colors, data, flags);
1050
+ case "events ls":
1051
+ return renderEventsListData(colors, data, flags);
1052
+ case "tools call":
997
1053
  return renderInvokeData(colors, data, flags);
998
- case "revoke":
1054
+ case "sessions revoke":
999
1055
  return renderRevokeData(colors, data);
1000
1056
  case "daemon run":
1001
1057
  return renderDaemonRunData(colors, data, flags);
@@ -1150,21 +1206,30 @@ var DAEMON_RESTART_ENV = "APPDUCT_DAEMON_RESTART";
1150
1206
  var isEnvTruthy = (value) => {
1151
1207
  return value === "1" || value?.toLowerCase() === "true";
1152
1208
  };
1209
+ var REMOVED_COMMANDS = {
1210
+ ls: "sessions ls",
1211
+ revoke: "sessions revoke",
1212
+ link: "sessions link",
1213
+ invoke: "tools call"
1214
+ };
1215
+ var rootUnknownCommandError = (word) => {
1216
+ if (word !== void 0 && Object.hasOwn(REMOVED_COMMANDS, word)) {
1217
+ return usageError(`Unknown command "${word}"; use "appduct ${REMOVED_COMMANDS[word]}".`);
1218
+ }
1219
+ return unknownCommandError(word);
1220
+ };
1153
1221
  var rootRouter = createRouter(
1154
1222
  {
1155
1223
  init: () => import("./cli/routes/init.js"),
1156
1224
  keygen: () => import("./cli/routes/keygen.js"),
1157
- link: () => import("./cli/routes/link.js"),
1158
- ls: () => import("./cli/routes/ls.js"),
1159
- tools: () => import("./cli/routes/tools.js"),
1160
- invoke: () => import("./cli/routes/invoke.js"),
1161
- revoke: () => import("./cli/routes/revoke.js"),
1162
- events: () => import("./cli/routes/events.js"),
1225
+ sessions: () => import("./cli/routes/sessions/index.js"),
1226
+ tools: () => import("./cli/routes/tools/index.js"),
1227
+ events: () => import("./cli/routes/events/index.js"),
1163
1228
  mcp: () => import("./cli/routes/mcp.js"),
1164
1229
  doctor: () => import("./cli/routes/doctor.js"),
1165
1230
  daemon: () => import("./cli/routes/daemon/index.js")
1166
1231
  },
1167
- { unknown: unknownCommandError }
1232
+ { unknown: rootUnknownCommandError }
1168
1233
  );
1169
1234
  var runCli = async (argv, options = {}) => {
1170
1235
  const writers = {
@@ -1203,7 +1268,7 @@ var runCli = async (argv, options = {}) => {
1203
1268
  return executeCommand(
1204
1269
  "cli",
1205
1270
  () => {
1206
- throw usageError(`Unknown command "${parsedArgs[0]}".`);
1271
+ throw rootUnknownCommandError(parsedArgs[0]);
1207
1272
  },
1208
1273
  {
1209
1274
  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 --since`,
11
- * whose cursor `0` is a meaningful "everything retained" value, not an omission. */
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. `invoke
23
- * [selector] <tool>`, `tools [selector] <name>`): the last positional is always the required
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
- * Splits the positional args of a command shaped `<command> [selector] [target]` (`tools [selector]
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
  */
@@ -4,7 +4,7 @@
4
4
  * the thin-client shapes for the v2 command table.
5
5
  */
6
6
  import type { OpenTarget } from "./open-target.js";
7
- import type { AgentEndpoint, EffectivePolicyDecision, EventNotification, SessionSummary, ToolGroupSummary, ToolsListEntry, ToolsListResult } from "@appduct/shared";
7
+ import type { AgentEndpoint, EffectivePolicyDecision, EventNotification, EventsListResult, SessionSummary, ToolGroupSummary, ToolsListEntry, ToolsListResult } from "@appduct/shared";
8
8
  /**
9
9
  * CLI-domain error classes, used only to pick a sysexits-style exit code (see `errors.ts`).
10
10
  * `CliError.type` itself is *not* restricted to this union: RPC/app-originated failures preserve
@@ -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 passthrough"). */
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,31 @@ 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 single entry when a tool name
133
- * resolved to a detail lookup. The detail form is the daemon's own listing entry, picked out of
134
- * `tools.list` — not a registration, so its `group` is `null` for an ungrouped tool. */
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 invoke`: the tool's raw result payload, printed as-is. */
138
+ /** `appduct events ls`: the daemon's `events.list` result, plus the `--name`/`--limit`/`--offset`
139
+ * inputs that produced it (only the ones actually given). */
140
+ export type EventsListing = EventsListResult & {
141
+ name?: string;
142
+ limit?: number;
143
+ offset?: number;
144
+ };
145
+ /** `appduct tools call`: the tool's raw result payload, printed as-is. */
137
146
  export type InvokeCommandData = unknown;
138
147
  export type RevokeCommandData = {
139
148
  ok: true;
140
149
  };
141
- /** A single `appduct events` line, in both human and NDJSON (`--json`) rendering. */
150
+ /** A single `appduct events tail`/`appduct events since` line, in both human and NDJSON
151
+ * (`--json`) rendering. */
142
152
  export type EventsCommandLine = EventNotification;
143
153
  export type DaemonRunCommandData = {
144
154
  daemon: {