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.
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 |
@@ -232,7 +232,7 @@ import { connect } from "appduct/client";
232
232
  // auto-spawns the daemon and picks the single session, or pass { selector: "pixel-8" } to target one explicitly
233
233
  const app = await connect();
234
234
 
235
- await app.tools(); // ToolDescriptor[]
235
+ await app.tools(); // ListedToolDescriptor[]; an ungrouped tool's group is null
236
236
  const { total } = await app.call("sum", { a: 2, b: 3 });
237
237
  const { payload } = await app.waitForEvent("checkout_done", { timeoutMs: 5_000 });
238
238
  app.close();
@@ -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"),
@@ -4,7 +4,7 @@
4
4
  * the thin-client shapes for the v2 command table.
5
5
  */
6
6
  import type { OpenTarget } from "./open-target.js";
7
- import type { AgentEndpoint, EffectivePolicyDecision, EventNotification, SessionSummary, ToolDescriptor, ToolGroupSummary, ToolsListResult } from "@appduct/shared";
7
+ import type { AgentEndpoint, EffectivePolicyDecision, EventNotification, SessionSummary, ToolGroupSummary, ToolsListEntry, ToolsListResult } from "@appduct/shared";
8
8
  /**
9
9
  * CLI-domain error classes, used only to pick a sysexits-style exit code (see `errors.ts`).
10
10
  * `CliError.type` itself is *not* restricted to this union: RPC/app-originated failures preserve
@@ -129,9 +129,10 @@ export type ToolGroupsListing = {
129
129
  /** Every tool in the session, grouped or not. */
130
130
  total: number;
131
131
  };
132
- /** `appduct tools`: a listing, a groups summary (`--groups`), or a single descriptor when a tool
133
- * name resolved to a detail lookup. */
134
- export type ToolsCommandData = ToolsListing | ToolGroupsListing | ToolDescriptor;
132
+ /** `appduct tools`: a listing, a groups summary (`--groups`), or a single entry when a tool name
133
+ * resolved to a detail lookup. The detail form is the daemon's own listing entry, picked out of
134
+ * `tools.list` — not a registration, so its `group` is `null` for an ungrouped tool. */
135
+ export type ToolsCommandData = ToolsListing | ToolGroupsListing | ToolsListEntry;
135
136
  /** `appduct invoke`: the tool's raw result payload, printed as-is. */
136
137
  export type InvokeCommandData = unknown;
