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.
@@ -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) {
@@ -7,7 +7,7 @@
7
7
  * `appduct_wait_for_event` MCP tool (`mcp/events-tool.ts`) — see that module's doc comment for
8
8
  * why the notification listener has to be registered before the `events.since` drain call is sent.
9
9
  */
10
- import { type ToolDescriptor } from "@appduct/shared";
10
+ import { type ListedToolDescriptor } from "@appduct/shared";
11
11
  import type { DaemonStream } from "../rpc/client.js";
12
12
  /** A project's own tool map — declared once, e.g. `type Tools = { sum: { args: { a: number; b:
13
13
  * number }; result: { total: number } } }` — to get typed {@link AppClient.call} throughout a test
@@ -80,8 +80,9 @@ export type AppEvent<TPayload = unknown> = {
80
80
  };
81
81
  export type AppClient<TTools = ToolMap> = {
82
82
  readonly sessionId: string;
83
- /** `tools.list` for this session. */
84
- tools(): Promise<ToolDescriptor[]>;
83
+ /** `tools.list` for this session. An entry spells an ungrouped tool's `group` as `null`,
84
+ * where a registration omits it. */
85
+ tools(): Promise<ListedToolDescriptor[]>;
85
86
  /** `tools.call`; rejects with a {@link AppductError} whose `type` preserves the wire error
86
87
  * type verbatim (e.g. `"tool_timeout"`, `"policy_denied"`, `"session_suspended"`). */
87
88
  call<K extends keyof TTools & string>(name: K, args: ToolArgs<TTools, K>, options?: CallOptions): Promise<ToolResult<TTools, K>>;
@@ -15,4 +15,4 @@ export { connect, type ConnectOptions } from "./connect.js";
15
15
  export { link, waitForSession, type LinkOptions, type LinkResult, type WaitForSessionOptions } from "./bootstrap.js";
16
16
  export { AppductError, type AppductErrorType } from "./errors.js";
17
17
  export type { AppClient, AppEvent, CallOptions, EventsOptions, EventsResult, ToolMap, WaitForEventOptions, } from "./app-client.js";
18
- export type { AgentEndpoint, ErrorType, ToolDescriptor } from "@appduct/shared";
18
+ export type { AgentEndpoint, ErrorType, ListedToolDescriptor, ToolDescriptor } from "@appduct/shared";
@@ -4,6 +4,7 @@ import { RPC_METHODS as RPC_METHODS3 } from "@appduct/shared";
4
4
  // src/daemon/state-dir.ts
5
5
  import { homedir } from "node:os";
6
6
  import { join } from "node:path";
7
+ import { mkdir, chmod } from "node:fs/promises";
7
8
  var resolveStateDir = (override) => {
8
9
  return override ?? process.env.APPDUCT_STATE_DIR ?? join(homedir(), ".appduct");
9
10
  };
@@ -13,12 +14,20 @@ var getStateDirPaths = (stateDir) => {
13
14
  socketPath: join(stateDir, "daemon.sock"),
14
15
  pidFilePath: join(stateDir, "daemon.pid"),
15
16
  logFilePath: join(stateDir, "daemon.log"),
17
+ eventsLogPath: join(stateDir, "events.log"),
16
18
  keyPath: join(stateDir, "key.pem"),
17
19
  configPath: join(stateDir, "config.json"),
18
20
  auditDir: join(stateDir, "audit"),
19
21
  spawnLockPath: join(stateDir, "daemon.spawn.lock")
20
22
  };
21
23
  };
24
+ var ensureStateDir = async (stateDir) => {
25
+ const paths = getStateDirPaths(stateDir);
26
+ await mkdir(paths.auditDir, { recursive: true });
27
+ await chmod(paths.root, 448);
28
+ await chmod(paths.auditDir, 448);
29
+ return paths;
30
+ };
22
31
 
23
32
  // src/rpc/client.ts
24
33
  import { connect as connect2 } from "node:net";
@@ -28,7 +37,7 @@ import { join as join3 } from "node:path";
28
37
  import { RPC_METHODS } from "@appduct/shared";
