appduct 0.11.0 → 0.11.1

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
@@ -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();
@@ -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 = {
@@ -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";
@@ -236,6 +236,7 @@ var isSocketConnectable = (socketPath) => {
236
236
 
237
237
  // src/daemon/state-dir.ts
238
238
  import { join } from "node:path";
239
+ import { mkdir, chmod } from "node:fs/promises";
239
240
  var getStateDirPaths = (stateDir) => {
240
241
  return {
241
242
  root: stateDir,
@@ -248,6 +249,13 @@ var getStateDirPaths = (stateDir) => {
248
249
  spawnLockPath: join(stateDir, "daemon.spawn.lock")
249
250
  };
250
251
  };
252
+ var ensureStateDir = async (stateDir) => {
253
+ const paths = getStateDirPaths(stateDir);
254
+ await mkdir(paths.auditDir, { recursive: true });
255
+ await chmod(paths.root, 448);
256
+ await chmod(paths.auditDir, 448);
257
+ return paths;
258
+ };
251
259
 
252
260
  // src/daemon/log-rotation.ts
253
261
  var STATE_FILE_MODE = 384;
@@ -280,7 +288,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
280
288
  const backupPath = daemonLogBackupPath(options.logFilePath);
281
289
  await rename(options.logFilePath, backupPath);
282
290
  try {
283
- await chmod(backupPath, STATE_FILE_MODE);
291
+ await chmod2(backupPath, STATE_FILE_MODE);
284
292
  } catch (error) {
285
293
  warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
286
294
  }
@@ -296,7 +304,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
296
304
  var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
297
305
  `)) => {
298
306
  try {
299
- await chmod(logFilePath, STATE_FILE_MODE);
307
+ await chmod2(logFilePath, STATE_FILE_MODE);
300
308
  return true;
301
309
  } catch (error) {
302
310
  warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
@@ -637,6 +645,7 @@ var acquireSpawnLock = async (lockPath) => {
637
645
  return false;
638
646
  };
639
647
  var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
648
+ await ensureStateDir(paths.root);
640
649
  const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
641
650
  let lockWaitTimedOut = false;
642
651
  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";
@@ -280,6 +280,7 @@ var isSocketConnectable = (socketPath) => {
280
280
 
281
281
  // src/daemon/state-dir.ts
282
282
  import { join as join3 } from "node:path";
283
+ import { mkdir, chmod } from "node:fs/promises";
283
284
  var getStateDirPaths = (stateDir) => {
284
285
  return {
285
286
  root: stateDir,
@@ -292,6 +293,13 @@ var getStateDirPaths = (stateDir) => {
292
293
  spawnLockPath: join3(stateDir, "daemon.spawn.lock")
293
294
  };
294
295
  };
296
+ var ensureStateDir = async (stateDir) => {
297
+ const paths = getStateDirPaths(stateDir);
298
+ await mkdir(paths.auditDir, { recursive: true });
299
+ await chmod(paths.root, 448);
300
+ await chmod(paths.auditDir, 448);
301
+ return paths;
302
+ };
295
303
 
296
304
  // src/daemon/log-rotation.ts
297
305
  var STATE_FILE_MODE = 384;
@@ -324,7 +332,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
324
332
  const backupPath = daemonLogBackupPath(options.logFilePath);
325
333
  await rename(options.logFilePath, backupPath);
326
334
  try {
327
- await chmod(backupPath, STATE_FILE_MODE);
335
+ await chmod2(backupPath, STATE_FILE_MODE);
328
336
  } catch (error) {
329
337
  warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
330
338
  }
@@ -340,7 +348,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
340
348
  var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
341
349
  `)) => {
342
350
  try {
343
- await chmod(logFilePath, STATE_FILE_MODE);
351
+ await chmod2(logFilePath, STATE_FILE_MODE);
344
352
  return true;
345
353
  } catch (error) {
346
354
  warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
@@ -646,6 +654,7 @@ var acquireSpawnLock = async (lockPath) => {
646
654
  return false;
647
655
  };
648
656
  var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
657
+ await ensureStateDir(paths.root);
649
658
  const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
650
659
  let lockWaitTimedOut = false;
651
660
  if (acquiredLock) {
@@ -42,6 +42,7 @@ 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,
@@ -54,6 +55,13 @@ var getStateDirPaths = (stateDir) => {
54
55
  spawnLockPath: join(stateDir, "daemon.spawn.lock")
55
56
  };
56
57
  };
58
+ var ensureStateDir = async (stateDir) => {
59
+ const paths = getStateDirPaths(stateDir);
60
+ await mkdir(paths.auditDir, { recursive: true });
61
+ await chmod(paths.root, 448);
62
+ await chmod(paths.auditDir, 448);
63
+ return paths;
64
+ };
57
65
 
58
66
  // src/rpc/client.ts
59
67
  import { connect as connect2 } from "node:net";
@@ -63,7 +71,7 @@ import { join as join3 } from "node:path";
63
71
  import { RPC_METHODS } from "@appduct/shared";
64
72
 
65
73
  // src/daemon/log-rotation.ts
66
- import { chmod, rename, stat } from "node:fs/promises";
74
+ import { chmod as chmod2, rename, stat } from "node:fs/promises";
67
75
 
68
76
  // src/daemon/config.ts
69
77
  import { readFile as readFile2 } from "node:fs/promises";
@@ -280,7 +288,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
280
288
  const backupPath = daemonLogBackupPath(options.logFilePath);
281
289
  await rename(options.logFilePath, backupPath);
282
290
  try {
283
- await chmod(backupPath, STATE_FILE_MODE);
291
+ await chmod2(backupPath, STATE_FILE_MODE);
284
292
  } catch (error) {
285
293
  warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
286
294
  }
@@ -296,7 +304,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
296
304
  var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
297
305
  `)) => {
298
306
  try {
299
- await chmod(logFilePath, STATE_FILE_MODE);
307
+ await chmod2(logFilePath, STATE_FILE_MODE);
300
308
  return true;
301
309
  } catch (error) {
302
310
  warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
@@ -637,6 +645,7 @@ var acquireSpawnLock = async (lockPath) => {
637
645
  return false;
638
646
  };
639
647
  var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
648
+ await ensureStateDir(paths.root);
640
649
  const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
641
650
  let lockWaitTimedOut = false;
642
651
  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";
@@ -236,6 +236,7 @@ var isSocketConnectable = (socketPath) => {
236
236
 
237
237
  // src/daemon/state-dir.ts
238
238
  import { join } from "node:path";
239
+ import { mkdir, chmod } from "node:fs/promises";
239
240
  var getStateDirPaths = (stateDir) => {
240
241
  return {
241
242
  root: stateDir,
@@ -248,6 +249,13 @@ var getStateDirPaths = (stateDir) => {
248
249
  spawnLockPath: join(stateDir, "daemon.spawn.lock")
249
250
  };
250
251
  };
252
+ var ensureStateDir = async (stateDir) => {
253
+ const paths = getStateDirPaths(stateDir);
254
+ await mkdir(paths.auditDir, { recursive: true });
255
+ await chmod(paths.root, 448);
256
+ await chmod(paths.auditDir, 448);
257
+ return paths;
258
+ };
251
259
 
252
260
  // src/daemon/log-rotation.ts
253
261
  var STATE_FILE_MODE = 384;
@@ -280,7 +288,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
280
288
  const backupPath = daemonLogBackupPath(options.logFilePath);
281
289
  await rename(options.logFilePath, backupPath);
282
290
  try {
283
- await chmod(backupPath, STATE_FILE_MODE);
291
+ await chmod2(backupPath, STATE_FILE_MODE);
284
292
  } catch (error) {
285
293
  warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
286
294
  }
@@ -296,7 +304,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
296
304
  var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
297
305
  `)) => {
298
306
  try {
299
- await chmod(logFilePath, STATE_FILE_MODE);
307
+ await chmod2(logFilePath, STATE_FILE_MODE);
300
308
  return true;
301
309
  } catch (error) {
302
310
  warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
@@ -637,6 +645,7 @@ var acquireSpawnLock = async (lockPath) => {
637
645
  return false;
638
646
  };
639
647
  var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
648
+ await ensureStateDir(paths.root);
640
649
  const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
641
650
  let lockWaitTimedOut = false;
642
651
  if (acquiredLock) {
@@ -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";
@@ -256,6 +256,7 @@ var isSocketConnectable = (socketPath) => {
256
256
 
257
257
  // src/daemon/state-dir.ts
258
258
  import { join } from "node:path";
259
+ import { mkdir, chmod } from "node:fs/promises";
259
260
  var getStateDirPaths = (stateDir) => {
260
261
  return {
261
262
  root: stateDir,
@@ -268,6 +269,13 @@ var getStateDirPaths = (stateDir) => {
268
269
  spawnLockPath: join(stateDir, "daemon.spawn.lock")
269
270
  };
270
271
  };
272
+ var ensureStateDir = async (stateDir) => {
273
+ const paths = getStateDirPaths(stateDir);
274
+ await mkdir(paths.auditDir, { recursive: true });
275
+ await chmod(paths.root, 448);
276
+ await chmod(paths.auditDir, 448);
277
+ return paths;
278
+ };
271
279
 
272
280
  // src/daemon/log-rotation.ts
273
281
  var STATE_FILE_MODE = 384;
@@ -300,7 +308,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
300
308
  const backupPath = daemonLogBackupPath(options.logFilePath);
301
309
  await rename(options.logFilePath, backupPath);
302
310
  try {
303
- await chmod(backupPath, STATE_FILE_MODE);
311
+ await chmod2(backupPath, STATE_FILE_MODE);
304
312
  } catch (error) {
305
313
  warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
306
314
  }
@@ -316,7 +324,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
316
324
  var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
317
325
  `)) => {
318
326
  try {
319
- await chmod(logFilePath, STATE_FILE_MODE);
327
+ await chmod2(logFilePath, STATE_FILE_MODE);
320
328
  return true;
321
329
  } catch (error) {
322
330
  warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
@@ -657,6 +665,7 @@ var acquireSpawnLock = async (lockPath) => {
657
665
  return false;
658
666
  };
659
667
  var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
668
+ await ensureStateDir(paths.root);
660
669
  const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
661
670
  let lockWaitTimedOut = false;
662
671
  if (acquiredLock) {
@@ -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";
@@ -239,6 +239,7 @@ var isSocketConnectable = (socketPath) => {
239
239
 
240
240
  // src/daemon/state-dir.ts
241
241
  import { join } from "node:path";
242
+ import { mkdir, chmod } from "node:fs/promises";
242
243
  var getStateDirPaths = (stateDir) => {
243
244
  return {
244
245
  root: stateDir,
@@ -251,6 +252,13 @@ var getStateDirPaths = (stateDir) => {
251
252
  spawnLockPath: join(stateDir, "daemon.spawn.lock")
252
253
  };
253
254
  };
255
+ var ensureStateDir = async (stateDir) => {
256
+ const paths = getStateDirPaths(stateDir);
257
+ await mkdir(paths.auditDir, { recursive: true });
258
+ await chmod(paths.root, 448);
259
+ await chmod(paths.auditDir, 448);
260
+ return paths;
261
+ };
254
262
 
255
263
  // src/daemon/log-rotation.ts
256
264
  var STATE_FILE_MODE = 384;
@@ -283,7 +291,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
283
291
  const backupPath = daemonLogBackupPath(options.logFilePath);
284
292
  await rename(options.logFilePath, backupPath);
285
293
  try {
286
- await chmod(backupPath, STATE_FILE_MODE);
294
+ await chmod2(backupPath, STATE_FILE_MODE);
287
295
  } catch (error) {
288
296
  warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
289
297
  }
@@ -299,7 +307,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
299
307
  var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
300
308
  `)) => {
301
309
  try {
302
- await chmod(logFilePath, STATE_FILE_MODE);
310
+ await chmod2(logFilePath, STATE_FILE_MODE);
303
311
  return true;
304
312
  } catch (error) {
305
313
  warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
@@ -640,6 +648,7 @@ var acquireSpawnLock = async (lockPath) => {
640
648
  return false;
641
649
  };
642
650
  var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
651
+ await ensureStateDir(paths.root);
643
652
  const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
644
653
  let lockWaitTimedOut = false;
645
654
  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";
@@ -236,6 +236,7 @@ var isSocketConnectable = (socketPath) => {
236
236
 
237
237
  // src/daemon/state-dir.ts
238
238
  import { join } from "node:path";
239
+ import { mkdir, chmod } from "node:fs/promises";
239
240
  var getStateDirPaths = (stateDir) => {
240
241
  return {
241
242
  root: stateDir,
@@ -248,6 +249,13 @@ var getStateDirPaths = (stateDir) => {
248
249
  spawnLockPath: join(stateDir, "daemon.spawn.lock")
249
250
  };
250
251
  };
252
+ var ensureStateDir = async (stateDir) => {
253
+ const paths = getStateDirPaths(stateDir);
254
+ await mkdir(paths.auditDir, { recursive: true });
255
+ await chmod(paths.root, 448);
256
+ await chmod(paths.auditDir, 448);
257
+ return paths;
258
+ };
251
259
 
252
260
  // src/daemon/log-rotation.ts
253
261
  var STATE_FILE_MODE = 384;
@@ -280,7 +288,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
280
288
  const backupPath = daemonLogBackupPath(options.logFilePath);
281
289
  await rename(options.logFilePath, backupPath);
282
290
  try {
283
- await chmod(backupPath, STATE_FILE_MODE);
291
+ await chmod2(backupPath, STATE_FILE_MODE);
284
292
  } catch (error) {
285
293
  warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
286
294
  }
@@ -296,7 +304,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
296
304
  var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
297
305
  `)) => {
298
306
  try {
299
- await chmod(logFilePath, STATE_FILE_MODE);
307
+ await chmod2(logFilePath, STATE_FILE_MODE);
300
308
  return true;
301
309
  } catch (error) {
302
310
  warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
@@ -637,6 +645,7 @@ var acquireSpawnLock = async (lockPath) => {
637
645
  return false;
638
646
  };
639
647
  var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
648
+ await ensureStateDir(paths.root);
640
649
  const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
641
650
  let lockWaitTimedOut = false;
642
651
  if (acquiredLock) {
@@ -171,6 +171,7 @@ var loadConfig = async (paths, options = {}) => {
171
171
 
172
172
  // src/daemon/state-dir.ts
173
173
  import { join } from "node:path";
174
+ import { mkdir, chmod } from "node:fs/promises";
174
175
  var getStateDirPaths = (stateDir) => {
175
176
  return {
176
177
  root: stateDir,
@@ -183,6 +184,13 @@ var getStateDirPaths = (stateDir) => {
183
184
  spawnLockPath: join(stateDir, "daemon.spawn.lock")
184
185
  };
185
186
  };
187
+ var ensureStateDir = async (stateDir) => {
188
+ const paths = getStateDirPaths(stateDir);
189
+ await mkdir(paths.auditDir, { recursive: true });
190
+ await chmod(paths.root, 448);
191
+ await chmod(paths.auditDir, 448);
192
+ return paths;
193
+ };
186
194
 
187
195
  // src/mcp/server.ts
188
196
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -267,7 +275,7 @@ import { join as join4 } from "node:path";
267
275
  import { RPC_METHODS } from "@appduct/shared";
268
276
 
269
277
  // src/daemon/log-rotation.ts
270
- import { chmod, rename, stat } from "node:fs/promises";
278
+ import { chmod as chmod2, rename, stat } from "node:fs/promises";
271
279
 
272
280
  // src/daemon/pidfile.ts
273
281
  import { readFileSync as readFileSync3 } from "node:fs";
@@ -355,7 +363,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
355
363
  const backupPath = daemonLogBackupPath(options.logFilePath);
356
364
  await rename(options.logFilePath, backupPath);
357
365
  try {
358
- await chmod(backupPath, STATE_FILE_MODE);
366
+ await chmod2(backupPath, STATE_FILE_MODE);
359
367
  } catch (error) {
360
368
  warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
361
369
  }
@@ -371,7 +379,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
371
379
  var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
372
380
  `)) => {
373
381
  try {
374
- await chmod(logFilePath, STATE_FILE_MODE);
382
+ await chmod2(logFilePath, STATE_FILE_MODE);
375
383
  return true;
376
384
  } catch (error) {
377
385
  warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
@@ -677,6 +685,7 @@ var acquireSpawnLock = async (lockPath) => {
677
685
  return false;
678
686
  };
679
687
  var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
688
+ await ensureStateDir(paths.root);
680
689
  const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
681
690
  let lockWaitTimedOut = false;
682
691
  if (acquiredLock) {
@@ -2531,7 +2540,10 @@ var toDescriptor = (entry) => {
2531
2540
  output_schema: entry.output_schema,
2532
2541
  annotations: entry.annotations,
2533
2542
  timeout_ms: entry.timeout_ms,
2534
- group: entry.group
2543
+ // Normalised the same way the listing normalises it, so `appduct_describe_tool` never
2544
+ // disagrees with `appduct_list_tools` about whether a tool has a group. The `?? null` also
2545
+ // covers a daemon that predates groups and omits the key entirely.
2546
+ group: entry.group ?? null
2535
2547
  };
2536
2548
  };
2537
2549
  var resolveSession = async (call, selector) => {
@@ -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";
@@ -236,6 +236,7 @@ var isSocketConnectable = (socketPath) => {
236
236
 
237
237
  // src/daemon/state-dir.ts
238
238
  import { join } from "node:path";
239
+ import { mkdir, chmod } from "node:fs/promises";
239
240
  var getStateDirPaths = (stateDir) => {
240
241
  return {
241
242
  root: stateDir,
@@ -248,6 +249,13 @@ var getStateDirPaths = (stateDir) => {
248
249
  spawnLockPath: join(stateDir, "daemon.spawn.lock")
249
250
  };
250
251
  };
252
+ var ensureStateDir = async (stateDir) => {
253
+ const paths = getStateDirPaths(stateDir);
254
+ await mkdir(paths.auditDir, { recursive: true });
255
+ await chmod(paths.root, 448);
256
+ await chmod(paths.auditDir, 448);
257
+ return paths;
258
+ };
251
259
 
252
260
  // src/daemon/log-rotation.ts
253
261
  var STATE_FILE_MODE = 384;
@@ -280,7 +288,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
280
288
  const backupPath = daemonLogBackupPath(options.logFilePath);
281
289
  await rename(options.logFilePath, backupPath);
282
290
  try {
283
- await chmod(backupPath, STATE_FILE_MODE);
291
+ await chmod2(backupPath, STATE_FILE_MODE);
284
292
  } catch (error) {
285
293
  warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
286
294
  }
@@ -296,7 +304,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
296
304
  var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
297
305
  `)) => {
298
306
  try {
299
- await chmod(logFilePath, STATE_FILE_MODE);
307
+ await chmod2(logFilePath, STATE_FILE_MODE);
300
308
  return true;
301
309
  } catch (error) {
302
310
  warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
@@ -637,6 +645,7 @@ var acquireSpawnLock = async (lockPath) => {
637
645
  return false;
638
646
  };
639
647
  var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
648
+ await ensureStateDir(paths.root);
640
649
  const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
641
650
  let lockWaitTimedOut = false;
642
651
  if (acquiredLock) {
@@ -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";
@@ -239,6 +239,7 @@ var isSocketConnectable = (socketPath) => {
239
239
 
240
240
  // src/daemon/state-dir.ts
241
241
  import { join } from "node:path";
242
+ import { mkdir, chmod } from "node:fs/promises";
242
243
  var getStateDirPaths = (stateDir) => {
243
244
  return {
244
245
  root: stateDir,
@@ -251,6 +252,13 @@ var getStateDirPaths = (stateDir) => {
251
252
  spawnLockPath: join(stateDir, "daemon.spawn.lock")
252
253
  };
253
254
  };
255
+ var ensureStateDir = async (stateDir) => {
256
+ const paths = getStateDirPaths(stateDir);
257
+ await mkdir(paths.auditDir, { recursive: true });
258
+ await chmod(paths.root, 448);
259
+ await chmod(paths.auditDir, 448);
260
+ return paths;
261
+ };
254
262
 
255
263
  // src/daemon/log-rotation.ts
256
264
  var STATE_FILE_MODE = 384;
@@ -283,7 +291,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
283
291
  const backupPath = daemonLogBackupPath(options.logFilePath);
284
292
  await rename(options.logFilePath, backupPath);
285
293
  try {
286
- await chmod(backupPath, STATE_FILE_MODE);
294
+ await chmod2(backupPath, STATE_FILE_MODE);
287
295
  } catch (error) {
288
296
  warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
289
297
  }
@@ -299,7 +307,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
299
307
  var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
300
308
  `)) => {
301
309
  try {
302
- await chmod(logFilePath, STATE_FILE_MODE);
310
+ await chmod2(logFilePath, STATE_FILE_MODE);
303
311
  return true;
304
312
  } catch (error) {
305
313
  warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
@@ -640,6 +648,7 @@ var acquireSpawnLock = async (lockPath) => {
640
648
  return false;
641
649
  };
642
650
  var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
651
+ await ensureStateDir(paths.root);
643
652
  const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
644
653
  let lockWaitTimedOut = false;
645
654
  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
  };
@@ -19,6 +20,13 @@ var getStateDirPaths = (stateDir) => {
19
20
  spawnLockPath: join(stateDir, "daemon.spawn.lock")
20
21
  };
21
22
  };
23
+ var ensureStateDir = async (stateDir) => {
24
+ const paths = getStateDirPaths(stateDir);
25
+ await mkdir(paths.auditDir, { recursive: true });
26
+ await chmod(paths.root, 448);
27
+ await chmod(paths.auditDir, 448);
28
+ return paths;
29
+ };
22
30
 
23
31
  // src/rpc/client.ts
24
32
  import { connect as connect2 } from "node:net";
@@ -28,7 +36,7 @@ import { join as join3 } from "node:path";
28
36
  import { RPC_METHODS } from "@appduct/shared";
29
37
 
30
38
  // src/daemon/log-rotation.ts
31
- import { chmod, rename, stat } from "node:fs/promises";
39
+ import { chmod as chmod2, rename, stat } from "node:fs/promises";
32
40
 
33
41
  // src/daemon/config.ts
34
42
  import { readFile } from "node:fs/promises";
@@ -284,7 +292,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
284
292
  const backupPath = daemonLogBackupPath(options.logFilePath);
285
293
  await rename(options.logFilePath, backupPath);
286
294
  try {
287
- await chmod(backupPath, STATE_FILE_MODE);
295
+ await chmod2(backupPath, STATE_FILE_MODE);
288
296
  } catch (error) {
289
297
  warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
290
298
  }
@@ -300,7 +308,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
300
308
  var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
301
309
  `)) => {
302
310
  try {
303
- await chmod(logFilePath, STATE_FILE_MODE);
311
+ await chmod2(logFilePath, STATE_FILE_MODE);
304
312
  return true;
305
313
  } catch (error) {
306
314
  warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
@@ -641,6 +649,7 @@ var acquireSpawnLock = async (lockPath) => {
641
649
  return false;
642
650
  };
643
651
  var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
652
+ await ensureStateDir(paths.root);
644
653
  const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
645
654
  let lockWaitTimedOut = false;
646
655
  if (acquiredLock) {
@@ -1096,7 +1105,8 @@ var makeAppClient = (stream, sessionId) => {
1096
1105
  const result = await stream.call(RPC_METHODS2.toolsList, {
1097
1106
  selector: sessionId
1098
1107
  });
1099
- return Array.isArray(result) ? result : result.tools;
1108
+ const entries = Array.isArray(result) ? result : result.tools;
1109
+ return entries.map((entry) => ({ ...entry, group: entry.group ?? null }));
1100
1110
  } catch (error) {
1101
1111
  throw toAppductError(error);
1102
1112
  }
@@ -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;
@@ -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.11.1",
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.11.1"
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
  }