appduct 0.12.0 → 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.
Files changed (69) hide show
  1. package/README.md +41 -36
  2. package/dist/bin.js +44 -36
  3. package/dist/cli/command-options.d.ts +7 -17
  4. package/dist/cli/result-types.d.ts +11 -8
  5. package/dist/cli/routes/daemon/index.js +12 -16
  6. package/dist/cli/routes/daemon/run.js +136 -28
  7. package/dist/cli/routes/daemon/start.js +12 -16
  8. package/dist/cli/routes/daemon/status.js +12 -16
  9. package/dist/cli/routes/daemon/stop.js +12 -16
  10. package/dist/cli/routes/doctor.js +12 -16
  11. package/dist/cli/routes/events/index.d.ts +9 -0
  12. package/dist/cli/routes/events/index.js +842 -0
  13. package/dist/cli/routes/events/shared-options.d.ts +12 -0
  14. package/dist/cli/routes/events/shared-options.js +102 -0
  15. package/dist/cli/routes/events/since.d.ts +6 -0
  16. package/dist/cli/routes/events/since.js +1977 -0
  17. package/dist/cli/routes/events/tail.d.ts +6 -0
  18. package/dist/cli/routes/{events.js → events/tail.js} +177 -162
  19. package/dist/cli/routes/init.js +14 -18
  20. package/dist/cli/routes/keygen.js +12 -16
  21. package/dist/cli/routes/mcp.js +181 -165
  22. package/dist/cli/routes/sessions/index.d.ts +7 -0
  23. package/dist/cli/routes/sessions/index.js +843 -0
  24. package/dist/cli/routes/sessions/link.d.ts +4 -0
  25. package/dist/cli/routes/{link.js → sessions/link.js} +13 -17
  26. package/dist/cli/routes/sessions/ls.d.ts +4 -0
  27. package/dist/cli/routes/{ls.js → sessions/ls.js} +13 -17
  28. package/dist/cli/routes/sessions/revoke.d.ts +4 -0
  29. package/dist/cli/routes/{revoke.js → sessions/revoke.js} +14 -18
  30. package/dist/cli/routes/tools/call.d.ts +4 -0
  31. package/dist/cli/routes/{invoke.js → tools/call.js} +15 -19
  32. package/dist/cli/routes/tools/describe.d.ts +6 -0
  33. package/dist/cli/routes/tools/describe.js +1837 -0
  34. package/dist/cli/routes/tools/index.d.ts +6 -0
  35. package/dist/cli/routes/tools/index.js +843 -0
  36. package/dist/cli/routes/tools/ls.d.ts +4 -0
  37. package/dist/cli/routes/{tools.js → tools/ls.js} +40 -73
  38. package/dist/cli/routes/tools/shared-options.d.ts +16 -0
  39. package/dist/cli/routes/tools/shared-options.js +122 -0
  40. package/dist/client/app-client.d.ts +96 -37
  41. package/dist/client/index.d.ts +3 -2
  42. package/dist/client/index.js +178 -138
  43. package/dist/commands/events.d.ts +31 -14
  44. package/dist/commands/invoke.d.ts +1 -1
  45. package/dist/commands/link.d.ts +1 -1
  46. package/dist/commands/ls.d.ts +1 -1
  47. package/dist/commands/revoke.d.ts +1 -1
  48. package/dist/commands/tools.d.ts +12 -13
  49. package/dist/daemon/config.d.ts +1 -1
  50. package/dist/daemon/event-bus.d.ts +32 -0
  51. package/dist/daemon/rpc-errors.d.ts +1 -1
  52. package/dist/errors.d.ts +1 -1
  53. package/dist/events/index.d.ts +60 -0
  54. package/dist/index.js +56 -39
  55. package/dist/link.d.ts +1 -1
  56. package/dist/mcp/app-tools.d.ts +3 -2
  57. package/dist/mcp/connect-tool.d.ts +1 -1
  58. package/dist/mcp/events-tool.d.ts +28 -27
  59. package/dist/native-scheme.d.ts +1 -1
  60. package/dist/output.d.ts +29 -6
  61. package/dist/rpc/client.d.ts +1 -1
  62. package/dist/scheme.d.ts +4 -4
  63. package/package.json +2 -2
  64. package/dist/cli/routes/events.d.ts +0 -3
  65. package/dist/cli/routes/invoke.d.ts +0 -3
  66. package/dist/cli/routes/link.d.ts +0 -3
  67. package/dist/cli/routes/ls.d.ts +0 -3
  68. package/dist/cli/routes/revoke.d.ts +0 -3
  69. package/dist/cli/routes/tools.d.ts +0 -3
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `waitForAppEvent` (issue #114): the one drain-then-live wait implementation shared by the
3
+ * built-in `appduct_wait_for_event` MCP tool (`mcp/events-tool.ts`) and `appduct/client`'s
4
+ * `AppClient.waitForEvent` (`client/app-client.ts`), which used to each carry their own copy.
5
+ *
6
+ * Filtering by `name` (a whole-name, case-sensitive glob — issue #112) and truncating a payload
7
+ * over `payloadMaxBytes` (issue #113) both happen daemon-side, via the same `projectAppEvent`
8
+ * (`daemon/event-bus.ts`) that `events.since`'s drain and `events.subscribe`'s live fan-out both
9
+ * already call — this module forwards `name`/`payloadMaxBytes` to both RPCs and never re-checks
10
+ * a name or re-truncates a payload itself.
11
+ *
12
+ * The drain-then-live shape exists to close the race `appduct_wait_for_session` doesn't have to
13
+ * worry about: a session is either claimed or not, but an app event can fire between "the caller
14
+ * decides to wait" and "the wait subscription lands". The notification listener is registered
15
+ * *before* `events.subscribe` is even sent — not merely before it resolves — because the
16
+ * daemon's response to that call and a `notify()` for a brand-new event can arrive in the same
17
+ * socket chunk; `rpc/client.ts`'s `DaemonStream` fans a chunk's notification lines out to
18
+ * whatever listeners exist at the moment it's processed, so a listener added only after `await`ing
19
+ * that response could miss a notification line from that same chunk. Arrivals before the drain
20
+ * has been merged into the backlog are buffered, then merged in (deduped by `seq`) before falling
21
+ * through to the live phase.
22
+ *
23
+ * `stream` is the caller's: opened and closed by the caller. Every caller of this module opens
24
+ * one stream per `waitForAppEvent` call — the MCP tool always did (`openDaemonStream` per
25
+ * `tools/call`), and the SDK now does too, so concurrent `waitForEvent` calls with different
26
+ * `name`s never clobber a shared connection's single `events.subscribe` filter.
27
+ */
28
+ import { type AppEvent } from "@appduct/shared";
29
+ import type { DaemonStream } from "../rpc/client.js";
30
+ export type WaitForAppEventOptions = {
31
+ sessionId: string;
32
+ /** Whole-name, case-sensitive glob (issue #112): `*` matches any run of characters, a pattern
33
+ * with no `*` is an exact name — forwarded verbatim to `events.since`/`events.subscribe`. */
34
+ name: string;
35
+ /** Exclusive lower bound on `AppEvent.seq`; omitted drains the whole retained buffer. */
36
+ since?: number;
37
+ timeoutMs: number;
38
+ /** Caps the resolved event's payload to this many UTF-8 bytes of its JSON, daemon-side, on
39
+ * both the drain and the live half. Omitted, payloads are delivered whole. */
40
+ payloadMaxBytes?: number;
41
+ };
42
+ export type WaitForAppEventResult = {
43
+ event: AppEvent;
44
+ /** `events.since`'s own `dropped` from the drain: app events after `since` that were evicted
45
+ * before the drain could return them (`0` once nothing has fallen off). Reflects only the
46
+ * drain — an event that resolves this wait live was delivered, not dropped. */
47
+ dropped: number;
48
+ };
49
+ /** Raised when nothing matching `name` arrived within `timeoutMs`. */
50
+ export declare class WaitForAppEventTimeoutError extends Error {
51
+ readonly eventName: string;
52
+ readonly timeoutMs: number;
53
+ constructor(eventName: string, timeoutMs: number);
54
+ }
55
+ /** Raised when `stream` closes (daemon gone, `stop()`, etc.) while a live wait was in flight. */
56
+ export declare class WaitForAppEventConnectionClosedError extends Error {
57
+ readonly eventName: string;
58
+ constructor(eventName: string);
59
+ }
60
+ export declare const waitForAppEvent: (stream: DaemonStream, options: WaitForAppEventOptions) => Promise<WaitForAppEventResult>;
package/dist/index.js CHANGED
@@ -121,27 +121,30 @@ var createCli = () => {
121
121
  'Android package name to write as "appId.android" (needed to deliver a link with --open android).'
122
122
  );
123
123
  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.");
124
- 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(
124
+ 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(
125
125
  "--scheme <scheme>",
126
- `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>).`
126
+ `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>).`
127
127
  ).option(
128
128
  "--open <target>",
129
- "Deliver the link automatically via adb/simctl/devicectl (android|ios-sim|ios-device; ios-device is experimental)."
130
- ).option("--device <id>", "adb serial, simulator udid or paired-device udid to target when --open is ambiguous.").option(
129
+ "link: deliver the link automatically via adb/simctl/devicectl (android|ios-sim|ios-device; ios-device is experimental)."
130
+ ).option("--device <id>", "link: adb serial, simulator udid or paired-device udid to target when --open is ambiguous.").option(
131
131
  "--app-id <id>",
132
- `Installed app id for --open android/ios-device (default: .appduct/config.json's "appId.<platform>").`
132
+ `link: installed app id for --open android/ios-device (default: .appduct/config.json's "appId.<platform>").`
133
133
  ).option(
134
134
  "--relaunch",
135
- "With --open ios-device, terminate a running instance first (try this if delivery to an already-running app does nothing)."
135
+ "link: with --open ios-device, terminate a running instance first (try this if delivery to an already-running app does nothing)."
136
136
  );
137
- cli.command("ls", "List Appduct sessions.");
138
- 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.");
139
- cli.command("invoke [selector] [tool]", "Call a tool on a session.").option("--input <json>", "Tool input arguments as a JSON object.").option(
137
+ 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(
140
138
  "--timeout <ms>",
141
- "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)."
139
+ "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)."
140
+ );
141
+ 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(
142
+ "--name <glob>",
143
+ 'Only events whose name matches this whole-name, case-sensitive glob ("*" matches any run of characters).'
144
+ ).option(
145
+ "--payload-max-bytes <n>",
146
+ "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."
142
147
  );
143
- 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.");
144
- cli.command("revoke [selector]", "Revoke a session.");
145
148
  cli.command("mcp", "Start a stdio MCP server that gives MCP clients access to connected apps' tools.").option(
146
149
  "--scheme <scheme>",
147
150
  "Deep-link URI scheme for appduct_connect (also: APPDUCT_SCHEME)."
@@ -660,9 +663,6 @@ var renderLsData = (colors, data, now) => {
660
663
  ...rows.map((row) => ` ${row.map((cell, index) => cell.padEnd(widths[index])).join(" ")}`)
661
664
  ];
662
665
  };
663
- var isToolsListing = (data) => {
664
- return typeof data === "object" && data !== null && Array.isArray(data.tools);
665
- };
666
666
  var isToolGroupsListing = (data) => {
667
667
  return typeof data === "object" && data !== null && Array.isArray(data.groups) && !Array.isArray(data.tools);
668
668
  };
@@ -680,7 +680,7 @@ var renderEmptyToolsLine = (data) => {
680
680
  }
681
681
  if (data.group !== void 0) {
682
682
  const match = data.filter === void 0 ? "" : ` match ${JSON.stringify(data.filter)}`;
683
- return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools --groups\` to see the session's groups.`;
683
+ return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools ls --groups\` to see the session's groups.`;
684
684
  }
685
685
  return data.filter === void 0 ? " No tools registered." : ` No tools match ${JSON.stringify(data.filter)}.`;
686
686
  };
@@ -762,7 +762,7 @@ var renderToolSummaryTable = (colors, data) => {
762
762
  ...grouped ? renderGroupedToolLines(colors, data.tools) : data.tools.flatMap((tool) => renderToolSummaryLines(tool, " ")),
763
763
  ...renderTruncationLine(data),
764
764
  "",
765
- "Run `appduct tools <name>` for a tool's full schema."
765
+ "Run `appduct tools describe <name>` for a tool's full schema."
766
766
  ];
767
767
  };
768
768
  var renderToolDetail = (colors, tool, flags) => {
@@ -814,16 +814,13 @@ var renderToolGroups = (colors, data) => {
814
814
  colors.green("Groups"),
815
815
  ...data.groups.map((entry) => ` ${label(entry).padEnd(width)} ${String(entry.total).padStart(countWidth)}`),
816
816
  "",
817
- `${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.")
817
+ `${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.")
818
818
  ];
819
819
  };
820
- var renderToolsData = (colors, data, flags, full) => {
820
+ var renderToolsListData = (colors, data, flags, full) => {
821
821
  if (isToolGroupsListing(data)) {
822
822
  return renderToolGroups(colors, data);
823
823
  }
824
- if (!isToolsListing(data)) {
825
- return renderToolDetail(colors, data, flags);
826
- }
827
824
  return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
828
825
  };
829
826
  var renderInvokeData = (colors, data, flags) => {
@@ -1009,15 +1006,17 @@ var renderSuccessData = (colors, command, data, options) => {
1009
1006
  return renderInitData(colors, data, flags);
1010
1007
  case "keygen":
1011
1008
  return renderKeygenData(colors, data, flags);
1012
- case "link":
1009
+ case "sessions link":
1013
1010
  return renderLinkData(colors, data, flags, options.qr);
1014
- case "ls":
1011
+ case "sessions ls":
1015
1012
  return renderLsData(colors, data, options.now ?? /* @__PURE__ */ new Date());
1016
- case "tools":
1017
- return renderToolsData(colors, data, flags, options.full);
1018
- case "invoke":
1013
+ case "tools ls":
1014
+ return renderToolsListData(colors, data, flags, options.full);
1015
+ case "tools describe":
1016
+ return renderToolDetail(colors, data, flags);
1017
+ case "tools call":
1019
1018
  return renderInvokeData(colors, data, flags);
1020
- case "revoke":
1019
+ case "sessions revoke":
1021
1020
  return renderRevokeData(colors, data);
1022
1021
  case "daemon run":
1023
1022
  return renderDaemonRunData(colors, data, flags);
@@ -1087,12 +1086,21 @@ var renderEventLine = (event, flags) => {
1087
1086
  const dataSuffix = event.data === void 0 ? "" : ` ${formatJson(event.data, flags)}`;
1088
1087
  return `${colors.dim(timestamp)} ${colors.green(event.kind)}${target ? ` ${target}` : ""}${dataSuffix}`;
1089
1088
  };
1090
- var renderEventsCursorLine = (cursor, flags) => {
1089
+ var shellQuote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
1090
+ var renderEventsCursorLine = (cursor, flags, selector, filters) => {
1091
1091
  if (flags.json) {
1092
- return JSON.stringify({ cursor });
1092
+ return JSON.stringify({ cursor: cursor.cursor, dropped: cursor.dropped, remaining: cursor.remaining });
1093
1093
  }
1094
1094
  const colors = pc.createColors(flags.color);
1095
- return colors.dim(`cursor: ${cursor} (pass --since ${cursor} to resume from here)`);
1095
+ const target = selector === void 0 ? `${cursor.cursor}` : `${selector} ${cursor.cursor}`;
1096
+ const flagTokens = [
1097
+ ...filters?.name === void 0 ? [] : [`--name ${shellQuote(filters.name)}`],
1098
+ ...filters?.payloadMaxBytes === void 0 ? [] : [`--payload-max-bytes ${filters.payloadMaxBytes}`]
1099
+ ];
1100
+ const command = [`appduct events since ${target}`, ...flagTokens].join(" ");
1101
+ return colors.dim(
1102
+ `cursor: ${cursor.cursor}, dropped: ${cursor.dropped}, remaining: ${cursor.remaining} (run "${command}" to resume from here)`
1103
+ );
1096
1104
  };
1097
1105
 
1098
1106
  // src/cli/envelope.ts
@@ -1189,21 +1197,30 @@ var DAEMON_RESTART_ENV = "APPDUCT_DAEMON_RESTART";
1189
1197
  var isEnvTruthy = (value) => {
1190
1198
  return value === "1" || value?.toLowerCase() === "true";
1191
1199
  };
1200
+ var REMOVED_COMMANDS = {
1201
+ ls: "sessions ls",
1202
+ revoke: "sessions revoke",
1203
+ link: "sessions link",
1204
+ invoke: "tools call"
1205
+ };
1206
+ var rootUnknownCommandError = (word) => {
1207
+ if (word !== void 0 && Object.hasOwn(REMOVED_COMMANDS, word)) {
1208
+ return usageError(`Unknown command "${word}"; use "appduct ${REMOVED_COMMANDS[word]}".`);
1209
+ }
1210
+ return unknownCommandError(word);
1211
+ };
1192
1212
  var rootRouter = createRouter(
1193
1213
  {
1194
1214
  init: () => import("./cli/routes/init.js"),
1195
1215
  keygen: () => import("./cli/routes/keygen.js"),
1196
- link: () => import("./cli/routes/link.js"),
1197
- ls: () => import("./cli/routes/ls.js"),
1198
- tools: () => import("./cli/routes/tools.js"),
1199
- invoke: () => import("./cli/routes/invoke.js"),
1200
- revoke: () => import("./cli/routes/revoke.js"),
1201
- events: () => import("./cli/routes/events.js"),
1216
+ sessions: () => import("./cli/routes/sessions/index.js"),
1217
+ tools: () => import("./cli/routes/tools/index.js"),
1218
+ events: () => import("./cli/routes/events/index.js"),
1202
1219
  mcp: () => import("./cli/routes/mcp.js"),
1203
1220
  doctor: () => import("./cli/routes/doctor.js"),
1204
1221
  daemon: () => import("./cli/routes/daemon/index.js")
1205
1222
  },
1206
- { unknown: unknownCommandError }
1223
+ { unknown: rootUnknownCommandError }
1207
1224
  );
1208
1225
  var runCli = async (argv, options = {}) => {
1209
1226
  const writers = {
@@ -1242,7 +1259,7 @@ var runCli = async (argv, options = {}) => {
1242
1259
  return executeCommand(
1243
1260
  "cli",
1244
1261
  () => {
1245
- throw usageError(`Unknown command "${parsedArgs[0]}".`);
1262
+ throw rootUnknownCommandError(parsedArgs[0]);
1246
1263
  },
1247
1264
  {
1248
1265
  flags,
package/dist/link.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Shared core of `link.create` + deep-link composition + optional emulator/simulator delivery
3
3
  * (ARCHITECTURE.md §8, §10): mints a pending session via `link.create`, then composes
4
4
  * `<scheme>:///?appduct=<payload>&pin=<spki-pin>` and optionally delivers it to a booted
5
- * Android emulator/device or iOS simulator. Used by `commands/link.ts` (`appduct link`) and
5
+ * Android emulator/device or iOS simulator. Used by `commands/link.ts` (`appduct sessions link`) and
6
6
  * `client/bootstrap.ts` (`appduct/client`'s `link()`) so this shape — scheme resolution, the
7
7
  * `pin` query param, the `127.0.0.1` emulator/simulator address override (which the experimental
8
8
  * `ios-device` target deliberately opts out of) — can't drift between the CLI and the programmatic
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * The built-in `appduct_list_tools` / `appduct_describe_tool` / `appduct_call_tool` MCP tools
3
3
  * (ARCHITECTURE.md §9). The app's own tools are never listed as MCP tools. An agent reaches them
4
- * the way the CLI does: compact signatures first (`appduct tools`), one full schema on demand
5
- * (`appduct tools <name>`), then a call by name (`appduct invoke`). A registry of hundreds of tools
4
+ * the way the CLI does: compact signatures first (`appduct tools ls`), one full schema on demand
5
+ * (`appduct tools describe <name>`), then a call by name (`appduct tools call`). A registry of
6
+ * hundreds of tools
6
7
  * therefore costs a client three fixed tool definitions, and `tools/list` never changes while an
7
8
  * agent works.
8
9
  *
@@ -71,7 +71,7 @@ export declare class McpBuiltinToolError extends Error {
71
71
  export type ConnectToolDeps = {
72
72
  call: DaemonCall;
73
73
  /** The scheme composing the deep link (`<scheme>:///?appduct=<payload>`), resolved once at
74
- * server startup against the shared order in `scheme.ts` — the same one `appduct link` uses,
74
+ * server startup against the shared order in `scheme.ts` — the same one `appduct sessions link` uses,
75
75
  * so an app root with an `app.json` needs no configuration at all. */
76
76
  scheme?: string;
77
77
  /** Every location `scheme.ts` consulted, named in the failure below so an agent can tell its
@@ -4,19 +4,12 @@
4
4
  * per-session retention buffer on the daemon side — see `daemon/event-bus.ts`). MCP is
5
5
  * request/response, so an agent otherwise has no way to observe anything the app pushes.
6
6
  *
7
- * `appduct_wait_for_event` drains the retained buffer for an already-arrived match before
8
- * falling back to a live wait, exactly to avoid the race `appduct_wait_for_session` doesn't
9
- * have to worry about (a session is either claimed or it isn't, but an `app_event` can easily fire
10
- * between "the agent decides to wait" and "the wait subscription lands"). The live notification
11
- * listener is registered *before* the `events.since` drain call is even sent — not merely before
12
- * it resolves — because the daemon's response to that call and a subsequent `notify()` for a new
13
- * event can arrive in the same socket chunk; `rpc/client.ts` fans a chunk's notification lines out
14
- * to whatever listeners exist at the moment it's processed, so a listener added only after `await
15
- * stream.call(eventsSince, …)` returns can miss an event that was already in that same chunk.
16
- * Arrivals before the drain has been merged into the backlog are buffered, then merged in
17
- * (deduped by `seq`) before falling through to the live phase — see `handleWaitForEventTool` below.
7
+ * `appduct_wait_for_event`'s drain-then-live wait is `events/index.ts`'s `waitForAppEvent`
8
+ * (issue #114) — the same implementation `appduct/client`'s `AppClient.waitForEvent` uses — this
9
+ * module only resolves the selector, applies MCP-specific defaults (`payloadMaxBytes`), and maps
10
+ * that module's typed errors onto this tool's `tool_timeout`/`tool_execution_error`.
18
11
  */
19
- import { type EventsSinceResult } from "@appduct/shared";
12
+ import { type AppEvent } from "@appduct/shared";
20
13
  import { type SpawnFn } from "../rpc/client.js";
21
14
  import type { DaemonCall } from "./daemon-tools.js";
22
15
  export declare const EVENTS_TOOL_NAME = "appduct_events";
@@ -42,6 +35,13 @@ export declare const EVENTS_TOOL_DESCRIPTOR: {
42
35
  readonly type: "integer";
43
36
  readonly exclusiveMinimum: 0;
44
37
  };
38
+ readonly name: {
39
+ readonly type: "string";
40
+ };
41
+ readonly payloadMaxBytes: {
42
+ readonly type: "integer";
43
+ readonly exclusiveMinimum: 0;
44
+ };
45
45
  };
46
46
  readonly additionalProperties: false;
47
47
  };
@@ -58,12 +58,6 @@ export declare const WAIT_FOR_EVENT_TOOL_DESCRIPTOR: {
58
58
  readonly name: {
59
59
  readonly type: "string";
60
60
  };
61
- readonly match: {
62
- readonly type: "object";
63
- readonly additionalProperties: {
64
- readonly type: readonly ["string", "number", "boolean", "null"];
65
- };
66
- };
67
61
  readonly since: {
68
62
  readonly type: "integer";
69
63
  readonly minimum: 0;
@@ -72,6 +66,10 @@ export declare const WAIT_FOR_EVENT_TOOL_DESCRIPTOR: {
72
66
  readonly type: "number";
73
67
  readonly exclusiveMinimum: 0;
74
68
  };
69
+ readonly payloadMaxBytes: {
70
+ readonly type: "integer";
71
+ readonly exclusiveMinimum: 0;
72
+ };
75
73
  };
76
74
  readonly required: readonly ["name"];
77
75
  readonly additionalProperties: false;
@@ -80,7 +78,13 @@ export declare const WAIT_FOR_EVENT_TOOL_DESCRIPTOR: {
80
78
  export type EventsToolDeps = {
81
79
  call: DaemonCall;
82
80
  };
83
- export declare const handleEventsTool: (rawArgs: unknown, deps: EventsToolDeps) => Promise<EventsSinceResult>;
81
+ export type EventsToolResult = {
82
+ events: AppEvent[];
83
+ cursor: number;
84
+ dropped: number;
85
+ remaining: number;
86
+ };
87
+ export declare const handleEventsTool: (rawArgs: unknown, deps: EventsToolDeps) => Promise<EventsToolResult>;
84
88
  export type WaitForEventToolDeps = {
85
89
  stateDir: string;
86
90
  spawn?: SpawnFn;
@@ -91,13 +95,10 @@ export type WaitForEventToolDeps = {
91
95
  sendNotification: (notification: unknown) => Promise<void>;
92
96
  };
93
97
  };
94
- export type WaitForEventToolResult = {
95
- sessionId: string;
96
- alias?: string;
97
- name: string;
98
- payload: unknown;
99
- /** Unix ms. */
100
- ts: number;
101
- seq: number;
98
+ /** An `AppEvent` (full or truncated — issue #113) plus `dropped` from the drain (issue #114). */
99
+ export type WaitForEventToolResult = AppEvent & {
100
+ /** App events after `since` that were evicted before this call could return them (`0` once
101
+ * nothing has fallen off). */
102
+ dropped: number;
102
103
  };
103
104
  export declare const handleWaitForEventTool: (rawArgs: unknown, deps: WaitForEventToolDeps) => Promise<WaitForEventToolResult>;
@@ -25,7 +25,7 @@
25
25
  * **Disagreement is not guessed away.** If more than one probe below resolves to a *different*
26
26
  * scheme (e.g. a monorepo whose `android/` and `ios/` declare different values), discovery throws
27
27
  * a usage error naming every conflicting source instead of picking one — silently choosing would
28
- * mean `appduct link` opens the wrong app on whichever platform lost.
28
+ * mean `appduct sessions link` opens the wrong app on whichever platform lost.
29
29
  */
30
30
  export type NativeSchemeSource = "android-gradle" | "android-manifest" | "ios-info-plist" | "ios-project-yml";
31
31
  export type NativeSchemeProbeResult = {
package/dist/output.d.ts CHANGED
@@ -8,18 +8,41 @@ export type RenderOptions = {
8
8
  * (which is only attached under `--verbose`). Defaults to the wall clock when omitted, matching
9
9
  * the pre-`--verbose` fallback behavior for a caller that doesn't care (e.g. non-`ls` tests). */
10
10
  now?: Date;
11
- /** `link`-only: also render the deep link as terminal QR art (never affects `--json` output). */
11
+ /** `sessions link`-only: also render the deep link as terminal QR art (never affects `--json`
12
+ * output). */
12
13
  qr?: boolean;
13
- /** `tools`-only: render full schemas/annotations for every listed tool, not just name+description. */
14
+ /** `tools ls`-only: render full schemas/annotations for every listed tool, not just
15
+ * name+description. */
14
16
  full?: boolean;
15
17
  };
16
18
  export declare const renderResult: (result: CliResult<unknown>, options: RenderOptions) => {
17
19
  stdout?: string;
18
20
  stderr?: string;
19
21
  };
20
- /** Renders one `appduct events` line: NDJSON under `--json`, a compact human line otherwise. */
22
+ /** Renders one `appduct events tail`/`appduct events since` line: NDJSON under `--json`, a
23
+ * compact human line otherwise. */
21
24
  export declare const renderEventLine: (event: EventNotification, flags: GlobalFlags) => string;
22
- /** Renders the trailing cursor line for `appduct events --since` (issue #6): NDJSON under
25
+ /** The filter flags to echo back into the hinted resume command in {@link renderEventsCursorLine}
26
+ * (issue #115's should-fix on #96's echoed selector): the same `--name`/`--payload-max-bytes`
27
+ * the caller passed to this pull, forwarded as-is so following the hint doesn't silently widen
28
+ * the query. */
29
+ export type EventsCursorLineFilters = {
30
+ name?: string;
31
+ payloadMaxBytes?: number;
32
+ };
33
+ /** Renders the trailing cursor line for `appduct events since` (issue #6): NDJSON under
23
34
  * `--json` so a scripted caller can parse the resume point without maxing `seq` over the printed
24
- * events (impossible when the response is empty), a human note otherwise. */
25
- export declare const renderEventsCursorLine: (cursor: number, flags: GlobalFlags) => string;
35
+ * events (impossible when the response is empty), a human note otherwise. `selector` is the
36
+ * device/session selector the caller passed (issue #96): with two devices connected, a resume
37
+ * hint that dropped it would resolve to `ambiguous_session`, so it is echoed back in the hinted
38
+ * command when present. `dropped`/`remaining` (issue #115) are carried straight from
39
+ * `events.since`'s own result: how many events fell off the retention buffer before this pull,
40
+ * and how many matching events are still waiting beyond this page. `filters` (issue #115) are the
41
+ * `--name`/`--payload-max-bytes` flags this pull used: without them the hint would silently drop
42
+ * the query's filter and cap, so they are echoed into the hinted command exactly like the
43
+ * selector (`--name`'s glob is shell-quoted since it can carry `*` or spaces). */
44
+ export declare const renderEventsCursorLine: (cursor: {
45
+ cursor: number;
46
+ dropped: number;
47
+ remaining: number;
48
+ }, flags: GlobalFlags, selector?: string, filters?: EventsCursorLineFilters) => string;
@@ -123,7 +123,7 @@ export type DaemonStream = {
123
123
  /** Subscribes to server→client `"event"` notifications; returns an unsubscribe function. */
124
124
  onNotification: (callback: (payload: unknown) => void) => () => void;
125
125
  /** Fires once when the underlying socket closes (daemon gone, stop(), etc.); returns an
126
- * unsubscribe function. Lets long-lived stream consumers (e.g. `appduct events`) end
126
+ * unsubscribe function. Lets long-lived stream consumers (e.g. `appduct events tail`) end
127
127
  * gracefully instead of hanging once the connection is no longer usable. */
128
128
  onClose: (callback: () => void) => () => void;
129
129
  close: () => void;
package/dist/scheme.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Deep-link scheme resolution (ARCHITECTURE.md §10, issue #29).
3
3
  *
4
- * A scheme is needed to compose `<scheme>:///?appduct=<payload>` in `appduct link`,
4
+ * A scheme is needed to compose `<scheme>:///?appduct=<payload>` in `appduct sessions link`,
5
5
  * `appduct/client`'s `link()` and the MCP `appduct_connect` tool. Before this module the only
6
6
  * source was `<state-dir>/config.json` — a single global file, which meant two apps with different
7
7
  * schemes on one machine required hand-editing it on every switch.
@@ -126,7 +126,7 @@ export declare const globalConfigDirs: (options?: ProjectConfigLookupOptions) =>
126
126
  * documented precedence:
127
127
  *
128
128
  * - `<homeDir>/.appduct`, the default state dir. Essentially every project lives somewhere under
129
- * the home directory, so without this a plain `appduct link` in any repo would pick up the
129
+ * the home directory, so without this a plain `appduct sessions link` in any repo would pick up the
130
130
  * global `config.json` as if it were the project's — and with `--state-dir` pointing elsewhere it
131
131
  * would shadow the state dir the operator explicitly chose.
132
132
  * - `stateDirRoot`, the state dir actually in use, for the same reason when it is not the default.
@@ -191,7 +191,7 @@ export type ResolveSchemeOptions = {
191
191
  export declare const resolveScheme: (options?: ResolveSchemeOptions) => Promise<ResolvedScheme>;
192
192
  /**
193
193
  * The shared "no scheme anywhere" message. Every caller renders the same body so the locations
194
- * listed (and the fixes suggested) can't drift between `appduct link`, `appduct mcp` and
194
+ * listed (and the fixes suggested) can't drift between `appduct sessions link`, `appduct mcp` and
195
195
  * `appduct_connect`.
196
196
  */
197
197
  export declare const describeMissingScheme: (tried: string[]) => string;
@@ -210,7 +210,7 @@ export type ResolveAppIdOptions = {
210
210
  /** Which `appId.<platform>` key to read from a project config; see `cli/open-target.ts`'s
211
211
  * `platformOf`. */
212
212
  platform: AppIdPlatform;
213
- /** `--app-id` (CLI `link`) / `appId` (MCP `appduct_connect`) / `appId` (`mintLink`, the
213
+ /** `--app-id` (CLI `sessions link`) / `appId` (MCP `appduct_connect`) / `appId` (`mintLink`, the
214
214
  * `appduct/client` `link()`) — highest precedence, because the target is known at the call
215
215
  * site. */
216
216
  flagAppId?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appduct",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Driving React Native tools without in-app debug UI.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -45,7 +45,7 @@
45
45
  "reflect-metadata": "^0.2.2",
46
46
  "toqr": "^0.1.1",
47
47
  "ws": "^8.18.3",
48
- "@appduct/shared": "0.12.0"
48
+ "@appduct/shared": "0.13.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "^22.10.0",
@@ -1,3 +0,0 @@
1
- /** Route for `appduct events` — loaded by `cli/dispatch.ts`'s router only when it runs. */
2
- import type { Route } from "../router.js";
3
- export declare const route: Route;
@@ -1,3 +0,0 @@
1
- /** Route for `appduct invoke` — loaded by `cli/dispatch.ts`'s router only when it runs. */
2
- import type { Route } from "../router.js";
3
- export declare const route: Route;
@@ -1,3 +0,0 @@
1
- /** Route for `appduct link` — loaded by `cli/dispatch.ts`'s router only when it runs. */
2
- import type { Route } from "../router.js";
3
- export declare const route: Route;
@@ -1,3 +0,0 @@
1
- /** Route for `appduct ls` — loaded by `cli/dispatch.ts`'s router only when it runs. */
2
- import type { Route } from "../router.js";
3
- export declare const route: Route;
@@ -1,3 +0,0 @@
1
- /** Route for `appduct revoke` — loaded by `cli/dispatch.ts`'s router only when it runs. */
2
- import type { Route } from "../router.js";
3
- export declare const route: Route;
@@ -1,3 +0,0 @@
1
- /** Route for `appduct tools` — loaded by `cli/dispatch.ts`'s router only when it runs. */
2
- import type { Route } from "../router.js";
3
- export declare const route: Route;