appduct 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +43 -38
  2. package/dist/bin.js +50 -36
  3. package/dist/cli/command-options.d.ts +7 -17
  4. package/dist/cli/result-types.d.ts +11 -8
  5. package/dist/cli/routes/daemon/index.js +12 -16
  6. package/dist/cli/routes/daemon/run.js +216 -46
  7. package/dist/cli/routes/daemon/start.js +18 -16
  8. package/dist/cli/routes/daemon/status.js +18 -16
  9. package/dist/cli/routes/daemon/stop.js +18 -16
  10. package/dist/cli/routes/doctor.js +12 -16
  11. package/dist/cli/routes/events/index.d.ts +9 -0
  12. package/dist/cli/routes/events/index.js +842 -0
  13. package/dist/cli/routes/events/shared-options.d.ts +12 -0
  14. package/dist/cli/routes/events/shared-options.js +102 -0
  15. package/dist/cli/routes/events/since.d.ts +6 -0
  16. package/dist/cli/routes/events/since.js +1977 -0
  17. package/dist/cli/routes/events/tail.d.ts +6 -0
  18. package/dist/cli/routes/{events.js → events/tail.js} +184 -162
  19. package/dist/cli/routes/init.js +14 -18
  20. package/dist/cli/routes/keygen.js +13 -16
  21. package/dist/cli/routes/mcp.js +193 -175
  22. package/dist/cli/routes/sessions/index.d.ts +7 -0
  23. package/dist/cli/routes/sessions/index.js +843 -0
  24. package/dist/cli/routes/sessions/link.d.ts +4 -0
  25. package/dist/cli/routes/{link.js → sessions/link.js} +19 -17
  26. package/dist/cli/routes/sessions/ls.d.ts +4 -0
  27. package/dist/cli/routes/{ls.js → sessions/ls.js} +19 -17
  28. package/dist/cli/routes/sessions/revoke.d.ts +4 -0
  29. package/dist/cli/routes/{revoke.js → sessions/revoke.js} +20 -18
  30. package/dist/cli/routes/tools/call.d.ts +4 -0
  31. package/dist/cli/routes/{invoke.js → tools/call.js} +21 -19
  32. package/dist/cli/routes/tools/describe.d.ts +6 -0
  33. package/dist/cli/routes/tools/describe.js +1837 -0
  34. package/dist/cli/routes/tools/index.d.ts +6 -0
  35. package/dist/cli/routes/tools/index.js +843 -0
  36. package/dist/cli/routes/tools/ls.d.ts +4 -0
  37. package/dist/cli/routes/{tools.js → tools/ls.js} +46 -73
  38. package/dist/cli/routes/tools/shared-options.d.ts +16 -0
  39. package/dist/cli/routes/tools/shared-options.js +122 -0
  40. package/dist/client/app-client.d.ts +96 -37
  41. package/dist/client/index.d.ts +3 -2
  42. package/dist/client/index.js +183 -139
  43. package/dist/commands/events.d.ts +31 -13
  44. package/dist/commands/invoke.d.ts +1 -1
  45. package/dist/commands/link.d.ts +1 -1
  46. package/dist/commands/ls.d.ts +1 -1
  47. package/dist/commands/revoke.d.ts +1 -1
  48. package/dist/commands/tools.d.ts +12 -13
  49. package/dist/daemon/config.d.ts +5 -2
  50. package/dist/daemon/daemon.d.ts +3 -0
  51. package/dist/daemon/event-bus.d.ts +36 -4
  52. package/dist/daemon/events-log.d.ts +31 -0
  53. package/dist/daemon/memory-append-only-file.d.ts +17 -0
  54. package/dist/daemon/node-append-only-file.d.ts +9 -0
  55. package/dist/daemon/rpc-errors.d.ts +1 -1
  56. package/dist/daemon/state-dir.d.ts +2 -0
  57. package/dist/errors.d.ts +1 -1
  58. package/dist/events/index.d.ts +60 -0
  59. package/dist/index.js +62 -39
  60. package/dist/link.d.ts +1 -1
  61. package/dist/mcp/app-tools.d.ts +3 -2
  62. package/dist/mcp/connect-tool.d.ts +1 -1
  63. package/dist/mcp/events-tool.d.ts +28 -34
  64. package/dist/native-scheme.d.ts +1 -1
  65. package/dist/output.d.ts +29 -6
  66. package/dist/rpc/client.d.ts +1 -1
  67. package/dist/scheme.d.ts +4 -4
  68. package/package.json +2 -2
  69. package/dist/cli/routes/events.d.ts +0 -3
  70. package/dist/cli/routes/invoke.d.ts +0 -3
  71. package/dist/cli/routes/link.d.ts +0 -3
  72. package/dist/cli/routes/ls.d.ts +0 -3
  73. package/dist/cli/routes/revoke.d.ts +0 -3
  74. package/dist/cli/routes/tools.d.ts +0 -3
