appduct 0.11.1 → 0.12.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.
package/README.md CHANGED
@@ -42,7 +42,7 @@ That's the whole loop. There is no host process to start — `appduct` auto-spaw
42
42
  | `appduct tools [selector] [name] [--full] [--group <name>] [--filter <text>] [--limit <n>] [--offset <n>]` | list a session's tools (one call signature + description per line), or show one tool's full schema |
43
43
  | `appduct tools [selector] --groups` | list a session's tool groups with their tool counts |
44
44
  | `appduct invoke [selector] <tool> --input '<json>' [--timeout <ms>]` | call a tool |
45
- | `appduct events [selector] [--follow] [--since <cursor>]` | stream session/tool events (default), or one-shot pull everything retained since `<cursor>` (`--since`); `--json` emits NDJSON |
45
+ | `appduct events [selector] [--follow] [--since <cursor>]` | stream the events the app posts with `postEvent` (default), or one-shot pull the ones retained since `<cursor>` (`--since`); `--json` emits NDJSON |
46
46
  | `appduct revoke [selector]` | revoke a session |
47
47
  | `appduct daemon run\|start\|stop\|status` | daemon lifecycle |
48
48
  | `appduct mcp [--scheme <s>]` | start a stdio MCP server proxying connected apps' tools to MCP clients |
@@ -289,13 +289,13 @@ This writes an unencrypted PEM private key (PKCS#8) to `<state-dir>/key.pem` by
289
289
 
290
290
  ## Daemon lifecycle
291
291
 
292
- The daemon auto-starts the first time any CLI or MCP command needs it — you don't normally run `appduct daemon start` yourself. It writes state to `<state-dir>/` (`daemon.sock`, `daemon.pid`, `daemon.log`, `daemon.log.1`, `key.pem`, `config.json`, `audit/`), holds a single-instance lock via the pidfile, and runs independently of any one device's connection, so a reload or crash on the device side never costs you the daemon process.
292
+ The daemon auto-starts the first time any CLI or MCP command needs it — you don't normally run `appduct daemon start` yourself. It writes state to `<state-dir>/` (`daemon.sock`, `daemon.pid`, `daemon.log`, `daemon.log.1`, `events.log`, `events.log.1`, `key.pem`, `config.json`, `audit/`), holds a single-instance lock via the pidfile, and runs independently of any one device's connection, so a reload or crash on the device side never costs you the daemon process.
293
293
 
294
294
  Use `appduct daemon status` to see what's running (version, pid, `wssPort`, pinned keys, live sessions, effective policy, and the audit log's retention window, file count, size, and failure counters) and `appduct daemon stop` to shut it down explicitly.
295
295
 
296
296
  Because the daemon outlives the CLI that started it, upgrading Appduct would otherwise leave the old daemon serving your commands. Every CLI/MCP process compares its version with the daemon's on its first command and replaces a stale daemon when nothing would be lost; when there is something at stake — connected sessions, or an unexpired link nobody has scanned yet — the command stops instead, naming both versions, until you run `appduct daemon stop` or pass `--daemon-restart`. A daemon newer than your CLI is left alone with a warning, so a project-local install never downgrades a global one's daemon.
297
297
 
298
- Neither log grows without bound: `audit/<YYYY-MM-DD>.jsonl` files older than `auditRetentionDays` (default 30) are pruned at daemon start and once a day after, and a `daemon.log` over `daemonLogMaxBytes` (default 10 MiB) is rotated to `daemon.log.1` when a daemon is next spawned. Both are `config.json` keys — see [`ARCHITECTURE.md` §3][architecture].
298
+ No log grows without bound: `audit/<YYYY-MM-DD>.jsonl` files older than `auditRetentionDays` (default 30) are pruned at daemon start and once a day after, a `daemon.log` over `daemonLogMaxBytes` (default 10 MiB) is rotated to `daemon.log.1` when a daemon is next spawned, and `events.log` is rotated to `events.log.1` as soon as it passes `eventsLogMaxBytes` (default 10 MiB). `events.log` records the daemon's own events (links, sessions, tool registries and tool calls), one JSON line each, for debugging Appduct itself; your app's events are never written to it. All three are `config.json` keys — see [`ARCHITECTURE.md` §3][architecture].
299
299
 
300
300
  ## Release gate: `appduct doctor`
301
301
 
package/dist/bin.js CHANGED
@@ -142,7 +142,7 @@ var createCli = () => {
142
142
  "--timeout <ms>",
143
143
  "Call timeout in milliseconds. Shortens the deadline; it cannot extend one past the app's own timer, which is the tool's declared timeoutMs (else 10000)."
144
144
  );
145
- cli.command("events [selector]", "Stream session/tool events until interrupted.").option("--follow", "Accepted for script readability; the default behavior already follows.").option("--since <cursor>", "One-shot: print events retained since this cursor instead of streaming live.");
145
+ cli.command("events [selector]", "Stream the events the app posts until interrupted.").option("--follow", "Accepted for script readability; the default behavior already follows.").option("--since <cursor>", "One-shot: print the app events retained since this cursor instead of streaming live.");
146
146
  cli.command("revoke [selector]", "Revoke a session.");