29
38
 
30
39
  // src/daemon/log-rotation.ts
31
- import { chmod, rename, stat } from "node:fs/promises";
40
+ import { chmod as chmod2, rename, stat } from "node:fs/promises";
32
41
 
33
42
  // src/daemon/config.ts
34
43
  import { readFile } from "node:fs/promises";
@@ -41,6 +50,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
41
50
  "eventBufferSize",
42
51
  "auditRetentionDays",
43
52
  "daemonLogMaxBytes",
53
+ "eventsLogMaxBytes",
44
54
  "policy",
45
55
  "advertisedIp",
46
56
  "scheme",
@@ -100,6 +110,7 @@ var defaultConfig = (paths) => {
100
110
  eventBufferSize: 256,
101
111
  auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
102
112
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
113
+ eventsLogMaxBytes: 10 * 1024 * 1024,
103
114
  restartDaemonOnVersionMismatch: false,
104
115
  policy: {
105
116
  default: "allow",
@@ -162,6 +173,9 @@ var loadConfig = async (paths, options = {}) => {
162
173
  if (parsed.daemonLogMaxBytes !== void 0) {
163
174
  config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
164
175
  }
176
+ if (parsed.eventsLogMaxBytes !== void 0) {
177
+ config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
178
+ }
165
179
  if (parsed.restartDaemonOnVersionMismatch !== void 0) {
166
180
  config.restartDaemonOnVersionMismatch = requireBoolean(
167
181
  parsed.restartDaemonOnVersionMismatch,
@@ -284,7 +298,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
284
298
  const backupPath = daemonLogBackupPath(options.logFilePath);
285
299
  await rename(options.logFilePath, backupPath);
286
300
  try {
287
- await chmod(backupPath, STATE_FILE_MODE);
301
+ await chmod2(backupPath, STATE_FILE_MODE);
288
302
  } catch (error) {
289
303
  warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
290
304
  }
@@ -300,7 +314,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
300
314
  var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
301
315
  `)) => {
302
316
  try {
303
- await chmod(logFilePath, STATE_FILE_MODE);
317
+ await chmod2(logFilePath, STATE_FILE_MODE);
304
318
  return true;
305
319
  } catch (error) {
306
320
  warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
@@ -641,6 +655,7 @@ var acquireSpawnLock = async (lockPath) => {
641
655
  return false;
642
656
  };
643
657
  var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
658
+ await ensureStateDir(paths.root);
644
659
  const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
645
660
  let lockWaitTimedOut = false;
646
661
  if (acquiredLock) {
@@ -1096,7 +1111,8 @@ var makeAppClient = (stream, sessionId) => {
1096
1111
  const result = await stream.call(RPC_METHODS2.toolsList, {
1097
1112
  selector: sessionId
1098
1113
  });
1099
- return Array.isArray(result) ? result : result.tools;
1114
+ const entries = Array.isArray(result) ? result : result.tools;
1115
+ return entries.map((entry) => ({ ...entry, group: entry.group ?? null }));
1100
1116
  } catch (error) {
1101
1117
  throw toAppductError(error);
1102
1118
  }
@@ -1124,8 +1140,7 @@ var makeAppClient = (stream, sessionId) => {
1124
1140
  const result = await stream.call(RPC_METHODS2.eventsSince, {
1125
1141
  selector: sessionId,
1126
1142
  since: options.since,
1127
- limit: options.limit,
1128
- kinds: ["app_event"]
1143
+ limit: options.limit
1129
1144
  });
1130
1145
  return {
1131
1146
  events: result.events.map((event) => appEventFrom(event, sessionId)).filter((event) => event !== void 0),
@@ -1184,8 +1199,7 @@ var makeAppClient = (stream, sessionId) => {
1184
1199
  await stream.call(RPC_METHODS2.eventsSubscribe, { sessionSelector: sessionId, kinds: ["app_event"] });
1185
1200
  const sinceResult = await stream.call(RPC_METHODS2.eventsSince, {
1186
1201
  selector: sessionId,
1187
- since: options.since,
1188
- kinds: ["app_event"]
1202
+ since: options.since
1189
1203
  });
1190
1204
  const seenSeqs = /* @__PURE__ */ new Set();
1191
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"),
@@ -9,7 +9,7 @@
9
9
  * Every lookup goes to the daemon live, with no caching: the session and its registry can change
10
10
  * between any two calls, and the daemon is the single source of truth for both.
11
11
  */
12
- import { type EffectivePolicyDecision, type ToolDescriptor } from "@appduct/shared";
12
+ import { type EffectivePolicyDecision, type ListedToolDescriptor } from "@appduct/shared";
13
13
  import type { DaemonCall } from "./daemon-tools.js";
14
14
  export declare const LIST_TOOLS_TOOL_NAME = "appduct_list_tools";
15
15
  export declare const DESCRIBE_TOOL_TOOL_NAME = "appduct_describe_tool";
@@ -103,7 +103,7 @@ export declare const CALL_TOOL_TOOL_DESCRIPTOR: {
103
103
  export type ResolvedAppTool = {
104
104
  sessionId: string;
105
105
  alias: string;
106
- descriptor: ToolDescriptor;
106
+ descriptor: ListedToolDescriptor;
107
107
  policy: EffectivePolicyDecision;
108
108
  };
109
109
  export type CallToolArgs = {
@@ -132,11 +132,11 @@ export declare const handleListToolsTool: (rawArgs: unknown, call: DaemonCall) =
132
132
  export declare const handleDescribeToolTool: (rawArgs: unknown, call: DaemonCall) => Promise<{
133
133
  name: string;
134
134
  description: string;
135
- input_schema?: import("@appduct/shared").ToolSchemaDescriptor;
136
- output_schema?: import("@appduct/shared").ToolSchemaDescriptor;
137
- annotations?: import("@appduct/shared").ToolAnnotations;
138
- timeout_ms?: number;
139
- group?: string | null;
135
+ input_schema?: import("@appduct/shared").ToolSchemaDescriptor | undefined;
136
+ output_schema?: import("@appduct/shared").ToolSchemaDescriptor | undefined;
137
+ annotations?: import("@appduct/shared").ToolAnnotations | undefined;
138
+ timeout_ms?: number | undefined;
139
+ group: string | null;
140
140
  session: string;
141
141
  signature: string;
142
142
  policy: EffectivePolicyDecision;
@@ -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;
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * The auto-spawning RPC client library (ARCHITECTURE.md §4, §5) used by the CLI, MCP server, and
3
- * tests to talk to `daemon.sock`. On `ENOENT`/`ECONNREFUSED` it takes an exclusive spawn-lock,
4
- * spawns `daemon run` detached, polls the socket until ready, then retries the request once.
3
+ * tests to talk to `daemon.sock`. On `ENOENT`/`ECONNREFUSED` it creates the state dir if it is
4
+ * missing, takes an exclusive spawn-lock, spawns `daemon run` detached, polls the socket until
5
+ * ready, then retries the request once.
5
6
  */
6
7
  import { type RpcErrorData } from "@appduct/shared";
7
8
  export type SpawnContext = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appduct",
3
- "version": "0.11.0",
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,12 +45,13 @@
45
45
  "reflect-metadata": "^0.2.2",
46
46
  "toqr": "^0.1.1",
47
47
  "ws": "^8.18.3",
48
- "@appduct/shared": "0.11.0"
48
+ "@appduct/shared": "0.12.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "^22.10.0",
52
52
  "@types/ws": "^8.18.1",
53
53
  "esbuild": "^0.25.12",
54
+ "eslint": "^10",
54
55
  "typescript": "~5.9.2"
55
56
  },
56
57
  "files": [
@@ -60,6 +61,7 @@
60
61
  "scripts": {
61
62
  "build": "tsc -p tsconfig.build.json && node scripts/bundle.mjs",
62
63
  "clean": "rm -rf dist",
64
+ "lint": "eslint src",
63
65
  "test": "vitest run --config ../../vitest.config.ts src/__tests__",
64
66
  "typecheck": "tsc -p tsconfig.json"
65
67
  }