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
@@ -208,7 +208,7 @@ import {
208
208
  McpError,
209
209
  ReadResourceRequestSchema
210
210
  } from "@modelcontextprotocol/sdk/types.js";
211
- import { RPC_METHODS as RPC_METHODS6 } from "@appduct/shared";
211
+ import { RPC_METHODS as RPC_METHODS7 } from "@appduct/shared";
212
212
 
213
213
  // src/daemon/calls.ts
214
214
  import {
@@ -2656,40 +2656,167 @@ var resolveAppTool = async (call, selector, name) => {
2656
2656
 
2657
2657
  // src/mcp/events-tool.ts
2658
2658
  import {
2659
- RPC_METHODS as RPC_METHODS5
2659
+ RPC_METHODS as RPC_METHODS6,
2660
+ renderEventSignature,
2661
+ toAppEvent as toAppEvent2
2660
2662
  } from "@appduct/shared";
2663
+
2664
+ // src/events/index.ts
2665
+ import {
2666
+ RPC_METHODS as RPC_METHODS5,
2667
+ toAppEvent
2668
+ } from "@appduct/shared";
2669
+ var WaitForAppEventTimeoutError = class extends Error {
2670
+ constructor(eventName, timeoutMs) {
2671
+ super(`Timed out after ${timeoutMs}ms waiting for event "${eventName}".`);
2672
+ this.eventName = eventName;
2673
+ this.timeoutMs = timeoutMs;
2674
+ this.name = "WaitForAppEventTimeoutError";
2675
+ }
2676
+ };
2677
+ var WaitForAppEventConnectionClosedError = class extends Error {
2678
+ constructor(eventName) {
2679
+ super(`The connection to the Appduct daemon closed while waiting for event "${eventName}".`);
2680
+ this.eventName = eventName;
2681
+ this.name = "WaitForAppEventConnectionClosedError";
2682
+ }
2683
+ };
2684
+ var waitForAppEvent = async (stream, options) => {
2685
+ const { sessionId, name, since, timeoutMs, payloadMaxBytes } = options;
2686
+ let liveHandler = null;
2687
+ const earlyEvents = [];
2688
+ const unsubscribeNotification = stream.onNotification((payload) => {
2689
+ const event = payload;
2690
+ if (liveHandler) {
2691
+ liveHandler(event);
2692
+ } else {
2693
+ earlyEvents.push(event);
2694
+ }
2695
+ });
2696
+ let unsubscribeClose = () => {
2697
+ };
2698
+ try {
2699
+ await stream.call(RPC_METHODS5.eventsSubscribe, {
2700
+ sessionSelector: sessionId,
2701
+ kinds: ["app_event"],
2702
+ name,
2703
+ payloadMaxBytes
2704
+ });
2705
+ const sinceResult = await stream.call(RPC_METHODS5.eventsSince, {
2706
+ selector: sessionId,
2707
+ since,
2708
+ name,
2709
+ payloadMaxBytes
2710
+ });
2711
+ const seenSeqs = /* @__PURE__ */ new Set();
2712
+ const backlog = [];
2713
+ for (const event of [...sinceResult.events, ...earlyEvents]) {
2714
+ if (event.sessionId === sessionId && !seenSeqs.has(event.seq)) {
2715
+ seenSeqs.add(event.seq);
2716
+ backlog.push(event);
2717
+ }
2718
+ }
2719
+ backlog.sort((a, b) => a.seq - b.seq);
2720
+ for (const event of backlog) {
2721
+ const appEvent = toAppEvent(event, sessionId);
2722
+ if (appEvent) {
2723
+ return { event: appEvent, dropped: sinceResult.dropped };
2724
+ }
2725
+ }
2726
+ const highestConsideredSeq = backlog.length > 0 ? backlog[backlog.length - 1].seq : sinceResult.cursor;
2727
+ return await new Promise((resolve2, reject) => {
2728
+ let settled = false;
2729
+ const settle = (fn) => {
2730
+ if (settled) {
2731
+ return;
2732
+ }
2733
+ settled = true;
2734
+ clearTimeout(timer);
2735
+ liveHandler = null;
2736
+ fn();
2737
+ };
2738
+ const timer = setTimeout(() => {
2739
+ settle(() => reject(new WaitForAppEventTimeoutError(name, timeoutMs)));
2740
+ }, timeoutMs);
2741
+ unsubscribeClose = stream.onClose(() => {
2742
+ settle(() => reject(new WaitForAppEventConnectionClosedError(name)));
2743
+ });
2744
+ liveHandler = (event) => {
2745
+ if (settled) {
2746
+ return;
2747
+ }
2748
+ if (event.sessionId === sessionId && event.seq <= highestConsideredSeq) {
2749
+ return;
2750
+ }
2751
+ const appEvent = toAppEvent(event, sessionId);
2752
+ if (appEvent) {
2753
+ settle(() => resolve2({ event: appEvent, dropped: sinceResult.dropped }));
2754
+ }
2755
+ };
2756
+ });
2757
+ } finally {
2758
+ unsubscribeNotification();
2759
+ unsubscribeClose();
2760
+ }
2761
+ };
2762
+
2763
+ // src/mcp/events-tool.ts
2764
+ var LIST_EVENTS_TOOL_NAME = "appduct_list_events";
2661
2765
  var EVENTS_TOOL_NAME = "appduct_events";
2662
2766
  var WAIT_FOR_EVENT_TOOL_NAME = "appduct_wait_for_event";
2767
+ var DEFAULT_EVENTS_LIMIT = 50;
2768
+ var DEFAULT_EVENTS_PAYLOAD_MAX_BYTES = 4096;
2769
+ var DEFAULT_LIST_EVENTS_LIMIT = 50;
2663
2770
  var DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MS = 12e4;
2664
2771
  var MAX_WAIT_FOR_EVENT_TIMEOUT_MS = 25 * 60 * 1e3;
2665
2772
  var WAIT_FOR_EVENT_PROGRESS_INTERVAL_MS = 6e4;
2666
2773
  var clampWaitForEventTimeoutMs = (requestedMs) => {
2667
2774
  return Math.min(requestedMs ?? DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MS, MAX_WAIT_FOR_EVENT_TIMEOUT_MS);
2668
2775
  };
2776
+ var LIST_EVENTS_TOOL_DESCRIPTOR = {
2777
+ name: LIST_EVENTS_TOOL_NAME,
2778
+ description: `List the events the connected app declared, as one-line signatures (\`name { field: type }\`) with each event's description. Call this before appduct_wait_for_event or appduct_events so you wait on a name the app really posts instead of guessing one. An app that declares no events returns an empty list, which does not mean it posts none. name is a whole-name, case-sensitive glob (* matches any run of characters), e.g. "cart.*"; a name without * that matches a declared event also returns that event's payload_schema. limit (default ${DEFAULT_LIST_EVENTS_LIMIT}) and offset page the name-sorted list, and total counts every match before paging. Returns { session, total, limit, events }, each event { name, signature, description }.`,
2779
+ inputSchema: {
2780
+ type: "object",
2781
+ properties: {
2782
+ selector: {
2783
+ type: "string",
2784
+ minLength: 1,
2785
+ description: "Session alias or id. Omit to target the sole active/suspended session."
2786
+ },
2787
+ name: { type: "string", minLength: 1 },
2788
+ limit: { type: "integer", exclusiveMinimum: 0 },
2789
+ offset: { type: "integer", minimum: 0 }
2790
+ },
2791
+ additionalProperties: false
2792
+ }
2793
+ };
2669
2794
  var EVENTS_TOOL_DESCRIPTOR = {
2670
2795
  name: EVENTS_TOOL_NAME,
2671
- 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. 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.",
2796
+ 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.`,
2672
2797
  inputSchema: {
2673
2798
  type: "object",
2674
2799
  properties: {
2675
2800
  selector: { type: "string" },
2676
2801
  since: { type: "integer", minimum: 0 },
2677
- limit: { type: "integer", exclusiveMinimum: 0 }
2802
+ limit: { type: "integer", exclusiveMinimum: 0 },
2803
+ name: { type: "string" },
2804
+ payloadMaxBytes: { type: "integer", exclusiveMinimum: 0 }
2678
2805
  },
2679
2806
  additionalProperties: false
2680
2807
  }
2681
2808
  };
2682
2809
  var WAIT_FOR_EVENT_TOOL_DESCRIPTOR = {
2683
2810
  name: WAIT_FOR_EVENT_TOOL_NAME,
2684
- 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.",
2811
+ 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.`,
2685
2812
  inputSchema: {
2686
2813
  type: "object",
2687
2814
  properties: {
2688
2815
  selector: { type: "string" },
2689
2816
  name: { type: "string" },
2690
- match: { type: "object", additionalProperties: { type: ["string", "number", "boolean", "null"] } },
2691
2817
  since: { type: "integer", minimum: 0 },
2692
- timeoutMs: { type: "number", exclusiveMinimum: 0 }
2818
+ timeoutMs: { type: "number", exclusiveMinimum: 0 },
2819
+ payloadMaxBytes: { type: "integer", exclusiveMinimum: 0 }
2693
2820
  },
2694
2821
  required: ["name"],
2695
2822
  additionalProperties: false
@@ -2745,169 +2872,108 @@ var rejectKinds = (args) => {
2745
2872
  );
2746
2873
  }
2747
2874
  };
2748
- var isMatchPrimitive = (value) => {
2749
- return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
2750
- };
2751
- var asOptionalMatch = (value) => {
2752
- if (value === void 0) {
2753
- return void 0;
2754
- }
2755
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
2756
- throw new McpBuiltinToolError("invalid_request", '"match" must be a JSON object.');
2757
- }
2758
- const record = value;
2759
- for (const [key, entry] of Object.entries(record)) {
2760
- if (!isMatchPrimitive(entry)) {
2761
- throw new McpBuiltinToolError(
2762
- "invalid_request",
2763
- `"match.${key}" must be a string, number, boolean, or null (objects/arrays can never match).`
2764
- );
2765
- }
2875
+ var rejectMatch = (args) => {
2876
+ if (args.match !== void 0) {
2877
+ throw new McpBuiltinToolError(
2878
+ "invalid_request",
2879
+ '"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.'
2880
+ );
2766
2881
  }
2767
- return record;
2882
+ };
2883
+ var handleListEventsTool = async (rawArgs, call) => {
2884
+ const args = asRecord3(rawArgs);
2885
+ rejectUnknownKeys(args, LIST_EVENTS_TOOL_NAME, ["selector", "name", "limit", "offset"]);
2886
+ const session = await resolveSession(call, asOptionalString(args.selector, "selector"));
2887
+ const name = asOptionalString(args.name, "name");
2888
+ const limit = args.limit ?? DEFAULT_LIST_EVENTS_LIMIT;
2889
+ const result = await call(RPC_METHODS6.eventsList, {
2890
+ selector: session.sessionId,
2891
+ ...name !== void 0 ? { name } : {},
2892
+ limit,
2893
+ ...args.offset !== void 0 && args.offset !== null ? { offset: args.offset } : {}
2894
+ });
2895
+ const lookup = name !== void 0 && !name.includes("*");
2896
+ return {
2897
+ session: session.alias,
2898
+ total: result.total,
2899
+ limit,
2900
+ events: result.events.map((event) => ({
2901
+ name: event.name,
2902
+ signature: renderEventSignature(event),
2903
+ description: event.description,
2904
+ ...lookup && event.payload_schema !== void 0 ? { payload_schema: event.payload_schema } : {}
2905
+ }))
2906
+ };
2768
2907
  };
2769
2908
  var handleEventsTool = async (rawArgs, deps) => {
2770
2909
  const args = asRecord3(rawArgs);
2771
2910
  rejectKinds(args);
2772
- return deps.call(RPC_METHODS5.eventsSince, {
2911
+ const result = await deps.call(RPC_METHODS6.eventsSince, {
2773
2912
  selector: asOptionalString2(args.selector, "selector"),
2774
2913
  since: asOptionalNonNegativeInteger(args.since, "since"),
2775
- limit: asOptionalPositiveInteger(args.limit, "limit")
2914
+ limit: asOptionalPositiveInteger(args.limit, "limit") ?? DEFAULT_EVENTS_LIMIT,
2915
+ name: asOptionalString2(args.name, "name"),
2916
+ // Issue #113: this tool always caps a payload, so a caller reading the result straight into
2917
+ // its context can't be blown out by one gigantic app-pushed payload; `payloadMaxBytes` lets a
2918
+ // caller raise or lower that default.
2919
+ payloadMaxBytes: asOptionalPositiveInteger(args.payloadMaxBytes, "payloadMaxBytes") ?? DEFAULT_EVENTS_PAYLOAD_MAX_BYTES
2776
2920
  });
2777
- };
2778
- var payloadShallowMatches = (payload, match) => {
2779
- if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
2780
- return false;
2781
- }
2782
- const record = payload;
2783
- return Object.entries(match).every(([key, value]) => record[key] === value);
2921
+ return {
2922
+ events: result.events.map((event) => event.sessionId === void 0 ? void 0 : toAppEvent2(event, event.sessionId)).filter((event) => event !== void 0),
2923
+ cursor: result.cursor,
2924
+ dropped: result.dropped,
2925
+ remaining: result.remaining
2926
+ };
2784
2927
  };
2785
2928
  var handleWaitForEventTool = async (rawArgs, deps) => {
2786
2929
  const args = asRecord3(rawArgs);
2787
2930
  rejectKinds(args);
2931
+ rejectMatch(args);
2788
2932
  const selector = asOptionalString2(args.selector, "selector");
2789
2933
  const name = asOptionalString2(args.name, "name");
2790
2934
  if (!name) {
2791
2935
  throw new McpBuiltinToolError("invalid_request", '"name" must be a non-empty string.');
2792
2936
  }
2793
- const match = asOptionalMatch(args.match);
2794
2937
  const since = asOptionalNonNegativeInteger(args.since, "since");
2795
2938
  const timeoutMs = clampWaitForEventTimeoutMs(asOptionalPositiveNumber2(args.timeoutMs, "timeoutMs"));
2939
+ const payloadMaxBytes = asOptionalPositiveInteger(args.payloadMaxBytes, "payloadMaxBytes") ?? DEFAULT_EVENTS_PAYLOAD_MAX_BYTES;
2796
2940
  const stream = await openDaemonStream({ stateDir: deps.stateDir, spawn: deps.spawn });
2797
2941
  try {
2798
- const described = await stream.call(RPC_METHODS5.sessionsDescribe, { selector });
2799
- const sessionId = described.sessionId;
2800
- const alias = described.alias;
2801
- const toResult = (event) => {
2802
- if (event.kind !== "app_event" || event.sessionId !== sessionId) {
2803
- return void 0;
2804
- }
2805
- const data = event.data;
2806
- if (data.name !== name) {
2807
- return void 0;
2808
- }
2809
- if (match && !payloadShallowMatches(data.payload, match)) {
2810
- return void 0;
2811
- }
2812
- return { sessionId, alias, name, payload: data.payload, ts: event.ts, seq: event.seq };
2813
- };
2814
- let liveHandler = null;
2815
- const earlyEvents = [];
2816
- const unsubscribeNotification = stream.onNotification((payload) => {
2817
- const event = payload;
2818
- if (liveHandler) {
2819
- liveHandler(event);
2820
- } else {
2821
- earlyEvents.push(event);
2822
- }
2823
- });
2824
- let unsubscribeClose = () => {
2825
- };
2942
+ const described = await stream.call(RPC_METHODS6.sessionsDescribe, { selector });
2943
+ const startedAt = Date.now();
2944
+ const progressTimer = deps.progress ? setInterval(() => {
2945
+ deps.progress.sendNotification({
2946
+ method: "notifications/progress",
2947
+ params: {
2948
+ progressToken: deps.progress.token,
2949
+ progress: Date.now() - startedAt,
2950
+ total: timeoutMs,
2951
+ message: `Still waiting for event "${name}"...`
2952
+ }
2953
+ }).catch(() => {
2954
+ });
2955
+ }, WAIT_FOR_EVENT_PROGRESS_INTERVAL_MS) : void 0;
2826
2956
  try {
2827
- await stream.call(RPC_METHODS5.eventsSubscribe, { sessionSelector: sessionId, kinds: ["app_event"] });
2828
- const sinceResult = await stream.call(RPC_METHODS5.eventsSince, {
2829
- selector: sessionId,
2830
- since
2957
+ const { event, dropped } = await waitForAppEvent(stream, {
2958
+ sessionId: described.sessionId,
2959
+ name,
2960
+ since,
2961
+ timeoutMs,
2962
+ payloadMaxBytes
2831
2963
  });
2832
- const seenSeqs = /* @__PURE__ */ new Set();
2833
- const backlog = [];
2834
- for (const event of [...sinceResult.events, ...earlyEvents]) {
2835
- if (event.sessionId === sessionId && !seenSeqs.has(event.seq)) {
2836
- seenSeqs.add(event.seq);
2837
- backlog.push(event);
2838
- }
2964
+ return { ...event, dropped };
2965
+ } catch (error) {
2966
+ if (error instanceof WaitForAppEventTimeoutError) {
2967
+ throw new McpBuiltinToolError("tool_timeout", error.message);
2839
2968
  }
2840
- backlog.sort((a, b) => a.seq - b.seq);
2841
- for (const event of backlog) {
2842
- const result = toResult(event);
2843
- if (result) {
2844
- return result;
2845
- }
2969
+ if (error instanceof WaitForAppEventConnectionClosedError) {
2970
+ throw new McpBuiltinToolError("tool_execution_error", error.message);
2846
2971
  }
2847
- const highestConsideredSeq = backlog.length > 0 ? backlog[backlog.length - 1].seq : sinceResult.cursor;
2848
- return await new Promise((resolve2, reject) => {
2849
- let settled = false;
2850
- const settle = (fn) => {
2851
- if (settled) {
2852
- return;
2853
- }
2854
- settled = true;
2855
- clearTimeout(timer);
2856
- if (progressTimer) {
2857
- clearInterval(progressTimer);
2858
- }
2859
- liveHandler = null;
2860
- fn();
2861
- };
2862
- const timer = setTimeout(() => {
2863
- settle(
2864
- () => reject(
2865
- new McpBuiltinToolError(
2866
- "tool_timeout",
2867
- `Timed out after ${timeoutMs}ms waiting for event "${name}".`
2868
- )
2869
- )
2870
- );
2871
- }, timeoutMs);
2872
- const startedAt = Date.now();
2873
- const progressTimer = deps.progress ? setInterval(() => {
2874
- deps.progress.sendNotification({
2875
- method: "notifications/progress",
2876
- params: {
2877
- progressToken: deps.progress.token,
2878
- progress: Date.now() - startedAt,
2879
- total: timeoutMs,
2880
- message: `Still waiting for event "${name}"...`
2881
- }
2882
- }).catch(() => {
2883
- });
2884
- }, WAIT_FOR_EVENT_PROGRESS_INTERVAL_MS) : void 0;
2885
- unsubscribeClose = stream.onClose(() => {
2886
- settle(
2887
- () => reject(
2888
- new McpBuiltinToolError(
2889
- "tool_execution_error",
2890
- `The connection to the Appduct daemon closed while waiting for event "${name}".`
2891
- )
2892
- )
2893
- );
2894
- });
2895
- liveHandler = (event) => {
2896
- if (settled) {
2897
- return;
2898
- }
2899
- if (event.sessionId === sessionId && event.seq <= highestConsideredSeq) {
2900
- return;
2901
- }
2902
- const result = toResult(event);
2903
- if (result) {
2904
- settle(() => resolve2(result));
2905
- }
2906
- };
2907
- });
2972
+ throw error;
2908
2973
  } finally {
2909
- unsubscribeNotification();
2910
- unsubscribeClose();
2974
+ if (progressTimer) {
2975
+ clearInterval(progressTimer);
2976
+ }
2911
2977
  }
2912
2978
  } finally {
2913
2979
  stream.close();
@@ -3047,7 +3113,7 @@ var createMcpServer = async (options) => {
3047
3113
  }
3048
3114
  if (progressToken === void 0) {
3049
3115
  const result = await stream.call(
3050
- RPC_METHODS6.toolsCall,
3116
+ RPC_METHODS7.toolsCall,
3051
3117
  {
3052
3118
  selector: tool.sessionId,
3053
3119
  name: tool.descriptor.name,
@@ -3062,7 +3128,7 @@ var createMcpServer = async (options) => {
3062
3128
  }
3063
3129
  const progressStream = await openStream({ stateDir: options.stateDir, spawn: options.spawn });
3064
3130
  try {
3065
- await progressStream.call(RPC_METHODS6.eventsSubscribe, {
3131
+ await progressStream.call(RPC_METHODS7.eventsSubscribe, {
3066
3132
  sessionSelector: tool.sessionId,
3067
3133
  kinds: ["tool_call_started", "tool_call_progress"]
3068
3134
  });
@@ -3072,7 +3138,7 @@ var createMcpServer = async (options) => {
3072
3138
  if (callId === void 0 || !cancelRequested) {
3073
3139
  return;
3074
3140
  }
3075
- progressStream.call(RPC_METHODS6.toolsCancel, { selector: tool.sessionId, callId, reason: "mcp_client_cancelled" }).catch((error) => {
3141
+ progressStream.call(RPC_METHODS7.toolsCancel, { selector: tool.sessionId, callId, reason: "mcp_client_cancelled" }).catch((error) => {
3076
3142
  console.error("appduct mcp: failed to cancel an app tool call:", error);
3077
3143
  });
3078
3144
  };
@@ -3105,7 +3171,7 @@ var createMcpServer = async (options) => {
3105
3171
  });
3106
3172
  try {
3107
3173
  const result = await progressStream.call(
3108
- RPC_METHODS6.toolsCall,
3174
+ RPC_METHODS7.toolsCall,
3109
3175
  {
3110
3176
  selector: tool.sessionId,
3111
3177
  name: tool.descriptor.name,
@@ -3133,6 +3199,7 @@ var createMcpServer = async (options) => {
3133
3199
  LIST_TOOLS_TOOL_DESCRIPTOR,
3134
3200
  DESCRIBE_TOOL_TOOL_DESCRIPTOR,
3135
3201
  CALL_TOOL_TOOL_DESCRIPTOR,
3202
+ LIST_EVENTS_TOOL_DESCRIPTOR,
3136
3203
  EVENTS_TOOL_DESCRIPTOR,
3137
3204
  WAIT_FOR_EVENT_TOOL_DESCRIPTOR
3138
3205
  ]
@@ -3161,6 +3228,9 @@ var createMcpServer = async (options) => {
3161
3228
  await handleWaitForSessionTool(args, { stateDir: options.stateDir, spawn: options.spawn })
3162
3229
  );
3163
3230
  }
3231
+ if (name === LIST_EVENTS_TOOL_NAME) {
3232
+ return toolSuccessContent(await handleListEventsTool(args, stream.call));
3233
+ }
3164
3234
  if (name === EVENTS_TOOL_NAME) {
3165
3235
  return toolSuccessContent(await handleEventsTool(args, { call: stream.call }));
3166
3236
  }
@@ -3217,7 +3287,7 @@ var createMcpServer = async (options) => {
3217
3287
  if (request.params.uri !== SESSIONS_RESOURCE_URI) {
3218
3288
  throw new Error(`Unknown resource "${request.params.uri}".`);
3219
3289
  }
3220
- const sessions = await stream.call(RPC_METHODS6.sessionsList);
3290
+ const sessions = await stream.call(RPC_METHODS7.sessionsList);
3221
3291
  return {
3222
3292
  contents: [{ uri: SESSIONS_RESOURCE_URI, mimeType: "application/json", text: JSON.stringify(sessions) }]
3223
3293
  };
@@ -3309,6 +3379,7 @@ var formatJson = (value, flags) => {
3309
3379
  import pc from "picocolors";
3310
3380
  import {
3311
3381
  formatAgentWebSocketUrl,
3382
+ renderEventSignature as renderEventSignature2,
3312
3383
  renderToolSignature as renderToolSignature2,
3313
3384
  summarizeToolDescription as summarizeToolDescription2
3314
3385
  } from "@appduct/shared";
@@ -3367,6 +3438,13 @@ var formatDevice = (device) => {
3367
3438
  const label = parts.length > 0 ? parts.join(" ") : "unknown device";
3368
3439
  return device.os ? `${label} (${device.os})` : label;
3369
3440
  };
3441
+ var SUSPEND_REASON_LABELS = {
3442
+ app_backgrounded: "app in background",
3443
+ connection_lost: "connection lost"
3444
+ };
3445
+ var formatSessionState = (session) => {
3446
+ return session.suspendReason ? `${session.state} (${SUSPEND_REASON_LABELS[session.suspendReason]})` : session.state;
3447
+ };
3370
3448
  var renderLsData = (colors, data, now) => {
3371
3449
  if (data.length === 0) {
3372
3450
  return [colors.dim("Sessions"), " No Appduct sessions are registered."];
@@ -3375,7 +3453,7 @@ var renderLsData = (colors, data, now) => {
3375
3453
  const headers = ["Alias", "State", "Device", "Tools", "Age"];
3376
3454
  const rows = data.map((session) => [
3377
3455
  session.alias,
3378
- session.state,
3456
+ formatSessionState(session),
3379
3457
  formatDevice(session.device),
3380
3458
  String(session.toolCount),
3381
3459
  formatAge(session.claimedAt ?? session.createdAt, nowIso)
@@ -3389,9 +3467,6 @@ var renderLsData = (colors, data, now) => {
3389
3467
  ...rows.map((row) => ` ${row.map((cell, index) => cell.padEnd(widths[index])).join(" ")}`)
3390
3468
  ];
3391
3469
  };
3392
- var isToolsListing = (data) => {
3393
- return typeof data === "object" && data !== null && Array.isArray(data.tools);
3394
- };
3395
3470
  var isToolGroupsListing = (data) => {
3396
3471
  return typeof data === "object" && data !== null && Array.isArray(data.groups) && !Array.isArray(data.tools);
3397
3472
  };
@@ -3409,7 +3484,7 @@ var renderEmptyToolsLine = (data) => {
3409
3484
  }
3410
3485
  if (data.group !== void 0) {
3411
3486
  const match = data.filter === void 0 ? "" : ` match ${JSON.stringify(data.filter)}`;
3412
- return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools --groups\` to see the session's groups.`;
3487
+ return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools ls --groups\` to see the session's groups.`;
3413
3488
  }
3414
3489
  return data.filter === void 0 ? " No tools registered." : ` No tools match ${JSON.stringify(data.filter)}.`;
3415
3490
  };
@@ -3491,7 +3566,7 @@ var renderToolSummaryTable = (colors, data) => {
3491
3566
  ...grouped ? renderGroupedToolLines(colors, data.tools) : data.tools.flatMap((tool) => renderToolSummaryLines(tool, " ")),
3492
3567
  ...renderTruncationLine(data),
3493
3568
  "",
3494
- "Run `appduct tools <name>` for a tool's full schema."
3569
+ "Run `appduct tools describe <name>` for a tool's full schema."
3495
3570
  ];
3496
3571
  };
3497
3572
  var renderToolDetail = (colors, tool, flags) => {
@@ -3543,18 +3618,62 @@ var renderToolGroups = (colors, data) => {
3543
3618
  colors.green("Groups"),
3544
3619
  ...data.groups.map((entry) => ` ${label(entry).padEnd(width)} ${String(entry.total).padStart(countWidth)}`),
3545
3620
  "",
3546
- `${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.")
3621
+ `${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.")
3547
3622
  ];
3548
3623
  };
3549
- var renderToolsData = (colors, data, flags, full) => {
3624
+ var renderToolsListData = (colors, data, flags, full) => {
3550
3625
  if (isToolGroupsListing(data)) {
3551
3626
  return renderToolGroups(colors, data);
3552
3627
  }
3553
- if (!isToolsListing(data)) {
3554
- return renderToolDetail(colors, data, flags);
3555
- }
3556
3628
  return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
3557
3629
  };
3630
+ var renderEventsTruncationLine = (data) => {
3631
+ const offset = data.offset ?? 0;
3632
+ if (data.events.length >= data.total) {
3633
+ return [];
3634
+ }
3635
+ return [
3636
+ "",
3637
+ `Showing ${data.events.length} of ${data.total} events (offset ${offset}). Narrow with --name <glob> or page with --offset <n>.`
3638
+ ];
3639
+ };
3640
+ var renderEventsListData = (colors, data, flags) => {
3641
+ const [only] = data.events;
3642
+ if (data.name !== void 0 && !data.name.includes("*") && only !== void 0) {
3643
+ return renderFields(
3644
+ colors.green(`Event: ${only.name}`),
3645
+ [
3646
+ ["Signature", renderEventSignature2(only)],
3647
+ ["Description", only.description],
3648
+ ["Payload schema", only.payload_schema]
3649
+ ],
3650
+ flags
3651
+ );
3652
+ }
3653
+ if (data.events.length === 0 && data.total > 0) {
3654
+ return [
3655
+ colors.green("Events"),
3656
+ ` No events at offset ${data.offset ?? 0}; ${data.total} matching event${data.total === 1 ? "" : "s"} in total.`
3657
+ ];
3658
+ }
3659
+ if (data.events.length === 0) {
3660
+ return [
3661
+ colors.green("Events"),
3662
+ data.name === void 0 ? " No events declared." : ` No events match ${JSON.stringify(data.name)}.`
3663
+ ];
3664
+ }
3665
+ const lines = data.events.flatMap((event) => [
3666
+ ` ${renderEventSignature2(event)}`,
3667
+ ` ${summarizeToolDescription2(event.description)}`
3668
+ ]);
3669
+ return [
3670
+ colors.green("Events"),
3671
+ ...lines,
3672
+ ...renderEventsTruncationLine(data),
3673
+ "",
3674
+ "Run `appduct events ls --name <name>` for an event's full payload schema."
3675
+ ];
3676
+ };
3558
3677
  var renderInvokeData = (colors, data, flags) => {
3559
3678
  return [colors.green("Result"), formatScalar(data, flags)];
3560
3679
  };
@@ -3738,15 +3857,19 @@ var renderSuccessData = (colors, command, data, options) => {
3738
3857
  return renderInitData(colors, data, flags);
3739
3858
  case "keygen":
3740
3859
  return renderKeygenData(colors, data, flags);
3741
- case "link":
3860
+ case "sessions link":
3742
3861
  return renderLinkData(colors, data, flags, options.qr);
3743
- case "ls":
3862
+ case "sessions ls":
3744
3863
  return renderLsData(colors, data, options.now ?? /* @__PURE__ */ new Date());
3745
- case "tools":
3746
- return renderToolsData(colors, data, flags, options.full);
3747
- case "invoke":
3864
+ case "tools ls":
3865
+ return renderToolsListData(colors, data, flags, options.full);
3866
+ case "tools describe":
3867
+ return renderToolDetail(colors, data, flags);
3868
+ case "events ls":
3869
+ return renderEventsListData(colors, data, flags);
3870
+ case "tools call":
3748
3871
  return renderInvokeData(colors, data, flags);
3749
- case "revoke":
3872
+ case "sessions revoke":
3750
3873
  return renderRevokeData(colors, data);
3751
3874
  case "daemon run":
3752
3875
  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;