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
@@ -484,6 +484,7 @@ var RpcApplicationError = class extends Error {
484
484
  this.name = "RpcApplicationError";
485
485
  }
486
486
  };
487
+ var appBackgroundedMessage = (alias) => `Session "${alias}" is suspended because the app is in the background. Bring the app to the foreground to resume it.`;
487
488
 
488
489
  // src/daemon/timers.ts
489
490
  var systemTimers = {
@@ -586,7 +587,7 @@ var createCallsManager = (options) => {
586
587
  const message = { type: "tool_cancel", session_id: sessionId, id: callId, reason };
587
588
  return options.sendCancel(sessionId, message);
588
589
  };
589
- const rejectSession = (sessionId, errorType) => {
590
+ const rejectSession = (sessionId, errorType, options2 = {}) => {
590
591
  const sessionMap = pendingBySession.get(sessionId);
591
592
  if (!sessionMap) {
592
593
  return;
@@ -594,13 +595,19 @@ var createCallsManager = (options) => {
594
595
  pendingBySession.delete(sessionId);
595
596
  for (const pending of sessionMap.values()) {
596
597
  timers.clearTimeout(pending.timer);
597
- pending.reject(new RpcApplicationError(errorType, `Session transitioned while the call was pending.`));
598
+ pending.reject(
599
+ new RpcApplicationError(
600
+ errorType,
601
+ options2.backgrounded ? appBackgroundedMessage(pending.alias) : "Session transitioned while the call was pending."
602
+ )
603
+ );
598
604
  }
599
605
  };
600
606
  const unsubscribeFromEventBus = options.eventBus.subscribe((event) => {
601
607
  const errorType = TERMINATING_KIND_ERROR_TYPES[event.kind];
602
608
  if (errorType && event.sessionId) {
603
- rejectSession(event.sessionId, errorType);
609
+ const backgrounded = event.kind === "session_suspended" && event.data.reason === "app_backgrounded";
610
+ rejectSession(event.sessionId, errorType, { backgrounded });
604
611
  }
605
612
  });
606
613
  const disposeAll = () => {
@@ -614,6 +621,71 @@ var createCallsManager = (options) => {
614
621
 
615
622
  // src/daemon/event-bus.ts
616
623
  var TERMINAL_EVENT_KINDS = /* @__PURE__ */ new Set(["session_expired", "session_revoked"]);
624
+ var utf8ByteLength = (text) => Buffer.byteLength(text, "utf8");
625
+ var truncateToUtf8Bytes = (text, maxBytes) => {
626
+ const encoded = Buffer.from(text, "utf8");
627
+ if (encoded.length <= maxBytes) {
628
+ return text;
629
+ }
630
+ const decoder = new TextDecoder("utf-8", { fatal: true });
631
+ for (let end = maxBytes; end > 0; end--) {
632
+ try {
633
+ return decoder.decode(encoded.subarray(0, end));
634
+ } catch {
635
+ }
636
+ }
637
+ return "";
638
+ };
639
+ var matchesNameGlob = (pattern, name) => {
640
+ const pieces = pattern.split("*");
641
+ const first = pieces[0] ?? "";
642
+ if (pieces.length === 1) {
643
+ return name === first;
644
+ }
645
+ const last = pieces[pieces.length - 1] ?? "";
646
+ if (first.length + last.length > name.length || !name.startsWith(first) || !name.endsWith(last)) {
647
+ return false;
648
+ }
649
+ const end = name.length - last.length;
650
+ let position = first.length;
651
+ for (const piece of pieces.slice(1, -1)) {
652
+ const found = name.indexOf(piece, position);
653
+ if (found === -1 || found + piece.length > end) {
654
+ return false;
655
+ }
656
+ position = found + piece.length;
657
+ }
658
+ return true;
659
+ };
660
+ var projectAppEvent = (event, options = {}) => {
661
+ if (event.kind !== "app_event") {
662
+ return event;
663
+ }
664
+ const data = event.data;
665
+ if (options.name !== void 0 && !(typeof data.name === "string" && matchesNameGlob(options.name, data.name))) {
666
+ return void 0;
667
+ }
668
+ if (options.payloadMaxBytes === void 0 || !("payload" in data)) {
669
+ return event;
670
+ }
671
+ const payloadJson = JSON.stringify(data.payload);
672
+ if (payloadJson === void 0) {
673
+ return event;
674
+ }
675
+ const payloadBytes = utf8ByteLength(payloadJson);
676
+ if (payloadBytes <= options.payloadMaxBytes) {
677
+ return event;
678
+ }
679
+ return {
680
+ ...event,
681
+ data: {
682
+ name: data.name,
683
+ payloadPreview: truncateToUtf8Bytes(payloadJson, options.payloadMaxBytes),
684
+ truncated: true,
685
+ payloadBytes
686
+ }
687
+ };
688
+ };
617
689
  var createEventBus = (options = {}) => {
618
690
  const clock = options.clock ?? { now: () => /* @__PURE__ */ new Date() };
619
691
  const bufferSize = options.bufferSize ?? 256;
@@ -631,8 +703,9 @@ var createEventBus = (options = {}) => {
631
703
  return {
632
704
  emit: (event) => {
633
705
  const sessionId = event.sessionId;
634
- const seq = sessionId !== void 0 ? (cursors.get(sessionId) ?? 0) + 1 : 0;
635
- if (sessionId !== void 0) {
706
+ let seq = 0;
707
+ if (sessionId !== void 0 && event.kind === "app_event") {
708
+ seq = (cursors.get(sessionId) ?? 0) + 1;
636
709
  cursors.set(sessionId, seq);
637
710
  }
638
711
  const notification = {
@@ -664,15 +737,26 @@ var createEventBus = (options = {}) => {
664
737
  since: (sessionId, query = {}) => {
665
738
  const buffer = buffers.get(sessionId) ?? [];
666
739
  const sessionCursor = cursors.get(sessionId) ?? 0;
667
- let events = buffer.slice();
740
+ const sinceValue = query.since ?? 0;
741
+ const oldestRetainedSeq = buffer.length > 0 ? buffer[0].seq : void 0;
742
+ const dropped = oldestRetainedSeq !== void 0 ? Math.max(0, oldestRetainedSeq - sinceValue - 1) : 0;
743
+ let matching = buffer.slice();
668
744
  if (query.since !== void 0) {
669
- events = events.filter((event) => event.seq > query.since);
745
+ matching = matching.filter((event) => event.seq > query.since);
746
+ }
747
+ if (query.name !== void 0) {
748
+ matching = matching.filter((event) => projectAppEvent(event, { name: query.name }) !== void 0);
670
749
  }
750
+ let events = matching;
671
751
  if (query.limit !== void 0 && events.length > query.limit) {
672
752
  events = events.slice(0, query.limit);
673
753
  }
754
+ const remaining = matching.length - events.length;
674
755
  const cursor = events.length > 0 ? events[events.length - 1].seq : sessionCursor;
675
- return { events, cursor };
756
+ if (query.payloadMaxBytes !== void 0) {
757
+ events = events.map((event) => projectAppEvent(event, { payloadMaxBytes: query.payloadMaxBytes }));
758
+ }
759
+ return { events, cursor, dropped, remaining };
676
760
  },
677
761
  drop: (sessionId) => {
678
762
  buffers.delete(sessionId);
@@ -714,6 +798,8 @@ import {
714
798
  isEventMessage,
715
799
  isToolCallProgressMessage,
716
800
  isToolErrorMessage,
801
+ isEventRegistryDeltaMessage,
802
+ isEventRegistrySnapshotMessage,
717
803
  isToolRegistryDeltaMessage,
718
804
  isToolRegistrySnapshotMessage,
719
805
  isToolResultMessage,
@@ -811,7 +897,7 @@ var createPendingLinkRegistry = (options) => {
811
897
  };
812
898
 
813
899
  // src/daemon/registry.ts
814
- var createToolRegistry = (onChange) => {
900
+ var createRegistry = (onChange) => {
815
901
  let tools = /* @__PURE__ */ new Map();
816
902
  const notify = () => {
817
903
  onChange?.();
@@ -841,6 +927,8 @@ var RESUME_TOKEN_BYTES = 32;
841
927
  var POST_CLAIM_MESSAGE_TYPES = /* @__PURE__ */ new Set([
842
928
  "tool_registry_snapshot",
843
929
  "tool_registry_delta",
930
+ "event_registry_snapshot",
931
+ "event_registry_delta",
844
932
  "tool_result",
845
933
  "tool_error",
846
934
  "tool_call_progress",
@@ -911,6 +999,7 @@ var createSessionManager = (options) => {
911
999
  createdAt: session.createdAt.toISOString(),
912
1000
  claimedAt: session.claimedAt.toISOString(),
913
1001
  ...session.suspendedAt ? { suspendedAt: session.suspendedAt.toISOString() } : {},
1002
+ ...session.suspendReason ? { suspendReason: session.suspendReason } : {},
914
1003
  toolCount: session.registry.count()
915
1004
  };
916
1005
  };
@@ -947,7 +1036,8 @@ var createSessionManager = (options) => {
947
1036
  alias: session.alias,
948
1037
  resume_token: session.resumeToken.toString("base64url"),
949
1038
  keepalive_interval_s: options.keepaliveIntervalSeconds,
950
- grace_s: options.graceSeconds
1039
+ grace_s: options.graceSeconds,
1040
+ event_registry: true
951
1041
  };
952
1042
  socket.send(JSON.stringify(ack), (error) => {
953
1043
  if (error) {
@@ -955,17 +1045,18 @@ var createSessionManager = (options) => {
955
1045
  }
956
1046
  });
957
1047
  };
958
- const suspend = (sessionId) => {
1048
+ const suspend = (sessionId, reason) => {
959
1049
  const session = sessions.get(sessionId);
960
1050
  if (!session || session.state !== "active") {
961
1051
  return;
962
1052
  }
963
1053
  session.state = "suspended";
964
1054
  session.suspendedAt = clock.now();
1055
+ session.suspendReason = reason;
965
1056
  session.socket = void 0;
966
1057
  session.missedPongs = 0;
967
1058
  session.graceTimer = timers.setTimeout(() => expire(sessionId), options.graceSeconds * 1e3);
968
- emit("session_suspended", session.sessionId, session.alias, {});
1059
+ emit("session_suspended", session.sessionId, session.alias, { reason });
969
1060
  };
970
1061
  const expire = (sessionId) => {
971
1062
  const session = sessions.get(sessionId);
@@ -982,9 +1073,10 @@ var createSessionManager = (options) => {
982
1073
  socket.on("pong", () => {
983
1074
  session.missedPongs = 0;
984
1075
  });
985
- socket.on("close", () => {
1076
+ socket.on("close", (code, reason) => {
986
1077
  if (session.socket === socket) {
987
- suspend(session.sessionId);
1078
+ const backgrounded = code === 1001 && reason.toString("utf8") === "app_backgrounded";
1079
+ suspend(session.sessionId, backgrounded ? "app_backgrounded" : "connection_lost");
988
1080
  }
989
1081
  });
990
1082
  };
@@ -996,7 +1088,7 @@ var createSessionManager = (options) => {
996
1088
  if (session.missedPongs >= 2) {
997
1089
  const staleSocket = session.socket;
998
1090
  session.socket = void 0;
999
- suspend(session.sessionId);
1091
+ suspend(session.sessionId, "connection_lost");
1000
1092
  staleSocket.terminate();
1001
1093
  continue;
1002
1094
  }
@@ -1061,8 +1153,11 @@ var createSessionManager = (options) => {
1061
1153
  resumeToken: randomBytes3(RESUME_TOKEN_BYTES),
1062
1154
  socket,
1063
1155
  missedPongs: 0,
1064
- registry: createToolRegistry(() => {
1156
+ registry: createRegistry(() => {
1065
1157
  emit("tools_changed", sessionId, alias, { toolCount: session.registry.count() });
1158
+ }),
1159
+ eventRegistry: createRegistry(() => {
1160
+ emit("events_changed", sessionId, alias, { eventCount: session.eventRegistry.count() });
1066
1161
  })
1067
1162
  };
1068
1163
  sessions.set(sessionId, session);
@@ -1094,6 +1189,7 @@ var createSessionManager = (options) => {
1094
1189
  session.resumeToken = randomBytes3(RESUME_TOKEN_BYTES);
1095
1190
  session.state = "active";
1096
1191
  session.suspendedAt = void 0;
1192
+ session.suspendReason = void 0;
1097
1193
  session.socket = socket;
1098
1194
  session.missedPongs = 0;
1099
1195
  attachSocketHandlers(session);
@@ -1128,6 +1224,26 @@ var createSessionManager = (options) => {
1128
1224
  }
1129
1225
  return;
1130
1226
  }
1227
+ case "event_registry_snapshot": {
1228
+ if (!isEventRegistrySnapshotMessage(message)) {
1229
+ closeSocket(socket, 1008, "invalid_registry");
1230
+ return;
1231
+ }
1232
+ session.eventRegistry.snapshot(message.events);
1233
+ return;
1234
+ }
1235
+ case "event_registry_delta": {
1236
+ if (!isEventRegistryDeltaMessage(message)) {
1237
+ closeSocket(socket, 1008, "invalid_registry");
1238
+ return;
1239
+ }
1240
+ if (message.operation === "upsert") {
1241
+ session.eventRegistry.upsert(message.event);
1242
+ } else {
1243
+ session.eventRegistry.remove(message.name);
1244
+ }
1245
+ return;
1246
+ }
1131
1247
  case "tool_result": {
1132
1248
  if (!isToolResultMessage(message)) {
1133
1249
  closeSocket(socket, 1008, "invalid_message");
@@ -1195,7 +1311,14 @@ var createSessionManager = (options) => {
1195
1311
  };
1196
1312
  const resolveForTools = (selector) => {
1197
1313
  const session = resolveSession(selector);
1198
- return { sessionId: session.sessionId, alias: session.alias, state: session.state, registry: session.registry };
1314
+ return {
1315
+ sessionId: session.sessionId,
1316
+ alias: session.alias,
1317
+ state: session.state,
1318
+ suspendReason: session.suspendReason,
1319
+ registry: session.registry,
1320
+ eventRegistry: session.eventRegistry
1321
+ };
1199
1322
  };
1200
1323
  const sendToApp = (sessionId, message) => {
1201
1324
  const session = sessions.get(sessionId);
@@ -1963,6 +2086,26 @@ var asSelectorParams = (params) => {
1963
2086
  }
1964
2087
  return { selector };
1965
2088
  };
2089
+ var asPageParams = (record) => {
2090
+ const limit = record.limit;
2091
+ if (limit !== void 0 && (typeof limit !== "number" || !Number.isInteger(limit) || limit <= 0)) {
2092
+ throw new RpcApplicationError("invalid_request", '"limit" must be a positive integer.');
2093
+ }
2094
+ const offset = record.offset;
2095
+ if (offset !== void 0 && (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0)) {
2096
+ throw new RpcApplicationError("invalid_request", '"offset" must be a non-negative integer.');
2097
+ }
2098
+ return { limit, offset };
2099
+ };
2100
+ var asEventsListParams = (params) => {
2101
+ const { selector } = asSelectorParams(params);
2102
+ const record = asRecordParams(params);
2103
+ const name = record.name;
2104
+ if (name !== void 0 && typeof name !== "string") {
2105
+ throw new RpcApplicationError("invalid_request", '"name" must be a string.');
2106
+ }
2107
+ return { selector, name, ...asPageParams(record) };
2108
+ };
1966
2109
  var asToolsListParams = (params) => {
1967
2110
  const { selector } = asSelectorParams(params);
1968
2111
  const record = asRecordParams(params);
@@ -1980,14 +2123,7 @@ var asToolsListParams = (params) => {
1980
2123
  `"filter" must be a string of at most ${MAX_TOOLS_FILTER_LENGTH} characters.`
1981
2124
  );
1982
2125
  }
1983
- const limit = record.limit;
1984
- if (limit !== void 0 && (typeof limit !== "number" || !Number.isInteger(limit) || limit <= 0)) {
1985
- throw new RpcApplicationError("invalid_request", '"limit" must be a positive integer.');
1986
- }
1987
- const offset = record.offset;
1988
- if (offset !== void 0 && (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0)) {
1989
- throw new RpcApplicationError("invalid_request", '"offset" must be a non-negative integer.');
1990
- }
2126
+ const { limit, offset } = asPageParams(record);
1991
2127
  return {
1992
2128
  selector,
1993
2129
  group,
@@ -2072,7 +2208,26 @@ var asEventsSubscribeParams = (params) => {
2072
2208
  }
2073
2209
  kinds = kindsRaw;
2074
2210
  }
2075
- return { sessionSelector, kinds };
2211
+ const name = record.name;
2212
+ if (name !== void 0 && typeof name !== "string") {
2213
+ throw new RpcApplicationError("invalid_request", '"name" must be a string.');
2214
+ }
2215
+ const payloadMaxBytes = asPayloadMaxBytes(record.payloadMaxBytes);
2216
+ return {
2217
+ sessionSelector,
2218
+ kinds,
2219
+ name,
2220
+ payloadMaxBytes
2221
+ };
2222
+ };
2223
+ var asPayloadMaxBytes = (value) => {
2224
+ if (value === void 0) {
2225
+ return void 0;
2226
+ }
2227
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
2228
+ throw new RpcApplicationError("invalid_request", '"payloadMaxBytes" must be a positive integer.');
2229
+ }
2230
+ return value;
2076
2231
  };
2077
2232
  var asEventsSinceParams = (params) => {
2078
2233
  const record = asRecordParams(params);
@@ -2088,10 +2243,17 @@ var asEventsSinceParams = (params) => {
2088
2243
  if (limit !== void 0 && (typeof limit !== "number" || !Number.isInteger(limit) || limit <= 0)) {
2089
2244
  throw new RpcApplicationError("invalid_request", '"limit" must be a positive integer.');
2090
2245
  }
2246
+ const name = record.name;
2247
+ if (name !== void 0 && typeof name !== "string") {
2248
+ throw new RpcApplicationError("invalid_request", '"name" must be a string.');
2249
+ }
2250
+ const payloadMaxBytes = asPayloadMaxBytes(record.payloadMaxBytes);
2091
2251
  return {
2092
2252
  selector,
2093
2253
  since,
2094
- limit
2254
+ limit,
2255
+ name,
2256
+ payloadMaxBytes
2095
2257
  };
2096
2258
  };
2097
2259
  var asLinkCreateParams = (params) => {
@@ -2238,10 +2400,17 @@ var startDaemon = async (options) => {
2238
2400
  if (subscription.kinds && !subscription.kinds.has(event.kind)) {
2239
2401
  continue;
2240
2402
  }
2403
+ const projected = projectAppEvent(event, {
2404
+ name: subscription.name,
2405
+ payloadMaxBytes: subscription.payloadMaxBytes
2406
+ });
2407
+ if (projected === void 0) {
2408
+ continue;
2409
+ }
2241
2410
  if (subscription.sessionSelector !== void 0 && event.sessionId !== subscription.sessionSelector && event.alias !== subscription.sessionSelector) {
2242
2411
  continue;
2243
2412
  }
2244
- server?.notify(connection, event);
2413
+ server?.notify(connection, projected);
2245
2414
  }
2246
2415
  });
2247
2416
  server = await startRpcServer({
@@ -2285,6 +2454,17 @@ var startDaemon = async (options) => {
2285
2454
  activeSessionManager.revoke(selector);
2286
2455
  return { ok: true };
2287
2456
  },
2457
+ [RPC_METHODS.eventsList]: (params) => {
2458
+ const { selector, name, limit, offset } = asEventsListParams(params);
2459
+ const registered = activeSessionManager.resolveForTools(selector).eventRegistry.list();
2460
+ const sorted = registered.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
2461
+ const matching = name === void 0 ? sorted : sorted.filter((event) => matchesNameGlob(name, event.name));
2462
+ const start = offset ?? 0;
2463
+ return {
2464
+ events: limit === void 0 ? matching.slice(start) : matching.slice(start, start + limit),
2465
+ total: matching.length
2466
+ };
2467
+ },
2288
2468
  [RPC_METHODS.toolsList]: (params) => {
2289
2469
  const { selector, group, filter, limit, offset } = asToolsListParams(params);
2290
2470
  const resolved = activeSessionManager.resolveForTools(selector);
@@ -2332,7 +2512,10 @@ var startDaemon = async (options) => {
2332
2512
  if (resolved.state !== "active") {
2333
2513
  const errorType = resolved.state === "suspended" ? "session_suspended" : "session_not_active";
2334
2514
  writeAudit("error", errorType);
2335
- throw new RpcApplicationError(errorType, `Session "${resolved.alias}" is not active.`);
2515
+ throw new RpcApplicationError(
2516
+ errorType,
2517
+ resolved.suspendReason === "app_backgrounded" ? appBackgroundedMessage(resolved.alias) : `Session "${resolved.alias}" is not active.`
2518
+ );
2336
2519
  }
2337
2520
  const tool = resolved.registry.get(name);
2338
2521
  if (!tool) {
@@ -2420,17 +2603,19 @@ var startDaemon = async (options) => {
2420
2603
  return { cancelled };
2421
2604
  },
2422
2605
  [RPC_METHODS.eventsSubscribe]: (params, context) => {
2423
- const { sessionSelector, kinds } = asEventsSubscribeParams(params);
2606
+ const { sessionSelector, kinds, name, payloadMaxBytes } = asEventsSubscribeParams(params);
2424
2607
  context.connection.state.eventSubscription = {
2425
2608
  sessionSelector,
2426
- kinds: kinds ? new Set(kinds) : void 0
2609
+ kinds: kinds ? new Set(kinds) : void 0,
2610
+ name,
2611
+ payloadMaxBytes
2427
2612
  };
2428
2613
  return { ok: true };
2429
2614
  },
2430
2615
  [RPC_METHODS.eventsSince]: (params) => {
2431
- const { selector, since, limit } = asEventsSinceParams(params);
2616
+ const { selector, since, limit, name, payloadMaxBytes } = asEventsSinceParams(params);
2432
2617
  const resolved = activeSessionManager.describe(selector);
2433
- return activeEventBus.since(resolved.sessionId, { since, limit });
2618
+ return activeEventBus.since(resolved.sessionId, { since, limit, name, payloadMaxBytes });
2434
2619
  }
2435
2620
  }
2436
2621
  });
@@ -2674,6 +2859,7 @@ var formatJson = (value, flags) => {
2674
2859
  import pc from "picocolors";
2675
2860
  import {
2676
2861
  formatAgentWebSocketUrl,
2862
+ renderEventSignature,
2677
2863
  renderToolSignature,
2678
2864
  summarizeToolDescription
2679
2865
  } from "@appduct/shared";
@@ -2769,6 +2955,13 @@ var formatDevice = (device) => {
2769
2955
  const label = parts.length > 0 ? parts.join(" ") : "unknown device";
2770
2956
  return device.os ? `${label} (${device.os})` : label;
2771
2957
  };
2958
+ var SUSPEND_REASON_LABELS = {
2959
+ app_backgrounded: "app in background",
2960
+ connection_lost: "connection lost"
2961
+ };
2962
+ var formatSessionState = (session) => {
2963
+ return session.suspendReason ? `${session.state} (${SUSPEND_REASON_LABELS[session.suspendReason]})` : session.state;
2964
+ };
2772
2965
  var renderLsData = (colors, data, now) => {
2773
2966
  if (data.length === 0) {
2774
2967
  return [colors.dim("Sessions"), " No Appduct sessions are registered."];
@@ -2777,7 +2970,7 @@ var renderLsData = (colors, data, now) => {
2777
2970
  const headers = ["Alias", "State", "Device", "Tools", "Age"];
2778
2971
  const rows = data.map((session) => [
2779
2972
  session.alias,
2780
- session.state,
2973
+ formatSessionState(session),
2781
2974
  formatDevice(session.device),
2782
2975
  String(session.toolCount),
2783
2976
  formatAge(session.claimedAt ?? session.createdAt, nowIso)
@@ -2791,9 +2984,6 @@ var renderLsData = (colors, data, now) => {
2791
2984
  ...rows.map((row) => ` ${row.map((cell, index) => cell.padEnd(widths[index])).join(" ")}`)
2792
2985
  ];
2793
2986
  };
2794
- var isToolsListing = (data) => {
2795
- return typeof data === "object" && data !== null && Array.isArray(data.tools);
2796
- };
2797
2987
  var isToolGroupsListing = (data) => {
2798
2988
  return typeof data === "object" && data !== null && Array.isArray(data.groups) && !Array.isArray(data.tools);
2799
2989
  };
@@ -2811,7 +3001,7 @@ var renderEmptyToolsLine = (data) => {
2811
3001
  }
2812
3002
  if (data.group !== void 0) {
2813
3003
  const match = data.filter === void 0 ? "" : ` match ${JSON.stringify(data.filter)}`;
2814
- return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools --groups\` to see the session's groups.`;
3004
+ return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools ls --groups\` to see the session's groups.`;
2815
3005
  }
2816
3006
  return data.filter === void 0 ? " No tools registered." : ` No tools match ${JSON.stringify(data.filter)}.`;
2817
3007
  };
@@ -2893,7 +3083,7 @@ var renderToolSummaryTable = (colors, data) => {
2893
3083
  ...grouped ? renderGroupedToolLines(colors, data.tools) : data.tools.flatMap((tool) => renderToolSummaryLines(tool, " ")),
2894
3084
  ...renderTruncationLine(data),
2895
3085
  "",
2896
- "Run `appduct tools <name>` for a tool's full schema."
3086
+ "Run `appduct tools describe <name>` for a tool's full schema."
2897
3087
  ];
2898
3088
  };
2899
3089
  var renderToolDetail = (colors, tool, flags) => {
@@ -2945,18 +3135,62 @@ var renderToolGroups = (colors, data) => {
2945
3135
  colors.green("Groups"),
2946
3136
  ...data.groups.map((entry) => ` ${label(entry).padEnd(width)} ${String(entry.total).padStart(countWidth)}`),
2947
3137
  "",
2948
- `${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.")
3138
+ `${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.")
2949
3139
  ];
2950
3140
  };
2951
- var renderToolsData = (colors, data, flags, full) => {
3141
+ var renderToolsListData = (colors, data, flags, full) => {
2952
3142
  if (isToolGroupsListing(data)) {
2953
3143
  return renderToolGroups(colors, data);
2954
3144
  }
2955
- if (!isToolsListing(data)) {
2956
- return renderToolDetail(colors, data, flags);
2957
- }
2958
3145
  return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
2959
3146
  };
3147
+ var renderEventsTruncationLine = (data) => {
3148
+ const offset = data.offset ?? 0;
3149
+ if (data.events.length >= data.total) {
3150
+ return [];
3151
+ }
3152
+ return [
3153
+ "",
3154
+ `Showing ${data.events.length} of ${data.total} events (offset ${offset}). Narrow with --name <glob> or page with --offset <n>.`
3155
+ ];
3156
+ };
3157
+ var renderEventsListData = (colors, data, flags) => {
3158
+ const [only] = data.events;
3159
+ if (data.name !== void 0 && !data.name.includes("*") && only !== void 0) {
3160
+ return renderFields(
3161
+ colors.green(`Event: ${only.name}`),
3162
+ [
3163
+ ["Signature", renderEventSignature(only)],
3164
+ ["Description", only.description],
3165
+ ["Payload schema", only.payload_schema]
3166
+ ],
3167
+ flags
3168
+ );
3169
+ }
3170
+ if (data.events.length === 0 && data.total > 0) {
3171
+ return [
3172
+ colors.green("Events"),
3173
+ ` No events at offset ${data.offset ?? 0}; ${data.total} matching event${data.total === 1 ? "" : "s"} in total.`
3174
+ ];
3175
+ }
3176
+ if (data.events.length === 0) {
3177
+ return [
3178
+ colors.green("Events"),
3179
+ data.name === void 0 ? " No events declared." : ` No events match ${JSON.stringify(data.name)}.`
3180
+ ];
3181
+ }
3182
+ const lines = data.events.flatMap((event) => [
3183
+ ` ${renderEventSignature(event)}`,
3184
+ ` ${summarizeToolDescription(event.description)}`
3185
+ ]);
3186
+ return [
3187
+ colors.green("Events"),
3188
+ ...lines,
3189
+ ...renderEventsTruncationLine(data),
3190
+ "",
3191
+ "Run `appduct events ls --name <name>` for an event's full payload schema."
3192
+ ];
3193
+ };
2960
3194
  var renderInvokeData = (colors, data, flags) => {
2961
3195
  return [colors.green("Result"), formatScalar(data, flags)];
2962
3196
  };
@@ -3140,15 +3374,19 @@ var renderSuccessData = (colors, command, data, options) => {
3140
3374
  return renderInitData(colors, data, flags);
3141
3375
  case "keygen":
3142
3376
  return renderKeygenData(colors, data, flags);
3143
- case "link":
3377
+ case "sessions link":
3144
3378
  return renderLinkData(colors, data, flags, options.qr);
3145
- case "ls":
3379
+ case "sessions ls":
3146
3380
  return renderLsData(colors, data, options.now ?? /* @__PURE__ */ new Date());
3147
- case "tools":
3148
- return renderToolsData(colors, data, flags, options.full);
3149
- case "invoke":
3381
+ case "tools ls":
3382
+ return renderToolsListData(colors, data, flags, options.full);
3383
+ case "tools describe":
3384
+ return renderToolDetail(colors, data, flags);
3385
+ case "events ls":
3386
+ return renderEventsListData(colors, data, flags);
3387
+ case "tools call":
3150
3388
  return renderInvokeData(colors, data, flags);
3151
- case "revoke":
3389
+ case "sessions revoke":
3152
3390
  return renderRevokeData(colors, data);
3153
3391
  case "daemon run":
3154
3392
  return renderDaemonRunData(colors, data, flags);