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
@@ -0,0 +1,6 @@
1
+ /** Route for `appduct events tail` — loaded by `routes/events/index.ts`'s router only when it
2
+ * runs (issue #96; replaces the removed `appduct events` streaming form). `--follow` is accepted
3
+ * (declared at the `events` noun level in `create-cli.ts`) but never read here: the default
4
+ * behavior already follows, so the flag exists only for script readability. */
5
+ import type { Route } from "../../router.js";
6
+ export declare const route: Route;
@@ -1015,12 +1015,14 @@ var handleEventsSinceCommand = async (options, context) => {
1015
1015
  try {
1016
1016
  const since = await stream.call(RPC_METHODS2.eventsSince, {
1017
1017
  selector: options.selector,
1018
- since: options.since
1018
+ since: options.since,
1019
+ name: options.name,
1020
+ payloadMaxBytes: options.payloadMaxBytes
1019
1021
  });
1020
1022
  for (const event of since.events) {
1021
1023
  context.onEvent(event);
1022
1024
  }
1023
- context.onCursor?.(since.cursor);
1025
+ context.onCursor?.({ cursor: since.cursor, dropped: since.dropped, remaining: since.remaining });
1024
1026
  } finally {
1025
1027
  stream.close();
1026
1028
  }
@@ -1035,7 +1037,9 @@ var handleEventsCommand = async (options, context) => {
1035
1037
  try {
1036
1038
  await stream.call(RPC_METHODS2.eventsSubscribe, {
1037
1039
  sessionSelector: options.selector,
1038
- kinds: ["app_event"]
1040
+ kinds: ["app_event"],
1041
+ name: options.name,
1042
+ payloadMaxBytes: options.payloadMaxBytes
1039
1043
  });
1040
1044
  } catch (error) {
1041
1045
  stream.close();
@@ -1071,128 +1075,11 @@ var handleEventsCommand = async (options, context) => {
1071
1075
  };
1072
1076
  };
1073
1077
 
1074
- // src/errors.ts
1075
- var EXIT_CODE_BY_ERROR_TYPE = {
1076
- usage_error: 64,
1077
- validation_error: 65,
1078
- // EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
1079
- // missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
1080
- // code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
1081
- // assertion failure (docs/tasks/08-appduct-doctor.md).
1082
- inspection_error: 66,
1083
- connection_error: 70,
1084
- session_error: 71,
1085
- tool_error: 72,
1086
- permission_error: 77,
1087
- internal_error: 1,
1088
- // `doctor --assert-present`/`--assert-absent`: the artifact was inspected successfully and the
1089
- // assertion did not hold. Distinct from `inspection_error` (66) — this means the answer is known
1090
- // and it's "no", not "couldn't tell" — and distinct from `internal_error` (1) so a caller can't
1091
- // conflate a normal assertion failure with an unexpected crash.
1092
- assertion_error: 3
1093
- };
1094
- var RPC_ERROR_EXIT_CLASS = {
1095
- no_session: "session_error",
1096
- ambiguous_session: "session_error",
1097
- unknown_session: "session_error",
1098
- session_not_active: "session_error",
1099
- session_suspended: "session_error",
1100
- tool_not_found: "tool_error",
1101
- tool_input_validation_error: "tool_error",
1102
- tool_output_validation_error: "tool_error",
1103
- tool_execution_error: "tool_error",
1104
- tool_serialization_error: "tool_error",
1105
- tool_timeout: "tool_error",
1106
- tool_cancelled: "tool_error",
1107
- policy_denied: "permission_error",
1108
- invalid_request: "validation_error"
1109
- };
1110
- var AppductCliError = class extends Error {
1111
- type;
1112
- details;
1113
- constructor(type, message, details) {
1114
- super(message);
1115
- this.name = "AppductCliError";
1116
- this.type = type;
1117
- this.details = details;
1118
- }
1119
- };
1120
- var usageError = (message, details) => {
1121
- return new AppductCliError("usage_error", message, details);
1122
- };
1123
- var isAppductCliError = (value) => {
1124
- return value instanceof AppductCliError;
1125
- };
1126
- var rpcErrorClass = (error) => {
1127
- const type = error.data?.type;
1128
- return type ? RPC_ERROR_EXIT_CLASS[type] : void 0;
1129
- };
1130
- var toCliError = (error) => {
1131
- if (isAppductCliError(error)) {
1132
- return {
1133
- type: error.type,
1134
- message: error.message,
1135
- details: error.details
1136
- };
1137
- }
1138
- if (error instanceof DaemonVersionMismatchError) {
1139
- return {
1140
- type: "connection_error",
1141
- message: error.message,
1142
- details: {
1143
- daemon_version: error.daemonVersion,
1144
- client_version: error.clientVersion,
1145
- session_count: error.sessionCount,
1146
- pending_link_count: error.pendingLinkCount
1147
- }
1148
- };
1149
- }
1150
- if (error instanceof DaemonRpcError && error.data?.type) {
1151
- return {
1152
- type: error.data.type,
1153
- message: error.message,
1154
- details: error.data.details
1155
- };
1156
- }
1157
- if (isDaemonUnreachableError(error)) {
1158
- return {
1159
- type: "connection_error",
1160
- message: error instanceof Error ? error.message : "The Appduct daemon is unreachable."
1161
- };
1162
- }
1163
- if (error instanceof Error) {
1164
- return {
1165
- type: "internal_error",
1166
- message: error.message
1167
- };
1168
- }
1169
- return {
1170
- type: "internal_error",
1171
- message: "An unexpected error occurred.",
1172
- details: error
1173
- };
1174
- };
1175
- var getExitCodeForError = (error) => {
1176
- if (isAppductCliError(error)) {
1177
- return EXIT_CODE_BY_ERROR_TYPE[error.type];
1178
- }
1179
- if (error instanceof DaemonVersionMismatchError) {
1180
- return EXIT_CODE_BY_ERROR_TYPE.connection_error;
1181
- }
1182
- if (error instanceof DaemonRpcError) {
1183
- const cliErrorClass = rpcErrorClass(error);
1184
- return EXIT_CODE_BY_ERROR_TYPE[cliErrorClass ?? "internal_error"];
1185
- }
1186
- if (isDaemonUnreachableError(error)) {
1187
- return EXIT_CODE_BY_ERROR_TYPE.connection_error;
1188
- }
1189
- return EXIT_CODE_BY_ERROR_TYPE.internal_error;
1190
- };
1191
-
1192
1078
  // src/output.ts
1193
1079
  import pc from "picocolors";
1194
1080
  import {
1195
1081
  formatAgentWebSocketUrl,
1082
+ renderEventSignature,
1196
1083
  renderToolSignature,
1197
1084
  summarizeToolDescription
1198
1085
  } from "@appduct/shared";
@@ -1293,6 +1180,13 @@ var formatDevice = (device) => {
1293
1180
  const label = parts.length > 0 ? parts.join(" ") : "unknown device";
1294
1181
  return device.os ? `${label} (${device.os})` : label;
1295
1182
  };
1183
+ var SUSPEND_REASON_LABELS = {
1184
+ app_backgrounded: "app in background",
1185
+ connection_lost: "connection lost"
1186
+ };
1187
+ var formatSessionState = (session) => {
1188
+ return session.suspendReason ? `${session.state} (${SUSPEND_REASON_LABELS[session.suspendReason]})` : session.state;
1189
+ };
1296
1190
  var renderLsData = (colors, data, now) => {
1297
1191
  if (data.length === 0) {
1298
1192
  return [colors.dim("Sessions"), " No Appduct sessions are registered."];
@@ -1301,7 +1195,7 @@ var renderLsData = (colors, data, now) => {
1301
1195
  const headers = ["Alias", "State", "Device", "Tools", "Age"];
1302
1196
  const rows = data.map((session) => [
1303
1197
  session.alias,
1304
- session.state,
1198
+ formatSessionState(session),
1305
1199
  formatDevice(session.device),
1306
1200
  String(session.toolCount),
1307
1201
  formatAge(session.claimedAt ?? session.createdAt, nowIso)
@@ -1315,9 +1209,6 @@ var renderLsData = (colors, data, now) => {
1315
1209
  ...rows.map((row) => ` ${row.map((cell, index) => cell.padEnd(widths[index])).join(" ")}`)
1316
1210
  ];
1317
1211
  };
1318
- var isToolsListing = (data) => {
1319
- return typeof data === "object" && data !== null && Array.isArray(data.tools);
1320
- };
1321
1212
  var isToolGroupsListing = (data) => {
1322
1213
  return typeof data === "object" && data !== null && Array.isArray(data.groups) && !Array.isArray(data.tools);
1323
1214
  };
@@ -1335,7 +1226,7 @@ var renderEmptyToolsLine = (data) => {
1335
1226
  }
1336
1227
  if (data.group !== void 0) {
1337
1228
  const match = data.filter === void 0 ? "" : ` match ${JSON.stringify(data.filter)}`;
1338
- return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools --groups\` to see the session's groups.`;
1229
+ return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools ls --groups\` to see the session's groups.`;
1339
1230
  }
1340
1231
  return data.filter === void 0 ? " No tools registered." : ` No tools match ${JSON.stringify(data.filter)}.`;
1341
1232
  };
@@ -1417,7 +1308,7 @@ var renderToolSummaryTable = (colors, data) => {
1417
1308
  ...grouped ? renderGroupedToolLines(colors, data.tools) : data.tools.flatMap((tool) => renderToolSummaryLines(tool, " ")),
1418
1309
  ...renderTruncationLine(data),
1419
1310
  "",
1420
- "Run `appduct tools <name>` for a tool's full schema."
1311
+ "Run `appduct tools describe <name>` for a tool's full schema."
1421
1312
  ];
1422
1313
  };
1423
1314
  var renderToolDetail = (colors, tool, flags) => {
@@ -1469,18 +1360,62 @@ var renderToolGroups = (colors, data) => {
1469
1360
  colors.green("Groups"),
1470
1361
  ...data.groups.map((entry) => ` ${label(entry).padEnd(width)} ${String(entry.total).padStart(countWidth)}`),
1471
1362
  "",
1472
- `${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.")
1363
+ `${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.")
1473
1364
  ];
1474
1365
  };
1475
- var renderToolsData = (colors, data, flags, full) => {
1366
+ var renderToolsListData = (colors, data, flags, full) => {
1476
1367
  if (isToolGroupsListing(data)) {
1477
1368
  return renderToolGroups(colors, data);
1478
1369
  }
1479
- if (!isToolsListing(data)) {
1480
- return renderToolDetail(colors, data, flags);
1481
- }
1482
1370
  return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
1483
1371
  };
1372
+ var renderEventsTruncationLine = (data) => {
1373
+ const offset = data.offset ?? 0;
1374
+ if (data.events.length >= data.total) {
1375
+ return [];
1376
+ }
1377
+ return [
1378
+ "",
1379
+ `Showing ${data.events.length} of ${data.total} events (offset ${offset}). Narrow with --name <glob> or page with --offset <n>.`
1380
+ ];
1381
+ };
1382
+ var renderEventsListData = (colors, data, flags) => {
1383
+ const [only] = data.events;
1384
+ if (data.name !== void 0 && !data.name.includes("*") && only !== void 0) {
1385
+ return renderFields(
1386
+ colors.green(`Event: ${only.name}`),
1387
+ [
1388
+ ["Signature", renderEventSignature(only)],
1389
+ ["Description", only.description],
1390
+ ["Payload schema", only.payload_schema]
1391
+ ],
1392
+ flags
1393
+ );
1394
+ }
1395
+ if (data.events.length === 0 && data.total > 0) {
1396
+ return [
1397
+ colors.green("Events"),
1398
+ ` No events at offset ${data.offset ?? 0}; ${data.total} matching event${data.total === 1 ? "" : "s"} in total.`
1399
+ ];
1400
+ }
1401
+ if (data.events.length === 0) {
1402
+ return [
1403
+ colors.green("Events"),
1404
+ data.name === void 0 ? " No events declared." : ` No events match ${JSON.stringify(data.name)}.`
1405
+ ];
1406
+ }
1407
+ const lines = data.events.flatMap((event) => [
1408
+ ` ${renderEventSignature(event)}`,
1409
+ ` ${summarizeToolDescription(event.description)}`
1410
+ ]);
1411
+ return [
1412
+ colors.green("Events"),
1413
+ ...lines,
1414
+ ...renderEventsTruncationLine(data),
1415
+ "",
1416
+ "Run `appduct events ls --name <name>` for an event's full payload schema."
1417
+ ];
1418
+ };
1484
1419
  var renderInvokeData = (colors, data, flags) => {
1485
1420
  return [colors.green("Result"), formatScalar(data, flags)];
1486
1421
  };
@@ -1664,15 +1599,19 @@ var renderSuccessData = (colors, command, data, options) => {
1664
1599
  return renderInitData(colors, data, flags);
1665
1600
  case "keygen":
1666
1601
  return renderKeygenData(colors, data, flags);
1667
- case "link":
1602
+ case "sessions link":
1668
1603
  return renderLinkData(colors, data, flags, options.qr);
1669
- case "ls":
1604
+ case "sessions ls":
1670
1605
  return renderLsData(colors, data, options.now ?? /* @__PURE__ */ new Date());
1671
- case "tools":
1672
- return renderToolsData(colors, data, flags, options.full);
1673
- case "invoke":
1606
+ case "tools ls":
1607
+ return renderToolsListData(colors, data, flags, options.full);
1608
+ case "tools describe":
1609
+ return renderToolDetail(colors, data, flags);
1610
+ case "events ls":
1611
+ return renderEventsListData(colors, data, flags);
1612
+ case "tools call":
1674
1613
  return renderInvokeData(colors, data, flags);
1675
- case "revoke":
1614
+ case "sessions revoke":
1676
1615
  return renderRevokeData(colors, data);
1677
1616
  case "daemon run":
1678
1617
  return renderDaemonRunData(colors, data, flags);
@@ -1742,25 +1681,158 @@ var renderEventLine = (event, flags) => {
1742
1681
  const dataSuffix = event.data === void 0 ? "" : ` ${formatJson(event.data, flags)}`;
1743
1682
  return `${colors.dim(timestamp)} ${colors.green(event.kind)}${target ? ` ${target}` : ""}${dataSuffix}`;
1744
1683
  };
1745
- var renderEventsCursorLine = (cursor, flags) => {
1746
- if (flags.json) {
1747
- return JSON.stringify({ cursor });
1684
+
1685
+ // src/errors.ts
1686
+ var EXIT_CODE_BY_ERROR_TYPE = {
1687
+ usage_error: 64,
1688
+ validation_error: 65,
1689
+ // EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
1690
+ // missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
1691
+ // code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
1692
+ // assertion failure (docs/tasks/08-appduct-doctor.md).
1693
+ inspection_error: 66,
1694
+ connection_error: 70,
1695
+ session_error: 71,
1696
+ tool_error: 72,
1697
+ permission_error: 77,
1698
+ internal_error: 1,
1699
+ // `doctor --assert-present`/`--assert-absent`: the artifact was inspected successfully and the
1700
+ // assertion did not hold. Distinct from `inspection_error` (66) — this means the answer is known
1701
+ // and it's "no", not "couldn't tell" — and distinct from `internal_error` (1) so a caller can't
1702
+ // conflate a normal assertion failure with an unexpected crash.
1703
+ assertion_error: 3
1704
+ };
1705
+ var RPC_ERROR_EXIT_CLASS = {
1706
+ no_session: "session_error",
1707
+ ambiguous_session: "session_error",
1708
+ unknown_session: "session_error",
1709
+ session_not_active: "session_error",
1710
+ session_suspended: "session_error",
1711
+ tool_not_found: "tool_error",
1712
+ tool_input_validation_error: "tool_error",
1713
+ tool_output_validation_error: "tool_error",
1714
+ tool_execution_error: "tool_error",
1715
+ tool_serialization_error: "tool_error",
1716
+ tool_timeout: "tool_error",
1717
+ tool_cancelled: "tool_error",
1718
+ policy_denied: "permission_error",
1719
+ invalid_request: "validation_error"
1720
+ };
1721
+ var AppductCliError = class extends Error {
1722
+ type;
1723
+ details;
1724
+ constructor(type, message, details) {
1725
+ super(message);
1726
+ this.name = "AppductCliError";
1727
+ this.type = type;
1728
+ this.details = details;
1748
1729
  }
1749
- const colors = pc.createColors(flags.color);
1750
- return colors.dim(`cursor: ${cursor} (pass --since ${cursor} to resume from here)`);
1730
+ };
1731
+ var usageError = (message, details) => {
1732
+ return new AppductCliError("usage_error", message, details);
1733
+ };
1734
+ var isAppductCliError = (value) => {
1735
+ return value instanceof AppductCliError;
1736
+ };
1737
+ var rpcErrorClass = (error) => {
1738
+ const type = error.data?.type;
1739
+ return type ? RPC_ERROR_EXIT_CLASS[type] : void 0;
1740
+ };
1741
+ var toCliError = (error) => {
1742
+ if (isAppductCliError(error)) {
1743
+ return {
1744
+ type: error.type,
1745
+ message: error.message,
1746
+ details: error.details
1747
+ };
1748
+ }
1749
+ if (error instanceof DaemonVersionMismatchError) {
1750
+ return {
1751
+ type: "connection_error",
1752
+ message: error.message,
1753
+ details: {
1754
+ daemon_version: error.daemonVersion,
1755
+ client_version: error.clientVersion,
1756
+ session_count: error.sessionCount,
1757
+ pending_link_count: error.pendingLinkCount
1758
+ }
1759
+ };
1760
+ }
1761
+ if (error instanceof DaemonRpcError && error.data?.type) {
1762
+ return {
1763
+ type: error.data.type,
1764
+ message: error.message,
1765
+ details: error.data.details
1766
+ };
1767
+ }
1768
+ if (isDaemonUnreachableError(error)) {
1769
+ return {
1770
+ type: "connection_error",
1771
+ message: error instanceof Error ? error.message : "The Appduct daemon is unreachable."
1772
+ };
1773
+ }
1774
+ if (error instanceof Error) {
1775
+ return {
1776
+ type: "internal_error",
1777
+ message: error.message
1778
+ };
1779
+ }
1780
+ return {
1781
+ type: "internal_error",
1782
+ message: "An unexpected error occurred.",
1783
+ details: error
1784
+ };
1785
+ };
1786
+ var getExitCodeForError = (error) => {
1787
+ if (isAppductCliError(error)) {
1788
+ return EXIT_CODE_BY_ERROR_TYPE[error.type];
1789
+ }
1790
+ if (error instanceof DaemonVersionMismatchError) {
1791
+ return EXIT_CODE_BY_ERROR_TYPE.connection_error;
1792
+ }
1793
+ if (error instanceof DaemonRpcError) {
1794
+ const cliErrorClass = rpcErrorClass(error);
1795
+ return EXIT_CODE_BY_ERROR_TYPE[cliErrorClass ?? "internal_error"];
1796
+ }
1797
+ if (isDaemonUnreachableError(error)) {
1798
+ return EXIT_CODE_BY_ERROR_TYPE.connection_error;
1799
+ }
1800
+ return EXIT_CODE_BY_ERROR_TYPE.internal_error;
1751
1801
  };
1752
1802
 
1753
1803
  // src/cli/command-options.ts
1754
- var parseNonNegativeIntegerOption = (value, flagName) => {
1804
+ var parsePositiveIntegerOption = (value, flagName) => {
1755
1805
  if (value === void 0) {
1756
1806
  return void 0;
1757
1807
  }
1758
1808
  const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
1759
- if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 0) {
1760
- throw usageError(`"${flagName}" must be a non-negative integer.`);
1809
+ if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) {
1810
+ throw usageError(`"${flagName}" must be a positive integer.`);
1761
1811
  }
1762
1812
  return parsed;
1763
1813
  };
1814
+ var readTextOption = (argv, value, flagName) => {
1815
+ if (value === void 0) {
1816
+ return void 0;
1817
+ }
1818
+ if (typeof value === "boolean") {
1819
+ throw usageError(`"${flagName}" requires a value.`);
1820
+ }
1821
+ let raw;
1822
+ for (let index = 0; index < argv.length; index += 1) {
1823
+ const token = argv[index];
1824
+ if (token === "--") {
1825
+ break;
1826
+ }
1827
+ if (token === flagName && index + 1 < argv.length) {
1828
+ raw = argv[index + 1];
1829
+ index += 1;
1830
+ } else if (token.startsWith(`${flagName}=`)) {
1831
+ raw = token.slice(flagName.length + 1);
1832
+ }
1833
+ }
1834
+ return raw ?? String(Array.isArray(value) ? value.at(-1) : value);
1835
+ };
1764
1836
  var splitOptionalSelector = (args, commandUsage) => {
1765
1837
  if (args.length > 1) {
1766
1838
  throw usageError(`Usage: ${commandUsage} (too many arguments).`);
@@ -1874,33 +1946,33 @@ var guarded = (context) => {
1874
1946
  };
1875
1947
  };
1876
1948
 
1877
- // src/cli/routes/events.ts
1949
+ // src/cli/routes/events/shared-options.ts
1950
+ var parseEventsFilterFlags = (context) => {
1951
+ const { options } = context;
1952
+ return {
1953
+ name: readTextOption(context.argv, options.name, "--name"),
1954
+ payloadMaxBytes: parsePositiveIntegerOption(options.payloadMaxBytes, "--payload-max-bytes")
1955
+ };
1956
+ };
1957
+
1958
+ // src/cli/routes/events/tail.ts
1878
1959
  var route = async (context) => {
1879
- const { options, stateDir, env } = context;
1880
- const follow = Boolean(options.follow);
1960
+ const { stateDir, env } = context;
1881
1961
  return executeHostedCommand(
1882
1962
  commandName(context),
1883
- // All argument parsing happens inside the handler (not the route body) so
1884
- // `executeHostedCommand`'s own try/catch renders a bad argument as a normal usage_error
1885
- // instead of an uncaught rejection, and before the version check so a typo never waits on
1886
- // the daemon.
1963
+ // Argument parsing runs inside the handler (not the route body) so `executeHostedCommand`'s
1964
+ // own try/catch renders a bad argument as a normal usage_error instead of an uncaught
1965
+ // rejection, and before the version check so a typo never waits on the daemon.
1887
1966
  () => {
1888
- const { selector } = splitOptionalSelector(context.args, "events [selector]");
1889
- const since = parseNonNegativeIntegerOption(options.since, "--since");
1890
- if (since !== void 0 && follow) {
1891
- throw usageError('"--since" is a one-shot pull and cannot be combined with "--follow".');
1892
- }
1967
+ const { selector } = splitOptionalSelector(context.args, "events tail [selector]");
1968
+ const { name, payloadMaxBytes } = parseEventsFilterFlags(context);
1893
1969
  return guarded(context)(
1894
1970
  () => handleEventsCommand(
1895
- { selector, since },
1971
+ { selector, name, payloadMaxBytes },
1896
1972
  {
1897
1973
  stateDir,
1898
1974
  onEvent: (event) => {
1899
1975
  env.stdout.write(`${renderEventLine(event, env.flags)}
1900
- `);
1901
- },
1902
- onCursor: (cursor) => {
1903
- env.stdout.write(`${renderEventsCursorLine(cursor, env.flags)}
1904
1976
  `);
1905
1977
  }
1906
1978
  }
@@ -841,10 +841,10 @@ Run \`appduct init --scheme <scheme>\` with the scheme your app registers for de
841
841
  nextSteps: [
842
842
  'Add `import "@appduct/react-native/auto";` to your app entry (index.js / App.tsx) \u2014 it is what starts the in-app agent endpoint.',
843
843
  `Add the Appduct MCP server entry to your agent's MCP config. "--scheme ${scheme}" keeps that entry self-contained; ${SCHEME_ENV_VAR} and this ${PROJECT_CONFIG_RELATIVE_PATH} work too.`,
844
- "With the app running, pair a device: `appduct link --open ios-sim` (or `--open android`).",
844
+ "With the app running, pair a device: `appduct sessions link --open ios-sim` (or `--open android`).",
845
845
  ...origin === void 0 ? [] : [`Scheme "${scheme}" was read from ${origin}.`],
846
846
  ...iosAppId === void 0 && androidAppId === void 0 ? [
847
- "Delivering to a physical iPhone (`--open ios-device`) or an Android device/emulator (`--open android`) needs the installed app's id: run `appduct init --ios-app-id <id> --android-app-id <id>`, or pass --app-id on `appduct link`."
847
+ "Delivering to a physical iPhone (`--open ios-device`) or an Android device/emulator (`--open android`) needs the installed app's id: run `appduct init --ios-app-id <id> --android-app-id <id>`, or pass --app-id on `appduct sessions link`."
848
848
  ] : [],
849
849
  `This file is safe to commit \u2014 it holds only "scheme" and "appId". Do not point --state-dir at this directory: the state dir holds the daemon's private key and audit log, which must never be committed.`
850
850
  ]
@@ -861,6 +861,7 @@ var formatJson = (value, flags) => {
861
861
  import pc from "picocolors";
862
862
  import {
863
863
  formatAgentWebSocketUrl,
864
+ renderEventSignature,
864
865
  renderToolSignature,
865
866
  summarizeToolDescription
866
867
  } from "@appduct/shared";
@@ -956,6 +957,13 @@ var formatDevice = (device) => {
956
957
  const label = parts.length > 0 ? parts.join(" ") : "unknown device";
957
958
  return device.os ? `${label} (${device.os})` : label;
958
959
  };
960
+ var SUSPEND_REASON_LABELS = {
961
+ app_backgrounded: "app in background",
962
+ connection_lost: "connection lost"
963
+ };
964
+ var formatSessionState = (session) => {
965
+ return session.suspendReason ? `${session.state} (${SUSPEND_REASON_LABELS[session.suspendReason]})` : session.state;
966
+ };
959
967
  var renderLsData = (colors, data, now) => {
960
968
  if (data.length === 0) {
961
969
  return [colors.dim("Sessions"), " No Appduct sessions are registered."];
@@ -964,7 +972,7 @@ var renderLsData = (colors, data, now) => {
964
972
  const headers = ["Alias", "State", "Device", "Tools", "Age"];
965
973
  const rows = data.map((session) => [
966
974
  session.alias,
967
- session.state,
975
+ formatSessionState(session),
968
976
  formatDevice(session.device),
969
977
  String(session.toolCount),
970
978
  formatAge(session.claimedAt ?? session.createdAt, nowIso)
@@ -978,9 +986,6 @@ var renderLsData = (colors, data, now) => {
978
986
  ...rows.map((row) => ` ${row.map((cell, index) => cell.padEnd(widths[index])).join(" ")}`)
979
987
  ];
980
988
  };
981
- var isToolsListing = (data) => {
982
- return typeof data === "object" && data !== null && Array.isArray(data.tools);
983
- };
984
989
  var isToolGroupsListing = (data) => {
985
990
  return typeof data === "object" && data !== null && Array.isArray(data.groups) && !Array.isArray(data.tools);
986
991
  };
@@ -998,7 +1003,7 @@ var renderEmptyToolsLine = (data) => {
998
1003
  }
999
1004
  if (data.group !== void 0) {
1000
1005
  const match = data.filter === void 0 ? "" : ` match ${JSON.stringify(data.filter)}`;
1001
- return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools --groups\` to see the session's groups.`;
1006
+ return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools ls --groups\` to see the session's groups.`;
1002
1007
  }
1003
1008
  return data.filter === void 0 ? " No tools registered." : ` No tools match ${JSON.stringify(data.filter)}.`;
1004
1009
  };
@@ -1080,7 +1085,7 @@ var renderToolSummaryTable = (colors, data) => {
1080
1085
  ...grouped ? renderGroupedToolLines(colors, data.tools) : data.tools.flatMap((tool) => renderToolSummaryLines(tool, " ")),
1081
1086
  ...renderTruncationLine(data),
1082
1087
  "",
1083
- "Run `appduct tools <name>` for a tool's full schema."
1088
+ "Run `appduct tools describe <name>` for a tool's full schema."
1084
1089
  ];
1085
1090
  };
1086
1091
  var renderToolDetail = (colors, tool, flags) => {
@@ -1132,18 +1137,62 @@ var renderToolGroups = (colors, data) => {
1132
1137
  colors.green("Groups"),
1133
1138
  ...data.groups.map((entry) => ` ${label(entry).padEnd(width)} ${String(entry.total).padStart(countWidth)}`),
1134
1139
  "",
1135
- `${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.")
1140
+ `${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.")
1136
1141
  ];
1137
1142
  };
1138
- var renderToolsData = (colors, data, flags, full) => {
1143
+ var renderToolsListData = (colors, data, flags, full) => {
1139
1144
  if (isToolGroupsListing(data)) {
1140
1145
  return renderToolGroups(colors, data);
1141
1146
  }
1142
- if (!isToolsListing(data)) {
1143
- return renderToolDetail(colors, data, flags);
1144
- }
1145
1147
  return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
1146
1148
  };
1149
+ var renderEventsTruncationLine = (data) => {
1150
+ const offset = data.offset ?? 0;
1151
+ if (data.events.length >= data.total) {
1152
+ return [];
1153
+ }
1154
+ return [
1155
+ "",
1156
+ `Showing ${data.events.length} of ${data.total} events (offset ${offset}). Narrow with --name <glob> or page with --offset <n>.`
1157
+ ];
1158
+ };
1159
+ var renderEventsListData = (colors, data, flags) => {
1160
+ const [only] = data.events;
1161
+ if (data.name !== void 0 && !data.name.includes("*") && only !== void 0) {
1162
+ return renderFields(
1163
+ colors.green(`Event: ${only.name}`),
1164
+ [
1165
+ ["Signature", renderEventSignature(only)],
1166
+ ["Description", only.description],
1167
+ ["Payload schema", only.payload_schema]
1168
+ ],
1169
+ flags
1170
+ );
1171
+ }
1172
+ if (data.events.length === 0 && data.total > 0) {
1173
+ return [
1174
+ colors.green("Events"),
1175
+ ` No events at offset ${data.offset ?? 0}; ${data.total} matching event${data.total === 1 ? "" : "s"} in total.`
1176
+ ];
1177
+ }
1178
+ if (data.events.length === 0) {
1179
+ return [
1180
+ colors.green("Events"),
1181
+ data.name === void 0 ? " No events declared." : ` No events match ${JSON.stringify(data.name)}.`
1182
+ ];
1183
+ }
1184
+ const lines = data.events.flatMap((event) => [
1185
+ ` ${renderEventSignature(event)}`,
1186
+ ` ${summarizeToolDescription(event.description)}`
1187
+ ]);
1188
+ return [
1189
+ colors.green("Events"),
1190
+ ...lines,
1191
+ ...renderEventsTruncationLine(data),
1192
+ "",
1193
+ "Run `appduct events ls --name <name>` for an event's full payload schema."
1194
+ ];
1195
+ };
1147
1196
  var renderInvokeData = (colors, data, flags) => {
1148
1197
  return [colors.green("Result"), formatScalar(data, flags)];
1149
1198
  };
@@ -1327,15 +1376,19 @@ var renderSuccessData = (colors, command, data, options) => {
1327
1376
  return renderInitData(colors, data, flags);
1328
1377
  case "keygen":
1329
1378
  return renderKeygenData(colors, data, flags);
1330
- case "link":
1379
+ case "sessions link":
1331
1380
  return renderLinkData(colors, data, flags, options.qr);
1332
- case "ls":
1381
+ case "sessions ls":
1333
1382
  return renderLsData(colors, data, options.now ?? /* @__PURE__ */ new Date());
1334
- case "tools":
1335
- return renderToolsData(colors, data, flags, options.full);
1336
- case "invoke":
1383
+ case "tools ls":
1384
+ return renderToolsListData(colors, data, flags, options.full);
1385
+ case "tools describe":
1386
+ return renderToolDetail(colors, data, flags);
1387
+ case "events ls":
1388
+ return renderEventsListData(colors, data, flags);
1389
+ case "tools call":
1337
1390
  return renderInvokeData(colors, data, flags);
1338
- case "revoke":
1391
+ case "sessions revoke":
1339
1392
  return renderRevokeData(colors, data);
1340
1393
  case "daemon run":
1341
1394
  return renderDaemonRunData(colors, data, flags);