137
138
  export type RevokeCommandData = {
@@ -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
@@ -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) {
@@ -53,7 +53,7 @@ import { join as join4 } from "node:path";
53
53
  import { RPC_METHODS } from "@appduct/shared";
54
54
 
55
55
  // src/daemon/log-rotation.ts
56
- import { chmod, rename, stat } from "node:fs/promises";
56
+ import { chmod as chmod2, rename, stat } from "node:fs/promises";
57
57
 
58
58
  // src/daemon/config.ts
59
59
  import { readFile } from "node:fs/promises";
@@ -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,
@@ -280,18 +285,27 @@ var isSocketConnectable = (socketPath) => {
280
285
 
281
286
  // src/daemon/state-dir.ts
282
287
  import { join as join3 } from "node:path";
288
+ import { mkdir, chmod } from "node:fs/promises";
283
289
  var getStateDirPaths = (stateDir) => {
284
290
  return {
285
291
  root: stateDir,
286
292
  socketPath: join3(stateDir, "daemon.sock"),
287
293
  pidFilePath: join3(stateDir, "daemon.pid"),
288
294
  logFilePath: join3(stateDir, "daemon.log"),
295
+ eventsLogPath: join3(stateDir, "events.log"),
289
296
  keyPath: join3(stateDir, "key.pem"),
290
297
  configPath: join3(stateDir, "config.json"),
291
298
  auditDir: join3(stateDir, "audit"),
292
299
  spawnLockPath: join3(stateDir, "daemon.spawn.lock")
293
300
  };
294
301
  };
302
+ var ensureStateDir = async (stateDir) => {
303
+ const paths = getStateDirPaths(stateDir);
304
+ await mkdir(paths.auditDir, { recursive: true });
305
+ await chmod(paths.root, 448);
306
+ await chmod(paths.auditDir, 448);
307
+ return paths;
308
+ };
295
309
 
296
310
  // src/daemon/log-rotation.ts
297
311
  var STATE_FILE_MODE = 384;
@@ -324,7 +338,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
324
338
  const backupPath = daemonLogBackupPath(options.logFilePath);
325
339
  await rename(options.logFilePath, backupPath);
326
340
  try {
327
- await chmod(backupPath, STATE_FILE_MODE);
341
+ await chmod2(backupPath, STATE_FILE_MODE);
328
342
  } catch (error) {
329
343
  warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
330
344
  }
@@ -340,7 +354,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
340
354
  var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
341
355
  `)) => {
342
356
  try {
343
- await chmod(logFilePath, STATE_FILE_MODE);
357
+ await chmod2(logFilePath, STATE_FILE_MODE);
344
358
  return true;
345
359
  } catch (error) {
346
360
  warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
@@ -646,6 +660,7 @@ var acquireSpawnLock = async (lockPath) => {
646
660
  return false;
647
661
  };
648
662
  var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
663
+ await ensureStateDir(paths.root);
649
664
  const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
650
665
  let lockWaitTimedOut = false;
651
666
  if (acquiredLock) {
@@ -42,18 +42,27 @@ var readPidFromFile = async (pidFilePath) => {
42
42
 
43
43
  // src/daemon/state-dir.ts
44
44
  import { join } from "node:path";
45
+ import { mkdir, chmod } from "node:fs/promises";
45
46
  var getStateDirPaths = (stateDir) => {
46
47
  return {
47
48
  root: stateDir,
48
49
  socketPath: join(stateDir, "daemon.sock"),
49
50
  pidFilePath: join(stateDir, "daemon.pid"),
50
51
  logFilePath: join(stateDir, "daemon.log"),
52
+ eventsLogPath: join(stateDir, "events.log"),
51
53
  keyPath: join(stateDir, "key.pem"),
52
54
  configPath: join(stateDir, "config.json"),
53
55
  auditDir: join(stateDir, "audit"),
54
56
  spawnLockPath: join(stateDir, "daemon.spawn.lock")
55
57
  };
56
58
  };
59
+ var ensureStateDir = async (stateDir) => {
60
+ const paths = getStateDirPaths(stateDir);
61
+ await mkdir(paths.auditDir, { recursive: true });
62
+ await chmod(paths.root, 448);
63
+ await chmod(paths.auditDir, 448);
64
+ return paths;
65
+ };
57
66
 
58
67
  // src/rpc/client.ts
59
68
  import { connect as connect2 } from "node:net";
@@ -63,7 +72,7 @@ import { join as join3 } from "node:path";
63
72
  import { RPC_METHODS } from "@appduct/shared";
64
73
 
65
74
  // src/daemon/log-rotation.ts
66
- import { chmod, rename, stat } from "node:fs/promises";
75
+ import { chmod as chmod2, rename, stat } from "node:fs/promises";
67
76
 
68
77
  // src/daemon/config.ts
69
78
  import { readFile as readFile2 } from "node:fs/promises";
@@ -76,6 +85,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
76
85
  "eventBufferSize",
77
86
  "auditRetentionDays",
78
87
  "daemonLogMaxBytes",
88
+ "eventsLogMaxBytes",
79
89
  "policy",
80
90
  "advertisedIp",
81
91
  "scheme",
@@ -135,6 +145,7 @@ var defaultConfig = (paths) => {
135
145
  eventBufferSize: 256,
136
146
  auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
137
147
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
148
+ eventsLogMaxBytes: 10 * 1024 * 1024,
138
149
  restartDaemonOnVersionMismatch: false,
139
150
  policy: {
140
151
  default: "allow",
@@ -197,6 +208,9 @@ var loadConfig = async (paths, options = {}) => {
197
208
  if (parsed.daemonLogMaxBytes !== void 0) {
198
209
  config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
199
210
  }
211
+ if (parsed.eventsLogMaxBytes !== void 0) {
212
+ config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
213
+ }
200
214
  if (parsed.restartDaemonOnVersionMismatch !== void 0) {
201
215
  config.restartDaemonOnVersionMismatch = requireBoolean(
202
216
  parsed.restartDaemonOnVersionMismatch,
@@ -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) {