@@ -1,5 +1,5 @@
1
1
  // src/client/connect.ts
2
- import { RPC_METHODS as RPC_METHODS3 } from "@appduct/shared";
2
+ import { RPC_METHODS as RPC_METHODS4 } from "@appduct/shared";
3
3
 
4
4
  // src/daemon/state-dir.ts
5
5
  import { homedir } from "node:os";
@@ -14,6 +14,7 @@ var getStateDirPaths = (stateDir) => {
14
14
  socketPath: join(stateDir, "daemon.sock"),
15
15
  pidFilePath: join(stateDir, "daemon.pid"),
16
16
  logFilePath: join(stateDir, "daemon.log"),
17
+ eventsLogPath: join(stateDir, "events.log"),
17
18
  keyPath: join(stateDir, "key.pem"),
18
19
  configPath: join(stateDir, "config.json"),
19
20
  auditDir: join(stateDir, "audit"),
@@ -49,6 +50,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
49
50
  "eventBufferSize",
50
51
  "auditRetentionDays",
51
52
  "daemonLogMaxBytes",
53
+ "eventsLogMaxBytes",
52
54
  "policy",
53
55
  "advertisedIp",
54
56
  "scheme",
@@ -108,6 +110,7 @@ var defaultConfig = (paths) => {
108
110
  eventBufferSize: 256,
109
111
  auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
110
112
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
113
+ eventsLogMaxBytes: 10 * 1024 * 1024,
111
114
  restartDaemonOnVersionMismatch: false,
112
115
  policy: {
113
116
  default: "allow",
@@ -170,6 +173,9 @@ var loadConfig = async (paths, options = {}) => {
170
173
  if (parsed.daemonLogMaxBytes !== void 0) {
171
174
  config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
172
175
  }
176
+ if (parsed.eventsLogMaxBytes !== void 0) {
177
+ config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
178
+ }
173
179
  if (parsed.restartDaemonOnVersionMismatch !== void 0) {
174
180
  config.restartDaemonOnVersionMismatch = requireBoolean(
175
181
  parsed.restartDaemonOnVersionMismatch,
@@ -1023,8 +1029,108 @@ var openDaemonStream = async (options) => {
1023
1029
  import {
1024
1030
  clampToolTimeoutMs,
1025
1031
  MAX_TOOL_TIMEOUT_MS,
1026
- RPC_METHODS as RPC_METHODS2
1032
+ RPC_METHODS as RPC_METHODS3,
1033
+ toAppEvent as toAppEvent2
1034
+ } from "@appduct/shared";
1035
+
1036
+ // src/events/index.ts
1037
+ import {
1038
+ RPC_METHODS as RPC_METHODS2,
1039
+ toAppEvent
1027
1040
  } from "@appduct/shared";
1041
+ var WaitForAppEventTimeoutError = class extends Error {
1042
+ constructor(eventName, timeoutMs) {
1043
+ super(`Timed out after ${timeoutMs}ms waiting for event "${eventName}".`);
1044
+ this.eventName = eventName;
1045
+ this.timeoutMs = timeoutMs;
1046
+ this.name = "WaitForAppEventTimeoutError";
1047
+ }
1048
+ };
1049
+ var WaitForAppEventConnectionClosedError = class extends Error {
1050
+ constructor(eventName) {
1051
+ super(`The connection to the Appduct daemon closed while waiting for event "${eventName}".`);
1052
+ this.eventName = eventName;
1053
+ this.name = "WaitForAppEventConnectionClosedError";
1054
+ }
1055
+ };
1056
+ var waitForAppEvent = async (stream, options) => {
1057
+ const { sessionId, name, since, timeoutMs, payloadMaxBytes } = options;
1058
+ let liveHandler = null;
1059
+ const earlyEvents = [];
1060
+ const unsubscribeNotification = stream.onNotification((payload) => {
1061
+ const event = payload;
1062
+ if (liveHandler) {
1063
+ liveHandler(event);
1064
+ } else {
1065
+ earlyEvents.push(event);
1066
+ }
1067
+ });
1068
+ let unsubscribeClose = () => {
1069
+ };
1070
+ try {
1071
+ await stream.call(RPC_METHODS2.eventsSubscribe, {
1072
+ sessionSelector: sessionId,
1073
+ kinds: ["app_event"],
1074
+ name,
1075
+ payloadMaxBytes
1076
+ });
1077
+ const sinceResult = await stream.call(RPC_METHODS2.eventsSince, {
1078
+ selector: sessionId,
1079
+ since,
1080
+ name,
1081
+ payloadMaxBytes
1082
+ });
1083
+ const seenSeqs = /* @__PURE__ */ new Set();
1084
+ const backlog = [];
1085
+ for (const event of [...sinceResult.events, ...earlyEvents]) {
1086
+ if (event.sessionId === sessionId && !seenSeqs.has(event.seq)) {
1087
+ seenSeqs.add(event.seq);
1088
+ backlog.push(event);
1089
+ }
1090
+ }
1091
+ backlog.sort((a, b) => a.seq - b.seq);
1092
+ for (const event of backlog) {
1093
+ const appEvent = toAppEvent(event, sessionId);
1094
+ if (appEvent) {
1095
+ return { event: appEvent, dropped: sinceResult.dropped };
1096
+ }
1097
+ }
1098
+ const highestConsideredSeq = backlog.length > 0 ? backlog[backlog.length - 1].seq : sinceResult.cursor;
1099
+ return await new Promise((resolve2, reject) => {
1100
+ let settled = false;
1101
+ const settle = (fn) => {
1102
+ if (settled) {
1103
+ return;
1104
+ }
1105
+ settled = true;
1106
+ clearTimeout(timer);
1107
+ liveHandler = null;
1108
+ fn();
1109
+ };
1110
+ const timer = setTimeout(() => {
1111
+ settle(() => reject(new WaitForAppEventTimeoutError(name, timeoutMs)));
1112
+ }, timeoutMs);
1113
+ unsubscribeClose = stream.onClose(() => {
1114
+ settle(() => reject(new WaitForAppEventConnectionClosedError(name)));
1115
+ });
1116
+ liveHandler = (event) => {
1117
+ if (settled) {
1118
+ return;
1119
+ }
1120
+ if (event.sessionId === sessionId && event.seq <= highestConsideredSeq) {
1121
+ return;
1122
+ }
1123
+ const appEvent = toAppEvent(event, sessionId);
1124
+ if (appEvent) {
1125
+ settle(() => resolve2({ event: appEvent, dropped: sinceResult.dropped }));
1126
+ }
1127
+ };
1128
+ });
1129
+ } finally {
1130
+ unsubscribeNotification();
1131
+ unsubscribeClose();
1132
+ }
1133
+ };
1028
1134
 
1029
1135
  // src/errors.ts
1030
1136
  var AppductCliError = class extends Error {
@@ -1097,12 +1203,14 @@ var transportTimeoutForToolCall = (timeoutMs) => {
1097
1203
  const clamped = timeoutMs === void 0 || !Number.isFinite(timeoutMs) ? MAX_TOOL_TIMEOUT_MS : clampToolTimeoutMs(timeoutMs);
1098
1204
  return clamped + CALL_TRANSPORT_TIMEOUT_SLACK_MS;
1099
1205
  };
1100
- var makeAppClient = (stream, sessionId) => {
1206
+ var makeAppClient = (stream, sessionId, openStream) => {
1207
+ let closed = false;
1208
+ const openWaitStreams = /* @__PURE__ */ new Set();
1101
1209
  const client = {
1102
1210
  sessionId,
1103
1211
  tools: async () => {
1104
1212
  try {
1105
- const result = await stream.call(RPC_METHODS2.toolsList, {
1213
+ const result = await stream.call(RPC_METHODS3.toolsList, {
1106
1214
  selector: sessionId
1107
1215
  });
1108
1216
  const entries = Array.isArray(result) ? result : result.tools;
@@ -1114,7 +1222,7 @@ var makeAppClient = (stream, sessionId) => {
1114
1222
  call: async (name, args, options) => {
1115
1223
  try {
1116
1224
  const result = await stream.call(
1117
- RPC_METHODS2.toolsCall,
1225
+ RPC_METHODS3.toolsCall,
1118
1226
  {
1119
1227
  selector: sessionId,
1120
1228
  name,
@@ -1129,153 +1237,82 @@ var makeAppClient = (stream, sessionId) => {
1129
1237
  throw toAppductError(error);
1130
1238
  }
1131
1239
  },
1240
+ // A single implementation backs both of the public type's overloaded signatures — `makeAppClient`
1241
+ // returns `client as unknown as AppClient<TTools>` below, so nothing here has to structurally
1242
+ // satisfy either signature on its own; the overloads are strictly a compile-time view for callers.
1132
1243
  events: async (options = {}) => {
1133
1244
  try {
1134
- const result = await stream.call(RPC_METHODS2.eventsSince, {
1245
+ const result = await stream.call(RPC_METHODS3.eventsSince, {
1135
1246
  selector: sessionId,
1136
1247
  since: options.since,
1137
1248
  limit: options.limit,
1138
- kinds: ["app_event"]
1249
+ payloadMaxBytes: options.payloadMaxBytes
1139
1250
  });
1140
1251
  return {
1141
- events: result.events.map((event) => appEventFrom(event, sessionId)).filter((event) => event !== void 0),
1142
- cursor: result.cursor
1252
+ events: result.events.map((event) => toAppEvent2(event, sessionId)).filter((event) => event !== void 0),
1253
+ cursor: result.cursor,
1254
+ dropped: result.dropped,
1255
+ remaining: result.remaining
1143
1256
  };
1144
1257
  } catch (error) {
1145
1258
  throw toAppductError(error);
1146
1259
  }
1147
1260
  },
1148
- waitForEvent: (name, options = {}) => {
1261
+ waitForEvent: async (name, options = {}) => {
1262
+ if (closed) {
1263
+ throw new AppductError(
1264
+ "connection_error",
1265
+ `Cannot wait for event "${name}": this AppClient (session "${sessionId}") is closed.`
1266
+ );
1267
+ }
1149
1268
  const timeoutMs = options.timeoutMs ?? DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MS;
1150
- const toMatch = (event) => {
1151
- const appEvent = appEventFrom(event, sessionId);
1152
- if (!appEvent || appEvent.name !== name) {
1153
- return void 0;
1269
+ let waitStream;
1270
+ try {
1271
+ waitStream = await openStream();
1272
+ } catch (error) {
1273
+ throw toAppductError(error);
1274
+ }
1275
+ if (closed) {
1276
+ waitStream.close();
1277
+ throw new AppductError(
1278
+ "connection_error",
1279
+ `Cannot wait for event "${name}": this AppClient (session "${sessionId}") is closed.`
1280
+ );
1281
+ }
1282
+ openWaitStreams.add(waitStream);
1283
+ try {
1284
+ const { event } = await waitForAppEvent(waitStream, {
1285
+ sessionId,
1286
+ name,
1287
+ since: options.since,
1288
+ timeoutMs,
1289
+ payloadMaxBytes: options.payloadMaxBytes
1290
+ });
1291
+ return event;
1292
+ } catch (error) {
1293
+ if (error instanceof WaitForAppEventTimeoutError) {
1294
+ throw new AppductError("timeout", error.message);
1154
1295
  }
1155
- if (options.match && !options.match(appEvent.payload)) {
1156
- return void 0;
1296
+ if (error instanceof WaitForAppEventConnectionClosedError) {
1297
+ throw new AppductError("connection_error", error.message);
1157
1298
  }
1158
- return appEvent;
1159
- };
1160
- return new Promise((resolve2, reject) => {
1161
- let settled = false;
1162
- const settle = (fn) => {
1163
- if (settled) {
1164
- return;
1165
- }
1166
- settled = true;
1167
- clearTimeout(timer);
1168
- unsubscribeClose();
1169
- liveHandler = null;
1170
- fn();
1171
- };
1172
- const timer = setTimeout(() => {
1173
- settle(
1174
- () => reject(new AppductError("timeout", `Timed out after ${timeoutMs}ms waiting for event "${name}".`))
1175
- );
1176
- }, timeoutMs);
1177
- let liveHandler = null;
1178
- const earlyEvents = [];
1179
- const unsubscribeNotification = stream.onNotification((payload) => {
1180
- const event = payload;
1181
- if (liveHandler) {
1182
- liveHandler(event);
1183
- } else {
1184
- earlyEvents.push(event);
1185
- }
1186
- });
1187
- let unsubscribeClose = () => {
1188
- };
1189
- const fail = (error) => {
1190
- settle(() => reject(toAppductError(error)));
1191
- unsubscribeNotification();
1192
- };
1193
- (async () => {
1194
- await stream.call(RPC_METHODS2.eventsSubscribe, { sessionSelector: sessionId, kinds: ["app_event"] });
1195
- const sinceResult = await stream.call(RPC_METHODS2.eventsSince, {
1196
- selector: sessionId,
1197
- since: options.since,
1198
- kinds: ["app_event"]
1199
- });
1200
- const seenSeqs = /* @__PURE__ */ new Set();
1201
- const backlog = [];
1202
- for (const event of [...sinceResult.events, ...earlyEvents]) {
1203
- if (event.sessionId === sessionId && !seenSeqs.has(event.seq)) {
1204
- seenSeqs.add(event.seq);
1205
- backlog.push(event);
1206
- }
1207
- }
1208
- backlog.sort((a, b) => a.seq - b.seq);
1209
- for (const event of backlog) {
1210
- try {
1211
- const matched = toMatch(event);
1212
- if (matched) {
1213
- settle(() => resolve2(matched));
1214
- unsubscribeNotification();
1215
- return;
1216
- }
1217
- } catch (error) {
1218
- fail(error);
1219
- return;
1220
- }
1221
- }
1222
- const highestConsideredSeq = backlog.length > 0 ? backlog[backlog.length - 1].seq : sinceResult.cursor;
1223
- unsubscribeClose = stream.onClose(() => {
1224
- settle(
1225
- () => reject(
1226
- new AppductError(
1227
- "connection_error",
1228
- `The connection to the Appduct daemon closed while waiting for event "${name}".`
1229
- )
1230
- )
1231
- );
1232
- unsubscribeNotification();
1233
- });
1234
- liveHandler = (event) => {
1235
- if (settled) {
1236
- return;
1237
- }
1238
- if (event.sessionId === sessionId && event.seq <= highestConsideredSeq) {
1239
- return;
1240
- }
1241
- try {
1242
- const matched = toMatch(event);
1243
- if (matched) {
1244
- settle(() => resolve2(matched));
1245
- unsubscribeNotification();
1246
- }
1247
- } catch (error) {
1248
- fail(error);
1249
- }
1250
- };
1251
- })().catch((error) => {
1252
- fail(error);
1253
- });
1254
- });
1299
+ throw toAppductError(error);
1300
+ } finally {
1301
+ openWaitStreams.delete(waitStream);
1302
+ waitStream.close();
1303
+ }
1255
1304
  },
1256
1305
  close: () => {
1306
+ closed = true;
1307
+ for (const waitStream of openWaitStreams) {
1308
+ waitStream.close();
1309
+ }
1310
+ openWaitStreams.clear();
1257
1311
  stream.close();
1258
1312
  }
1259
1313
  };
1260
1314
  return client;
1261
1315
  };
1262
- var appEventFrom = (event, sessionId) => {
1263
- if (event.kind !== "app_event" || event.sessionId !== sessionId) {
1264
- return void 0;
1265
- }
1266
- const data = event.data;
1267
- if (typeof data.name !== "string") {
1268
- return void 0;
1269
- }
1270
- return {
1271
- name: data.name,
1272
- payload: data.payload,
1273
- ts: event.ts,
1274
- sessionId: event.sessionId,
1275
- alias: event.alias,
1276
- seq: event.seq
1277
- };
1278
- };
1279
1316
 
1280
1317
  // src/client/connect.ts
1281
1318
  var connect3 = async (options = {}) => {
@@ -1292,10 +1329,16 @@ var connect3 = async (options = {}) => {
1292
1329
  throw toAppductError(error);
1293
1330
  }
1294
1331
  try {
1295
- const detail = await stream.call(RPC_METHODS3.sessionsDescribe, {
1332
+ const detail = await stream.call(RPC_METHODS4.sessionsDescribe, {
1296
1333
  selector: options.selector
1297
1334
  });
1298
- return makeAppClient(stream, detail.sessionId);
1335
+ const openStream = () => openDaemonStream({
1336
+ stateDir,
1337
+ spawn: options.spawn,
1338
+ autoSpawn: options.autoSpawn,
1339
+ requestTimeoutMs: options.requestTimeoutMs
1340
+ });
1341
+ return makeAppClient(stream, detail.sessionId, openStream);
1299
1342
  } catch (error) {
1300
1343
  stream.close();
1301
1344
  throw toAppductError(error);
@@ -1303,7 +1346,7 @@ var connect3 = async (options = {}) => {
1303
1346
  };
1304
1347
 
1305
1348
  // src/client/bootstrap.ts
1306
- import { RPC_METHODS as RPC_METHODS5 } from "@appduct/shared";
1349
+ import { RPC_METHODS as RPC_METHODS6 } from "@appduct/shared";
1307
1350
 
1308
1351
  // src/cli/open-target.ts
1309
1352
  import { execFile } from "node:child_process";
@@ -1600,7 +1643,7 @@ var deliverToOpenTarget = async (options) => {
1600
1643
  };
1601
1644
 
1602
1645
  // src/link.ts
1603
- import { RPC_METHODS as RPC_METHODS4 } from "@appduct/shared";
1646
+ import { RPC_METHODS as RPC_METHODS5 } from "@appduct/shared";
1604
1647
 
1605
1648
  // src/scheme.ts
1606
1649
  import { existsSync as existsSync2 } from "node:fs";
@@ -2248,7 +2291,7 @@ var mintLink = async (options) => {
2248
2291
  throw usageError(invalidAppIdMessage(appId));
2249
2292
  }
2250
2293
  const result = await callDaemon(
2251
- RPC_METHODS4.linkCreate,
2294
+ RPC_METHODS5.linkCreate,
2252
2295
  {
2253
2296
  ttlSeconds: options.ttlSeconds,
2254
2297
  addressOverride: options.target && usesLoopbackAddress(options.target) ? OPEN_TARGET_ADDRESS_OVERRIDE : void 0
@@ -2321,10 +2364,11 @@ var waitForSession = async (sessionId, options = {}) => {
2321
2364
  } catch (error) {
2322
2365
  throw toAppductError(error);
2323
2366
  }
2367
+ const openStream = () => openDaemonStream({ stateDir, spawn: options.spawn, autoSpawn: options.autoSpawn });
2324
2368
  try {
2325
2369
  try {
2326
- await stream.call(RPC_METHODS5.sessionsDescribe, { selector: sessionId });
2327
- return makeAppClient(stream, sessionId);
2370
+ await stream.call(RPC_METHODS6.sessionsDescribe, { selector: sessionId });
2371
+ return makeAppClient(stream, sessionId, openStream);
2328
2372
  } catch (error) {
2329
2373
  if (!(error instanceof DaemonRpcError) || error.data?.type !== "unknown_session") {
2330
2374
  throw error;
@@ -2371,11 +2415,11 @@ var waitForSession = async (sessionId, options = {}) => {
2371
2415
  settle(resolve2);
2372
2416
  }
2373
2417
  });
2374
- stream.call(RPC_METHODS5.eventsSubscribe, { sessionSelector: sessionId, kinds: ["session_claimed"] }).catch((error) => {
2418
+ stream.call(RPC_METHODS6.eventsSubscribe, { sessionSelector: sessionId, kinds: ["session_claimed"] }).catch((error) => {
2375
2419
  settle(() => reject(toAppductError(error)));
2376
2420
  });
2377
2421
  });
2378
- return makeAppClient(stream, sessionId);
2422
+ return makeAppClient(stream, sessionId, openStream);
2379
2423
  } catch (error) {
2380
2424
  stream.close();
2381
2425
  throw toAppductError(error);
@@ -1,14 +1,17 @@
1
1
  /**
2
- * `appduct events` (ARCHITECTURE.md §10): subscribes to `events.subscribe` over a persistent
3
- * connection and streams notifications until interrupted (Ctrl-C) or the connection drops. This is
4
- * the one command whose output isn't a single rendered `CliResult` — each event is written as it
5
- * arrives (NDJSON under `--json`, a human line otherwise) — so it plugs into `executeHostedCommand`
6
- * with a live reporter that suppresses the default one-shot bootstrap render (`cli/runner.ts`).
2
+ * `appduct events tail` (ARCHITECTURE.md §10): subscribes to `events.subscribe` for `app_event`
3
+ * only over a persistent connection and streams the app's own events until interrupted (Ctrl-C) or
4
+ * the connection drops. Appduct's lifecycle and tool-call kinds are not printed (issue #98). This
5
+ * is the one command whose output isn't a single rendered `CliResult` — each event is written as
6
+ * it arrives (NDJSON under `--json`, a human line otherwise) — so it plugs into
7
+ * `executeHostedCommand` with a live reporter that suppresses the default one-shot bootstrap
8
+ * render (`cli/runner.ts`).
7
9
  *
8
- * `--since <cursor>` (issue #6) switches to a one-shot mode: a single `events.since` pull instead
9
- * of a live subscription. It falls out of the same `EventsHostedResult` shape (each retained event
10
- * is written through `onEvent` exactly like a live one) so the CLI plumbing needs no forking — the
11
- * "hosted command" here just completes immediately instead of running until interrupted.
10
+ * `appduct events since <cursor>` (issue #6) is a one-shot mode: a single `events.since` pull
11
+ * instead of a live subscription, selected here by `since !== undefined`. It falls out of the same
12
+ * `EventsHostedResult` shape (each retained event is written through `onEvent` exactly like a live
13
+ * one) so the CLI plumbing needs no forking — the "hosted command" here just completes immediately
14
+ * instead of running until interrupted.
12
15
  */
13
16
  import { type EventNotification } from "@appduct/shared";
14
17
  import type { CliResult } from "../cli/result-types.js";
@@ -18,15 +21,30 @@ export type EventsCommandOptions = {
18
21
  /** Exclusive lower bound on `EventNotification.seq`; switches this command to a one-shot
19
22
  * `events.since` pull instead of a live `events.subscribe` stream. */
20
23
  since?: number;
24
+ /** Whole-name, case-sensitive glob (issue #115), forwarded to `events.subscribe`/`events.since`.
25
+ * No default — everything is delivered unfiltered unless asked. */
26
+ name?: string;
27
+ /** Caps an `app_event`'s payload to this many UTF-8 bytes of its JSON (issue #115), forwarded to
28
+ * `events.subscribe`/`events.since`. No default — nothing is truncated unless asked. */
29
+ payloadMaxBytes?: number;
30
+ };
31
+ /** `events since` mode's resulting cursor, plus how far behind or ahead of the full picture it
32
+ * is (issue #115): `dropped` events fell off the retention buffer before this pull, `remaining`
33
+ * matching events are still waiting beyond this page. */
34
+ export type EventsSinceCursor = {
35
+ cursor: number;
36
+ dropped: number;
37
+ remaining: number;
21
38
  };
22
39
  export type EventsCommandContext = {
23
40
  stateDir: string;
24
41
  spawn?: SpawnFn;
25
42
  onEvent: (event: EventNotification) => void;
26
- /** `--since` mode only: called once with the pull's resulting cursor, so a scripted caller
27
- * doesn't have to reconstruct it by maxing `seq` over the printed lines (impossible when the
28
- * response is empty — the whole point of a cursor is knowing where to resume from either way). */
29
- onCursor?: (cursor: number) => void;
43
+ /** `events since` mode only: called once with the pull's resulting cursor (plus `dropped`/
44
+ * `remaining`), so a scripted caller doesn't have to reconstruct it by maxing `seq` over the
45
+ * printed lines (impossible when the response is empty — the whole point of a cursor is
46
+ * knowing where to resume from either way). */
47
+ onCursor?: (cursor: EventsSinceCursor) => void;
30
48
  };
31
49
  export type EventsHostedResult = {
32
50
  result: CliResult<never>;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `appduct invoke` (ARCHITECTURE.md §10): `tools.call` round trip. Failures propagate as-is
2
+ * `appduct tools call` (ARCHITECTURE.md §10): `tools.call` round trip. Failures propagate as-is
3
3
  * (`DaemonRpcError`, preserving `error.data.type` verbatim) — `errors.ts`'s `toCliError` is what
4
4
  * renders `error.type` as the app's wire error type end-to-end, not this module's job.
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `appduct link` (ARCHITECTURE.md §10, §8): CLI-flag validation (`--open`/`--device`'s
2
+ * `appduct sessions link` (ARCHITECTURE.md §10, §8): CLI-flag validation (`--open`/`--device`'s
3
3
  * CLI-specific error wording) around the shared `mintLink` core (`../link.ts`), which both this
4
4
  * command and `appduct/client`'s `link()` use so the deep-link shape can't drift between them.
5
5
  */
@@ -1,4 +1,4 @@
1
- /** `appduct ls` (ARCHITECTURE.md §10): `sessions.list` passthrough. */
1
+ /** `appduct sessions ls` (ARCHITECTURE.md §10): `sessions.list` passthrough. */
2
2
  import type { CliResult, LsCommandData } from "../cli/result-types.js";
3
3
  import { type SpawnFn } from "../rpc/client.js";
4
4
  export type LsCommandContext = {
@@ -1,4 +1,4 @@
1
- /** `appduct revoke` (ARCHITECTURE.md §10): revokes a session, optionally by selector. */
1
+ /** `appduct sessions revoke` (ARCHITECTURE.md §10): revokes a session, optionally by selector. */
2
2
  import type { CliResult, RevokeCommandData } from "../cli/result-types.js";
3
3
  import { type SpawnFn } from "../rpc/client.js";
4
4
  export type RevokeCommandOptions = {
@@ -1,19 +1,18 @@
1
1
  /**
2
- * `appduct tools` (ARCHITECTURE.md §10): `tools [selector] [--full] [--group <name>] [--filter
3
- * <text>] [--limit <n>] [--offset <n>]` lists tools for a session; `tools [selector] --groups`
4
- * lists only the session's groups with their tool counts; `tools [selector] <name>` shows one
5
- * tool's full schema/annotations.
2
+ * `appduct tools ls`/`appduct tools describe` (ARCHITECTURE.md §10, issue #96): `ls [selector]
3
+ * [--full] [--group <name>] [--filter <text>] [--limit <n>] [--offset <n>]` lists tools for a
4
+ * session; `ls [selector] --groups` lists only the session's groups with their tool counts;
5
+ * `describe [selector] <name>` shows one tool's full schema/annotations.
6
6
  *
7
- * The command table gives both forms a leading optional `[selector]`, which makes a single
8
- * positional argument inherently ambiguous (is it the selector, or the tool name in `tools <name>`
9
- * with the selector omitted?). This resolves it the same way a human reading the table would:
10
- * first try the arg as a tool name in the implicit-selector session's registry; if no such tool
11
- * exists there (or the implicit selector doesn't resolve, e.g. `ambiguous_session`), fall back to
12
- * treating it as a selector and list that session's tools instead.
7
+ * The two verbs (`cli/routes/tools/ls.ts` and `cli/routes/tools/describe.ts`) split `[selector]`
8
+ * from `<name>` unambiguously before this handler ever runs — `describe`'s `<name>` is always the
9
+ * last positional, required, never a candidate selector — so there is no probing here: a listing
10
+ * request (`options.name === undefined`) always lists, a lookup (`options.name !== undefined`)
11
+ * always looks up, on the given selector or the implicit session if none was given.
13
12
  *
14
- * `--group`/`--filter`/`--limit`/`--offset` only ever reach the daemon on a *listing* request: the detail
15
- * path (an explicit `<name>`, or the ambiguous single-arg probe above) always asks for the whole,
16
- * unpaged registry, so a name lookup can never miss a tool that paging would have left off a page.
13
+ * `--group`/`--filter`/`--limit`/`--offset` only ever reach the daemon on a *listing* request: the
14
+ * detail path always asks for the whole, unpaged registry, so a name lookup can never miss a tool
15
+ * that paging would have left off a page.
17
16
  */
18
17
  import type { CliResult, ToolsCommandData } from "../cli/result-types.js";
19
18
  import { type SpawnFn } from "../rpc/client.js";
@@ -34,7 +34,7 @@ export type AppductConfig = {
34
34
  graceSeconds: number;
35
35
  linkTtlSeconds: number;
36
36
  keepaliveIntervalSeconds: number;
37
- /** Max retained `app_event`/etc. events per session (ARCHITECTURE.md §5's `events.since`
37
+ /** Max retained `app_event`s per session (ARCHITECTURE.md §5's `events.since`
38
38
  * retention buffer); default 256. */
39
39
  eventBufferSize: number;
40
40
  /** Days of `audit/<YYYY-MM-DD>.jsonl` history to keep; files older than this are pruned on
@@ -43,6 +43,9 @@ export type AppductConfig = {
43
43
  /** Size at which `daemon.log` is rotated to `daemon.log.1` before a daemon is spawned
44
44
  * (ARCHITECTURE.md §3/§4). Default 10 MiB. */
45
45
  daemonLogMaxBytes: number;
46
+ /** Size past which the daemon rotates `events.log` to `events.log.1` (ARCHITECTURE.md §3).
47
+ * Default 10 MiB. */
48
+ eventsLogMaxBytes: number;
46
49
  policy: AppductPolicyConfig;
47
50
  /**
48
51
  * When the CLI/MCP client finds the running daemon on a different Appduct version, restart it
@@ -54,7 +57,7 @@ export type AppductConfig = {
54
57
  /** Operator override for advertised-address detection (daemon/address.ts); undefined = auto-detect. */
55
58
  advertisedIp?: string;
56
59
  /**
57
- * Deep-link URI scheme used to compose `appduct link`'s output (ARCHITECTURE.md §10: "taken
60
+ * Deep-link URI scheme used to compose `appduct sessions link`'s output (ARCHITECTURE.md §10: "taken
58
61
  * from the flag, else `config.json`, else the CLI errors with a clear message"). Not part of the
59
62
  * `config.json` shape enumerated in ARCHITECTURE.md §3 (which only covers daemon-side settings),
60
63
  * but `config.json` is explicitly "all fields optional" there and this is the natural home for a
@@ -14,6 +14,7 @@ import { type DaemonListener } from "./listener.js";
14
14
  import { type RpcServer } from "./rpc-server.js";
15
15
  import { type StateDirPaths } from "./state-dir.js";
16
16
  import { type TimerFns } from "./timers.js";
17
+ import { type TlsManager } from "./tls.js";
17
18
  /** How often audit retention runs after the startup prune (ARCHITECTURE.md §3: "once a day"). */
18
19
  export declare const AUDIT_PRUNE_INTERVAL_MS: number;
19
20
  export type DaemonOptions = {
@@ -33,6 +34,8 @@ export type RunningDaemon = {
33
34
  startedAt: Date;
34
35
  server: RpcServer;
35
36
  listener: DaemonListener;
37
+ /** The host certificate the listener serves, so an in-process client can trust it. */
38
+ tls: TlsManager;
36
39
  eventBus: EventBus;
37
40
  /** Resolves once graceful teardown (sockets closed, RPC/listener closed, pidfile released) completes. */
38
41
  exited: Promise<void>;