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.
Files changed (74) hide show
  1. package/README.md +43 -38
  2. package/dist/bin.js +50 -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 +216 -46
  7. package/dist/cli/routes/daemon/start.js +18 -16
  8. package/dist/cli/routes/daemon/status.js +18 -16
  9. package/dist/cli/routes/daemon/stop.js +18 -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} +184 -162
  19. package/dist/cli/routes/init.js +14 -18
  20. package/dist/cli/routes/keygen.js +13 -16
  21. package/dist/cli/routes/mcp.js +193 -175
  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} +19 -17
  26. package/dist/cli/routes/sessions/ls.d.ts +4 -0
  27. package/dist/cli/routes/{ls.js → sessions/ls.js} +19 -17
  28. package/dist/cli/routes/sessions/revoke.d.ts +4 -0
  29. package/dist/cli/routes/{revoke.js → sessions/revoke.js} +20 -18
  30. package/dist/cli/routes/tools/call.d.ts +4 -0
  31. package/dist/cli/routes/{invoke.js → tools/call.js} +21 -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} +46 -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 +183 -139
  43. package/dist/commands/events.d.ts +31 -13
  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 +5 -2
  50. package/dist/daemon/daemon.d.ts +3 -0
  51. package/dist/daemon/event-bus.d.ts +36 -4
  52. package/dist/daemon/events-log.d.ts +31 -0
  53. package/dist/daemon/memory-append-only-file.d.ts +17 -0
  54. package/dist/daemon/node-append-only-file.d.ts +9 -0
  55. package/dist/daemon/rpc-errors.d.ts +1 -1
  56. package/dist/daemon/state-dir.d.ts +2 -0
  57. package/dist/errors.d.ts +1 -1
  58. package/dist/events/index.d.ts +60 -0
  59. package/dist/index.js +62 -39
  60. package/dist/link.d.ts +1 -1
  61. package/dist/mcp/app-tools.d.ts +3 -2
  62. package/dist/mcp/connect-tool.d.ts +1 -1
  63. package/dist/mcp/events-tool.d.ts +28 -34
  64. package/dist/native-scheme.d.ts +1 -1
  65. package/dist/output.d.ts +29 -6
  66. package/dist/rpc/client.d.ts +1 -1
  67. package/dist/scheme.d.ts +4 -4
  68. package/package.json +2 -2
  69. package/dist/cli/routes/events.d.ts +0 -3
  70. package/dist/cli/routes/invoke.d.ts +0 -3
  71. package/dist/cli/routes/link.d.ts +0 -3
  72. package/dist/cli/routes/ls.d.ts +0 -3
  73. package/dist/cli/routes/revoke.d.ts +0 -3
  74. package/dist/cli/routes/tools.d.ts +0 -3
@@ -12,6 +12,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
12
12
  "eventBufferSize",
13
13
  "auditRetentionDays",
14
14
  "daemonLogMaxBytes",
15
+ "eventsLogMaxBytes",
15
16
  "policy",
16
17
  "advertisedIp",
17
18
  "scheme",
