appduct 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +41 -36
  2. package/dist/bin.js +44 -36
  3. package/dist/cli/command-options.d.ts +7 -17
  4. package/dist/cli/result-types.d.ts +11 -8
  5. package/dist/cli/routes/daemon/index.js +12 -16
  6. package/dist/cli/routes/daemon/run.js +136 -28
  7. package/dist/cli/routes/daemon/start.js +12 -16
  8. package/dist/cli/routes/daemon/status.js +12 -16
  9. package/dist/cli/routes/daemon/stop.js +12 -16
  10. package/dist/cli/routes/doctor.js +12 -16
  11. package/dist/cli/routes/events/index.d.ts +9 -0
  12. package/dist/cli/routes/events/index.js +842 -0
  13. package/dist/cli/routes/events/shared-options.d.ts +12 -0
  14. package/dist/cli/routes/events/shared-options.js +102 -0
  15. package/dist/cli/routes/events/since.d.ts +6 -0
  16. package/dist/cli/routes/events/since.js +1977 -0
  17. package/dist/cli/routes/events/tail.d.ts +6 -0
  18. package/dist/cli/routes/{events.js → events/tail.js} +177 -162
  19. package/dist/cli/routes/init.js +14 -18
  20. package/dist/cli/routes/keygen.js +12 -16
  21. package/dist/cli/routes/mcp.js +181 -165
  22. package/dist/cli/routes/sessions/index.d.ts +7 -0
  23. package/dist/cli/routes/sessions/index.js +843 -0
  24. package/dist/cli/routes/sessions/link.d.ts +4 -0
  25. package/dist/cli/routes/{link.js → sessions/link.js} +13 -17
  26. package/dist/cli/routes/sessions/ls.d.ts +4 -0
  27. package/dist/cli/routes/{ls.js → sessions/ls.js} +13 -17
  28. package/dist/cli/routes/sessions/revoke.d.ts +4 -0
  29. package/dist/cli/routes/{revoke.js → sessions/revoke.js} +14 -18
  30. package/dist/cli/routes/tools/call.d.ts +4 -0
  31. package/dist/cli/routes/{invoke.js → tools/call.js} +15 -19
  32. package/dist/cli/routes/tools/describe.d.ts +6 -0
  33. package/dist/cli/routes/tools/describe.js +1837 -0
  34. package/dist/cli/routes/tools/index.d.ts +6 -0
  35. package/dist/cli/routes/tools/index.js +843 -0
  36. package/dist/cli/routes/tools/ls.d.ts +4 -0
  37. package/dist/cli/routes/{tools.js → tools/ls.js} +40 -73
  38. package/dist/cli/routes/tools/shared-options.d.ts +16 -0
  39. package/dist/cli/routes/tools/shared-options.js +122 -0
  40. package/dist/client/app-client.d.ts +96 -37
  41. package/dist/client/index.d.ts +3 -2
  42. package/dist/client/index.js +178 -138
  43. package/dist/commands/events.d.ts +31 -14
  44. package/dist/commands/invoke.d.ts +1 -1
  45. package/dist/commands/link.d.ts +1 -1
  46. package/dist/commands/ls.d.ts +1 -1
  47. package/dist/commands/revoke.d.ts +1 -1
  48. package/dist/commands/tools.d.ts +12 -13
  49. package/dist/daemon/config.d.ts +1 -1
  50. package/dist/daemon/event-bus.d.ts +32 -0
  51. package/dist/daemon/rpc-errors.d.ts +1 -1
  52. package/dist/errors.d.ts +1 -1
  53. package/dist/events/index.d.ts +60 -0
  54. package/dist/index.js +56 -39
  55. package/dist/link.d.ts +1 -1
  56. package/dist/mcp/app-tools.d.ts +3 -2
  57. package/dist/mcp/connect-tool.d.ts +1 -1
  58. package/dist/mcp/events-tool.d.ts +28 -27
  59. package/dist/native-scheme.d.ts +1 -1
  60. package/dist/output.d.ts +29 -6
  61. package/dist/rpc/client.d.ts +1 -1
  62. package/dist/scheme.d.ts +4 -4
  63. package/package.json +2 -2
  64. package/dist/cli/routes/events.d.ts +0 -3
  65. package/dist/cli/routes/invoke.d.ts +0 -3
  66. package/dist/cli/routes/link.d.ts +0 -3
  67. package/dist/cli/routes/ls.d.ts +0 -3
  68. package/dist/cli/routes/revoke.d.ts +0 -3
  69. package/dist/cli/routes/tools.d.ts +0 -3
