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
@@ -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,10 +2656,114 @@ 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
+ toAppEvent as toAppEvent2
2660
2661
  } from "@appduct/shared";
2662
+
2663
+ // src/events/index.ts
2664
+ import {
2665
+ RPC_METHODS as RPC_METHODS5,
2666
+ toAppEvent
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
2661
2763
  var EVENTS_TOOL_NAME = "appduct_events";
2662
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;
2663
2767
  var DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MS = 12e4;
2664
2768
  var MAX_WAIT_FOR_EVENT_TIMEOUT_MS = 25 * 60 * 1e3;
2665
2769
  var WAIT_FOR_EVENT_PROGRESS_INTERVAL_MS = 6e4;
@@ -2668,28 +2772,30 @@ var clampWaitForEventTimeoutMs = (requestedMs) => {
2668
2772
  };
2669
2773
  var EVENTS_TOOL_DESCRIPTOR = {
2670
2774
  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.",
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.`,
2672
2776
  inputSchema: {
2673
2777
  type: "object",
2674
2778
  properties: {
2675
2779
  selector: { type: "string" },
2676
2780
  since: { type: "integer", minimum: 0 },
2677
- limit: { type: "integer", exclusiveMinimum: 0 }
2781
+ limit: { type: "integer", exclusiveMinimum: 0 },
2782
+ name: { type: "string" },
2783
+ payloadMaxBytes: { type: "integer", exclusiveMinimum: 0 }
2678
2784
  },
2679
2785
  additionalProperties: false
2680
2786
  }
2681
2787
  };
2682
2788
  var WAIT_FOR_EVENT_TOOL_DESCRIPTOR = {
2683
2789
  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.",
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.`,
2685
2791
  inputSchema: {
2686
2792
  type: "object",
2687
2793
  properties: {
2688
2794
  selector: { type: "string" },
2689
2795
  name: { type: "string" },
2690
- match: { type: "object", additionalProperties: { type: ["string", "number", "boolean", "null"] } },
2691
2796
  since: { type: "integer", minimum: 0 },
2692
- timeoutMs: { type: "number", exclusiveMinimum: 0 }
2797
+ timeoutMs: { type: "number", exclusiveMinimum: 0 },
2798
+ payloadMaxBytes: { type: "integer", exclusiveMinimum: 0 }
2693
2799
  },
2694
2800
  required: ["name"],
2695
2801
  additionalProperties: false
@@ -2745,169 +2851,83 @@ var rejectKinds = (args) => {
2745
2851
  );
2746
2852
  }
2747
2853
  };
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
- }
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
+ );
2766
2860
  }
2767
- return record;
2768
2861
  };
2769
2862
  var handleEventsTool = async (rawArgs, deps) => {
2770
2863
  const args = asRecord3(rawArgs);
2771
2864
  rejectKinds(args);
2772
- return deps.call(RPC_METHODS5.eventsSince, {
2865
+ const result = await deps.call(RPC_METHODS6.eventsSince, {
2773
2866
  selector: asOptionalString2(args.selector, "selector"),
2774
2867
  since: asOptionalNonNegativeInteger(args.since, "since"),
2775
- 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
2776
2874
  });
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);
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
+ };
2784
2881
  };
2785
2882
  var handleWaitForEventTool = async (rawArgs, deps) => {
2786
2883
  const args = asRecord3(rawArgs);
2787
2884
  rejectKinds(args);
2885
+ rejectMatch(args);
2788
2886
  const selector = asOptionalString2(args.selector, "selector");
2789
2887
  const name = asOptionalString2(args.name, "name");
2790
2888
  if (!name) {
2791
2889
  throw new McpBuiltinToolError("invalid_request", '"name" must be a non-empty string.');
2792
2890
  }
2793
- const match = asOptionalMatch(args.match);
2794
2891
  const since = asOptionalNonNegativeInteger(args.since, "since");
2795
2892
  const timeoutMs = clampWaitForEventTimeoutMs(asOptionalPositiveNumber2(args.timeoutMs, "timeoutMs"));
2893
+ const payloadMaxBytes = asOptionalPositiveInteger(args.payloadMaxBytes, "payloadMaxBytes") ?? DEFAULT_EVENTS_PAYLOAD_MAX_BYTES;
2796
2894
  const stream = await openDaemonStream({ stateDir: deps.stateDir, spawn: deps.spawn });
2797
2895
  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
- };
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;
2826
2910
  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
2911
+ const { event, dropped } = await waitForAppEvent(stream, {
2912
+ sessionId: described.sessionId,
2913
+ name,
2914
+ since,
2915
+ timeoutMs,
2916
+ payloadMaxBytes
2831
2917
  });
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
- }
2918
+ return { ...event, dropped };
2919
+ } catch (error) {
2920
+ if (error instanceof WaitForAppEventTimeoutError) {
2921
+ throw new McpBuiltinToolError("tool_timeout", error.message);
2839
2922
  }
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
- }
2923
+ if (error instanceof WaitForAppEventConnectionClosedError) {
2924
+ throw new McpBuiltinToolError("tool_execution_error", error.message);
2846
2925
  }
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
- });
2926
+ throw error;
2908
2927
  } finally {
2909
- unsubscribeNotification();
2910
- unsubscribeClose();
2928
+ if (progressTimer) {
2929
+ clearInterval(progressTimer);
2930
+ }
2911
2931
  }