@@ -71,6 +72,7 @@ var defaultConfig = (paths) => {
71
72
  eventBufferSize: 256,
72
73
  auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
73
74
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
75
+ eventsLogMaxBytes: 10 * 1024 * 1024,
74
76
  restartDaemonOnVersionMismatch: false,
75
77
  policy: {
76
78
  default: "allow",
@@ -133,6 +135,9 @@ var loadConfig = async (paths, options = {}) => {
133
135
  if (parsed.daemonLogMaxBytes !== void 0) {
134
136
  config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
135
137
  }
138
+ if (parsed.eventsLogMaxBytes !== void 0) {
139
+ config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
140
+ }
136
141
  if (parsed.restartDaemonOnVersionMismatch !== void 0) {
137
142
  config.restartDaemonOnVersionMismatch = requireBoolean(
138
143
  parsed.restartDaemonOnVersionMismatch,
@@ -178,6 +183,7 @@ var getStateDirPaths = (stateDir) => {
178
183
  socketPath: join(stateDir, "daemon.sock"),
179
184
  pidFilePath: join(stateDir, "daemon.pid"),
180
185
  logFilePath: join(stateDir, "daemon.log"),
186
+ eventsLogPath: join(stateDir, "events.log"),
181
187
  keyPath: join(stateDir, "key.pem"),
182
188
  configPath: join(stateDir, "config.json"),
183
189
  auditDir: join(stateDir, "audit"),
@@ -202,7 +208,7 @@ import {
202
208
  McpError,
203
209
  ReadResourceRequestSchema
204
210
  } from "@modelcontextprotocol/sdk/types.js";
205
- import { RPC_METHODS as RPC_METHODS6 } from "@appduct/shared";
211
+ import { RPC_METHODS as RPC_METHODS7 } from "@appduct/shared";
206
212
 
207
213
  // src/daemon/calls.ts
208
214
  import {
@@ -2649,44 +2655,147 @@ var resolveAppTool = async (call, selector, name) => {
2649
2655
  };
2650
2656
 
2651
2657
  // src/mcp/events-tool.ts
2658
+ import {
2659
+ RPC_METHODS as RPC_METHODS6,
2660
+ toAppEvent as toAppEvent2
2661
+ } from "@appduct/shared";
2662
+
2663
+ // src/events/index.ts
2652
2664
  import {
2653
2665
  RPC_METHODS as RPC_METHODS5,
2654
- EVENT_KINDS
2666
+ toAppEvent
2655
2667
  } from "@appduct/shared";
2668
+ var WaitForAppEventTimeoutError = class extends Error {
2669
+ constructor(eventName, timeoutMs) {
2670
+ super(`Timed out after ${timeoutMs}ms waiting for event "${eventName}".`);
2671
+ this.eventName = eventName;
2672
+ this.timeoutMs = timeoutMs;
2673
+ this.name = "WaitForAppEventTimeoutError";
2674
+ }
2675
+ };
2676
+ var WaitForAppEventConnectionClosedError = class extends Error {
2677
+ constructor(eventName) {
2678
+ super(`The connection to the Appduct daemon closed while waiting for event "${eventName}".`);
2679
+ this.eventName = eventName;
2680
+ this.name = "WaitForAppEventConnectionClosedError";
2681
+ }
2682
+ };
2683
+ var waitForAppEvent = async (stream, options) => {
2684
+ const { sessionId, name, since, timeoutMs, payloadMaxBytes } = options;
2685
+ let liveHandler = null;
2686
+ const earlyEvents = [];
2687
+ const unsubscribeNotification = stream.onNotification((payload) => {
2688
+ const event = payload;
2689
+ if (liveHandler) {
2690
+ liveHandler(event);
2691
+ } else {
2692
+ earlyEvents.push(event);
2693
+ }
2694
+ });
2695
+ let unsubscribeClose = () => {
2696
+ };
2697
+ try {
2698
+ await stream.call(RPC_METHODS5.eventsSubscribe, {
2699
+ sessionSelector: sessionId,
2700
+ kinds: ["app_event"],
2701
+ name,
2702
+ payloadMaxBytes
2703
+ });
2704
+ const sinceResult = await stream.call(RPC_METHODS5.eventsSince, {
2705
+ selector: sessionId,
2706
+ since,
2707
+ name,
2708
+ payloadMaxBytes
2709
+ });
2710
+ const seenSeqs = /* @__PURE__ */ new Set();
2711
+ const backlog = [];
2712
+ for (const event of [...sinceResult.events, ...earlyEvents]) {
2713
+ if (event.sessionId === sessionId && !seenSeqs.has(event.seq)) {
2714
+ seenSeqs.add(event.seq);
2715
+ backlog.push(event);
2716
+ }
2717
+ }
2718
+ backlog.sort((a, b) => a.seq - b.seq);
2719
+ for (const event of backlog) {
2720
+ const appEvent = toAppEvent(event, sessionId);
2721
+ if (appEvent) {
2722
+ return { event: appEvent, dropped: sinceResult.dropped };
2723
+ }
2724
+ }
2725
+ const highestConsideredSeq = backlog.length > 0 ? backlog[backlog.length - 1].seq : sinceResult.cursor;
2726
+ return await new Promise((resolve2, reject) => {
2727
+ let settled = false;
2728
+ const settle = (fn) => {
2729
+ if (settled) {
2730
+ return;
2731
+ }
2732
+ settled = true;
2733
+ clearTimeout(timer);
2734
+ liveHandler = null;
2735
+ fn();
2736
+ };
2737
+ const timer = setTimeout(() => {
2738
+ settle(() => reject(new WaitForAppEventTimeoutError(name, timeoutMs)));
2739
+ }, timeoutMs);
2740
+ unsubscribeClose = stream.onClose(() => {
2741
+ settle(() => reject(new WaitForAppEventConnectionClosedError(name)));
2742
+ });
2743
+ liveHandler = (event) => {
2744
+ if (settled) {
2745
+ return;
2746
+ }
2747
+ if (event.sessionId === sessionId && event.seq <= highestConsideredSeq) {
2748
+ return;
2749
+ }
2750
+ const appEvent = toAppEvent(event, sessionId);
2751
+ if (appEvent) {
2752
+ settle(() => resolve2({ event: appEvent, dropped: sinceResult.dropped }));
2753
+ }
2754
+ };
2755
+ });
2756
+ } finally {
2757
+ unsubscribeNotification();
2758
+ unsubscribeClose();
2759
+ }
2760
+ };
2761
+
2762
+ // src/mcp/events-tool.ts
2656
2763
  var EVENTS_TOOL_NAME = "appduct_events";
2657
2764
  var WAIT_FOR_EVENT_TOOL_NAME = "appduct_wait_for_event";
2765
+ var DEFAULT_EVENTS_LIMIT = 50;
2766
+ var DEFAULT_EVENTS_PAYLOAD_MAX_BYTES = 4096;
2658
2767
  var DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MS = 12e4;
2659
2768
  var MAX_WAIT_FOR_EVENT_TIMEOUT_MS = 25 * 60 * 1e3;
2660
2769
  var WAIT_FOR_EVENT_PROGRESS_INTERVAL_MS = 6e4;
2661
- var KNOWN_EVENT_KINDS = new Set(EVENT_KINDS);
2662
2770
  var clampWaitForEventTimeoutMs = (requestedMs) => {
2663
2771
  return Math.min(requestedMs ?? DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MS, MAX_WAIT_FOR_EVENT_TIMEOUT_MS);
2664
2772
  };
2665
2773
  var EVENTS_TOOL_DESCRIPTOR = {
2666
2774
  name: EVENTS_TOOL_NAME,
2667
- description: "Drain events retained in the daemon's per-session ring buffer since a cursor \u2014 the pull counterpart to a live subscription, for checking what happened after calling a tool or triggering app behavior. With no selector, targets the sole active/suspended session. Returns { events, cursor }; pass cursor back as since on the next call to avoid re-reading events you've already seen. limit (if given) keeps the OLDEST events in the window and advances cursor only past what was actually returned, so repeated calls page forward through everything retained rather than skipping ahead.",
2775
+ description: `Drain the events the app posted with postEvent(name, payload) that the daemon retained for a session since a cursor, for checking what the app reported after calling a tool or triggering app behavior. With no selector, targets the sole active/suspended session. name filters to events whose name matches a whole-name, case-sensitive glob (* matches any run of characters; a pattern without * is an exact name), e.g. "cart.*". Returns { events, cursor, dropped, remaining } with each event as { name, payload, ts, seq, sessionId, alias } or, once a payload's JSON exceeds payloadMaxBytes (default 4096), as { name, payloadPreview, truncated: true, payloadBytes, ts, seq, sessionId, alias } instead \u2014 check truncated before reading payload. pass cursor back as since on the next call to avoid re-reading events you've already seen; dropped counts app events after since that were evicted before this call could return them (0 once nothing has fallen off), and remaining counts events still matching this query after the returned page (0 on the last page). limit (default 50) keeps the OLDEST events in the window and advances cursor only past what was actually returned, so repeated calls page forward through everything retained rather than skipping ahead.`,
2668
2776
  inputSchema: {
2669
2777
  type: "object",
2670
2778
  properties: {
2671
2779
  selector: { type: "string" },
2672
2780
  since: { type: "integer", minimum: 0 },
2673
- kinds: { type: "array", items: { type: "string", enum: EVENT_KINDS } },
2674
- limit: { type: "integer", exclusiveMinimum: 0 }
2781
+ limit: { type: "integer", exclusiveMinimum: 0 },
2782
+ name: { type: "string" },
2783
+ payloadMaxBytes: { type: "integer", exclusiveMinimum: 0 }
2675
2784
  },
2676
2785
  additionalProperties: false
2677
2786
  }
2678
2787
  };
2679
2788
  var WAIT_FOR_EVENT_TOOL_DESCRIPTOR = {
2680
2789
  name: WAIT_FOR_EVENT_TOOL_NAME,
2681
- description: "Wait for the connected app to push an event via postEvent(name, payload) matching name (and, if match is given, every one of its keys strictly-equal \u2014 primitives only, not objects/arrays \u2014 in the event's payload). Checks already-retained events first, so an event that already fired before this call still resolves immediately; pass since (a cursor from a previous appduct_events/appduct_wait_for_event call) to skip events you've already handled and wait only for a new one \u2014 omitting it can return an old match instantly on every call. Requires a claimed session to already exist (use appduct_wait_for_session first if not). Resolves with { sessionId, alias, name, payload, ts, seq }, or rejects with tool_timeout after timeoutMs (default 120000ms, capped server-side at 1500000ms). A call still running after about two minutes moves to a Claude Code background task, so the result may arrive well after this call returns.",
2790
+ description: `Wait for the connected app to push an event via postEvent(name, payload) whose name matches name, a whole-name, case-sensitive glob (* matches any run of characters; a pattern without * is an exact name, e.g. "cart.*", or "*" for the next event of any name). Checks already-retained events first, so an event that already fired before this call still resolves immediately; pass since (a cursor from a previous appduct_events/appduct_wait_for_event call) to skip events you've already handled and wait only for a new one \u2014 omitting it can return an old match instantly on every call. Requires a claimed session to already exist (use appduct_wait_for_session first if not). Resolves with { sessionId, alias, name, payload, ts, seq, dropped } or, once a payload's JSON exceeds payloadMaxBytes (default 4096), with { payloadPreview, truncated: true, payloadBytes } in place of payload \u2014 check truncated before reading payload; dropped counts app events after since that were evicted before this call could return them (0 once nothing has fallen off). Rejects with tool_timeout after timeoutMs (default 120000ms, capped server-side at 1500000ms). A call still running after about two minutes moves to a Claude Code background task, so the result may arrive well after this call returns.`,
2682
2791
  inputSchema: {
2683
2792
  type: "object",
2684
2793
  properties: {
2685
2794
  selector: { type: "string" },
2686
2795
  name: { type: "string" },
2687
- match: { type: "object", additionalProperties: { type: ["string", "number", "boolean", "null"] } },
2688
2796
  since: { type: "integer", minimum: 0 },
2689
- timeoutMs: { type: "number", exclusiveMinimum: 0 }
2797
+ timeoutMs: { type: "number", exclusiveMinimum: 0 },
2798
+ payloadMaxBytes: { type: "integer", exclusiveMinimum: 0 }
2690
2799
  },
2691
2800
  required: ["name"],
2692
2801
  additionalProperties: false
@@ -2734,178 +2843,91 @@ var asOptionalPositiveNumber2 = (value, field) => {
2734
2843
  }
2735
2844
  return value;
2736
2845
  };
2737
- var asOptionalEventKinds = (value) => {
2738
- if (value === void 0) {
2739
- return void 0;
2740
- }
2741
- if (!Array.isArray(value) || !value.every((kind) => typeof kind === "string" && KNOWN_EVENT_KINDS.has(kind))) {
2742
- throw new McpBuiltinToolError("invalid_request", '"kinds" must be an array of known event kinds.');
2846
+ var rejectKinds = (args) => {
2847
+ if (args.kinds !== void 0) {
2848
+ throw new McpBuiltinToolError(
2849
+ "invalid_request",
2850
+ '"kinds" is not supported: these tools return only the events the app posted with postEvent.'
2851
+ );
2743
2852
  }
2744
- return value;
2745
2853
  };
2746
- var isMatchPrimitive = (value) => {
2747
- return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
2748
- };
2749
- var asOptionalMatch = (value) => {
2750
- if (value === void 0) {
2751
- return void 0;
2752
- }
2753
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
2754
- throw new McpBuiltinToolError("invalid_request", '"match" must be a JSON object.');
2755
- }
2756
- const record = value;
2757
- for (const [key, entry] of Object.entries(record)) {
2758
- if (!isMatchPrimitive(entry)) {
2759
- throw new McpBuiltinToolError(
2760
- "invalid_request",
2761
- `"match.${key}" must be a string, number, boolean, or null (objects/arrays can never match).`
2762
- );
2763
- }
2854
+ var rejectMatch = (args) => {
2855
+ if (args.match !== void 0) {
2856
+ throw new McpBuiltinToolError(
2857
+ "invalid_request",
2858
+ '"match" is not supported: "name" is a glob, and a payload predicate is not offered on any surface \u2014 loop on appduct_wait_for_event with "since" instead.'
2859
+ );
2764
2860
  }
2765
- return record;
2766
2861
  };
2767
2862
  var handleEventsTool = async (rawArgs, deps) => {
2768
2863
  const args = asRecord3(rawArgs);
2769
- return deps.call(RPC_METHODS5.eventsSince, {
2864
+ rejectKinds(args);
2865
+ const result = await deps.call(RPC_METHODS6.eventsSince, {
2770
2866
  selector: asOptionalString2(args.selector, "selector"),
2771
2867
  since: asOptionalNonNegativeInteger(args.since, "since"),
2772
- kinds: asOptionalEventKinds(args.kinds),
2773
- limit: asOptionalPositiveInteger(args.limit, "limit")
2868
+ limit: asOptionalPositiveInteger(args.limit, "limit") ?? DEFAULT_EVENTS_LIMIT,
2869
+ name: asOptionalString2(args.name, "name"),
2870
+ // Issue #113: this tool always caps a payload, so a caller reading the result straight into
2871
+ // its context can't be blown out by one gigantic app-pushed payload; `payloadMaxBytes` lets a
2872
+ // caller raise or lower that default.
2873
+ payloadMaxBytes: asOptionalPositiveInteger(args.payloadMaxBytes, "payloadMaxBytes") ?? DEFAULT_EVENTS_PAYLOAD_MAX_BYTES
2774
2874
  });
2775
- };
2776
- var payloadShallowMatches = (payload, match) => {
2777
- if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
2778
- return false;
2779
- }
2780
- const record = payload;
2781
- return Object.entries(match).every(([key, value]) => record[key] === value);
2875
+ return {
2876
+ events: result.events.map((event) => event.sessionId === void 0 ? void 0 : toAppEvent2(event, event.sessionId)).filter((event) => event !== void 0),
2877
+ cursor: result.cursor,
2878
+ dropped: result.dropped,
2879
+ remaining: result.remaining
2880
+ };
2782
2881
  };
2783
2882
  var handleWaitForEventTool = async (rawArgs, deps) => {
2784
2883
  const args = asRecord3(rawArgs);
2884
+ rejectKinds(args);
2885
+ rejectMatch(args);
2785
2886
  const selector = asOptionalString2(args.selector, "selector");
2786
2887
  const name = asOptionalString2(args.name, "name");
2787
2888
  if (!name) {
2788
2889
  throw new McpBuiltinToolError("invalid_request", '"name" must be a non-empty string.');
2789
2890
  }
2790
- const match = asOptionalMatch(args.match);
2791
2891
  const since = asOptionalNonNegativeInteger(args.since, "since");
2792
2892
  const timeoutMs = clampWaitForEventTimeoutMs(asOptionalPositiveNumber2(args.timeoutMs, "timeoutMs"));
2893
+ const payloadMaxBytes = asOptionalPositiveInteger(args.payloadMaxBytes, "payloadMaxBytes") ?? DEFAULT_EVENTS_PAYLOAD_MAX_BYTES;
2793
2894
  const stream = await openDaemonStream({ stateDir: deps.stateDir, spawn: deps.spawn });
2794
2895
  try {
2795
- const described = await stream.call(RPC_METHODS5.sessionsDescribe, { selector });
2796
- const sessionId = described.sessionId;
2797
- const alias = described.alias;
2798
- const toResult = (event) => {
2799
- if (event.kind !== "app_event" || event.sessionId !== sessionId) {
2800
- return void 0;
2801
- }
2802
- const data = event.data;
2803
- if (data.name !== name) {
2804
- return void 0;
2805
- }
2806
- if (match && !payloadShallowMatches(data.payload, match)) {
2807
- return void 0;
2808
- }
2809
- return { sessionId, alias, name, payload: data.payload, ts: event.ts, seq: event.seq };
2810
- };
2811
- let liveHandler = null;
2812
- const earlyEvents = [];
2813
- const unsubscribeNotification = stream.onNotification((payload) => {
2814
- const event = payload;
2815
- if (liveHandler) {
2816
- liveHandler(event);
2817
- } else {
2818
- earlyEvents.push(event);
2819
- }
2820
- });
2821
- let unsubscribeClose = () => {
2822
- };
2896
+ const described = await stream.call(RPC_METHODS6.sessionsDescribe, { selector });
2897
+ const startedAt = Date.now();
2898
+ const progressTimer = deps.progress ? setInterval(() => {
2899
+ deps.progress.sendNotification({
2900
+ method: "notifications/progress",
2901
+ params: {
2902
+ progressToken: deps.progress.token,
2903
+ progress: Date.now() - startedAt,
2904
+ total: timeoutMs,
2905
+ message: `Still waiting for event "${name}"...`
2906
+ }
2907
+ }).catch(() => {
2908
+ });
2909
+ }, WAIT_FOR_EVENT_PROGRESS_INTERVAL_MS) : void 0;
2823
2910
  try {
2824
- await stream.call(RPC_METHODS5.eventsSubscribe, { sessionSelector: sessionId, kinds: ["app_event"] });
2825
- const sinceResult = await stream.call(RPC_METHODS5.eventsSince, {
2826
- selector: sessionId,
2911
+ const { event, dropped } = await waitForAppEvent(stream, {
2912
+ sessionId: described.sessionId,
2913
+ name,
2827
2914
  since,
2828
- kinds: ["app_event"]
2915
+ timeoutMs,
2916
+ payloadMaxBytes
2829
2917
  });
2830
- const seenSeqs = /* @__PURE__ */ new Set();
2831
- const backlog = [];
2832
- for (const event of [...sinceResult.events, ...earlyEvents]) {
2833
- if (event.sessionId === sessionId && !seenSeqs.has(event.seq)) {
2834
- seenSeqs.add(event.seq);
2835
- backlog.push(event);
2836
- }
2918
+ return { ...event, dropped };
2919
+ } catch (error) {
2920
+ if (error instanceof WaitForAppEventTimeoutError) {
2921
+ throw new McpBuiltinToolError("tool_timeout", error.message);
2837
2922
  }
2838
- backlog.sort((a, b) => a.seq - b.seq);
2839
- for (const event of backlog) {
2840
- const result = toResult(event);
2841
- if (result) {
2842
- return result;
2843
- }
2923
+ if (error instanceof WaitForAppEventConnectionClosedError) {
2924
+ throw new McpBuiltinToolError("tool_execution_error", error.message);
2844
2925
  }
2845
- const highestConsideredSeq = backlog.length > 0 ? backlog[backlog.length - 1].seq : sinceResult.cursor;
2846
- return await new Promise((resolve2, reject) => {
2847
- let settled = false;
2848
- const settle = (fn) => {
2849
- if (settled) {
2850
- return;
2851
- }
2852
- settled = true;
2853
- clearTimeout(timer);
2854
- if (progressTimer) {
2855
- clearInterval(progressTimer);
2856
- }
2857
- liveHandler = null;
2858
- fn();
2859
- };
2860
- const timer = setTimeout(() => {
2861
- settle(
2862
- () => reject(
2863
- new McpBuiltinToolError(
2864
- "tool_timeout",
2865
- `Timed out after ${timeoutMs}ms waiting for event "${name}".`
2866
- )
2867
- )
2868
- );
2869
- }, timeoutMs);
2870
- const startedAt = Date.now();
2871
- const progressTimer = deps.progress ? setInterval(() => {
2872
- deps.progress.sendNotification({
2873
- method: "notifications/progress",
2874
- params: {
2875
- progressToken: deps.progress.token,
2876
- progress: Date.now() - startedAt,
2877
- total: timeoutMs,
2878
- message: `Still waiting for event "${name}"...`
2879
- }
2880
- }).catch(() => {
2881
- });
2882
- }, WAIT_FOR_EVENT_PROGRESS_INTERVAL_MS) : void 0;
2883
- unsubscribeClose = stream.onClose(() => {
2884
- settle(
2885
- () => reject(
2886
- new McpBuiltinToolError(
2887
- "tool_execution_error",
2888
- `The connection to the Appduct daemon closed while waiting for event "${name}".`
2889
- )
2890
- )
2891
- );
2892
- });
2893
- liveHandler = (event) => {
2894
- if (settled) {
2895
- return;
2896
- }
2897
- if (event.sessionId === sessionId && event.seq <= highestConsideredSeq) {
2898
- return;
2899
- }
2900
- const result = toResult(event);
2901
- if (result) {
2902
- settle(() => resolve2(result));
2903
- }
2904
- };
2905
- });
2926
+ throw error;
2906
2927
  } finally {
2907
- unsubscribeNotification();
2908
- unsubscribeClose();
2928
+ if (progressTimer) {
2929
+ clearInterval(progressTimer);
2930
+ }
2909
2931
  }
2910
2932
  } finally {
2911
2933
  stream.close();
@@ -3045,7 +3067,7 @@ var createMcpServer = async (options) => {
3045
3067
  }
3046
3068
  if (progressToken === void 0) {
3047
3069
  const result = await stream.call(
3048
- RPC_METHODS6.toolsCall,
3070
+ RPC_METHODS7.toolsCall,
3049
3071
  {
3050
3072
  selector: tool.sessionId,
3051
3073
  name: tool.descriptor.name,
@@ -3060,7 +3082,7 @@ var createMcpServer = async (options) => {
3060
3082
  }
3061
3083
  const progressStream = await openStream({ stateDir: options.stateDir, spawn: options.spawn });
3062
3084
  try {
3063
- await progressStream.call(RPC_METHODS6.eventsSubscribe, {
3085
+ await progressStream.call(RPC_METHODS7.eventsSubscribe, {
3064
3086
  sessionSelector: tool.sessionId,
3065
3087
  kinds: ["tool_call_started", "tool_call_progress"]
3066
3088
  });
@@ -3070,7 +3092,7 @@ var createMcpServer = async (options) => {
3070
3092
  if (callId === void 0 || !cancelRequested) {
3071
3093
  return;
3072
3094
  }
3073
- progressStream.call(RPC_METHODS6.toolsCancel, { selector: tool.sessionId, callId, reason: "mcp_client_cancelled" }).catch((error) => {
3095
+ progressStream.call(RPC_METHODS7.toolsCancel, { selector: tool.sessionId, callId, reason: "mcp_client_cancelled" }).catch((error) => {
3074
3096
  console.error("appduct mcp: failed to cancel an app tool call:", error);
3075
3097
  });
3076
3098
  };
@@ -3103,7 +3125,7 @@ var createMcpServer = async (options) => {
3103
3125
  });
3104
3126
  try {
3105
3127
  const result = await progressStream.call(
3106
- RPC_METHODS6.toolsCall,
3128
+ RPC_METHODS7.toolsCall,
3107
3129
  {
3108
3130
  selector: tool.sessionId,
3109
3131
  name: tool.descriptor.name,
@@ -3215,7 +3237,7 @@ var createMcpServer = async (options) => {
3215
3237
  if (request.params.uri !== SESSIONS_RESOURCE_URI) {
3216
3238
  throw new Error(`Unknown resource "${request.params.uri}".`);
3217
3239
  }
3218
- const sessions = await stream.call(RPC_METHODS6.sessionsList);
3240
+ const sessions = await stream.call(RPC_METHODS7.sessionsList);
3219
3241
  return {
3220
3242
  contents: [{ uri: SESSIONS_RESOURCE_URI, mimeType: "application/json", text: JSON.stringify(sessions) }]
3221
3243
  };
@@ -3387,9 +3409,6 @@ var renderLsData = (colors, data, now) => {
3387
3409
  ...rows.map((row) => ` ${row.map((cell, index) => cell.padEnd(widths[index])).join(" ")}`)
3388
3410
  ];
3389
3411
  };
3390
- var isToolsListing = (data) => {
3391
- return typeof data === "object" && data !== null && Array.isArray(data.tools);
3392
- };
3393
3412
  var isToolGroupsListing = (data) => {
3394
3413
  return typeof data === "object" && data !== null && Array.isArray(data.groups) && !Array.isArray(data.tools);
3395
3414
  };
@@ -3407,7 +3426,7 @@ var renderEmptyToolsLine = (data) => {
3407
3426
  }
3408
3427
  if (data.group !== void 0) {
3409
3428
  const match = data.filter === void 0 ? "" : ` match ${JSON.stringify(data.filter)}`;
3410
- return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools --groups\` to see the session's groups.`;
3429
+ return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools ls --groups\` to see the session's groups.`;
3411
3430
  }
3412
3431
  return data.filter === void 0 ? " No tools registered." : ` No tools match ${JSON.stringify(data.filter)}.`;
3413
3432
  };
@@ -3489,7 +3508,7 @@ var renderToolSummaryTable = (colors, data) => {
3489
3508
  ...grouped ? renderGroupedToolLines(colors, data.tools) : data.tools.flatMap((tool) => renderToolSummaryLines(tool, " ")),
3490
3509
  ...renderTruncationLine(data),
3491
3510
  "",
3492
- "Run `appduct tools <name>` for a tool's full schema."
3511
+ "Run `appduct tools describe <name>` for a tool's full schema."
3493
3512
  ];
3494
3513
  };
3495
3514
  var renderToolDetail = (colors, tool, flags) => {
@@ -3541,16 +3560,13 @@ var renderToolGroups = (colors, data) => {
3541
3560
  colors.green("Groups"),
3542
3561
  ...data.groups.map((entry) => ` ${label(entry).padEnd(width)} ${String(entry.total).padStart(countWidth)}`),
3543
3562
  "",
3544
- `${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.")
3563
+ `${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.")
3545
3564
  ];
3546
3565
  };
3547
- var renderToolsData = (colors, data, flags, full) => {
3566
+ var renderToolsListData = (colors, data, flags, full) => {
3548
3567
  if (isToolGroupsListing(data)) {
3549
3568
  return renderToolGroups(colors, data);
3550
3569
  }
3551
- if (!isToolsListing(data)) {
3552
- return renderToolDetail(colors, data, flags);
3553
- }
3554
3570
  return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
3555
3571
  };
3556
3572
  var renderInvokeData = (colors, data, flags) => {
@@ -3736,15 +3752,17 @@ var renderSuccessData = (colors, command, data, options) => {
3736
3752
  return renderInitData(colors, data, flags);
3737
3753
  case "keygen":
3738
3754
  return renderKeygenData(colors, data, flags);
3739
- case "link":
3755
+ case "sessions link":
3740
3756
  return renderLinkData(colors, data, flags, options.qr);
3741
- case "ls":
3757
+ case "sessions ls":
3742
3758
  return renderLsData(colors, data, options.now ?? /* @__PURE__ */ new Date());
3743
- case "tools":
3744
- return renderToolsData(colors, data, flags, options.full);
3745
- case "invoke":
3759
+ case "tools ls":
3760
+ return renderToolsListData(colors, data, flags, options.full);
3761
+ case "tools describe":
3762
+ return renderToolDetail(colors, data, flags);
3763
+ case "tools call":
3746
3764
  return renderInvokeData(colors, data, flags);
3747
- case "revoke":
3765
+ case "sessions revoke":
3748
3766
  return renderRevokeData(colors, data);
3749
3767
  case "daemon run":
3750
3768
  return renderDaemonRunData(colors, data, flags);
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Route for `appduct sessions` — itself a router over the session verbs, so `sessions ls` loads
3
+ * only `ls.ts` (issue #96). cac matches only the noun (`cli/create-cli.ts`), so the verb arrives
4
+ * here as the first positional rather than as a cac command of its own — the same shape
5
+ * `routes/daemon/index.ts` already uses for `daemon status`.
6
+ */
7
+ export declare const route: import("../../router.js").Route;