@@ -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";
@@ -1029,9 +1029,109 @@ var openDaemonStream = async (options) => {
1029
1029
  import {
1030
1030
  clampToolTimeoutMs,
1031
1031
  MAX_TOOL_TIMEOUT_MS,
1032
- RPC_METHODS as RPC_METHODS2
1032
+ RPC_METHODS as RPC_METHODS3,
1033
+ toAppEvent as toAppEvent2
1033
1034
  } from "@appduct/shared";
1034
1035
 
1036
+ // src/events/index.ts
1037
+ import {
1038
+ RPC_METHODS as RPC_METHODS2,
1039
+ toAppEvent
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
+ };
1134
+
1035
1135
  // src/errors.ts
1036
1136
  var AppductCliError = class extends Error {
1037
1137
  type;
@@ -1103,12 +1203,14 @@ var transportTimeoutForToolCall = (timeoutMs) => {
1103
1203
  const clamped = timeoutMs === void 0 || !Number.isFinite(timeoutMs) ? MAX_TOOL_TIMEOUT_MS : clampToolTimeoutMs(timeoutMs);
1104
1204
  return clamped + CALL_TRANSPORT_TIMEOUT_SLACK_MS;
1105
1205
  };
1106
- var makeAppClient = (stream, sessionId) => {
1206
+ var makeAppClient = (stream, sessionId, openStream) => {
1207
+ let closed = false;
1208
+ const openWaitStreams = /* @__PURE__ */ new Set();
1107
1209
  const client = {
1108
1210
  sessionId,
1109
1211
  tools: async () => {
1110
1212
  try {
1111
- const result = await stream.call(RPC_METHODS2.toolsList, {
1213
+ const result = await stream.call(RPC_METHODS3.toolsList, {
1112
1214
  selector: sessionId
1113
1215
  });
1114
1216
  const entries = Array.isArray(result) ? result : result.tools;
@@ -1120,7 +1222,7 @@ var makeAppClient = (stream, sessionId) => {
1120
1222
  call: async (name, args, options) => {
1121
1223
  try {
1122
1224
  const result = await stream.call(
1123
- RPC_METHODS2.toolsCall,
1225
+ RPC_METHODS3.toolsCall,
1124
1226
  {
1125
1227
  selector: sessionId,
1126
1228
  name,
@@ -1135,151 +1237,82 @@ var makeAppClient = (stream, sessionId) => {
1135
1237
  throw toAppductError(error);
1136
1238
  }
1137
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.
1138
1243
  events: async (options = {}) => {
1139
1244
  try {
1140
- const result = await stream.call(RPC_METHODS2.eventsSince, {
1245
+ const result = await stream.call(RPC_METHODS3.eventsSince, {
1141
1246
  selector: sessionId,
1142
1247
  since: options.since,
1143
- limit: options.limit
1248
+ limit: options.limit,
1249
+ payloadMaxBytes: options.payloadMaxBytes
1144
1250
  });
1145
1251
  return {
1146
- events: result.events.map((event) => appEventFrom(event, sessionId)).filter((event) => event !== void 0),
1147
- 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
1148
1256
  };
1149
1257
  } catch (error) {
1150
1258
  throw toAppductError(error);
1151
1259
  }
1152
1260
  },
1153
- 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
+ }
1154
1268
  const timeoutMs = options.timeoutMs ?? DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MS;
1155
- const toMatch = (event) => {
1156
- const appEvent = appEventFrom(event, sessionId);
1157
- if (!appEvent || appEvent.name !== name) {
1158
- 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);
1159
1295
  }
1160
- if (options.match && !options.match(appEvent.payload)) {
1161
- return void 0;
1296
+ if (error instanceof WaitForAppEventConnectionClosedError) {
1297
+ throw new AppductError("connection_error", error.message);
1162
1298
  }
1163
- return appEvent;
1164
- };
1165
- return new Promise((resolve2, reject) => {
1166
- let settled = false;
1167
- const settle = (fn) => {
1168
- if (settled) {
1169
- return;
1170
- }
1171
- settled = true;
1172
- clearTimeout(timer);
1173
- unsubscribeClose();
1174
- liveHandler = null;
1175
- fn();
1176
- };
1177
- const timer = setTimeout(() => {
1178
- settle(
1179
- () => reject(new AppductError("timeout", `Timed out after ${timeoutMs}ms waiting for event "${name}".`))
1180
- );
1181
- }, timeoutMs);
1182
- let liveHandler = null;
1183
- const earlyEvents = [];
1184
- const unsubscribeNotification = stream.onNotification((payload) => {
1185
- const event = payload;
1186
- if (liveHandler) {
1187
- liveHandler(event);
1188
- } else {
1189
- earlyEvents.push(event);
1190
- }
1191
- });
1192
- let unsubscribeClose = () => {
1193
- };
1194
- const fail = (error) => {
1195
- settle(() => reject(toAppductError(error)));
1196
- unsubscribeNotification();
1197
- };
1198
- (async () => {
1199
- await stream.call(RPC_METHODS2.eventsSubscribe, { sessionSelector: sessionId, kinds: ["app_event"] });
1200
- const sinceResult = await stream.call(RPC_METHODS2.eventsSince, {
1201
- selector: sessionId,
1202
- since: options.since
1203
- });
1204
- const seenSeqs = /* @__PURE__ */ new Set();
1205
- const backlog = [];
1206
- for (const event of [...sinceResult.events, ...earlyEvents]) {
1207
- if (event.sessionId === sessionId && !seenSeqs.has(event.seq)) {
1208
- seenSeqs.add(event.seq);
1209
- backlog.push(event);
1210
- }
1211
- }
1212
- backlog.sort((a, b) => a.seq - b.seq);
1213
- for (const event of backlog) {
1214
- try {
1215
- const matched = toMatch(event);
1216
- if (matched) {
1217
- settle(() => resolve2(matched));
1218
- unsubscribeNotification();
1219
- return;
1220
- }
1221
- } catch (error) {
1222
- fail(error);
1223
- return;
1224
- }
1225
- }
1226
- const highestConsideredSeq = backlog.length > 0 ? backlog[backlog.length - 1].seq : sinceResult.cursor;
1227
- unsubscribeClose = stream.onClose(() => {
1228
- settle(
1229
- () => reject(
1230
- new AppductError(
1231
- "connection_error",
1232
- `The connection to the Appduct daemon closed while waiting for event "${name}".`
1233
- )
1234
- )
1235
- );
1236
- unsubscribeNotification();
1237
- });
1238
- liveHandler = (event) => {
1239
- if (settled) {
1240
- return;
1241
- }
1242
- if (event.sessionId === sessionId && event.seq <= highestConsideredSeq) {
1243
- return;
1244
- }
1245
- try {
1246
- const matched = toMatch(event);
1247
- if (matched) {
1248
- settle(() => resolve2(matched));
1249
- unsubscribeNotification();
1250
- }
1251
- } catch (error) {
1252
- fail(error);
1253
- }
1254
- };
1255
- })().catch((error) => {
1256
- fail(error);
1257
- });
1258
- });
1299
+ throw toAppductError(error);
1300
+ } finally {
1301
+ openWaitStreams.delete(waitStream);
1302
+ waitStream.close();
1303
+ }
1259
1304
  },
1260
1305
  close: () => {
1306
+ closed = true;
1307
+ for (const waitStream of openWaitStreams) {
1308
+ waitStream.close();
1309
+ }
1310
+ openWaitStreams.clear();
1261
1311
  stream.close();
1262
1312
  }
1263
1313
  };
1264
1314
  return client;
1265
1315
  };
1266
- var appEventFrom = (event, sessionId) => {
1267
- if (event.kind !== "app_event" || event.sessionId !== sessionId) {
1268
- return void 0;
1269
- }
1270
- const data = event.data;
1271
- if (typeof data.name !== "string") {
1272
- return void 0;
1273
- }
1274
- return {
1275
- name: data.name,
1276
- payload: data.payload,
1277
- ts: event.ts,
1278
- sessionId: event.sessionId,
1279
- alias: event.alias,
1280
- seq: event.seq
1281
- };
1282
- };
1283
1316
 
1284
1317
  // src/client/connect.ts
1285
1318
  var connect3 = async (options = {}) => {
@@ -1296,10 +1329,16 @@ var connect3 = async (options = {}) => {
1296
1329
  throw toAppductError(error);
1297
1330
  }
1298
1331
  try {
1299
- const detail = await stream.call(RPC_METHODS3.sessionsDescribe, {
1332
+ const detail = await stream.call(RPC_METHODS4.sessionsDescribe, {
1300
1333
  selector: options.selector
1301
1334
  });
1302
- 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);
1303
1342
  } catch (error) {
1304
1343
  stream.close();
1305
1344
  throw toAppductError(error);
@@ -1307,7 +1346,7 @@ var connect3 = async (options = {}) => {
1307
1346
  };
1308
1347
 
1309
1348
  // src/client/bootstrap.ts
1310
- import { RPC_METHODS as RPC_METHODS5 } from "@appduct/shared";
1349
+ import { RPC_METHODS as RPC_METHODS6 } from "@appduct/shared";
1311
1350
 
1312
1351
  // src/cli/open-target.ts
1313
1352
  import { execFile } from "node:child_process";
@@ -1604,7 +1643,7 @@ var deliverToOpenTarget = async (options) => {
1604
1643
  };
1605
1644
 
1606
1645
  // src/link.ts
1607
- import { RPC_METHODS as RPC_METHODS4 } from "@appduct/shared";
1646
+ import { RPC_METHODS as RPC_METHODS5 } from "@appduct/shared";
1608
1647
 
1609
1648
  // src/scheme.ts
1610
1649
  import { existsSync as existsSync2 } from "node:fs";
@@ -2252,7 +2291,7 @@ var mintLink = async (options) => {
2252
2291
  throw usageError(invalidAppIdMessage(appId));
2253
2292
  }
2254
2293
  const result = await callDaemon(
2255
- RPC_METHODS4.linkCreate,
2294
+ RPC_METHODS5.linkCreate,
2256
2295
  {
2257
2296
  ttlSeconds: options.ttlSeconds,
2258
2297
  addressOverride: options.target && usesLoopbackAddress(options.target) ? OPEN_TARGET_ADDRESS_OVERRIDE : void 0
@@ -2325,10 +2364,11 @@ var waitForSession = async (sessionId, options = {}) => {
2325
2364
  } catch (error) {
2326
2365
  throw toAppductError(error);
2327
2366
  }
2367
+ const openStream = () => openDaemonStream({ stateDir, spawn: options.spawn, autoSpawn: options.autoSpawn });
2328
2368
  try {
2329
2369
  try {
2330
- await stream.call(RPC_METHODS5.sessionsDescribe, { selector: sessionId });
2331
- return makeAppClient(stream, sessionId);
2370
+ await stream.call(RPC_METHODS6.sessionsDescribe, { selector: sessionId });
2371
+ return makeAppClient(stream, sessionId, openStream);
2332
2372
  } catch (error) {
2333
2373
  if (!(error instanceof DaemonRpcError) || error.data?.type !== "unknown_session") {
2334
2374
  throw error;
@@ -2375,11 +2415,11 @@ var waitForSession = async (sessionId, options = {}) => {
2375
2415
  settle(resolve2);
2376
2416
  }
2377
2417
  });
2378
- 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) => {
2379
2419
  settle(() => reject(toAppductError(error)));
2380
2420
  });
2381
2421
  });
2382
- return makeAppClient(stream, sessionId);
2422
+ return makeAppClient(stream, sessionId, openStream);
2383
2423
  } catch (error) {
2384
2424
  stream.close();
2385
2425
  throw toAppductError(error);
@@ -1,15 +1,17 @@
1
1
  /**
2
- * `appduct events` (ARCHITECTURE.md §10): subscribes to `events.subscribe` for `app_event` only
3
- * over a persistent connection and streams the app's own events until interrupted (Ctrl-C) or the
4
- * connection drops. Appduct's lifecycle and tool-call kinds are not printed (issue #98). This is
5
- * the one command whose output isn't a single rendered `CliResult` — each event is written as it
6
- * arrives (NDJSON under `--json`, a human line otherwise) — so it plugs into `executeHostedCommand`
7
- * 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`).
8
9
  *
9
- * `--since <cursor>` (issue #6) switches to a one-shot mode: a single `events.since` pull instead
10
- * of a live subscription. It falls out of the same `EventsHostedResult` shape (each retained event
11
- * is written through `onEvent` exactly like a live one) so the CLI plumbing needs no forking — the
12
- * "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.
13
15
  */
14
16
  import { type EventNotification } from "@appduct/shared";
15
17
  import type { CliResult } from "../cli/result-types.js";
@@ -19,15 +21,30 @@ export type EventsCommandOptions = {
19
21
  /** Exclusive lower bound on `EventNotification.seq`; switches this command to a one-shot
20
22
  * `events.since` pull instead of a live `events.subscribe` stream. */
21
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;
22
38
  };
23
39
  export type EventsCommandContext = {
24
40
  stateDir: string;
25
41
  spawn?: SpawnFn;
26
42
  onEvent: (event: EventNotification) => void;
27
- /** `--since` mode only: called once with the pull's resulting cursor, so a scripted caller
28
- * doesn't have to reconstruct it by maxing `seq` over the printed lines (impossible when the
29
- * response is empty — the whole point of a cursor is knowing where to resume from either way). */
30
- 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;
31
48
  };
32
49
  export type EventsHostedResult = {
33
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";
@@ -57,7 +57,7 @@ export type AppductConfig = {
57
57
  /** Operator override for advertised-address detection (daemon/address.ts); undefined = auto-detect. */
58
58
  advertisedIp?: string;
59
59
  /**
60
- * 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
61
61
  * from the flag, else `config.json`, else the CLI errors with a clear message"). Not part of the
62
62
  * `config.json` shape enumerated in ARCHITECTURE.md §3 (which only covers daemon-side settings),
63
63
  * but `config.json` is explicitly "all fields optional" there and this is the natural home for a
@@ -21,13 +21,45 @@ export type EventBusEmitInput = Omit<EventNotification, "ts" | "seq"> & {
21
21
  ts?: number;
22
22
  };
23
23
  export type EventBusListener = (event: EventNotification) => void;
24
+ export type ProjectAppEventOptions = {
25
+ /** Whole-name, case-sensitive glob (issue #112): `*` matches any run of characters, a pattern
26
+ * with no `*` is an exact name. */
27
+ name?: string;
28
+ /** Caps the payload's JSON to this many UTF-8 bytes (issue #113); see {@link truncateToUtf8Bytes}. */
29
+ payloadMaxBytes?: number;
30
+ };
31
+ /**
32
+ * Whether `event` survives `options.name`'s glob, and truncates its payload to `options
33
+ * .payloadMaxBytes` when given — the one implementation `since()`'s drain and `daemon.ts`'s
34
+ * `events.subscribe` fan-out both call, so the two can't drift apart (issue #112, #113). Returns
35
+ * `event` (unchanged, or with its payload replaced by a preview) when it should be kept,
36
+ * `undefined` when it should be dropped.
37
+ *
38
+ * Only an `app_event` carries a `name`/`payload` to filter or truncate, so every other kind passes
39
+ * through unfiltered and untouched regardless of either option — the `kinds` filter
40
+ * (`events.subscribe`) is what narrows those.
41
+ */
42
+ export declare const projectAppEvent: (event: EventNotification, options?: ProjectAppEventOptions) => EventNotification | undefined;
24
43
  export type EventsSinceQuery = {
25
44
  since?: number;
26
45
  limit?: number;
46
+ /** Whole-name, case-sensitive glob (issue #112): `*` matches any run of characters, a pattern
47
+ * with no `*` is an exact name. See {@link projectAppEvent}. */
48
+ name?: string;
49
+ /** Caps a returned `app_event`'s payload to this many UTF-8 bytes of its JSON (issue #113);
50
+ * applied to the page actually returned, after `name`/`limit`, via {@link projectAppEvent}. */
51
+ payloadMaxBytes?: number;
27
52
  };
28
53
  export type EventsSinceQueryResult = {
29
54
  events: EventNotification[];
30
55
  cursor: number;
56
+ /** How many app events after `since` were evicted before this call could return them (issue
57
+ * #113): `max(0, oldest.seq - since - 1)`, `oldest` being the buffer's current oldest retained
58
+ * entry. `0` once nothing has fallen off, and `since` defaults to `0` when omitted. */
59
+ dropped: number;
60
+ /** How many events still match `since`/`name` after the returned (possibly `limit`-truncated)
61
+ * page. `0` on the last page. */
62
+ remaining: number;
31
63
  };
32
64
  export type EventBus = {
33
65
  emit: (event: EventBusEmitInput) => void;
@@ -2,7 +2,7 @@
2
2
  * The error the daemon's RPC handlers throw to answer a request with a typed wire error
3
3
  * (ARCHITECTURE.md §5). It lives apart from `rpc-server.ts` so the modules that *raise* it — the
4
4
  * session engine, the call engine — don't drag the socket server itself into every bundle that
5
- * needs, say, a call-timeout constant from `calls.ts` (the CLI's `invoke` route does).
5
+ * needs, say, a call-timeout constant from `calls.ts` (the CLI's `tools call` route does).
6
6
  */
7
7
  import type { ErrorType } from "@appduct/shared";
8
8
  export declare class RpcApplicationError extends Error {
package/dist/errors.d.ts CHANGED
@@ -18,7 +18,7 @@ export declare const isAppductCliError: (value: unknown) => value is AppductCliE
18
18
  * Converts any error thrown from a command handler into the CLI's rendered `CliError` shape.
19
19
  * RPC/app-originated failures (`DaemonRpcError` with a preserved `data.type`) keep that type
20
20
  * **verbatim** in `type` — never re-wrapped under a generic bucket (ARCHITECTURE.md §5) — so e.g.
21
- * `invoke --json` on a failing tool call renders `error.type === "tool_execution_error"`, not
21
+ * `tools call --json` on a failing tool call renders `error.type === "tool_execution_error"`, not
22
22
  * `"tool_error"`. `getExitCodeForError` still buckets that same error into a sysexits class.
23
23
  */
24
24
  export declare const toCliError: (error: unknown) => CliError;