appduct 0.11.0 → 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.
@@ -9,7 +9,7 @@ import { join as join3 } from "node:path";
9
9
  import { RPC_METHODS } from "@appduct/shared";
10
10
 
11
11
  // src/daemon/log-rotation.ts
12
- import { chmod, rename, stat } from "node:fs/promises";
12
+ import { chmod as chmod2, rename, stat } from "node:fs/promises";
13
13
 
14
14
  // src/daemon/config.ts
15
15
  import { readFile } from "node:fs/promises";
@@ -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,
@@ -236,18 +241,27 @@ var isSocketConnectable = (socketPath) => {
236
241
 
237
242
  // src/daemon/state-dir.ts
238
243
  import { join } from "node:path";
244
+ import { mkdir, chmod } from "node:fs/promises";
239
245
  var getStateDirPaths = (stateDir) => {
240
246
  return {
241
247
  root: stateDir,
242
248
  socketPath: join(stateDir, "daemon.sock"),
243
249
  pidFilePath: join(stateDir, "daemon.pid"),
244
250
  logFilePath: join(stateDir, "daemon.log"),
251
+ eventsLogPath: join(stateDir, "events.log"),
245
252
  keyPath: join(stateDir, "key.pem"),
246
253
  configPath: join(stateDir, "config.json"),
247
254
  auditDir: join(stateDir, "audit"),
248
255
  spawnLockPath: join(stateDir, "daemon.spawn.lock")
249
256
  };
250
257
  };
258
+ var ensureStateDir = async (stateDir) => {
259
+ const paths = getStateDirPaths(stateDir);
260
+ await mkdir(paths.auditDir, { recursive: true });
261
+ await chmod(paths.root, 448);
262
+ await chmod(paths.auditDir, 448);
263
+ return paths;
264
+ };
251
265
 
252
266
  // src/daemon/log-rotation.ts
253
267
  var STATE_FILE_MODE = 384;
@@ -280,7 +294,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
280
294
  const backupPath = daemonLogBackupPath(options.logFilePath);
281
295
  await rename(options.logFilePath, backupPath);
282
296
  try {
283
- await chmod(backupPath, STATE_FILE_MODE);
297
+ await chmod2(backupPath, STATE_FILE_MODE);
284
298
  } catch (error) {
285
299
  warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
286
300
  }
@@ -296,7 +310,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
296
310
  var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
297
311
  `)) => {
298
312
  try {
299
- await chmod(logFilePath, STATE_FILE_MODE);
313
+ await chmod2(logFilePath, STATE_FILE_MODE);
300
314
  return true;
301
315
  } catch (error) {
302
316
  warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
@@ -637,6 +651,7 @@ var acquireSpawnLock = async (lockPath) => {
637
651
  return false;
638
652
  };
639
653
  var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
654
+ await ensureStateDir(paths.root);
640
655
  const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
641
656
  let lockWaitTimedOut = false;
642
657
  if (acquiredLock) {
@@ -1019,7 +1034,8 @@ var handleEventsCommand = async (options, context) => {
1019
1034
  const stream = await openDaemonStream({ stateDir: context.stateDir, spawn: context.spawn });
1020
1035
  try {
1021
1036
  await stream.call(RPC_METHODS2.eventsSubscribe, {
1022
- sessionSelector: options.selector
1037
+ sessionSelector: options.selector,
1038
+ kinds: ["app_event"]
1023
1039
  });
1024
1040
  } catch (error) {
1025
1041
  stream.close();
@@ -29,7 +29,7 @@ import { join as join3 } from "node:path";
29
29
  import { RPC_METHODS } from "@appduct/shared";
30
30
 
31
31
  // src/daemon/log-rotation.ts
32
- import { chmod, rename, stat } from "node:fs/promises";
32
+ import { chmod as chmod2, rename, stat } from "node:fs/promises";
33
33
 
34
34
  // src/daemon/config.ts
35
35
  import { readFile } from "node:fs/promises";
@@ -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,
@@ -256,18 +261,27 @@ var isSocketConnectable = (socketPath) => {
256
261
 
257
262
  // src/daemon/state-dir.ts
258
263
  import { join } from "node:path";
264
+ import { mkdir, chmod } from "node:fs/promises";
259
265
  var getStateDirPaths = (stateDir) => {
260
266
  return {
261
267
  root: stateDir,
262
268
  socketPath: join(stateDir, "daemon.sock"),
263
269
  pidFilePath: join(stateDir, "daemon.pid"),
264
270
  logFilePath: join(stateDir, "daemon.log"),
271
+ eventsLogPath: join(stateDir, "events.log"),
265
272
  keyPath: join(stateDir, "key.pem"),
266
273
  configPath: join(stateDir, "config.json"),
267
274
  auditDir: join(stateDir, "audit"),
268
275
  spawnLockPath: join(stateDir, "daemon.spawn.lock")
269
276
  };
270
277
  };
278
+ var ensureStateDir = async (stateDir) => {
279
+ const paths = getStateDirPaths(stateDir);
280
+ await mkdir(paths.auditDir, { recursive: true });
281
+ await chmod(paths.root, 448);
282
+ await chmod(paths.auditDir, 448);
283
+ return paths;
284
+ };
271
285
 
272
286
  // src/daemon/log-rotation.ts
273
287
  var STATE_FILE_MODE = 384;
@@ -300,7 +314,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
300
314
  const backupPath = daemonLogBackupPath(options.logFilePath);
301
315
  await rename(options.logFilePath, backupPath);
302
316
  try {
303
- await chmod(backupPath, STATE_FILE_MODE);
317
+ await chmod2(backupPath, STATE_FILE_MODE);
304
318
  } catch (error) {
305
319
  warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
306
320
  }
@@ -316,7 +330,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
316
330
  var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
317
331
  `)) => {
318
332
  try {
319
- await chmod(logFilePath, STATE_FILE_MODE);
333
+ await chmod2(logFilePath, STATE_FILE_MODE);
320
334
  return true;
321
335
  } catch (error) {
322
336
  warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
@@ -657,6 +671,7 @@ var acquireSpawnLock = async (lockPath) => {
657
671
  return false;
658
672
  };
659
673
  var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
674
+ await ensureStateDir(paths.root);
660
675
  const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
661
676
  let lockWaitTimedOut = false;
662
677
  if (acquiredLock) {
@@ -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"),
@@ -12,7 +12,7 @@ import { join as join3 } from "node:path";
12
12
  import { RPC_METHODS } from "@appduct/shared";
13
13
 
14
14
  // src/daemon/log-rotation.ts
15
- import { chmod, rename, stat } from "node:fs/promises";
15
+ import { chmod as chmod2, rename, stat } from "node:fs/promises";
16
16
 
17
17
  // src/daemon/config.ts
18
18
  import { readFile } from "node:fs/promises";
@@ -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,
@@ -239,18 +244,27 @@ var isSocketConnectable = (socketPath) => {
239
244
 
240
245
  // src/daemon/state-dir.ts
241
246
  import { join } from "node:path";
247
+ import { mkdir, chmod } from "node:fs/promises";
242
248
  var getStateDirPaths = (stateDir) => {
243
249
  return {
244
250
  root: stateDir,
245
251
  socketPath: join(stateDir, "daemon.sock"),
246
252
  pidFilePath: join(stateDir, "daemon.pid"),
247
253
  logFilePath: join(stateDir, "daemon.log"),
254
+ eventsLogPath: join(stateDir, "events.log"),
248
255
  keyPath: join(stateDir, "key.pem"),
249
256
  configPath: join(stateDir, "config.json"),
250
257
  auditDir: join(stateDir, "audit"),
251
258
  spawnLockPath: join(stateDir, "daemon.spawn.lock")
252
259
  };
253
260
  };
261
+ var ensureStateDir = async (stateDir) => {
262
+ const paths = getStateDirPaths(stateDir);
263
+ await mkdir(paths.auditDir, { recursive: true });
264
+ await chmod(paths.root, 448);
265
+ await chmod(paths.auditDir, 448);
266
+ return paths;
267
+ };
254
268
 
255
269
  // src/daemon/log-rotation.ts
256
270
  var STATE_FILE_MODE = 384;
@@ -283,7 +297,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
283
297
  const backupPath = daemonLogBackupPath(options.logFilePath);
284
298
  await rename(options.logFilePath, backupPath);
285
299
  try {
286
- await chmod(backupPath, STATE_FILE_MODE);
300
+ await chmod2(backupPath, STATE_FILE_MODE);
287
301
  } catch (error) {
288
302
  warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
289
303
  }
@@ -299,7 +313,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
299
313
  var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
300
314
  `)) => {
301
315
  try {
302
- await chmod(logFilePath, STATE_FILE_MODE);
316
+ await chmod2(logFilePath, STATE_FILE_MODE);
303
317
  return true;
304
318
  } catch (error) {
305
319
  warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
@@ -640,6 +654,7 @@ var acquireSpawnLock = async (lockPath) => {
640
654
  return false;
641
655
  };
642
656
  var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
657
+ await ensureStateDir(paths.root);
643
658
  const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
644
659
  let lockWaitTimedOut = false;
645
660
  if (acquiredLock) {
@@ -9,7 +9,7 @@ import { join as join3 } from "node:path";
9
9
  import { RPC_METHODS } from "@appduct/shared";
10
10
 
11
11
  // src/daemon/log-rotation.ts
12
- import { chmod, rename, stat } from "node:fs/promises";
12
+ import { chmod as chmod2, rename, stat } from "node:fs/promises";
13
13
 
14
14
  // src/daemon/config.ts
15
15
  import { readFile } from "node:fs/promises";
@@ -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,
@@ -236,18 +241,27 @@ var isSocketConnectable = (socketPath) => {
236
241
 
237
242
  // src/daemon/state-dir.ts
238
243
  import { join } from "node:path";
244
+ import { mkdir, chmod } from "node:fs/promises";
239
245
  var getStateDirPaths = (stateDir) => {
240
246
  return {
241
247
  root: stateDir,
242
248
  socketPath: join(stateDir, "daemon.sock"),
243
249
  pidFilePath: join(stateDir, "daemon.pid"),
244
250
  logFilePath: join(stateDir, "daemon.log"),
251
+ eventsLogPath: join(stateDir, "events.log"),
245
252
  keyPath: join(stateDir, "key.pem"),
246
253
  configPath: join(stateDir, "config.json"),
247
254
  auditDir: join(stateDir, "audit"),
248
255
  spawnLockPath: join(stateDir, "daemon.spawn.lock")
249
256
  };
250
257
  };
258
+ var ensureStateDir = async (stateDir) => {
259
+ const paths = getStateDirPaths(stateDir);
260
+ await mkdir(paths.auditDir, { recursive: true });
261
+ await chmod(paths.root, 448);
262
+ await chmod(paths.auditDir, 448);
263
+ return paths;
264
+ };
251
265
 
252
266
  // src/daemon/log-rotation.ts
253
267
  var STATE_FILE_MODE = 384;
@@ -280,7 +294,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
280
294
  const backupPath = daemonLogBackupPath(options.logFilePath);
281
295
  await rename(options.logFilePath, backupPath);
282
296
  try {
283
- await chmod(backupPath, STATE_FILE_MODE);
297
+ await chmod2(backupPath, STATE_FILE_MODE);
284
298
  } catch (error) {
285
299
  warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
286
300
  }
@@ -296,7 +310,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
296
310
  var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
297
311
  `)) => {
298
312
  try {
299
- await chmod(logFilePath, STATE_FILE_MODE);
313
+ await chmod2(logFilePath, STATE_FILE_MODE);
300
314
  return true;
301
315
  } catch (error) {
302
316
  warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
@@ -637,6 +651,7 @@ var acquireSpawnLock = async (lockPath) => {
637
651
  return false;
638
652
  };
639
653
  var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
654
+ await ensureStateDir(paths.root);
640
655
  const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
641
656
  let lockWaitTimedOut = false;
642
657
  if (acquiredLock) {
@@ -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,
@@ -171,18 +176,27 @@ var loadConfig = async (paths, options = {}) => {
171
176
 
172
177
  // src/daemon/state-dir.ts
173
178
  import { join } from "node:path";
179
+ import { mkdir, chmod } from "node:fs/promises";
174
180
  var getStateDirPaths = (stateDir) => {
175
181
  return {
176
182
  root: stateDir,
177
183
  socketPath: join(stateDir, "daemon.sock"),
178
184
  pidFilePath: join(stateDir, "daemon.pid"),
179
185
  logFilePath: join(stateDir, "daemon.log"),
186
+ eventsLogPath: join(stateDir, "events.log"),
180
187
  keyPath: join(stateDir, "key.pem"),
181
188
  configPath: join(stateDir, "config.json"),
182
189
  auditDir: join(stateDir, "audit"),
183
190
  spawnLockPath: join(stateDir, "daemon.spawn.lock")
184
191
  };
185
192
  };
193
+ var ensureStateDir = async (stateDir) => {
194
+ const paths = getStateDirPaths(stateDir);
195
+ await mkdir(paths.auditDir, { recursive: true });
196
+ await chmod(paths.root, 448);
197
+ await chmod(paths.auditDir, 448);
198
+ return paths;
199
+ };
186
200
 
187
201
  // src/mcp/server.ts
188
202
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -267,7 +281,7 @@ import { join as join4 } from "node:path";
267
281
  import { RPC_METHODS } from "@appduct/shared";
268
282
 
269
283
  // src/daemon/log-rotation.ts
270
- import { chmod, rename, stat } from "node:fs/promises";
284
+ import { chmod as chmod2, rename, stat } from "node:fs/promises";
271
285
 
272
286
  // src/daemon/pidfile.ts
273
287
  import { readFileSync as readFileSync3 } from "node:fs";
@@ -355,7 +369,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
355
369
  const backupPath = daemonLogBackupPath(options.logFilePath);
356
370
  await rename(options.logFilePath, backupPath);
357
371
  try {
358
- await chmod(backupPath, STATE_FILE_MODE);
372
+ await chmod2(backupPath, STATE_FILE_MODE);
359
373
  } catch (error) {
360
374
  warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
361
375
  }
@@ -371,7 +385,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
371
385
  var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
372
386
  `)) => {
373
387
  try {
374
- await chmod(logFilePath, STATE_FILE_MODE);
388
+ await chmod2(logFilePath, STATE_FILE_MODE);
375
389
  return true;
376
390
  } catch (error) {
377
391
  warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
@@ -677,6 +691,7 @@ var acquireSpawnLock = async (lockPath) => {
677
691
  return false;
678
692
  };
679
693
  var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
694
+ await ensureStateDir(paths.root);
680
695
  const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
681
696
  let lockWaitTimedOut = false;
682
697
  if (acquiredLock) {
@@ -2531,7 +2546,10 @@ var toDescriptor = (entry) => {
2531
2546
  output_schema: entry.output_schema,
2532
2547
  annotations: entry.annotations,
2533
2548
  timeout_ms: entry.timeout_ms,
2534
- group: entry.group
2549
+ // Normalised the same way the listing normalises it, so `appduct_describe_tool` never
2550
+ // disagrees with `appduct_list_tools` about whether a tool has a group. The `?? null` also
2551
+ // covers a daemon that predates groups and omits the key entirely.
2552
+ group: entry.group ?? null
2535
2553
  };
2536
2554
  };
2537
2555
  var resolveSession = async (call, selector) => {
@@ -2638,27 +2656,24 @@ var resolveAppTool = async (call, selector, name) => {
2638
2656
 
2639
2657
  // src/mcp/events-tool.ts
2640
2658
  import {
2641
- RPC_METHODS as RPC_METHODS5,
2642
- EVENT_KINDS
2659
+ RPC_METHODS as RPC_METHODS5
2643
2660
  } from "@appduct/shared";
2644
2661
  var EVENTS_TOOL_NAME = "appduct_events";
2645
2662
  var WAIT_FOR_EVENT_TOOL_NAME = "appduct_wait_for_event";
2646
2663
  var DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MS = 12e4;
2647
2664
  var MAX_WAIT_FOR_EVENT_TIMEOUT_MS = 25 * 60 * 1e3;
2648
2665
  var WAIT_FOR_EVENT_PROGRESS_INTERVAL_MS = 6e4;
2649
- var KNOWN_EVENT_KINDS = new Set(EVENT_KINDS);
2650
2666
  var clampWaitForEventTimeoutMs = (requestedMs) => {
2651
2667
  return Math.min(requestedMs ?? DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MS, MAX_WAIT_FOR_EVENT_TIMEOUT_MS);
2652
2668
  };
2653
2669
  var EVENTS_TOOL_DESCRIPTOR = {
2654
2670
  name: EVENTS_TOOL_NAME,
2655
- 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.",
2656
2672
  inputSchema: {
2657
2673
  type: "object",
2658
2674
  properties: {
2659
2675
  selector: { type: "string" },
2660
2676
  since: { type: "integer", minimum: 0 },
2661
- kinds: { type: "array", items: { type: "string", enum: EVENT_KINDS } },
2662
2677
  limit: { type: "integer", exclusiveMinimum: 0 }
2663
2678
  },
2664
2679
  additionalProperties: false
@@ -2722,14 +2737,13 @@ var asOptionalPositiveNumber2 = (value, field) => {
2722
2737
  }
2723
2738
  return value;
2724
2739
  };
2725
- var asOptionalEventKinds = (value) => {
2726
- if (value === void 0) {
2727
- return void 0;
2728
- }
2729
- if (!Array.isArray(value) || !value.every((kind) => typeof kind === "string" && KNOWN_EVENT_KINDS.has(kind))) {
2730
- 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
+ );
2731
2746
  }
2732
- return value;
2733
2747
  };
2734
2748
  var isMatchPrimitive = (value) => {
2735
2749
  return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
@@ -2754,10 +2768,10 @@ var asOptionalMatch = (value) => {
2754
2768
  };
2755
2769
  var handleEventsTool = async (rawArgs, deps) => {
2756
2770
  const args = asRecord3(rawArgs);
2771
+ rejectKinds(args);
2757
2772
  return deps.call(RPC_METHODS5.eventsSince, {
2758
2773
  selector: asOptionalString2(args.selector, "selector"),
2759
2774
  since: asOptionalNonNegativeInteger(args.since, "since"),
2760
- kinds: asOptionalEventKinds(args.kinds),
2761
2775
  limit: asOptionalPositiveInteger(args.limit, "limit")
2762
2776
  });
2763
2777
  };
@@ -2770,6 +2784,7 @@ var payloadShallowMatches = (payload, match) => {
2770
2784
  };
2771
2785
  var handleWaitForEventTool = async (rawArgs, deps) => {
2772
2786
  const args = asRecord3(rawArgs);
2787
+ rejectKinds(args);
2773
2788
  const selector = asOptionalString2(args.selector, "selector");
2774
2789
  const name = asOptionalString2(args.name, "name");
2775
2790
  if (!name) {
@@ -2812,8 +2827,7 @@ var handleWaitForEventTool = async (rawArgs, deps) => {
2812
2827
  await stream.call(RPC_METHODS5.eventsSubscribe, { sessionSelector: sessionId, kinds: ["app_event"] });
2813
2828
  const sinceResult = await stream.call(RPC_METHODS5.eventsSince, {
2814
2829
  selector: sessionId,
2815
- since,
2816
- kinds: ["app_event"]
2830
+ since
2817
2831
  });
2818
2832
  const seenSeqs = /* @__PURE__ */ new Set();
2819
2833
  const backlog = [];
@@ -9,7 +9,7 @@ import { join as join3 } from "node:path";
9
9
  import { RPC_METHODS } from "@appduct/shared";
10
10
 
11
11
  // src/daemon/log-rotation.ts
12
- import { chmod, rename, stat } from "node:fs/promises";
12
+ import { chmod as chmod2, rename, stat } from "node:fs/promises";
13
13
 
14
14
  // src/daemon/config.ts
15
15
  import { readFile } from "node:fs/promises";
@@ -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,
@@ -236,18 +241,27 @@ var isSocketConnectable = (socketPath) => {
236
241
 
237
242
  // src/daemon/state-dir.ts
238
243
  import { join } from "node:path";
244
+ import { mkdir, chmod } from "node:fs/promises";
239
245
  var getStateDirPaths = (stateDir) => {
240
246
  return {
241
247
  root: stateDir,
242
248
  socketPath: join(stateDir, "daemon.sock"),
243
249
  pidFilePath: join(stateDir, "daemon.pid"),
244
250
  logFilePath: join(stateDir, "daemon.log"),
251
+ eventsLogPath: join(stateDir, "events.log"),
245
252
  keyPath: join(stateDir, "key.pem"),
246
253
  configPath: join(stateDir, "config.json"),
247
254
  auditDir: join(stateDir, "audit"),
248
255
  spawnLockPath: join(stateDir, "daemon.spawn.lock")
249
256
  };
250
257
  };
258
+ var ensureStateDir = async (stateDir) => {
259
+ const paths = getStateDirPaths(stateDir);
260
+ await mkdir(paths.auditDir, { recursive: true });
261
+ await chmod(paths.root, 448);
262
+ await chmod(paths.auditDir, 448);
263
+ return paths;
264
+ };
251
265
 
252
266
  // src/daemon/log-rotation.ts
253
267
  var STATE_FILE_MODE = 384;
@@ -280,7 +294,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
280
294
  const backupPath = daemonLogBackupPath(options.logFilePath);
281
295
  await rename(options.logFilePath, backupPath);
282
296
  try {
283
- await chmod(backupPath, STATE_FILE_MODE);
297
+ await chmod2(backupPath, STATE_FILE_MODE);
284
298
  } catch (error) {
285
299
  warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
286
300
  }
@@ -296,7 +310,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
296
310
  var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
297
311
  `)) => {
298
312
  try {
299
- await chmod(logFilePath, STATE_FILE_MODE);
313
+ await chmod2(logFilePath, STATE_FILE_MODE);
300
314
  return true;
301
315
  } catch (error) {
302
316
  warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
@@ -637,6 +651,7 @@ var acquireSpawnLock = async (lockPath) => {
637
651
  return false;
638
652
  };
639
653
  var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
654
+ await ensureStateDir(paths.root);
640
655
  const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
641
656
  let lockWaitTimedOut = false;
642
657
  if (acquiredLock) {