147
147
  cli.command("mcp", "Start a stdio MCP server that gives MCP clients access to connected apps' tools.").option(
148
148
  "--scheme <scheme>",
@@ -166,6 +166,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
166
166
  "eventBufferSize",
167
167
  "auditRetentionDays",
168
168
  "daemonLogMaxBytes",
169
+ "eventsLogMaxBytes",
169
170
  "policy",
170
171
  "advertisedIp",
171
172
  "scheme",
@@ -225,6 +226,7 @@ var defaultConfig = (paths) => {
225
226
  eventBufferSize: 256,
226
227
  auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
227
228
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
229
+ eventsLogMaxBytes: 10 * 1024 * 1024,
228
230
  restartDaemonOnVersionMismatch: false,
229
231
  policy: {
230
232
  default: "allow",
@@ -287,6 +289,9 @@ var loadConfig = async (paths, options = {}) => {
287
289
  if (parsed.daemonLogMaxBytes !== void 0) {
288
290
  config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
289
291
  }
292
+ if (parsed.eventsLogMaxBytes !== void 0) {
293
+ config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
294
+ }
290
295
  if (parsed.restartDaemonOnVersionMismatch !== void 0) {
291
296
  config.restartDaemonOnVersionMismatch = requireBoolean(
292
297
  parsed.restartDaemonOnVersionMismatch,
@@ -335,6 +340,7 @@ var getStateDirPaths = (stateDir) => {
335
340
  socketPath: join3(stateDir, "daemon.sock"),
336
341
  pidFilePath: join3(stateDir, "daemon.pid"),
337
342
  logFilePath: join3(stateDir, "daemon.log"),
343
+ eventsLogPath: join3(stateDir, "events.log"),
338
344
  keyPath: join3(stateDir, "key.pem"),
339
345
  configPath: join3(stateDir, "config.json"),
340
346
  auditDir: join3(stateDir, "audit"),
@@ -139,6 +139,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
139
139
  "eventBufferSize",
140
140
  "auditRetentionDays",
141
141
  "daemonLogMaxBytes",
142
+ "eventsLogMaxBytes",
142
143
  "policy",
143
144
  "advertisedIp",
144
145
  "scheme",
@@ -198,6 +199,7 @@ var defaultConfig = (paths) => {
198
199
  eventBufferSize: 256,
199
200
  auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
200
201
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
202
+ eventsLogMaxBytes: 10 * 1024 * 1024,
201
203
  restartDaemonOnVersionMismatch: false,
202
204
  policy: {
203
205
  default: "allow",
@@ -260,6 +262,9 @@ var loadConfig = async (paths, options = {}) => {
260
262
  if (parsed.daemonLogMaxBytes !== void 0) {
261
263
  config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
262
264
  }
265
+ if (parsed.eventsLogMaxBytes !== void 0) {
266
+ config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
267
+ }
263
268
  if (parsed.restartDaemonOnVersionMismatch !== void 0) {
264
269
  config.restartDaemonOnVersionMismatch = requireBoolean(
265
270
  parsed.restartDaemonOnVersionMismatch,
@@ -609,7 +614,6 @@ var createCallsManager = (options) => {
609
614
 
610
615
  // src/daemon/event-bus.ts
611
616
  var TERMINAL_EVENT_KINDS = /* @__PURE__ */ new Set(["session_expired", "session_revoked"]);
612
- var EXCLUDED_FROM_RETENTION = /* @__PURE__ */ new Set(["tool_call_progress"]);
613
617
  var createEventBus = (options = {}) => {
614
618
  const clock = options.clock ?? { now: () => /* @__PURE__ */ new Date() };
615
619
  const bufferSize = options.bufferSize ?? 256;
@@ -640,7 +644,7 @@ var createEventBus = (options = {}) => {
640
644
  if (TERMINAL_EVENT_KINDS.has(notification.kind)) {
641
645
  buffers.delete(sessionId);
642
646
  cursors.delete(sessionId);
643
- } else if (!EXCLUDED_FROM_RETENTION.has(notification.kind)) {
647
+ } else if (notification.kind === "app_event") {
644
648
  appendToBuffer(sessionId, notification);
645
649
  }
646
650
  }
@@ -664,10 +668,6 @@ var createEventBus = (options = {}) => {
664
668
  if (query.since !== void 0) {
665
669
  events = events.filter((event) => event.seq > query.since);
666
670
  }
667
- if (query.kinds !== void 0) {
668
- const kinds = new Set(query.kinds);
669
- events = events.filter((event) => kinds.has(event.kind));
670
- }
671
671
  if (query.limit !== void 0 && events.length > query.limit) {
672
672
  events = events.slice(0, query.limit);
673
673
  }
@@ -681,6 +681,28 @@ var createEventBus = (options = {}) => {
681
681
  };
682
682
  };
683
683
 
684
+ // src/daemon/events-log.ts
685
+ var createEventsLog = (options) => {
686
+ const { file, maxBytes, warn } = options;
687
+ let queue = Promise.resolve();
688
+ return {
689
+ record: (event) => {
690
+ const line = JSON.stringify(event);
691
+ queue = queue.then(async () => {
692
+ try {
693
+ await file.append(line);
694
+ if (await file.size() > maxBytes) {
695
+ await file.rotate();
696
+ }
697
+ } catch (error) {
698
+ warn(`appduct daemon: failed to write events.log: ${error.message}`);
699
+ }
700
+ });
701
+ },
702
+ flush: () => queue
703
+ };
704
+ };
705
+
684
706
  // src/daemon/listener.ts
685
707
  import { createServer as createHttpsServer } from "node:https";
686
708
  import { isSessionClaimMessage, isSessionResumeMessage, isSessionBoundMessage } from "@appduct/shared";
@@ -1409,6 +1431,37 @@ var acquirePidfile = async (pidFilePath, options = {}) => {
1409
1431
  throw new Error(`Failed to acquire the Appduct daemon pidfile at "${pidFilePath}".`);
1410
1432
  };
1411
1433
 
1434
+ // src/daemon/node-append-only-file.ts
1435
+ import { open as open3, rename, stat as stat2 } from "node:fs/promises";
1436
+ var NodeAppendOnlyFile = class {
1437
+ constructor(path) {
1438
+ this.path = path;
1439
+ }
1440
+ async append(line) {
1441
+ const handle = await open3(this.path, "a", 384);
1442
+ try {
1443
+ await handle.appendFile(`${line}
1444
+ `);
1445
+ await handle.chmod(384);
1446
+ } finally {
1447
+ await handle.close();
1448
+ }
1449
+ }
1450
+ async size() {
1451
+ try {
1452
+ return (await stat2(this.path)).size;
1453
+ } catch (error) {
1454
+ if (error.code === "ENOENT") {
1455
+ return 0;
1456
+ }
1457
+ throw error;
1458
+ }
1459
+ }
1460
+ async rotate() {
1461
+ await rename(this.path, `${this.path}.1`);
1462
+ }
1463
+ };
1464
+
1412
1465
  // src/daemon/rpc-server.ts
1413
1466
  import { createServer } from "node:net";
1414
1467
  import { chmod as chmod2, rm as rm3 } from "node:fs/promises";
@@ -1594,6 +1647,7 @@ var getStateDirPaths = (stateDir) => {
1594
1647
  socketPath: join4(stateDir, "daemon.sock"),
1595
1648
  pidFilePath: join4(stateDir, "daemon.pid"),
1596
1649
  logFilePath: join4(stateDir, "daemon.log"),
1650
+ eventsLogPath: join4(stateDir, "events.log"),
1597
1651
  keyPath: join4(stateDir, "key.pem"),
1598
1652
  configPath: join4(stateDir, "config.json"),
1599
1653
  auditDir: join4(stateDir, "audit"),
@@ -1612,7 +1666,7 @@ var ensureStateDir = async (stateDir) => {
1612
1666
  };
1613
1667
 
1614
1668
  // src/daemon/tls.ts
1615
- import { stat as stat2, readFile as readFile3 } from "node:fs/promises";
1669
+ import { stat as stat3, readFile as readFile3 } from "node:fs/promises";
1616
1670
 
1617
1671
  // src/host-certificate.ts
1618
1672
  import "reflect-metadata";
@@ -1789,7 +1843,7 @@ var generateHostCertificate = async (keyPem, advertisedIp) => {
1789
1843
  // src/key-material.ts
1790
1844
  import { generateKeyPairSync } from "node:crypto";
1791
1845
  import { dirname as dirname2 } from "node:path";
1792
- import { mkdir as mkdir3, rename, rm as rm4, writeFile } from "node:fs/promises";
1846
+ import { mkdir as mkdir3, rename as rename2, rm as rm4, writeFile } from "node:fs/promises";
1793
1847
  var generatePrivateKeyPem = () => {
1794
1848
  const { privateKey } = generateKeyPairSync("rsa", {
1795
1849
  modulusLength: 2048,
@@ -1809,7 +1863,7 @@ var writePrivateKeyAtomically = async (path, keyPem) => {
1809
1863
  mode: 384,
1810
1864
  flag: "wx"
1811
1865
  });
1812
- await rename(temporaryPath, path);
1866
+ await rename2(temporaryPath, path);
1813
1867
  } catch (error) {
1814
1868
  await rm4(temporaryPath, { force: true }).catch(() => {
1815
1869
  });
@@ -1825,7 +1879,7 @@ var HostKeyError = class extends Error {
1825
1879
  }
1826
1880
  };
1827
1881
  var loadHostKeyPem = async (keyPath) => {
1828
- const fileStat = await stat2(keyPath);
1882
+ const fileStat = await stat3(keyPath);
1829
1883
  if ((fileStat.mode & 63) !== 0) {
1830
1884
  throw new HostKeyError(
1831
1885
  `Appduct host key at "${keyPath}" is group- or world-readable (mode ${(fileStat.mode & 511).toString(8)}). Refusing to load it \u2014 run "chmod 600 ${keyPath}" or re-run "appduct keygen".`
@@ -2030,14 +2084,6 @@ var asEventsSinceParams = (params) => {
2030
2084
  if (since !== void 0 && (typeof since !== "number" || !Number.isInteger(since) || since < 0)) {
2031
2085
  throw new RpcApplicationError("invalid_request", '"since" must be a non-negative integer.');
2032
2086
  }
2033
- const kindsRaw = record.kinds;
2034
- let kinds;
2035
- if (kindsRaw !== void 0) {
2036
- if (!Array.isArray(kindsRaw) || !kindsRaw.every((kind) => typeof kind === "string" && KNOWN_EVENT_KINDS.has(kind))) {
2037
- throw new RpcApplicationError("invalid_request", '"kinds" must be an array of known event kinds.');
2038
- }
2039
- kinds = kindsRaw;
2040
- }
2041
2087
  const limit = record.limit;
2042
2088
  if (limit !== void 0 && (typeof limit !== "number" || !Number.isInteger(limit) || limit <= 0)) {
2043
2089
  throw new RpcApplicationError("invalid_request", '"limit" must be a positive integer.');
@@ -2045,7 +2091,6 @@ var asEventsSinceParams = (params) => {
2045
2091
  return {
2046
2092
  selector,
2047
2093
  since,
2048
- kinds,
2049
2094
  limit
2050
2095
  };
2051
2096
  };
@@ -2080,10 +2125,19 @@ var startDaemon = async (options) => {
2080
2125
  retentionDays: config.auditRetentionDays,
2081
2126
  warn: options.warn
2082
2127
  });
2128
+ const eventsLog = createEventsLog({
2129
+ file: new NodeAppendOnlyFile(paths.eventsLogPath),
2130
+ maxBytes: config.eventsLogMaxBytes,
2131
+ warn: options.warn ?? ((message) => {
2132
+ process.stderr.write(`${message}
2133
+ `);
2134
+ })
2135
+ });
2083
2136
  let auditPruneInterval;
2084
2137
  let pidfile;
2085
2138
  let server;
2086
2139
  let listener;
2140
+ let tlsManager;
2087
2141
  let sessionManager;
2088
2142
  let callsManager;
2089
2143
  let eventBus;
@@ -2110,6 +2164,7 @@ var startDaemon = async (options) => {
2110
2164
  await pidfile?.release();
2111
2165
  await rm5(getSocketPath(paths), { force: true });
2112
2166
  await auditLogger.flush();
2167
+ await eventsLog.flush();
2113
2168
  } finally {
2114
2169
  resolveExited();
2115
2170
  }
@@ -2122,8 +2177,14 @@ var startDaemon = async (options) => {
2122
2177
  });
2123
2178
  eventBus = createEventBus({ clock, bufferSize: config.eventBufferSize });
2124
2179
  const activeEventBus = eventBus;
2180
+ activeEventBus.subscribe((event) => {
2181
+ if (event.kind !== "app_event") {
2182
+ eventsLog.record(event);
2183
+ }
2184
+ });
2125
2185
  const detectAddress = options.detectAddress ?? (() => detectAdvertisedAddress({ override: config.advertisedIp }));
2126
2186
  const tls = await createTlsManager({ keyPath: config.keyPath, detectAddress, warn: options.warn });
2187
+ tlsManager = tls;
2127
2188
  sessionManager = createSessionManager({
2128
2189
  graceSeconds: config.graceSeconds,
2129
2190
  keepaliveIntervalSeconds: config.keepaliveIntervalSeconds,
@@ -2367,9 +2428,9 @@ var startDaemon = async (options) => {
2367
2428
  return { ok: true };
2368
2429
  },
2369
2430
  [RPC_METHODS.eventsSince]: (params) => {
2370
- const { selector, since, kinds, limit } = asEventsSinceParams(params);
2431
+ const { selector, since, limit } = asEventsSinceParams(params);
2371
2432
  const resolved = activeSessionManager.describe(selector);
2372
- return activeEventBus.since(resolved.sessionId, { since, kinds, limit });
2433
+ return activeEventBus.since(resolved.sessionId, { since, limit });
2373
2434
  }
2374
2435
  }
2375
2436
  });
@@ -2395,6 +2456,7 @@ var startDaemon = async (options) => {
2395
2456
  startedAt,
2396
2457
  server,
2397
2458
  listener,
2459
+ tls: tlsManager,
2398
2460
  eventBus,
2399
2461
  exited,
2400
2462
  shutdown
@@ -22,6 +22,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
22
22
  "eventBufferSize",
23
23
  "auditRetentionDays",
24
24
  "daemonLogMaxBytes",
25
+ "eventsLogMaxBytes",
25
26
  "policy",
26
27
  "advertisedIp",
27
28
  "scheme",
@@ -81,6 +82,7 @@ var defaultConfig = (paths) => {
81
82
  eventBufferSize: 256,
82
83
  auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
83
84
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
85
+ eventsLogMaxBytes: 10 * 1024 * 1024,
84
86
  restartDaemonOnVersionMismatch: false,
85
87
  policy: {
86
88
  default: "allow",
@@ -143,6 +145,9 @@ var loadConfig = async (paths, options = {}) => {
143
145
  if (parsed.daemonLogMaxBytes !== void 0) {
144
146
  config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
145
147
  }
148
+ if (parsed.eventsLogMaxBytes !== void 0) {
149
+ config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
150
+ }
146
151
  if (parsed.restartDaemonOnVersionMismatch !== void 0) {
147
152
  config.restartDaemonOnVersionMismatch = requireBoolean(
148
153
  parsed.restartDaemonOnVersionMismatch,
@@ -243,6 +248,7 @@ var getStateDirPaths = (stateDir) => {
243
248
  socketPath: join(stateDir, "daemon.sock"),
244
249
  pidFilePath: join(stateDir, "daemon.pid"),
245
250
  logFilePath: join(stateDir, "daemon.log"),
251
+ eventsLogPath: join(stateDir, "events.log"),
246
252
  keyPath: join(stateDir, "key.pem"),
247
253
  configPath: join(stateDir, "config.json"),
248
254
  auditDir: join(stateDir, "audit"),
@@ -66,6 +66,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
66
66
  "eventBufferSize",
67
67
  "auditRetentionDays",
68
68
  "daemonLogMaxBytes",
69
+ "eventsLogMaxBytes",
69
70
  "policy",
70
71
  "advertisedIp",
71
72
  "scheme",
@@ -125,6 +126,7 @@ var defaultConfig = (paths) => {
125
126
  eventBufferSize: 256,
126
127
  auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
127
128
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
129
+ eventsLogMaxBytes: 10 * 1024 * 1024,
128
130
  restartDaemonOnVersionMismatch: false,
129
131
  policy: {
130
132
  default: "allow",
@@ -187,6 +189,9 @@ var loadConfig = async (paths, options = {}) => {
187
189
  if (parsed.daemonLogMaxBytes !== void 0) {
188
190
  config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
189
191
  }
192
+ if (parsed.eventsLogMaxBytes !== void 0) {
193
+ config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
194
+ }
190
195
  if (parsed.restartDaemonOnVersionMismatch !== void 0) {
191
196
  config.restartDaemonOnVersionMismatch = requireBoolean(
192
197
  parsed.restartDaemonOnVersionMismatch,
@@ -287,6 +292,7 @@ var getStateDirPaths = (stateDir) => {
287
292
  socketPath: join3(stateDir, "daemon.sock"),
288
293
  pidFilePath: join3(stateDir, "daemon.pid"),
289
294
  logFilePath: join3(stateDir, "daemon.log"),
295
+ eventsLogPath: join3(stateDir, "events.log"),
290
296
  keyPath: join3(stateDir, "key.pem"),
291
297
  configPath: join3(stateDir, "config.json"),
292
298
  auditDir: join3(stateDir, "audit"),
@@ -49,6 +49,7 @@ var getStateDirPaths = (stateDir) => {
49
49
  socketPath: join(stateDir, "daemon.sock"),
50
50
  pidFilePath: join(stateDir, "daemon.pid"),
51
51
  logFilePath: join(stateDir, "daemon.log"),
52
+ eventsLogPath: join(stateDir, "events.log"),
52
53
  keyPath: join(stateDir, "key.pem"),
53
54
  configPath: join(stateDir, "config.json"),
54
55
  auditDir: join(stateDir, "audit"),
@@ -84,6 +85,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
84
85
  "eventBufferSize",
85
86
  "auditRetentionDays",
86
87
  "daemonLogMaxBytes",
88
+ "eventsLogMaxBytes",
87
89
  "policy",
88
90
  "advertisedIp",
89
91
  "scheme",
@@ -143,6 +145,7 @@ var defaultConfig = (paths) => {
143
145
  eventBufferSize: 256,
144
146
  auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
145
147
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
148
+ eventsLogMaxBytes: 10 * 1024 * 1024,
146
149
  restartDaemonOnVersionMismatch: false,
147
150
  policy: {
148
151
  default: "allow",
@@ -205,6 +208,9 @@ var loadConfig = async (paths, options = {}) => {
205
208
  if (parsed.daemonLogMaxBytes !== void 0) {
206
209
  config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
207
210
  }
211
+ if (parsed.eventsLogMaxBytes !== void 0) {
212
+ config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
213
+ }
208
214
  if (parsed.restartDaemonOnVersionMismatch !== void 0) {
209
215
  config.restartDaemonOnVersionMismatch = requireBoolean(
210
216
  parsed.restartDaemonOnVersionMismatch,
@@ -22,6 +22,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
22
22
  "eventBufferSize",
23
23
  "auditRetentionDays",
24
24
  "daemonLogMaxBytes",
25
+ "eventsLogMaxBytes",
25
26
  "policy",
26
27
  "advertisedIp",
27
28
  "scheme",
@@ -81,6 +82,7 @@ var defaultConfig = (paths) => {
81
82
  eventBufferSize: 256,
82
83
  auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
83
84
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
85
+ eventsLogMaxBytes: 10 * 1024 * 1024,
84
86
  restartDaemonOnVersionMismatch: false,
85
87
  policy: {
86
88
  default: "allow",
@@ -143,6 +145,9 @@ var loadConfig = async (paths, options = {}) => {
143
145
  if (parsed.daemonLogMaxBytes !== void 0) {
144
146
  config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
145
147
  }
148
+ if (parsed.eventsLogMaxBytes !== void 0) {
149
+ config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
150
+ }
146
151
  if (parsed.restartDaemonOnVersionMismatch !== void 0) {
147
152
  config.restartDaemonOnVersionMismatch = requireBoolean(
148
153
  parsed.restartDaemonOnVersionMismatch,
@@ -243,6 +248,7 @@ var getStateDirPaths = (stateDir) => {
243
248
  socketPath: join(stateDir, "daemon.sock"),
244
249
  pidFilePath: join(stateDir, "daemon.pid"),
245
250
  logFilePath: join(stateDir, "daemon.log"),
251
+ eventsLogPath: join(stateDir, "events.log"),
246
252
  keyPath: join(stateDir, "key.pem"),
247
253
  configPath: join(stateDir, "config.json"),
248
254
  auditDir: join(stateDir, "audit"),
@@ -1028,7 +1034,8 @@ var handleEventsCommand = async (options, context) => {
1028
1034
  const stream = await openDaemonStream({ stateDir: context.stateDir, spawn: context.spawn });
1029
1035
  try {
1030
1036
  await stream.call(RPC_METHODS2.eventsSubscribe, {
1031
- sessionSelector: options.selector
1037
+ sessionSelector: options.selector,
1038
+ kinds: ["app_event"]
1032
1039
  });
1033
1040
  } catch (error) {
1034
1041
  stream.close();
@@ -42,6 +42,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
42
42
  "eventBufferSize",
43
43
  "auditRetentionDays",
44
44
  "daemonLogMaxBytes",
45
+ "eventsLogMaxBytes",
45
46
  "policy",
46
47
  "advertisedIp",
47
48
  "scheme",
@@ -101,6 +102,7 @@ var defaultConfig = (paths) => {
101
102
  eventBufferSize: 256,
102
103
  auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
103
104
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
105
+ eventsLogMaxBytes: 10 * 1024 * 1024,
104
106
  restartDaemonOnVersionMismatch: false,
105
107
  policy: {
106
108
  default: "allow",
@@ -163,6 +165,9 @@ var loadConfig = async (paths, options = {}) => {
163
165
  if (parsed.daemonLogMaxBytes !== void 0) {
164
166
  config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
165
167
  }
168
+ if (parsed.eventsLogMaxBytes !== void 0) {
169
+ config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
170
+ }
166
171
  if (parsed.restartDaemonOnVersionMismatch !== void 0) {
167
172
  config.restartDaemonOnVersionMismatch = requireBoolean(
168
173
  parsed.restartDaemonOnVersionMismatch,
@@ -263,6 +268,7 @@ var getStateDirPaths = (stateDir) => {
263
268
  socketPath: join(stateDir, "daemon.sock"),
264
269
  pidFilePath: join(stateDir, "daemon.pid"),
265
270
  logFilePath: join(stateDir, "daemon.log"),
271
+ eventsLogPath: join(stateDir, "events.log"),
266
272
  keyPath: join(stateDir, "key.pem"),
267
273
  configPath: join(stateDir, "config.json"),
268
274
  auditDir: join(stateDir, "audit"),
@@ -9,6 +9,7 @@ var getStateDirPaths = (stateDir) => {
9
9
  socketPath: join(stateDir, "daemon.sock"),
10
10
  pidFilePath: join(stateDir, "daemon.pid"),
11
11
  logFilePath: join(stateDir, "daemon.log"),
12
+ eventsLogPath: join(stateDir, "events.log"),
12
13
  keyPath: join(stateDir, "key.pem"),
13
14
  configPath: join(stateDir, "config.json"),
14
15
  auditDir: join(stateDir, "audit"),
@@ -25,6 +25,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
25
25
  "eventBufferSize",
26
26
  "auditRetentionDays",
27
27
  "daemonLogMaxBytes",
28
+ "eventsLogMaxBytes",
28
29
  "policy",
29
30
  "advertisedIp",
30
31
  "scheme",
@@ -84,6 +85,7 @@ var defaultConfig = (paths) => {
84
85
  eventBufferSize: 256,
85
86
  auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
86
87
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
88
+ eventsLogMaxBytes: 10 * 1024 * 1024,
87
89
  restartDaemonOnVersionMismatch: false,
88
90
  policy: {
89
91
  default: "allow",
@@ -146,6 +148,9 @@ var loadConfig = async (paths, options = {}) => {
146
148
  if (parsed.daemonLogMaxBytes !== void 0) {
147
149
  config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
148
150
  }
151
+ if (parsed.eventsLogMaxBytes !== void 0) {
152
+ config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
153
+ }
149
154
  if (parsed.restartDaemonOnVersionMismatch !== void 0) {
150
155
  config.restartDaemonOnVersionMismatch = requireBoolean(
151
156
  parsed.restartDaemonOnVersionMismatch,
@@ -246,6 +251,7 @@ var getStateDirPaths = (stateDir) => {
246
251
  socketPath: join(stateDir, "daemon.sock"),
247
252
  pidFilePath: join(stateDir, "daemon.pid"),
248
253
  logFilePath: join(stateDir, "daemon.log"),
254
+ eventsLogPath: join(stateDir, "events.log"),
249
255
  keyPath: join(stateDir, "key.pem"),
250
256
  configPath: join(stateDir, "config.json"),
251
257
  auditDir: join(stateDir, "audit"),
@@ -22,6 +22,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
22
22
  "eventBufferSize",
23
23
  "auditRetentionDays",
24
24
  "daemonLogMaxBytes",
25
+ "eventsLogMaxBytes",
25
26
  "policy",
26
27
  "advertisedIp",
27
28
  "scheme",
@@ -81,6 +82,7 @@ var defaultConfig = (paths) => {
81
82
  eventBufferSize: 256,
82
83
  auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
83
84
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
85
+ eventsLogMaxBytes: 10 * 1024 * 1024,
84
86
  restartDaemonOnVersionMismatch: false,
85
87
  policy: {
86
88
  default: "allow",
@@ -143,6 +145,9 @@ var loadConfig = async (paths, options = {}) => {
143
145
  if (parsed.daemonLogMaxBytes !== void 0) {
144
146
  config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
145
147
  }
148
+ if (parsed.eventsLogMaxBytes !== void 0) {
149
+ config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
150
+ }
146
151
  if (parsed.restartDaemonOnVersionMismatch !== void 0) {
147
152
  config.restartDaemonOnVersionMismatch = requireBoolean(
148
153
  parsed.restartDaemonOnVersionMismatch,
@@ -243,6 +248,7 @@ var getStateDirPaths = (stateDir) => {
243
248
  socketPath: join(stateDir, "daemon.sock"),
244
249
  pidFilePath: join(stateDir, "daemon.pid"),
245
250
  logFilePath: join(stateDir, "daemon.log"),
251
+ eventsLogPath: join(stateDir, "events.log"),
246
252
  keyPath: join(stateDir, "key.pem"),
247
253
  configPath: join(stateDir, "config.json"),
248
254
  auditDir: join(stateDir, "audit"),
@@ -12,6 +12,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
12
12
  "eventBufferSize",
13
13
  "auditRetentionDays",
14
14
  "daemonLogMaxBytes",
15
+ "eventsLogMaxBytes",
15
16
  "policy",
16
17
  "advertisedIp",
17
18
  "scheme",
@@ -71,6 +72,7 @@ var defaultConfig = (paths) => {
71
72
  eventBufferSize: 256,
72
73
  auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
73
74
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
75
+ eventsLogMaxBytes: 10 * 1024 * 1024,
74
76
  restartDaemonOnVersionMismatch: false,
75
77
  policy: {
76
78
  default: "allow",
@@ -133,6 +135,9 @@ var loadConfig = async (paths, options = {}) => {
133
135
  if (parsed.daemonLogMaxBytes !== void 0) {
134
136
  config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
135
137
  }
138
+ if (parsed.eventsLogMaxBytes !== void 0) {
139
+ config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
140
+ }
136
141
  if (parsed.restartDaemonOnVersionMismatch !== void 0) {
137
142
  config.restartDaemonOnVersionMismatch = requireBoolean(
138
143
  parsed.restartDaemonOnVersionMismatch,
@@ -178,6 +183,7 @@ var getStateDirPaths = (stateDir) => {
178
183
  socketPath: join(stateDir, "daemon.sock"),
179
184
  pidFilePath: join(stateDir, "daemon.pid"),
180
185
  logFilePath: join(stateDir, "daemon.log"),
186
+ eventsLogPath: join(stateDir, "events.log"),
181
187
  keyPath: join(stateDir, "key.pem"),
182
188
  configPath: join(stateDir, "config.json"),
183
189
  auditDir: join(stateDir, "audit"),
@@ -2650,27 +2656,24 @@ var resolveAppTool = async (call, selector, name) => {
2650
2656
 
2651
2657
  // src/mcp/events-tool.ts
2652
2658
  import {
2653
- RPC_METHODS as RPC_METHODS5,
2654
- EVENT_KINDS
2659
+ RPC_METHODS as RPC_METHODS5
2655
2660
  } from "@appduct/shared";
2656
2661
  var EVENTS_TOOL_NAME = "appduct_events";
2657
2662
  var WAIT_FOR_EVENT_TOOL_NAME = "appduct_wait_for_event";
2658
2663
  var DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MS = 12e4;
2659
2664
  var MAX_WAIT_FOR_EVENT_TIMEOUT_MS = 25 * 60 * 1e3;
2660
2665
  var WAIT_FOR_EVENT_PROGRESS_INTERVAL_MS = 6e4;
2661
- var KNOWN_EVENT_KINDS = new Set(EVENT_KINDS);
2662
2666
  var clampWaitForEventTimeoutMs = (requestedMs) => {
2663
2667
  return Math.min(requestedMs ?? DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MS, MAX_WAIT_FOR_EVENT_TIMEOUT_MS);
2664
2668
  };
2665
2669
  var EVENTS_TOOL_DESCRIPTOR = {
2666
2670
  name: EVENTS_TOOL_NAME,
2667
- description: "Drain events retained in the daemon's per-session ring buffer since a cursor \u2014 the pull counterpart to a live subscription, for checking what happened after calling a tool or triggering app behavior. With no selector, targets the sole active/suspended session. Returns { events, cursor }; pass cursor back as since on the next call to avoid re-reading events you've already seen. limit (if given) keeps the OLDEST events in the window and advances cursor only past what was actually returned, so repeated calls page forward through everything retained rather than skipping ahead.",
2671
+ description: "Drain the events the app posted with postEvent(name, payload) that the daemon retained for a session since a cursor, for checking what the app reported after calling a tool or triggering app behavior. With no selector, targets the sole active/suspended session. Returns { events, cursor }; pass cursor back as since on the next call to avoid re-reading events you've already seen. limit (if given) keeps the OLDEST events in the window and advances cursor only past what was actually returned, so repeated calls page forward through everything retained rather than skipping ahead.",
2668
2672
  inputSchema: {
2669
2673
  type: "object",
2670
2674
  properties: {
2671
2675
  selector: { type: "string" },
2672
2676
  since: { type: "integer", minimum: 0 },
2673
- kinds: { type: "array", items: { type: "string", enum: EVENT_KINDS } },
2674
2677
  limit: { type: "integer", exclusiveMinimum: 0 }
2675
2678
  },
2676
2679
  additionalProperties: false
@@ -2734,14 +2737,13 @@ var asOptionalPositiveNumber2 = (value, field) => {
2734
2737
  }
2735
2738
  return value;
2736
2739
  };
2737
- var asOptionalEventKinds = (value) => {
2738
- if (value === void 0) {
2739
- return void 0;
2740
- }
2741
- if (!Array.isArray(value) || !value.every((kind) => typeof kind === "string" && KNOWN_EVENT_KINDS.has(kind))) {
2742
- throw new McpBuiltinToolError("invalid_request", '"kinds" must be an array of known event kinds.');
2740
+ var rejectKinds = (args) => {
2741
+ if (args.kinds !== void 0) {
2742
+ throw new McpBuiltinToolError(
2743
+ "invalid_request",
2744
+ '"kinds" is not supported: these tools return only the events the app posted with postEvent.'
2745
+ );
2743
2746
  }
2744
- return value;
2745
2747
  };
2746
2748
  var isMatchPrimitive = (value) => {
2747
2749
  return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
@@ -2766,10 +2768,10 @@ var asOptionalMatch = (value) => {
2766
2768
  };
2767
2769
  var handleEventsTool = async (rawArgs, deps) => {
2768
2770
  const args = asRecord3(rawArgs);
2771
+ rejectKinds(args);
2769
2772
  return deps.call(RPC_METHODS5.eventsSince, {
2770
2773
  selector: asOptionalString2(args.selector, "selector"),
2771
2774
  since: asOptionalNonNegativeInteger(args.since, "since"),
2772
- kinds: asOptionalEventKinds(args.kinds),
2773
2775
  limit: asOptionalPositiveInteger(args.limit, "limit")
2774
2776
  });
2775
2777
  };
@@ -2782,6 +2784,7 @@ var payloadShallowMatches = (payload, match) => {
2782
2784
  };
2783
2785
  var handleWaitForEventTool = async (rawArgs, deps) => {
2784
2786
  const args = asRecord3(rawArgs);
2787
+ rejectKinds(args);
2785
2788
  const selector = asOptionalString2(args.selector, "selector");
2786
2789
  const name = asOptionalString2(args.name, "name");
2787
2790
  if (!name) {
@@ -2824,8 +2827,7 @@ var handleWaitForEventTool = async (rawArgs, deps) => {
2824
2827
  await stream.call(RPC_METHODS5.eventsSubscribe, { sessionSelector: sessionId, kinds: ["app_event"] });
2825
2828
  const sinceResult = await stream.call(RPC_METHODS5.eventsSince, {
2826
2829
  selector: sessionId,
2827
- since,
2828
- kinds: ["app_event"]
2830
+ since
2829
2831
  });
2830
2832
  const seenSeqs = /* @__PURE__ */ new Set();
2831
2833
  const backlog = [];
@@ -22,6 +22,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
22
22
  "eventBufferSize",
23
23
  "auditRetentionDays",
24
24
  "daemonLogMaxBytes",
25
+ "eventsLogMaxBytes",
25
26
  "policy",
26
27
  "advertisedIp",
27
28
  "scheme",
@@ -81,6 +82,7 @@ var defaultConfig = (paths) => {
81
82
  eventBufferSize: 256,
82
83
  auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
83
84
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
85
+ eventsLogMaxBytes: 10 * 1024 * 1024,
84
86
  restartDaemonOnVersionMismatch: false,
85
87
  policy: {
86
88
  default: "allow",
@@ -143,6 +145,9 @@ var loadConfig = async (paths, options = {}) => {
143
145
  if (parsed.daemonLogMaxBytes !== void 0) {
144
146
  config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
145
147
  }
148
+ if (parsed.eventsLogMaxBytes !== void 0) {
149
+ config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
150
+ }
146
151
  if (parsed.restartDaemonOnVersionMismatch !== void 0) {
147
152
  config.restartDaemonOnVersionMismatch = requireBoolean(
148
153
  parsed.restartDaemonOnVersionMismatch,
@@ -243,6 +248,7 @@ var getStateDirPaths = (stateDir) => {
243
248
  socketPath: join(stateDir, "daemon.sock"),
244
249
  pidFilePath: join(stateDir, "daemon.pid"),
245
250
  logFilePath: join(stateDir, "daemon.log"),
251
+ eventsLogPath: join(stateDir, "events.log"),
246
252
  keyPath: join(stateDir, "key.pem"),
247
253
  configPath: join(stateDir, "config.json"),
248
254
  auditDir: join(stateDir, "audit"),
@@ -25,6 +25,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
25
25
  "eventBufferSize",
26
26
  "auditRetentionDays",
27
27
  "daemonLogMaxBytes",
28
+ "eventsLogMaxBytes",
28
29
  "policy",
29
30
  "advertisedIp",
30
31
  "scheme",
@@ -84,6 +85,7 @@ var defaultConfig = (paths) => {
84
85
  eventBufferSize: 256,
85
86
  auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
86
87
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
88
+ eventsLogMaxBytes: 10 * 1024 * 1024,
87
89
  restartDaemonOnVersionMismatch: false,
88
90
  policy: {
89
91
  default: "allow",
@@ -146,6 +148,9 @@ var loadConfig = async (paths, options = {}) => {
146
148
  if (parsed.daemonLogMaxBytes !== void 0) {
147
149
  config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
148
150
  }
151
+ if (parsed.eventsLogMaxBytes !== void 0) {
152
+ config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
153
+ }
149
154
  if (parsed.restartDaemonOnVersionMismatch !== void 0) {
150
155
  config.restartDaemonOnVersionMismatch = requireBoolean(
151
156
  parsed.restartDaemonOnVersionMismatch,
@@ -246,6 +251,7 @@ var getStateDirPaths = (stateDir) => {
246
251
  socketPath: join(stateDir, "daemon.sock"),
247
252
  pidFilePath: join(stateDir, "daemon.pid"),
248
253
  logFilePath: join(stateDir, "daemon.log"),
254
+ eventsLogPath: join(stateDir, "events.log"),
249
255
  keyPath: join(stateDir, "key.pem"),
250
256
  configPath: join(stateDir, "config.json"),
251
257
  auditDir: join(stateDir, "audit"),
@@ -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,
@@ -1134,8 +1140,7 @@ var makeAppClient = (stream, sessionId) => {
1134
1140
  const result = await stream.call(RPC_METHODS2.eventsSince, {
1135
1141
  selector: sessionId,
1136
1142
  since: options.since,
1137
- limit: options.limit,
1138
- kinds: ["app_event"]
1143
+ limit: options.limit
1139
1144
  });
1140
1145
  return {
1141
1146
  events: result.events.map((event) => appEventFrom(event, sessionId)).filter((event) => event !== void 0),
@@ -1194,8 +1199,7 @@ var makeAppClient = (stream, sessionId) => {
1194
1199
  await stream.call(RPC_METHODS2.eventsSubscribe, { sessionSelector: sessionId, kinds: ["app_event"] });
1195
1200
  const sinceResult = await stream.call(RPC_METHODS2.eventsSince, {
1196
1201
  selector: sessionId,
1197
- since: options.since,
1198
- kinds: ["app_event"]
1202
+ since: options.since
1199
1203
  });
1200
1204
  const seenSeqs = /* @__PURE__ */ new Set();
1201
1205
  const backlog = [];
@@ -1,6 +1,7 @@
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
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
4
5
  * the one command whose output isn't a single rendered `CliResult` — each event is written as it
5
6
  * arrives (NDJSON under `--json`, a human line otherwise) — so it plugs into `executeHostedCommand`
6
7
  * with a live reporter that suppresses the default one-shot bootstrap render (`cli/runner.ts`).
@@ -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
@@ -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>;
@@ -4,9 +4,10 @@
4
4
  * per-session retention ring buffer, and must never let a throwing subscriber take down the daemon
5
5
  * or another subscriber.
6
6
  *
7
- * Retention (issue #6): every session-scoped event (`sessionId` set), except the high-frequency
8
- * kinds in `EXCLUDED_FROM_RETENTION`, is appended to a per-session ring buffer with a
9
- * monotonically increasing `seq`, capped at `bufferSize` entries (oldest dropped first). A
7
+ * Retention (issue #6, #98): every session-scoped event gets a monotonically increasing per-session
8
+ * `seq`, but only `app_event` is appended to the per-session ring buffer, capped at `bufferSize`
9
+ * entries (oldest dropped first). Appduct's own kinds (lifecycle, tool calls) are fanned out live
10
+ * and never retained, so no number of tool calls can evict an app event. A
10
11
  * session's buffer is discarded the moment it emits a terminal event (`session_expired` /
11
12
  * `session_revoked`) — "terminal states free the alias" (sessions.ts) applies to retained events
12
13
  * too, matching "no persisted history" (ARCHITECTURE.md §3/§13) — or via an explicit `drop()` for
@@ -22,7 +23,6 @@ export type EventBusEmitInput = Omit<EventNotification, "ts" | "seq"> & {
22
23
  export type EventBusListener = (event: EventNotification) => void;
23
24
  export type EventsSinceQuery = {
24
25
  since?: number;
25
- kinds?: EventKind[];
26
26
  limit?: number;
27
27
  };
28
28
  export type EventsSinceQueryResult = {
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The daemon's own event log, `<stateDir>/events.log` (ARCHITECTURE.md §3): one JSON line per
3
+ * event, for debugging Appduct itself. The daemon decides which events to record; this module only
4
+ * writes them and keeps the file bounded.
5
+ *
6
+ * Writes are serialized on a promise queue, so {@link EventsLog.record} returns at once and never
7
+ * blocks the emitter. After each append the file is rotated to a single backup once it is over
8
+ * `maxBytes`, so the current file is never above the cap once a write has settled. A failed write
9
+ * or rotation is warned, never thrown, and never stops later writes.
10
+ */
11
+ import type { EventNotification } from "@appduct/shared";
12
+ /** A file that only ever grows by whole lines, with one backup slot. */
13
+ export type AppendOnlyFile = {
14
+ /** Appends `line` plus a newline, creating the file at mode `0600` if it is missing. */
15
+ append(line: string): Promise<void>;
16
+ /** Current size in bytes; `0` when the file does not exist. */
17
+ size(): Promise<number>;
18
+ /** Moves the file to the backup slot, replacing any previous backup. */
19
+ rotate(): Promise<void>;
20
+ };
21
+ export type EventsLog = {
22
+ /** Queues `event` as one JSON line; returns immediately. */
23
+ record(event: EventNotification): void;
24
+ /** Resolves once every write queued so far has settled. */
25
+ flush(): Promise<void>;
26
+ };
27
+ export declare const createEventsLog: (options: {
28
+ file: AppendOnlyFile;
29
+ maxBytes: number;
30
+ warn: (message: string) => void;
31
+ }) => EventsLog;
@@ -0,0 +1,17 @@
1
+ /** In-memory {@link AppendOnlyFile} for tests: inspect the lines written and the backup, or make
2
+ * appends fail. */
3
+ import type { AppendOnlyFile } from "./events-log.js";
4
+ export declare class MemoryAppendOnlyFile implements AppendOnlyFile {
5
+ private current;
6
+ private backup;
7
+ private appendError;
8
+ append(line: string): Promise<void>;
9
+ size(): Promise<number>;
10
+ rotate(): Promise<void>;
11
+ /** Lines in the current file, without their newlines. */
12
+ lines(): string[];
13
+ /** Lines in the backup, without their newlines. */
14
+ backupLines(): string[];
15
+ /** Makes every append throw `error` until called again with `undefined`. */
16
+ failAppendsWith(error: Error | undefined): void;
17
+ }
@@ -0,0 +1,9 @@
1
+ /** {@link AppendOnlyFile} on the real filesystem; the backup is `<path>.1`. */
2
+ import type { AppendOnlyFile } from "./events-log.js";
3
+ export declare class NodeAppendOnlyFile implements AppendOnlyFile {
4
+ private readonly path;
5
+ constructor(path: string);
6
+ append(line: string): Promise<void>;
7
+ size(): Promise<number>;
8
+ rotate(): Promise<void>;
9
+ }
@@ -11,6 +11,8 @@ export type StateDirPaths = {
11
11
  socketPath: string;
12
12
  pidFilePath: string;
13
13
  logFilePath: string;
14
+ /** The daemon's own event log; rotated to `<eventsLogPath>.1`. */
15
+ eventsLogPath: string;
14
16
  keyPath: string;
15
17
  configPath: string;
16
18
  auditDir: string;
package/dist/index.js CHANGED
@@ -140,7 +140,7 @@ var createCli = () => {
140
140
  "--timeout <ms>",
141
141
  "Call timeout in milliseconds. Shortens the deadline; it cannot extend one past the app's own timer, which is the tool's declared timeoutMs (else 10000)."
142
142
  );
143
- cli.command("events [selector]", "Stream session/tool events until interrupted.").option("--follow", "Accepted for script readability; the default behavior already follows.").option("--since <cursor>", "One-shot: print events retained since this cursor instead of streaming live.");
143
+ cli.command("events [selector]", "Stream the events the app posts until interrupted.").option("--follow", "Accepted for script readability; the default behavior already follows.").option("--since <cursor>", "One-shot: print the app events retained since this cursor instead of streaming live.");
144
144
  cli.command("revoke [selector]", "Revoke a session.");
145
145
  cli.command("mcp", "Start a stdio MCP server that gives MCP clients access to connected apps' tools.").option(
146
146
  "--scheme <scheme>",
@@ -164,6 +164,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
164
164
  "eventBufferSize",
165
165
  "auditRetentionDays",
166
166
  "daemonLogMaxBytes",
167
+ "eventsLogMaxBytes",
167
168
  "policy",
168
169
  "advertisedIp",
169
170
  "scheme",
@@ -223,6 +224,7 @@ var defaultConfig = (paths) => {
223
224
  eventBufferSize: 256,
224
225
  auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
225
226
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
227
+ eventsLogMaxBytes: 10 * 1024 * 1024,
226
228
  restartDaemonOnVersionMismatch: false,
227
229
  policy: {
228
230
  default: "allow",
@@ -285,6 +287,9 @@ var loadConfig = async (paths, options = {}) => {
285
287
  if (parsed.daemonLogMaxBytes !== void 0) {
286
288
  config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
287
289
  }
290
+ if (parsed.eventsLogMaxBytes !== void 0) {
291
+ config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
292
+ }
288
293
  if (parsed.restartDaemonOnVersionMismatch !== void 0) {
289
294
  config.restartDaemonOnVersionMismatch = requireBoolean(
290
295
  parsed.restartDaemonOnVersionMismatch,
@@ -333,6 +338,7 @@ var getStateDirPaths = (stateDir) => {
333
338
  socketPath: join3(stateDir, "daemon.sock"),
334
339
  pidFilePath: join3(stateDir, "daemon.pid"),
335
340
  logFilePath: join3(stateDir, "daemon.log"),
341
+ eventsLogPath: join3(stateDir, "events.log"),
336
342
  keyPath: join3(stateDir, "key.pem"),
337
343
  configPath: join3(stateDir, "config.json"),
338
344
  auditDir: join3(stateDir, "audit"),
@@ -38,13 +38,6 @@ export declare const EVENTS_TOOL_DESCRIPTOR: {
38
38
  readonly type: "integer";
39
39
  readonly minimum: 0;
40
40
  };
41
- readonly kinds: {
42
- readonly type: "array";
43
- readonly items: {
44
- readonly type: "string";
45
- readonly enum: readonly ["daemon_started", "link_created", "link_expired", "session_claimed", "session_suspended", "session_resumed", "session_revoked", "session_expired", "tools_changed", "app_event", "tool_call_started", "tool_call_progress", "tool_call_finished"];
46
- };
47
- };
48
41
  readonly limit: {
49
42
  readonly type: "integer";
50
43
  readonly exclusiveMinimum: 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appduct",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Driving React Native tools without in-app debug UI.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -45,7 +45,7 @@
45
45
  "reflect-metadata": "^0.2.2",
46
46
  "toqr": "^0.1.1",
47
47
  "ws": "^8.18.3",
48
- "@appduct/shared": "0.11.1"
48
+ "@appduct/shared": "0.12.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "^22.10.0",