2912
2932
  } finally {
2913
2933
  stream.close();
@@ -3047,7 +3067,7 @@ var createMcpServer = async (options) => {
3047
3067
  }
3048
3068
  if (progressToken === void 0) {
3049
3069
  const result = await stream.call(
3050
- RPC_METHODS6.toolsCall,
3070
+ RPC_METHODS7.toolsCall,
3051
3071
  {
3052
3072
  selector: tool.sessionId,
3053
3073
  name: tool.descriptor.name,
@@ -3062,7 +3082,7 @@ var createMcpServer = async (options) => {
3062
3082
  }
3063
3083
  const progressStream = await openStream({ stateDir: options.stateDir, spawn: options.spawn });
3064
3084
  try {
3065
- await progressStream.call(RPC_METHODS6.eventsSubscribe, {
3085
+ await progressStream.call(RPC_METHODS7.eventsSubscribe, {
3066
3086
  sessionSelector: tool.sessionId,
3067
3087
  kinds: ["tool_call_started", "tool_call_progress"]
3068
3088
  });
@@ -3072,7 +3092,7 @@ var createMcpServer = async (options) => {
3072
3092
  if (callId === void 0 || !cancelRequested) {
3073
3093
  return;
3074
3094
  }
3075
- 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) => {
3076
3096
  console.error("appduct mcp: failed to cancel an app tool call:", error);
3077
3097
  });
3078
3098
  };
@@ -3105,7 +3125,7 @@ var createMcpServer = async (options) => {
3105
3125
  });
3106
3126
  try {
3107
3127
  const result = await progressStream.call(
3108
- RPC_METHODS6.toolsCall,
3128
+ RPC_METHODS7.toolsCall,
3109
3129
  {
3110
3130
  selector: tool.sessionId,
3111
3131
  name: tool.descriptor.name,
@@ -3217,7 +3237,7 @@ var createMcpServer = async (options) => {
3217
3237
  if (request.params.uri !== SESSIONS_RESOURCE_URI) {
3218
3238
  throw new Error(`Unknown resource "${request.params.uri}".`);
3219
3239
  }
3220
- const sessions = await stream.call(RPC_METHODS6.sessionsList);
3240
+ const sessions = await stream.call(RPC_METHODS7.sessionsList);
3221
3241
  return {
3222
3242
  contents: [{ uri: SESSIONS_RESOURCE_URI, mimeType: "application/json", text: JSON.stringify(sessions) }]
3223
3243
  };
@@ -3389,9 +3409,6 @@ var renderLsData = (colors, data, now) => {
3389
3409
  ...rows.map((row) => ` ${row.map((cell, index) => cell.padEnd(widths[index])).join(" ")}`)
3390
3410
  ];
3391
3411
  };
3392
- var isToolsListing = (data) => {
3393
- return typeof data === "object" && data !== null && Array.isArray(data.tools);
3394
- };
3395
3412
  var isToolGroupsListing = (data) => {
3396
3413
  return typeof data === "object" && data !== null && Array.isArray(data.groups) && !Array.isArray(data.tools);
3397
3414
  };
@@ -3409,7 +3426,7 @@ var renderEmptyToolsLine = (data) => {
3409
3426
  }
3410
3427
  if (data.group !== void 0) {
3411
3428
  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.`;
3429
+ return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools ls --groups\` to see the session's groups.`;
3413
3430
  }
3414
3431
  return data.filter === void 0 ? " No tools registered." : ` No tools match ${JSON.stringify(data.filter)}.`;
3415
3432
  };
@@ -3491,7 +3508,7 @@ var renderToolSummaryTable = (colors, data) => {
3491
3508
  ...grouped ? renderGroupedToolLines(colors, data.tools) : data.tools.flatMap((tool) => renderToolSummaryLines(tool, " ")),
3492
3509
  ...renderTruncationLine(data),
3493
3510
  "",
3494
- "Run `appduct tools <name>` for a tool's full schema."
3511
+ "Run `appduct tools describe <name>` for a tool's full schema."
3495
3512
  ];
3496
3513
  };
3497
3514
  var renderToolDetail = (colors, tool, flags) => {
@@ -3543,16 +3560,13 @@ var renderToolGroups = (colors, data) => {
3543
3560
  colors.green("Groups"),
3544
3561
  ...data.groups.map((entry) => ` ${label(entry).padEnd(width)} ${String(entry.total).padStart(countWidth)}`),
3545
3562
  "",
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.")
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.")
3547
3564
  ];
3548
3565
  };
3549
- var renderToolsData = (colors, data, flags, full) => {
3566
+ var renderToolsListData = (colors, data, flags, full) => {
3550
3567
  if (isToolGroupsListing(data)) {
3551
3568
  return renderToolGroups(colors, data);
3552
3569
  }
3553
- if (!isToolsListing(data)) {
3554
- return renderToolDetail(colors, data, flags);
3555
- }
3556
3570
  return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
3557
3571
  };
3558
3572
  var renderInvokeData = (colors, data, flags) => {
@@ -3738,15 +3752,17 @@ var renderSuccessData = (colors, command, data, options) => {
3738
3752
  return renderInitData(colors, data, flags);
3739
3753
  case "keygen":
3740
3754
  return renderKeygenData(colors, data, flags);
3741
- case "link":
3755
+ case "sessions link":
3742
3756
  return renderLinkData(colors, data, flags, options.qr);
3743
- case "ls":
3757
+ case "sessions ls":
3744
3758
  return renderLsData(colors, data, options.now ?? /* @__PURE__ */ new Date());
3745
- case "tools":
3746
- return renderToolsData(colors, data, flags, options.full);
3747
- 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":
3748
3764
  return renderInvokeData(colors, data, flags);
3749
- case "revoke":
3765
+ case "sessions revoke":
3750
3766
  return renderRevokeData(colors, data);
3751
3767
  case "daemon run":
3752
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;