@ghostlytools/cli 1.3.9 → 1.4.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.
Files changed (98) hide show
  1. package/README.md +13 -4
  2. package/SKILL.md +7 -4
  3. package/dist/assets/{api-Cp9GxjUO.js → api-BqLfti32.js} +2 -2
  4. package/dist/assets/{api-BCrwJ57L.js → api-JMkEO0RR.js} +130 -50
  5. package/dist/assets/api-JMkEO0RR.js.map +1 -0
  6. package/dist/assets/{apiKit-B4qhnLQT.js → apiKit-BpIUCwa-.js} +3 -3
  7. package/dist/assets/{apiKit-B4qhnLQT.js.map → apiKit-BpIUCwa-.js.map} +1 -1
  8. package/dist/assets/{apps-CpxENMgl.js → apps-BdfyGVZ1.js} +1 -1
  9. package/dist/assets/{apps-DOH1P_S0.js → apps-Bettj7WQ.js} +6 -6
  10. package/dist/assets/apps-Bettj7WQ.js.map +1 -0
  11. package/dist/assets/{arkade-Cyf5MGxG.js → arkade-CP4HvKaa.js} +2 -2
  12. package/dist/assets/{arkade-Cyf5MGxG.js.map → arkade-CP4HvKaa.js.map} +1 -1
  13. package/dist/assets/{backup-CqM-0-2v.js → backup-BbO2zLjx.js} +1 -1
  14. package/dist/assets/{backup-CTpFFQ-4.js → backup-BmBtta66.js} +7 -7
  15. package/dist/assets/{backup-CTpFFQ-4.js.map → backup-BmBtta66.js.map} +1 -1
  16. package/dist/assets/blobFiles-B0rNnHlO.js +35 -0
  17. package/dist/assets/blobFiles-B0rNnHlO.js.map +1 -0
  18. package/dist/assets/{buttons-BtxwyM2l.js → buttons-YgijrprJ.js} +4 -4
  19. package/dist/assets/{buttons-BtxwyM2l.js.map → buttons-YgijrprJ.js.map} +1 -1
  20. package/dist/assets/buttons-_GBlxAti.js +2 -0
  21. package/dist/assets/{endpoint-BOTFMpfS.js → endpoint-Cl3cgAYl.js} +3 -2
  22. package/dist/assets/endpoint-Cl3cgAYl.js.map +1 -0
  23. package/dist/assets/{envelope-DW8TWKPD.js → envelope-C7vnmsJN.js} +2 -2
  24. package/dist/assets/{envelope-DW8TWKPD.js.map → envelope-C7vnmsJN.js.map} +1 -1
  25. package/dist/assets/{errors-B-zMxbcd.js → errors-BLSPp0tK.js} +3 -1
  26. package/dist/assets/errors-BLSPp0tK.js.map +1 -0
  27. package/dist/assets/{fileBytes-Di7VZtmU.js → fileBytes-B-fqGdEd.js} +3 -3
  28. package/dist/assets/{fileBytes-Di7VZtmU.js.map → fileBytes-B-fqGdEd.js.map} +1 -1
  29. package/dist/assets/{fileBytes-CgbibpbV.js → fileBytes-C2jycN_7.js} +1 -1
  30. package/dist/assets/{fileBytes-CfSXjTFa.js → fileBytes-D6fqFQEX.js} +6 -3
  31. package/dist/assets/fileBytes-D6fqFQEX.js.map +1 -0
  32. package/dist/assets/fileBytesIdb-DOxm_BKt.js +2 -0
  33. package/dist/assets/{fileBytesIdb-D5u6H8X_.js → fileBytesIdb-_HnxdacI.js} +3 -3
  34. package/dist/assets/{fileBytesIdb-D5u6H8X_.js.map → fileBytesIdb-_HnxdacI.js.map} +1 -1
  35. package/dist/assets/{fileBytesOpfs-Dd1N4ERH.js → fileBytesOpfs-p_CANncD.js} +3 -3
  36. package/dist/assets/{fileBytesOpfs-Dd1N4ERH.js.map → fileBytesOpfs-p_CANncD.js.map} +1 -1
  37. package/dist/assets/{groupFiles-uxa0U9_n.js → groupFiles-sIdGq4AV.js} +5 -5
  38. package/dist/assets/groupFiles-sIdGq4AV.js.map +1 -0
  39. package/dist/assets/{host-DPy0eaH9.js → host-Drd-c4VO.js} +147 -25
  40. package/dist/assets/host-Drd-c4VO.js.map +1 -0
  41. package/dist/assets/{hyperdhtRelay-De5FT7Am.js → hyperdhtRelay-13aQ4KOf.js} +2 -2
  42. package/dist/assets/{hyperdhtRelay-De5FT7Am.js.map → hyperdhtRelay-13aQ4KOf.js.map} +1 -1
  43. package/dist/assets/{idb-B5L8wuOm.js → idb-CMVgtn2k.js} +59 -6
  44. package/dist/assets/idb-CMVgtn2k.js.map +1 -0
  45. package/dist/assets/{irohWeb-z5QDDWa-.js → irohWeb-DHCxSvHF.js} +2 -2
  46. package/dist/assets/{irohWeb-z5QDDWa-.js.map → irohWeb-DHCxSvHF.js.map} +1 -1
  47. package/dist/assets/{main-Dt7xrqtb.js → main-BWAwKY4f.js} +476 -141
  48. package/dist/assets/main-BWAwKY4f.js.map +1 -0
  49. package/dist/assets/{mainline-Cld-bKeC.js → mainline-CsG-DdJ5.js} +2 -2
  50. package/dist/assets/{mainline-Cld-bKeC.js.map → mainline-CsG-DdJ5.js.map} +1 -1
  51. package/dist/assets/{networkReason-vUHo6A_C.js → networkReason-B91upMCb.js} +3 -3
  52. package/dist/assets/{networkReason-vUHo6A_C.js.map → networkReason-B91upMCb.js.map} +1 -1
  53. package/dist/assets/{nwc-zXpmd2FN.js → nwc-BFv67iSz.js} +3 -3
  54. package/dist/assets/{nwc-zXpmd2FN.js.map → nwc-BFv67iSz.js.map} +1 -1
  55. package/dist/assets/{nwcProvider-D482SQ1a.js → nwcProvider-BKNFdH9v.js} +2 -2
  56. package/dist/assets/{nwcProvider-D482SQ1a.js.map → nwcProvider-BKNFdH9v.js.map} +1 -1
  57. package/dist/assets/{profiles-gPCjLOQq.js → profiles-DSxPYdlU.js} +13 -3
  58. package/dist/assets/profiles-DSxPYdlU.js.map +1 -0
  59. package/dist/assets/{registry-CxsDR3c6.js → registry-CHPd_hf7.js} +1 -1
  60. package/dist/assets/{registry-OVx2kY3d.js → registry-qbnWmwCE.js} +3 -3
  61. package/dist/assets/registry-qbnWmwCE.js.map +1 -0
  62. package/dist/assets/{resolve-CPTAnu-9.js → resolve-B63_uSu7.js} +2 -2
  63. package/dist/assets/{resolve-CPTAnu-9.js.map → resolve-B63_uSu7.js.map} +1 -1
  64. package/dist/assets/{restoredRows-Db3oUTko.js → restoredRows-DeBImGRd.js} +2 -2
  65. package/dist/assets/{restoredRows-Db3oUTko.js.map → restoredRows-DeBImGRd.js.map} +1 -1
  66. package/dist/assets/{server-p5DJnN7T.js → server-Cd1dLOB8.js} +1485 -240
  67. package/dist/assets/server-Cd1dLOB8.js.map +1 -0
  68. package/dist/assets/{src-Dg1bvMNv.js → src-DzXL-GYu.js} +7277 -5975
  69. package/dist/assets/src-DzXL-GYu.js.map +1 -0
  70. package/dist/assets/turnRefs-BCtM10d3.js +357 -0
  71. package/dist/assets/turnRefs-BCtM10d3.js.map +1 -0
  72. package/dist/assets/{usdt-BY5OIIvG.js → usdt-B7JWDhQV.js} +3 -3
  73. package/dist/assets/{usdt-BY5OIIvG.js.map → usdt-B7JWDhQV.js.map} +1 -1
  74. package/dist/ghostly.mjs +1 -1
  75. package/package.json +7 -6
  76. package/skills/ghostly/README.md +41 -0
  77. package/skills/ghostly/SKILL.md +79 -0
  78. package/skills/ghostly/scripts/listen +67 -0
  79. package/skills/ghostly/scripts/react +25 -0
  80. package/skills/ghostly/scripts/say +44 -0
  81. package/skills/ghostly/scripts/status +27 -0
  82. package/dist/assets/api-BCrwJ57L.js.map +0 -1
  83. package/dist/assets/apps-DOH1P_S0.js.map +0 -1
  84. package/dist/assets/buttons-CJqNkG-Q.js +0 -2
  85. package/dist/assets/endpoint-BOTFMpfS.js.map +0 -1
  86. package/dist/assets/errors-B-zMxbcd.js.map +0 -1
  87. package/dist/assets/fileBytes-CfSXjTFa.js.map +0 -1
  88. package/dist/assets/fileBytesIdb-CFP-DSkZ.js +0 -2
  89. package/dist/assets/groupFiles-uxa0U9_n.js.map +0 -1
  90. package/dist/assets/host-DPy0eaH9.js.map +0 -1
  91. package/dist/assets/idb-B5L8wuOm.js.map +0 -1
  92. package/dist/assets/main-Dt7xrqtb.js.map +0 -1
  93. package/dist/assets/profiles-gPCjLOQq.js.map +0 -1
  94. package/dist/assets/registry-OVx2kY3d.js.map +0 -1
  95. package/dist/assets/server-p5DJnN7T.js.map +0 -1
  96. package/dist/assets/src-Dg1bvMNv.js.map +0 -1
  97. package/dist/assets/turnRefs-WnggXm2T.js +0 -323
  98. package/dist/assets/turnRefs-WnggXm2T.js.map +0 -1
@@ -1,8 +1,8 @@
1
- import { $n as GROUP_FILE_LIMITS, Ja as PR_CHECKS, Xa as RUN_RESULTS, Ya as PR_STATES, Za as STATUS_CARD_LIMITS, qa as ITEM_STATES, to as cardUrl } from "./src-Dg1bvMNv.js";
2
- import { a as SECRET_RESULTS, i as ENGINE_READS, r as ENGINE_METHODS, t as turnRef } from "./turnRefs-WnggXm2T.js";
3
- import { i as localFileError, n as EXIT, r as asCliError, t as CliError } from "./errors-B-zMxbcd.js";
4
- import { t as CONTACT_KEY } from "./apiKit-B4qhnLQT.js";
5
- import { a as createProfile, c as listProfiles, d as profilePaths, f as selectProfile, i as checkProfileName, l as lockOwner, o as currentProfile, s as ghostlyHome, u as profileExists } from "./profiles-gPCjLOQq.js";
1
+ import { Fa as ITEM_STATES, Ia as PR_CHECKS, La as PR_STATES, Ra as RUN_RESULTS, Ua as cardUrl, Un as GROUP_FILE_LIMITS, za as STATUS_CARD_LIMITS } from "./src-DzXL-GYu.js";
2
+ import { a as ENGINE_PARAMS, c as SECRET_RESULTS, i as ENGINE_METHODS, o as ENGINE_READS, s as READ_PARAMS, t as turnRef } from "./turnRefs-BCtM10d3.js";
3
+ import { i as localFileError, n as EXIT, r as asCliError, t as CliError } from "./errors-BLSPp0tK.js";
4
+ import { t as CONTACT_KEY } from "./apiKit-BpIUCwa-.js";
5
+ import { a as checkShownName, c as ghostlyHome, d as profileExists, f as profilePaths, i as checkProfileName, l as listProfiles, o as createProfile, p as selectProfile, s as currentProfile, u as lockOwner } from "./profiles-DSxPYdlU.js";
6
6
  import { a as provesDaemon, c as ownSocket, o as readDaemonSecret, r as newNonce, t as HELLO } from "./daemonProof-B53VnZ1Z.js";
7
7
  import { accessSync, constants, existsSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
8
8
  import { dirname, resolve } from "node:path";
@@ -12,7 +12,7 @@ import { createInterface } from "node:readline";
12
12
  import { format } from "node:util";
13
13
  var package_default = {
14
14
  name: "@ghostlytools/cli",
15
- version: "1.3.9",
15
+ version: "1.4.0",
16
16
  type: "module",
17
17
  description: "Ghostly for bots and terminals: the app's own engine, headless on Node, behind a daemon and a `ghostly` command with JSON output.",
18
18
  license: "MIT",
@@ -28,6 +28,7 @@ var package_default = {
28
28
  files: [
29
29
  "dist",
30
30
  "examples",
31
+ "skills",
31
32
  "README.md",
32
33
  "SKILL.md"
33
34
  ],
@@ -38,7 +39,7 @@ var package_default = {
38
39
  "test": "vitest run"
39
40
  },
40
41
  dependencies: {
41
- "@arkade-os/sdk": "0.4.78",
42
+ "@arkade-os/sdk": "0.4.79",
42
43
  "@atproto/jwk-webcrypto": "^0.3.4",
43
44
  "@atproto/oauth-client": "^0.8.9",
44
45
  "@bitcoindevkit/bdk-wallet-web": "0.3.0",
@@ -57,7 +58,7 @@ var package_default = {
57
58
  "@scure/btc-signer": "2.4.1",
58
59
  "@secondts/bark": "0.25.0",
59
60
  "@serenity-kit/opaque": "1.1.0",
60
- "@synonymdev/pubky": "0.14.0",
61
+ "@synonymdev/pubky": "0.16.1",
61
62
  "@tetherto/wdk-wallet-evm": "1.0.0-beta.20",
62
63
  "b4a": "1.9.0",
63
64
  "bittorrent-dht": "11.0.12",
@@ -76,10 +77,10 @@ var package_default = {
76
77
  "@ghostly/browser": "*",
77
78
  "@ghostly/core": "*",
78
79
  "@ghostly/iroh-web": "*",
79
- "@types/node": "^26.6.4",
80
+ "@types/node": "^26.6.5",
80
81
  "typescript": "^5.9.3",
81
- "vite": "^8.3.3",
82
- "vitest": "^5.0.3"
82
+ "vite": "^8.3.4",
83
+ "vitest": "^5.0.4"
83
84
  }
84
85
  };
85
86
  //#endregion
@@ -241,11 +242,12 @@ function isNotTheDaemon(error) {
241
242
  }
242
243
  /**
243
244
  * A connection to a profile's daemon over its socket (WISP 1100 § Local control API). `null` from `connectDaemon`
244
- * means no daemon answers there: a one-shot command runs the profile itself. *
245
+ * means no daemon answers there: a one-shot command runs the profile itself.
246
+ *
245
247
  * The first line on it is a `daemon.hello`, and no call is written until its answer proves the other end read the
246
- * profile's daemon secret (`daemonProof.ts`): until then every call waits, and a listener that does not prove it gets
247
- * every call refused (`refused`, `isNotTheDaemon`) having read nothing but the hello. Nothing it writes before that
248
- * is read either.
248
+ * profile's daemon secret (`daemonProof.ts`): until then every call waits, under its own deadline, and a listener
249
+ * that does not prove it gets every call refused (`refused`, `isNotTheDaemon`) having read nothing but the hello.
250
+ * Nothing it writes before that is read either.
249
251
  */
250
252
  var DaemonClient = class {
251
253
  socket;
@@ -254,6 +256,8 @@ var DaemonClient = class {
254
256
  pending = /* @__PURE__ */ new Map();
255
257
  eventListener = null;
256
258
  closedListener = null;
259
+ /** While paused: the events of the chunk already read, given to the listener on resume. */
260
+ held = null;
257
261
  verified = false;
258
262
  refusal = null;
259
263
  ready;
@@ -291,7 +295,8 @@ var DaemonClient = class {
291
295
  return;
292
296
  }
293
297
  if (message.event) {
294
- this.eventListener?.(message.event);
298
+ if (this.held) this.held.push(message.event);
299
+ else this.eventListener?.(message.event);
295
300
  return;
296
301
  }
297
302
  const waiting = typeof message.id === "number" ? this.pending.get(message.id) : void 0;
@@ -319,14 +324,35 @@ var DaemonClient = class {
319
324
  this.settle.reject(this.refusal);
320
325
  this.socket.destroy();
321
326
  }
322
- call(method, params = {}) {
323
- if (this.socket.destroyed && this.refusal) return Promise.reject(this.refusal);
327
+ /**
328
+ * `timeoutMs`: give up (code `timeout`, exit 4) when the daemon has not answered by then; without it the call waits
329
+ * for as long as the connection holds. A call that timed out closes the connection: its answer, if one ever comes,
330
+ * is of no use, and the process leaves once nothing else keeps it (not every caller reaches `exit`).
331
+ */
332
+ call(method, params = {}, { timeoutMs } = {}) {
333
+ if (this.socket.destroyed) return Promise.reject(this.refusal ?? new CliError("unavailable", "The daemon went away"));
324
334
  const id = this.nextId++;
325
335
  return new Promise((resolve, reject) => {
326
- this.pending.set(id, {
327
- resolve,
328
- reject
329
- });
336
+ let timer;
337
+ if (timeoutMs !== void 0) {
338
+ timer = setTimeout(() => {
339
+ if (!this.pending.delete(id)) return;
340
+ reject(new CliError("timeout", `The daemon did not answer ${method} in ${timeoutMs / 1e3} s: \`ghostly daemon restart\` starts a fresh one`, { method }));
341
+ this.destroy();
342
+ }, timeoutMs);
343
+ timer.unref?.();
344
+ }
345
+ const settle = {
346
+ resolve: (value) => {
347
+ if (timer) clearTimeout(timer);
348
+ resolve(value);
349
+ },
350
+ reject: (error) => {
351
+ if (timer) clearTimeout(timer);
352
+ reject(error);
353
+ }
354
+ };
355
+ this.pending.set(id, settle);
330
356
  this.ready.then(() => {
331
357
  if (this.pending.has(id) && !this.socket.destroyed) this.socket.write(JSON.stringify({
332
358
  id,
@@ -334,7 +360,7 @@ var DaemonClient = class {
334
360
  params
335
361
  }) + "\n");
336
362
  }, (error) => {
337
- if (this.pending.delete(id)) reject(error);
363
+ if (this.pending.delete(id)) settle.reject(error);
338
364
  });
339
365
  });
340
366
  }
@@ -344,6 +370,29 @@ var DaemonClient = class {
344
370
  this.closedListener = onClosed ?? null;
345
371
  return this.call("events.subscribe", since === void 0 ? {} : { since });
346
372
  }
373
+ /**
374
+ * Stops reading: what the daemon sends waits in the connection (and past its bound the daemon ends it), and no
375
+ * event reaches the listener until `resume`.
376
+ */
377
+ pause() {
378
+ if (this.held) return;
379
+ this.held = [];
380
+ this.socket.pause();
381
+ }
382
+ /** Gives the listener the events held, then reads again; a listener that pauses again keeps the rest held. */
383
+ resume() {
384
+ const held = this.held;
385
+ if (!held) return;
386
+ this.held = null;
387
+ for (let i = 0; i < held.length; i++) {
388
+ this.eventListener?.(held[i]);
389
+ if (this.held) {
390
+ this.held.unshift(...held.slice(i + 1));
391
+ return;
392
+ }
393
+ }
394
+ this.socket.resume();
395
+ }
347
396
  close() {
348
397
  this.socket.end();
349
398
  }
@@ -942,7 +991,7 @@ var commands$8 = {
942
991
  description: "Replace the three files when they are there"
943
992
  }
944
993
  },
945
- run: async ({ options }, a) => (await import("./apps-CpxENMgl.js")).initApp(a.dir, {
994
+ run: async ({ options }, a) => (await import("./apps-BdfyGVZ1.js")).initApp(a.dir, {
946
995
  name: options.name,
947
996
  title: options.title,
948
997
  force: options.force === true
@@ -982,7 +1031,7 @@ var commands$8 = {
982
1031
  description: "This version's sequence, higher than the bundle at --out's (default: one more than it)"
983
1032
  }
984
1033
  },
985
- run: async ({ options }, a) => (await import("./apps-CpxENMgl.js")).publishApp(a.dir, keyFile(options.key, "app publish <dir> --key <file> [--out <file>] [--sequence n]"), here(options.out), options.sequence)
1034
+ run: async ({ options }, a) => (await import("./apps-BdfyGVZ1.js")).publishApp(a.dir, keyFile(options.key, "app publish <dir> --key <file> [--out <file>] [--sequence n]"), here(options.out), options.sequence)
986
1035
  },
987
1036
  "app revoke": {
988
1037
  method: "app.revoke",
@@ -1015,7 +1064,7 @@ var commands$8 = {
1015
1064
  description: "Allow --up-to past the bundle's own sequence (versions published from elsewhere)"
1016
1065
  }
1017
1066
  },
1018
- run: async ({ options }, a) => (await import("./apps-CpxENMgl.js")).revokeApp(a.dir, keyFile(options.key, "app revoke <dir> --key <file> (--digest <digest>... | --up-to <sequence>) [--reason <text>]"), {
1067
+ run: async ({ options }, a) => (await import("./apps-BdfyGVZ1.js")).revokeApp(a.dir, keyFile(options.key, "app revoke <dir> --key <file> (--digest <digest>... | --up-to <sequence>) [--reason <text>]"), {
1019
1068
  digests: options.digest,
1020
1069
  upTo: options["up-to"],
1021
1070
  reason: options.reason,
@@ -1076,7 +1125,7 @@ var commands$8 = {
1076
1125
  usage: "app verify <bundle|url>",
1077
1126
  summary: "Check a .ghostlyapp as a client would, from a file or an https URL: its manifest and digest, or refused (exit 1) with the reason, revoked when the ghostly-revoke.json beside it revokes this version",
1078
1127
  args: ["bundle"],
1079
- run: async (_, a) => (await import("./apps-CpxENMgl.js")).verifyApp(a.bundle)
1128
+ run: async (_, a) => (await import("./apps-BdfyGVZ1.js")).verifyApp(a.bundle)
1080
1129
  },
1081
1130
  "store sign": {
1082
1131
  method: "store.sign",
@@ -1093,7 +1142,7 @@ var commands$8 = {
1093
1142
  description: "The folder to write ghostly-store.json and ghostly-store.sig in (default: the index's)"
1094
1143
  }
1095
1144
  },
1096
- run: async ({ options }, a) => (await import("./apps-CpxENMgl.js")).signStore(a.index, keyFile(options.key, "store sign <index> --key <file> [--out <dir>]"), here(options.out))
1145
+ run: async ({ options }, a) => (await import("./apps-BdfyGVZ1.js")).signStore(a.index, keyFile(options.key, "store sign <index> --key <file> [--out <dir>]"), here(options.out))
1097
1146
  }
1098
1147
  };
1099
1148
  //#endregion
@@ -1417,7 +1466,7 @@ var commands$6 = {
1417
1466
  "invite join": {
1418
1467
  method: "invite.join",
1419
1468
  usage: "invite join [<invite-or-link> | -] [--label <name>]",
1420
- summary: "Join a chat from its invite; the invite on stdin keeps it out of ps",
1469
+ summary: "Join a chat from its invite; stdin keeps it out of ps (without a daemon it waits up to 60 s for the inviter)",
1421
1470
  args: ["invite?"],
1422
1471
  options: { label: {
1423
1472
  type: "string",
@@ -2167,7 +2216,7 @@ var commands$3 = {
2167
2216
  };
2168
2217
  //#endregion
2169
2218
  //#region src/commands/identities.ts
2170
- /** Identity proofs: one entry per command, in alphabetical order (test/commands.test.ts checks). */
2219
+ /** Identity proofs and x401 credentials: one entry per command, in alphabetical order (test/commands.test.ts checks). */
2171
2220
  var commands$2 = {
2172
2221
  "identity cancel": {
2173
2222
  method: "identity.cancel",
@@ -2253,6 +2302,42 @@ var commands$2 = {
2253
2302
  chat: a.chat,
2254
2303
  id: a.id
2255
2304
  })
2305
+ },
2306
+ "x401 credential add": {
2307
+ method: "x401.credential.add",
2308
+ usage: "x401 credential add <file|-> [--label <name>]",
2309
+ summary: "Keep an issued SD-JWT VC bound to a key from x401 key new (a file, or - for stdin)",
2310
+ args: ["file"],
2311
+ options: { label: {
2312
+ type: "string",
2313
+ description: "Your name for it, up to 80 characters on one line (default: its type)"
2314
+ } },
2315
+ params: ({ options }, a) => ({
2316
+ file: a.file === "-" ? "-" : here(a.file),
2317
+ label: options.label
2318
+ })
2319
+ },
2320
+ "x401 credential list": {
2321
+ method: "x401.credential.list",
2322
+ usage: "x401 credential list [--show-secret]",
2323
+ summary: "This profile's x401 credentials: issuer, type, claim paths",
2324
+ options: { "show-secret": {
2325
+ type: "boolean",
2326
+ description: "Print the claims' values too (a name, a document number...)"
2327
+ } },
2328
+ params: ({ options }) => secret(options)
2329
+ },
2330
+ "x401 credential remove": {
2331
+ method: "x401.credential.remove",
2332
+ usage: "x401 credential remove <id>",
2333
+ summary: "Remove a credential and its holder key",
2334
+ args: ["id"],
2335
+ params: (_, a) => ({ id: a.id })
2336
+ },
2337
+ "x401 key new": {
2338
+ method: "x401.key.new",
2339
+ usage: "x401 key new",
2340
+ summary: "A fresh P-256 holder key: its public JWK goes to the issuer (the 8 newest wait for a credential; past them, the oldest is dropped)"
2256
2341
  }
2257
2342
  };
2258
2343
  //#endregion
@@ -2979,7 +3064,13 @@ function checkWebhook(url) {
2979
3064
  }
2980
3065
  function readCursor(path) {
2981
3066
  if (!path || !existsSync(path)) return void 0;
2982
- const value = Number(readFileSync(path, "utf8").trim());
3067
+ let text;
3068
+ try {
3069
+ text = readFileSync(path, "utf8");
3070
+ } catch (error) {
3071
+ throw localFileError(error, path) ?? error;
3072
+ }
3073
+ const value = Number(text.trim());
2983
3074
  return Number.isInteger(value) && value >= 0 ? value : void 0;
2984
3075
  }
2985
3076
  /**
@@ -3211,7 +3302,7 @@ function eventHandler(options) {
3211
3302
  }
3212
3303
  /**
3213
3304
  * Bounds what a listener holds when its output or hooks fall behind (a stdout reader that hung, a slow --exec): past
3214
- * `MAX_BEHIND` events not handled it stops taking events (`pause`: the subscription closes) and drops what still
3305
+ * `MAX_BEHIND` events not handled it stops taking events (`pause`: the subscription stops reading) and drops what still
3215
3306
  * arrives; once every event taken is handled, `resume` opens it again after the last one taken. The daemon's journal
3216
3307
  * keeps what came meanwhile (and says `events.gap` past what it keeps), so a consumer that hangs for a day does not
3217
3308
  * grow the listener by the day's traffic.
@@ -3245,6 +3336,64 @@ var Feed = class {
3245
3336
  });
3246
3337
  };
3247
3338
  };
3339
+ /** How long a feed that fell behind keeps its connection to the daemon open, unread, while its hooks take no event (`followDaemon`). */
3340
+ var PAUSE_HOLD_MS = 6e4;
3341
+ /**
3342
+ * Follows a daemon's events into `handle` until `close`; a daemon that restarts is picked up again after the last event
3343
+ * seen. A feed that falls behind stops reading the connection, not closes it: what the daemon sent meanwhile (a replay
3344
+ * written whole) is read on resume, so a catch-up reads the journal once, not once per pause. A connection the daemon
3345
+ * ended meanwhile (past what it lets a client leave unread) is opened again after the last event taken.
3346
+ * A feed whose hooks take no event for `holdMs` (`PAUSE_HOLD_MS`: a hook that hung) ends the connection, as a full one
3347
+ * does, so the daemon does not keep a replay and what came live for it while it hangs; resume opens it again after the
3348
+ * last event taken.
3349
+ * `subscribed`: the first subscription's answer (a journal that cannot be replayed rejects it).
3350
+ */
3351
+ function followDaemon(address, client, handle, since, holdMs = PAUSE_HOLD_MS) {
3352
+ let current = client;
3353
+ let holding;
3354
+ const reconnect = (wait) => void (async () => {
3355
+ while (!current && !feed.paused) {
3356
+ await new Promise((resolve) => setTimeout(resolve, wait));
3357
+ wait = 1e3;
3358
+ if (current || feed.paused) return;
3359
+ const next = await connectDaemon(address, 1e3);
3360
+ if (next) {
3361
+ current = next;
3362
+ await follow(next).catch(() => {
3363
+ if (current === next) current = null;
3364
+ });
3365
+ }
3366
+ }
3367
+ })();
3368
+ const hold = (behind) => {
3369
+ holding = setTimeout(() => {
3370
+ if (handle.behind() < behind) return hold(handle.behind());
3371
+ const c = current;
3372
+ current = null;
3373
+ c?.destroy();
3374
+ }, holdMs);
3375
+ };
3376
+ const feed = new Feed(handle, since, () => {
3377
+ current?.pause();
3378
+ hold(handle.behind());
3379
+ }, () => {
3380
+ clearTimeout(holding);
3381
+ if (current) current.resume();
3382
+ else reconnect(0);
3383
+ });
3384
+ const follow = (c) => c.subscribe(feed.since, feed.take, () => {
3385
+ if (current !== c) return;
3386
+ current = null;
3387
+ reconnect(1e3);
3388
+ });
3389
+ return {
3390
+ subscribed: follow(client),
3391
+ close: () => {
3392
+ clearTimeout(holding);
3393
+ current?.close();
3394
+ }
3395
+ };
3396
+ }
3248
3397
  //#endregion
3249
3398
  //#region src/secretInput.ts
3250
3399
  /**
@@ -3267,7 +3416,14 @@ var JOINS = {
3267
3416
  what: "An invite"
3268
3417
  }
3269
3418
  };
3270
- async function secretsFromStdin(method, params, read, tty = false) {
3419
+ /**
3420
+ * A join given no invite reads stdin, but a pipe nobody writes or closes (what Node's exec and most harnesses give a
3421
+ * child) would hold it for ever without a word: a note once nothing has come for a while, the usage error once nothing
3422
+ * has come at all. A producer that started writing is waited for to the end; `-` asked for stdin and waits as long.
3423
+ */
3424
+ var STDIN_NOTE_MS = 2e3;
3425
+ var STDIN_GIVE_UP_MS = 6e4;
3426
+ async function secretsFromStdin(method, params, read, tty = false, note = () => {}) {
3271
3427
  const warning = (what) => `${what} on the command line shows in ps and the shell's history: give it on stdin instead`;
3272
3428
  if (method === "wallet.redeem") {
3273
3429
  if (params.token !== void 0) return warning("A token");
@@ -3283,7 +3439,7 @@ async function secretsFromStdin(method, params, read, tty = false) {
3283
3439
  if (params[join.param] !== void 0 && params[join.param] !== "-") return warning(join.what);
3284
3440
  const missing = () => new CliError("usage", `Missing ${join.arg}: give it on stdin (… | ghostly ${join.command}) or as an argument`);
3285
3441
  if (tty) throw missing();
3286
- const value = (await read()).trim();
3442
+ const value = (params[join.param] === "-" ? await read() : await readUnasked(read, note, join)).trim();
3287
3443
  if (!value) throw missing();
3288
3444
  params[join.param] = value;
3289
3445
  return;
@@ -3306,6 +3462,22 @@ async function secretsFromStdin(method, params, read, tty = false) {
3306
3462
  return onLine ? warning("An API key") : void 0;
3307
3463
  }
3308
3464
  }
3465
+ async function readUnasked(read, note, join) {
3466
+ let noteTimer, giveUpTimer;
3467
+ const stop = () => {
3468
+ clearTimeout(noteTimer);
3469
+ clearTimeout(giveUpTimer);
3470
+ };
3471
+ const nothingCame = new Promise((_, reject) => {
3472
+ noteTimer = setTimeout(() => note(`Waiting for ${join.what.toLowerCase()} on stdin (… | ghostly ${join.command}): give it there or as an argument`), STDIN_NOTE_MS);
3473
+ giveUpTimer = setTimeout(() => reject(new CliError("usage", `Missing ${join.arg}: nothing came on stdin in ${STDIN_GIVE_UP_MS / 1e3} s; give it there (… | ghostly ${join.command}) or as an argument`)), STDIN_GIVE_UP_MS);
3474
+ });
3475
+ try {
3476
+ return await Promise.race([read(stop), nothingCame]);
3477
+ } finally {
3478
+ stop();
3479
+ }
3480
+ }
3309
3481
  /**
3310
3482
  * `wallet create --value`: the names given on the command line that the chosen source's form marks `kind: "secret"`
3311
3483
  * (an NWC URI, a macaroon, a rune, an RPC password), as a warning. `list`: a `wallet.list` answer, where the forms are.
@@ -3326,8 +3498,8 @@ var VERSION = package_default.version;
3326
3498
  * command a running daemon answers loads none of them, and starts about as fast as Node does. Statically imported, they
3327
3499
  * cost every command ~100 ms before its socket was opened.
3328
3500
  */
3329
- var engineApi = () => import("./api-Cp9GxjUO.js");
3330
- var engineHost = () => import("./host-DPy0eaH9.js");
3501
+ var engineApi = () => import("./api-BqLfti32.js");
3502
+ var engineHost = () => import("./host-Drd-c4VO.js");
3331
3503
  var pretty = false;
3332
3504
  function print(value) {
3333
3505
  process.stdout.write(JSON.stringify(value, null, pretty ? 2 : void 0) + "\n");
@@ -3371,6 +3543,11 @@ function versionWarning(daemon, cli) {
3371
3543
  if (typeof daemon !== "string" || daemon === cli) return null;
3372
3544
  return `ghostly: the daemon runs ${daemon} and this command is ${cli}: \`ghostly daemon restart\` runs the new code`;
3373
3545
  }
3546
+ /**
3547
+ * Never fails, and has no deadline of its own: a daemon that does not answer is the command's own call to find out,
3548
+ * at the command's deadline, which closes the connection and ends this check with it. A deadline here ended every
3549
+ * command on a wedged daemon at 15 s with `unavailable` ("The daemon went away"), whatever its own `--timeout` said.
3550
+ */
3374
3551
  async function warnVersion(client) {
3375
3552
  const warning = versionWarning((await client.call("status").catch(() => null))?.version, VERSION);
3376
3553
  if (warning) process.stderr.write(warning + "\n");
@@ -3413,7 +3590,8 @@ var READS = /* @__PURE__ */ new Set([
3413
3590
  "service.list",
3414
3591
  "service.peer",
3415
3592
  "call.list",
3416
- "app.served"
3593
+ "app.served",
3594
+ "x401.credential.list"
3417
3595
  ]);
3418
3596
  /** Whether a one-shot for this call opens the profile offline; test/readOnlyCommands.test.ts names every table command. */
3419
3597
  function readOnly(method, params) {
@@ -3421,15 +3599,66 @@ function readOnly(method, params) {
3421
3599
  if (method === "call.auto") return params.on === void 0;
3422
3600
  return READS.has(method);
3423
3601
  }
3602
+ /**
3603
+ * Calls whose change stays on this device: a chat's name here, a chat removed (nothing goes to the contact), auto-answer
3604
+ * (calls.json), a proof dropped, a web app added (shared with no one yet), an x401 key or credential (WISP 3xx: never
3605
+ * shared with a contact). Opened online, each put the profile's records on the relays and waited for them (6 PUT + 4
3606
+ * GET, 3.3 s with a relay that takes 1.5 s), and the contacts' apps saw this device come online. `message delete` stays
3607
+ * online: it settles a file offer already on the DHT.
3608
+ */
3609
+ var LOCAL_WRITES = /* @__PURE__ */ new Set([
3610
+ "chat.rename",
3611
+ "chat.remove",
3612
+ "call.auto",
3613
+ "identity.cancel",
3614
+ "service.add",
3615
+ "x401.key.new",
3616
+ "x401.credential.add",
3617
+ "x401.credential.remove"
3618
+ ]);
3619
+ /** Settings of this device only: no contact and no relay hears of them (nick, avatar and the relays' do). */
3620
+ var LOCAL_SETTINGS = /* @__PURE__ */ new Set([
3621
+ "sendTyping",
3622
+ "autoDownloads",
3623
+ "serveFiles"
3624
+ ]);
3625
+ /** Whether a one-shot for this call opens the profile offline: a read, or a change that stays on this device. */
3626
+ function offlineCall(method, params) {
3627
+ return readOnly(method, params) || LOCAL_WRITES.has(method);
3628
+ }
3629
+ /** How long a daemon gets to answer before a command gives up: a read is in memory (15 s); a call that waits for the
3630
+ * contact carries its own `--timeout` (or the daemon's default for it), plus a margin; anything else may take as long
3631
+ * as it takes. */
3632
+ var ANSWER_MS = 15e3;
3633
+ function answerWithin(method, params) {
3634
+ const waits = typeof params.timeout === "number" ? Math.min(Math.max(params.timeout, 1), 86400) : waitsFor(method, params);
3635
+ if (waits !== void 0) return (waits + 10) * 1e3;
3636
+ if (readOnly(method, params)) return ANSWER_MS;
3637
+ }
3638
+ /**
3639
+ * The seconds a call without `--timeout` waits for the contact: the daemon's defaults (`num(params, "timeout", …)` in
3640
+ * api.ts, files.ts, buttons.ts and statusCards.ts), so that a bot's `chat wait` or `send --wait sent` on a daemon that
3641
+ * stopped answering ends when the wait would have. A call that does not wait is none.
3642
+ */
3643
+ function waitsFor(method, params) {
3644
+ if (method === "chat.wait") return 60;
3645
+ if (method === "file.wait" || method === "file.save" && params.wait === true) return 300;
3646
+ if (typeof params.wait === "string" && params.wait !== "none") return 30;
3647
+ }
3424
3648
  async function session(g, options) {
3425
3649
  requireProfile(g);
3426
3650
  const client = await connectDaemon(g.paths);
3427
- if (client) await warnVersion(client);
3428
- if (client) return {
3429
- mode: "daemon",
3430
- call: (m, p) => client.call(m, p),
3431
- close: async () => client.close()
3432
- };
3651
+ if (client) {
3652
+ const warned = warnVersion(client);
3653
+ return {
3654
+ mode: "daemon",
3655
+ call: (m, p) => client.call(m, p, { timeoutMs: answerWithin(m, p ?? {}) }),
3656
+ close: async () => {
3657
+ await warned;
3658
+ client.close();
3659
+ }
3660
+ };
3661
+ }
3433
3662
  const [{ openHost }, { callApi }] = await Promise.all([engineHost(), engineApi()]);
3434
3663
  const host = await openHost(g.paths, "one-shot", VERSION, options);
3435
3664
  return {
@@ -3440,7 +3669,7 @@ async function session(g, options) {
3440
3669
  }
3441
3670
  /**
3442
3671
  * How a one-shot opens the profile: `deferGroups` when the command is about one 1:1 chat (`chatOnly`) and needs
3443
- * nothing of the groups, `offline` when it only reads (`readOnly`).
3672
+ * nothing of the groups, `offline` when it only reads or changes only this device (`offlineCall`).
3444
3673
  */
3445
3674
  async function withSession(g, work, options = {}) {
3446
3675
  const s = await session(g, options);
@@ -3450,9 +3679,43 @@ async function withSession(g, work, options = {}) {
3450
3679
  await s.close();
3451
3680
  }
3452
3681
  }
3453
- async function readStdin() {
3682
+ /**
3683
+ * A one-shot join waits up to 60 s for the inviter. Its answer says whether the chat paired and at what stage it is; one
3684
+ * not paired yet comes with the next step, since nothing pairs once the command has left.
3685
+ */
3686
+ async function oneShotJoinWait(s, result, label) {
3687
+ const view = await s.call("chat.wait", {
3688
+ chat: result.chat,
3689
+ until: "paired",
3690
+ timeout: 60
3691
+ }).then((v) => ({
3692
+ paired: true,
3693
+ chat: v
3694
+ }), () => null) ?? await s.call("chat.get", { chat: result.chat }).then((v) => ({
3695
+ paired: false,
3696
+ chat: v
3697
+ }), () => ({
3698
+ paired: false,
3699
+ chat: null
3700
+ }));
3701
+ const stage = view.chat?.stage ?? null;
3702
+ const answer = {
3703
+ ...result,
3704
+ paired: view.paired,
3705
+ stage
3706
+ };
3707
+ if (view.paired) return { answer };
3708
+ return {
3709
+ answer,
3710
+ note: `Not paired yet: the inviter's app is offline. It pairs when both are online: keep this profile running (ghostly daemon --detach), then ghostly chat wait ${label?.trim() && !/\s/.test(label.trim()) ? label.trim() : String(result.chat)} --until paired`
3711
+ };
3712
+ }
3713
+ async function readStdin(onData) {
3454
3714
  const chunks = [];
3455
- for await (const chunk of process.stdin) chunks.push(chunk);
3715
+ for await (const chunk of process.stdin) {
3716
+ onData?.();
3717
+ chunks.push(chunk);
3718
+ }
3456
3719
  return Buffer.concat(chunks).toString("utf8");
3457
3720
  }
3458
3721
  var o = (type, description) => ({
@@ -3509,7 +3772,11 @@ var SPECIAL = [
3509
3772
  timeout: o("number", "Seconds --detach waits for it to start (default 60)")
3510
3773
  }
3511
3774
  ],
3512
- ["daemon status", "Whether a daemon runs the profile, its version, and its socket (for the socket API)"],
3775
+ [
3776
+ "daemon status [--timeout s]",
3777
+ "Whether a daemon runs the profile, its version, and its socket (for the socket API)",
3778
+ { timeout: o("number", "Seconds to wait for its answer (default 15)") }
3779
+ ],
3513
3780
  [
3514
3781
  "daemon stop [--timeout s]",
3515
3782
  "Stop the profile's daemon",
@@ -3542,15 +3809,20 @@ var SPECIAL = [
3542
3809
  ],
3543
3810
  ["settings set <key> <json-value>", "Change one: relays, irohRelays, hyperdhtRelay, readRelays, iceServers, publicProfiles, online, shareProfile, sendTyping, autoDownloads, serveFiles, nick"],
3544
3811
  [
3545
- "engine <method> [json-params | -] [--confirm-real] [--show-secret] [--force]",
3546
- "Any call of the app's engine, with its own parameters",
3812
+ "engine <method> [json-params | -] [--confirm-real] [--show-secret] [--yes] [--force]",
3813
+ "Any call of the app's engine, with its own parameters (engine --list --verbose names them)",
3547
3814
  {
3548
3815
  "confirm-real": o("boolean", "Confirm a Mainnet (real money) spend"),
3549
3816
  "show-secret": o("boolean", "Print a call's secrets"),
3817
+ yes: o("boolean", "Confirm a call that deletes chats or groups"),
3550
3818
  force: o("boolean", "Send a text that looks like a secret")
3551
3819
  }
3552
3820
  ],
3553
- ["engine --list", "The engine's calls"],
3821
+ [
3822
+ "engine --list [--verbose]",
3823
+ "The engine's calls",
3824
+ { verbose: o("boolean", "With what each call takes: { linkId, label }, ? for an optional one") }
3825
+ ],
3554
3826
  ["call pipe [<chat|call>]", "A call's audio on stdin/stdout: raw s16le mono PCM at the call's rate (e.g. with sox or ffmpeg)"],
3555
3827
  [
3556
3828
  "identity add <provider> [subject] [--signer id] [--field name=value]... [--days n]",
@@ -3584,7 +3856,8 @@ function help() {
3584
3856
  ...Object.entries(GLOBAL_OPTIONS).map(([name, o]) => ` --${name}${o.short ? `, -${o.short}` : ""}`.padEnd(24) + o.description),
3585
3857
  "",
3586
3858
  "A <chat> is its id, a unique prefix of it, or its name; a <group> the same. Exit codes: 0 done, 1 failed,",
3587
- "2 usage, 3 not found, 4 timed out, 5 needs --force/--yes/--confirm-real. See packages/cli/README.md."
3859
+ "2 usage, 3 not found, 4 timed out, 5 needs --force/--yes/--confirm-real.",
3860
+ "More: https://ghostly.tools/cli, or the README.md beside this CLI."
3588
3861
  ].join("\n");
3589
3862
  }
3590
3863
  /** What `ghostly help help` answers. */
@@ -3611,7 +3884,7 @@ function helpRows() {
3611
3884
  function commandWords(usage) {
3612
3885
  const words = [];
3613
3886
  for (const word of usage.split(" ")) {
3614
- if (!/^[a-z][a-z-]*$/.test(word)) break;
3887
+ if (!/^[a-z][a-z0-9-]*$/.test(word)) break;
3615
3888
  words.push(word);
3616
3889
  }
3617
3890
  return words;
@@ -3632,7 +3905,7 @@ function commandHelp(words) {
3632
3905
  const asked = words.join(" ");
3633
3906
  if (!rows.length) throw new CliError("usage", `Unknown command: ${asked} (ghostly help)`);
3634
3907
  const exact = rows.filter((row) => commandWords(row.usage).join(" ") === asked);
3635
- if (!exact.length && words.length < given.length) throw unknownCommand(given[0], given[1]);
3908
+ if (!exact.length && words.length < given.length) throw unknownCommand(given[0], given[1], given[2]);
3636
3909
  const lines = [];
3637
3910
  for (const row of exact) {
3638
3911
  lines.push(`Usage: ghostly ${row.usage}`, "", ` ${row.summary}`);
@@ -3714,14 +3987,23 @@ async function profileCommand(sub, argv) {
3714
3987
  case "create": {
3715
3988
  const [name] = parsed.positionals;
3716
3989
  if (!name || parsed.positionals.length > 1) throw new CliError("usage", "ghostly profile create <name>");
3990
+ const shownName = parsed.options.name === void 0 ? void 0 : checkShownName(parsed.options.name);
3717
3991
  const paths = createProfile(g.home, name);
3718
- if (parsed.options.use) selectProfile(g.home, name);
3719
3992
  let shown = null;
3720
- if (typeof parsed.options.name === "string") shown = await withSession({
3721
- ...g,
3722
- profile: name,
3723
- paths
3724
- }, (s) => s.call("profile.set", { name: parsed.options.name }));
3993
+ if (shownName !== void 0) try {
3994
+ shown = await withSession({
3995
+ ...g,
3996
+ profile: name,
3997
+ paths
3998
+ }, (s) => s.call("profile.set", { name: shownName }));
3999
+ } catch (e) {
4000
+ rmSync(paths.dir, {
4001
+ recursive: true,
4002
+ force: true
4003
+ });
4004
+ throw e;
4005
+ }
4006
+ if (parsed.options.use) selectProfile(g.home, name);
3725
4007
  print({
3726
4008
  created: name,
3727
4009
  folder: paths.dir,
@@ -3791,7 +4073,7 @@ async function profileCommand(sub, argv) {
3791
4073
  const [file, name] = parsed.positionals;
3792
4074
  if (!file || !name) throw new CliError("usage", "ghostly profile restore <file> <new profile> [--passphrase-file f] [--use]");
3793
4075
  noMoreThan(2, parsed.positionals, "profile restore <file> <new profile> [--passphrase-file f] [--use]");
3794
- const { backupFileProtection, restoreProfile } = await import("./backup-CqM-0-2v.js");
4076
+ const { backupFileProtection, restoreProfile } = await import("./backup-BbO2zLjx.js");
3795
4077
  const protection = await backupFileProtection(file);
3796
4078
  const paths = await restoreProfile(g.home, checkProfileName(name), file, protection === "none" ? void 0 : backupPassphrase(parsed.options["passphrase-file"], RESTORE_PASSPHRASE));
3797
4079
  if (parsed.options.use) selectProfile(g.home, name);
@@ -3901,7 +4183,7 @@ async function daemonCommand(argv) {
3901
4183
  "status",
3902
4184
  "stop",
3903
4185
  "restart"
3904
- ].includes(sub)) noMoreThan(0, parsed.positionals, sub === "status" ? "daemon status" : sub ? `daemon ${sub} [--timeout s]` : "daemon [--detach] [--timeout s]");
4186
+ ].includes(sub)) noMoreThan(0, parsed.positionals, sub ? `daemon ${sub} [--timeout s]` : "daemon [--detach] [--timeout s]");
3905
4187
  if (sub === "status") {
3906
4188
  knownProfile(g);
3907
4189
  const client = await connectDaemon(g.paths);
@@ -3916,7 +4198,7 @@ async function daemonCommand(argv) {
3916
4198
  try {
3917
4199
  print({
3918
4200
  running: true,
3919
- ...await client.call("status"),
4201
+ ...await client.call("status", {}, { timeoutMs: seconds(parsed, 15) * 1e3 }),
3920
4202
  socket: g.paths.socket
3921
4203
  });
3922
4204
  } finally {
@@ -3938,11 +4220,11 @@ async function daemonCommand(argv) {
3938
4220
  if (sub === "restart") {
3939
4221
  const stopFor = seconds(parsed, 20), startFor = seconds(parsed, 60);
3940
4222
  const client = await connectDaemon(g.paths);
3941
- const before = client ? (await client.call("status").catch(() => null))?.version ?? null : null;
4223
+ const probe = client ? client.call("status").catch(() => null) : null;
3942
4224
  print({
3943
4225
  restarted: true,
3944
4226
  stopped: client ? await stopDaemon(g, client, stopFor) : null,
3945
- before,
4227
+ before: (await probe)?.version ?? null,
3946
4228
  version: VERSION,
3947
4229
  ...await startDetached(g, startFor)
3948
4230
  });
@@ -3965,34 +4247,55 @@ function seconds(parsed, fallback) {
3965
4247
  }
3966
4248
  /**
3967
4249
  * Asks the daemon to stop and waits until its process is gone, not only until it let go of the profile (it releases
3968
- * the lock a moment before it exits); its pid.
4250
+ * the lock a moment before it exits); its pid. One that does not answer the ask in `seconds` (its event loop wedged)
4251
+ * is sent SIGTERM, and SIGKILL when that did not end it either in as long: `daemon restart` then starts a fresh one,
4252
+ * as the timeout error promises. The pid is the profile lock's holder, which a daemon writes itself. On Windows
4253
+ * `process.kill` with either name is TerminateProcess: the first signal ends it, there is no second.
3969
4254
  */
3970
4255
  async function stopDaemon(g, client, seconds) {
3971
- let pid;
4256
+ let pid, answered = true;
3972
4257
  try {
3973
- ({pid} = await client.call("daemon.stop"));
4258
+ ({pid} = await client.call("daemon.stop", {}, { timeoutMs: seconds * 1e3 }));
3974
4259
  } catch (error) {
3975
- if (!isNotTheDaemon(error)) throw error;
4260
+ const refused = isNotTheDaemon(error);
4261
+ if (!(error instanceof CliError && error.code === "timeout") && !refused) throw error;
3976
4262
  const holder = lockOwner(g.paths);
3977
4263
  if (holder === null) throw error;
3978
- process.stderr.write(`ghostly: what answers on the socket did not prove it is this profile's daemon (an older release's cannot): stopping the process that holds the profile (${holder}) with SIGTERM\n`);
4264
+ process.stderr.write(refused ? `ghostly: what answers on the socket did not prove it is this profile's daemon (an older release's cannot): stopping the process that holds the profile (${holder}) with SIGTERM\n` : `ghostly: the daemon (process ${holder}) did not answer daemon.stop in ${seconds} s: stopping it with SIGTERM\n`);
3979
4265
  pid = holder;
3980
- try {
3981
- process.kill(pid, "SIGTERM");
3982
- } catch {}
4266
+ answered = false;
4267
+ signal(pid, "SIGTERM");
3983
4268
  }
3984
4269
  client.close();
3985
- const until = Date.now() + seconds * 1e3;
3986
4270
  const running = () => lockOwner(g.paths) === pid || processAlive(pid);
3987
- while (running() && Date.now() < until) await new Promise((resolve) => setTimeout(resolve, 100));
3988
- if (running()) throw new CliError("timeout", `The daemon (process ${pid}) has not stopped yet`);
3989
- return pid;
4271
+ const gone = async () => {
4272
+ const until = Date.now() + seconds * 1e3;
4273
+ while (running()) {
4274
+ const left = until - Date.now();
4275
+ if (left <= 0) break;
4276
+ await new Promise((resolve) => setTimeout(resolve, Math.min(100, left)));
4277
+ }
4278
+ return !running();
4279
+ };
4280
+ if (await gone()) return pid;
4281
+ if (!answered) {
4282
+ process.stderr.write(`ghostly: the daemon (process ${pid}) did not stop on SIGTERM in ${seconds} s: killing it\n`);
4283
+ signal(pid, "SIGKILL");
4284
+ if (await gone()) return pid;
4285
+ }
4286
+ throw new CliError("timeout", `The daemon (process ${pid}) has not stopped yet`, { pid });
3990
4287
  }
3991
4288
  /** Throws busy, with its pid, when a process other than `except` holds the profile. */
3992
4289
  function heldBy(g, except) {
3993
4290
  const owner = lockOwner(g.paths);
3994
4291
  if (owner !== null && owner !== except) throw new CliError("busy", `Profile ${g.profile} is in use by process ${owner}`, { pid: owner });
3995
4292
  }
4293
+ /** Gone already, or not ours to signal: the wait that follows says so. */
4294
+ function signal(pid, name) {
4295
+ try {
4296
+ process.kill(pid, name);
4297
+ } catch {}
4298
+ }
3996
4299
  function processAlive(pid) {
3997
4300
  try {
3998
4301
  process.kill(pid, 0);
@@ -4024,20 +4327,26 @@ async function startDetached(g, seconds) {
4024
4327
  ]
4025
4328
  });
4026
4329
  child.unref();
4330
+ const spawned = { error: null };
4331
+ child.once("error", (error) => {
4332
+ spawned.error = error;
4333
+ });
4027
4334
  const until = Date.now() + seconds * 1e3;
4028
4335
  let client = null;
4029
- while (!client && Date.now() < until) {
4030
- if (child.exitCode !== null) break;
4031
- await new Promise((resolve) => setTimeout(resolve, 200));
4032
- client = await connectDaemon(g.paths, 500);
4336
+ while (child.exitCode === null && child.signalCode === null && !spawned.error) {
4337
+ const left = until - Date.now();
4338
+ if (left <= 0) break;
4339
+ client = await connectDaemon(g.paths, Math.min(500, left));
4340
+ if (client) break;
4341
+ await new Promise((resolve) => setTimeout(resolve, Math.max(0, Math.min(200, until - Date.now()))));
4033
4342
  }
4034
4343
  if (!client) {
4035
4344
  heldBy(g, child.pid);
4036
- if (child.exitCode === null && child.pid !== void 0 && processAlive(child.pid)) throw new CliError("timeout", `The daemon (process ${child.pid}) is still starting; ghostly daemon status shows when it is up`, {
4345
+ if (child.exitCode === null && child.signalCode === null && child.pid !== void 0 && processAlive(child.pid)) throw new CliError("timeout", `The daemon (process ${child.pid}) is still starting; ghostly daemon status shows when it is up`, {
4037
4346
  pid: child.pid,
4038
4347
  log: g.paths.log
4039
4348
  });
4040
- throw new CliError("engine", `The daemon did not start; see ${g.paths.log}`);
4349
+ throw new CliError("engine", spawned.error ? `The daemon could not be spawned: ${spawned.error.message}` : `The daemon did not start; see ${g.paths.log}`);
4041
4350
  }
4042
4351
  client.close();
4043
4352
  return {
@@ -4048,6 +4357,20 @@ async function startDetached(g, seconds) {
4048
4357
  log: g.paths.log
4049
4358
  };
4050
4359
  }
4360
+ /**
4361
+ * Nobody reads `listen`'s output any more (`listen | head -n 1`, a `grep -m1` that found its line): it stops as on
4362
+ * SIGTERM, exit 0. Through a daemon it died on an 'error' nobody handled; on its own profile it served on with nobody
4363
+ * to tell, holding the profile until `daemon stop`. Any other failure of the output is still an error.
4364
+ */
4365
+ function onReaderGone(stop) {
4366
+ let gone = false;
4367
+ process.stdout.on("error", (error) => {
4368
+ if (gone) return;
4369
+ if (error.code !== "EPIPE") throw error;
4370
+ gone = true;
4371
+ stop();
4372
+ });
4373
+ }
4051
4374
  async function listenCommand(argv) {
4052
4375
  const parsed = parseArgs(argv, {
4053
4376
  since: {
@@ -4134,7 +4457,7 @@ async function listenCommand(argv) {
4134
4457
  const c = await connectDaemon(g.paths, 1e3);
4135
4458
  if (!c) return null;
4136
4459
  try {
4137
- return (await c.call("chat.get", { chat })).peer || null;
4460
+ return (await c.call("chat.get", { chat }, { timeoutMs: ANSWER_MS })).peer || null;
4138
4461
  } finally {
4139
4462
  c.close();
4140
4463
  }
@@ -4142,42 +4465,15 @@ async function listenCommand(argv) {
4142
4465
  client.close();
4143
4466
  throw error;
4144
4467
  });
4145
- let current = client;
4146
- const reconnect = (wait) => void (async () => {
4147
- while (!current && !feed.paused) {
4148
- await new Promise((resolve) => setTimeout(resolve, wait));
4149
- wait = 1e3;
4150
- if (current || feed.paused) return;
4151
- const next = await connectDaemon(g.paths, 1e3);
4152
- if (next) {
4153
- current = next;
4154
- await follow(next).catch(() => {
4155
- if (current === next) current = null;
4156
- });
4157
- }
4158
- }
4159
- })();
4160
- const feed = new Feed(handle, since, () => {
4161
- const c = current;
4162
- current = null;
4163
- c?.destroy();
4164
- }, () => reconnect(0));
4165
- const follow = async (c) => {
4166
- await c.subscribe(feed.since, feed.take, () => {
4167
- if (current !== c) return;
4168
- current = null;
4169
- reconnect(1e3);
4170
- });
4171
- };
4172
- await follow(client);
4173
- process.on("SIGINT", () => {
4174
- current?.close();
4175
- process.exit(0);
4176
- });
4177
- process.on("SIGTERM", () => {
4178
- current?.close();
4468
+ const following = followDaemon(g.paths, client, handle, since);
4469
+ const stop = () => {
4470
+ following.close();
4179
4471
  process.exit(0);
4180
- });
4472
+ };
4473
+ onReaderGone(stop);
4474
+ await following.subscribed;
4475
+ process.on("SIGINT", stop);
4476
+ process.on("SIGTERM", stop);
4181
4477
  return;
4182
4478
  }
4183
4479
  const [{ keepServing, openHost, serve }, { callApi }] = await Promise.all([engineHost(), engineApi()]);
@@ -4198,6 +4494,7 @@ async function listenCommand(argv) {
4198
4494
  });
4199
4495
  host.ctx.stop = stop;
4200
4496
  onStopSignals(stop);
4497
+ onReaderGone(stop);
4201
4498
  let off = null, left = [];
4202
4499
  const open = (from) => {
4203
4500
  for (;;) {
@@ -4236,6 +4533,10 @@ async function engineCommand(argv) {
4236
4533
  type: "boolean",
4237
4534
  description: ""
4238
4535
  },
4536
+ yes: {
4537
+ type: "boolean",
4538
+ description: ""
4539
+ },
4239
4540
  force: {
4240
4541
  type: "boolean",
4241
4542
  description: ""
@@ -4243,13 +4544,21 @@ async function engineCommand(argv) {
4243
4544
  list: {
4244
4545
  type: "boolean",
4245
4546
  description: ""
4547
+ },
4548
+ verbose: {
4549
+ type: "boolean",
4550
+ description: ""
4246
4551
  }
4247
4552
  });
4248
4553
  const g = globals(parsed);
4249
4554
  pretty = g.pretty;
4250
4555
  if (parsed.options.list) {
4251
- noMoreThan(0, parsed.positionals, "engine --list");
4252
- print({ methods: [...ENGINE_METHODS, ...ENGINE_READS].sort() });
4556
+ noMoreThan(0, parsed.positionals, "engine --list [--verbose]");
4557
+ const methods = [...ENGINE_METHODS, ...ENGINE_READS].sort();
4558
+ print(parsed.options.verbose ? {
4559
+ methods,
4560
+ takes: Object.fromEntries(methods.map((m) => [m, ENGINE_PARAMS[m] ?? READ_PARAMS[m]]))
4561
+ } : { methods });
4253
4562
  return;
4254
4563
  }
4255
4564
  const [method, raw] = parsed.positionals;
@@ -4266,10 +4575,12 @@ async function engineCommand(argv) {
4266
4575
  }
4267
4576
  const confirmReal = parsed.options["confirm-real"] === true;
4268
4577
  if (confirmReal && params && typeof params === "object") params.confirmedReal = true;
4578
+ const yes = parsed.options.yes === true;
4269
4579
  let result = await withSession(g, (s) => s.call("engine.call", {
4270
4580
  method,
4271
4581
  params,
4272
4582
  confirmReal,
4583
+ yes,
4273
4584
  force: parsed.options.force === true
4274
4585
  }), { offline: ENGINE_READS.includes(method) });
4275
4586
  if (method === "getState" && !showSecret && result && typeof result === "object") {
@@ -4300,7 +4611,7 @@ async function settingsCommand(sub, argv) {
4300
4611
  parsedValue = value;
4301
4612
  }
4302
4613
  if ((await engineApi()).SETTABLE[key] === "string" && typeof parsedValue !== "string") parsedValue = value;
4303
- printAnswer(await withSession(g, (s) => s.call("settings.set", { [key]: parsedValue })));
4614
+ printAnswer(await withSession(g, (s) => s.call("settings.set", { [key]: parsedValue }), { offline: LOCAL_SETTINGS.has(key) }));
4304
4615
  return;
4305
4616
  }
4306
4617
  throw unknownCommand("settings", sub);
@@ -4330,7 +4641,7 @@ async function textCommand(name, argv) {
4330
4641
  };
4331
4642
  if (spec.target === "group") params.mentions = parsed.options.mention ?? [];
4332
4643
  if (parsed.options.reply !== void 0) params.reply = parsed.options.reply;
4333
- if (!spec.message) Object.assign(params, (await import("./buttons-CJqNkG-Q.js")).buttonsOf(parsed.options));
4644
+ if (!spec.message) Object.assign(params, (await import("./buttons-_GBlxAti.js")).buttonsOf(parsed.options));
4334
4645
  print(await withSession(g, (s) => {
4335
4646
  if (spec.target === "chat") {
4336
4647
  params.wait = parsed.options.wait ?? (s.mode === "one-shot" ? spec.message ? "confirmed" : "sent" : "none");
@@ -4438,8 +4749,17 @@ async function tableCommand(name, argv) {
4438
4749
  delete params.evidenceFile;
4439
4750
  delete params.stdin;
4440
4751
  }
4752
+ if (command.method === "x401.credential.add") {
4753
+ try {
4754
+ params.credential = params.file === "-" ? await readStdin() : readFileSync(params.file, "utf8");
4755
+ } catch (error) {
4756
+ throw localFileError(error, params.file) ?? new CliError("usage", `Cannot read the credential: ${error instanceof Error ? error.message : String(error)}`);
4757
+ }
4758
+ if (!params.credential) throw new CliError("bad_request", params.file === "-" ? "Nothing on stdin: give the SD-JWT VC the issuer sent" : `${params.file} is empty: it holds the SD-JWT VC the issuer sent`);
4759
+ delete params.file;
4760
+ }
4441
4761
  const lineValues = command.method === "wallet.create" ? Object.keys(params.values ?? {}) : [];
4442
- const secretWarning = await secretsFromStdin(command.method, params, readStdin, process.stdin.isTTY === true);
4762
+ const secretWarning = await secretsFromStdin(command.method, params, readStdin, process.stdin.isTTY === true, (note) => process.stderr.write(`ghostly: ${note}\n`));
4443
4763
  if (secretWarning) process.stderr.write(`ghostly: ${secretWarning}\n`);
4444
4764
  printAnswer(await withSession(g, async (s) => {
4445
4765
  if (lineValues.length) {
@@ -4447,15 +4767,15 @@ async function tableCommand(name, argv) {
4447
4767
  if (warning) process.stderr.write(`ghostly: ${warning}\n`);
4448
4768
  }
4449
4769
  const result = await s.call(command.method, params);
4450
- if (command.method === "invite.join" && s.mode === "one-shot") await s.call("chat.wait", {
4451
- chat: result.chat,
4452
- until: "paired",
4453
- timeout: 60
4454
- }).catch(() => {});
4770
+ if (command.method === "invite.join" && s.mode === "one-shot") {
4771
+ const joined = await oneShotJoinWait(s, result, typeof params.label === "string" ? params.label : void 0);
4772
+ if (joined.note) process.stderr.write(`ghostly: ${joined.note}\n`);
4773
+ return joined.answer;
4774
+ }
4455
4775
  return result;
4456
4776
  }, {
4457
4777
  deferGroups: chatOnly(command.method, params),
4458
- offline: readOnly(command.method, params)
4778
+ offline: offlineCall(command.method, params)
4459
4779
  }));
4460
4780
  }
4461
4781
  /**
@@ -4470,7 +4790,7 @@ async function callPipeCommand(argv) {
4470
4790
  if (!client) throw new CliError("unavailable", `No daemon runs profile ${g.profile}: calls need one (ghostly daemon --detach)`);
4471
4791
  let call;
4472
4792
  try {
4473
- call = await client.call("call.get", ref ? { call: ref } : {});
4793
+ call = await client.call("call.get", ref ? { call: ref } : {}, { timeoutMs: ANSWER_MS });
4474
4794
  } finally {
4475
4795
  client.close();
4476
4796
  }
@@ -4520,8 +4840,9 @@ async function main(input) {
4520
4840
  print({ version: VERSION });
4521
4841
  return 0;
4522
4842
  }
4523
- const [first, second] = argv;
4843
+ const [first, second, third] = argv;
4524
4844
  const two = second && !second.startsWith("-") ? `${first} ${second}` : "";
4845
+ const three = two && third && !third.startsWith("-") ? `${two} ${third}` : "";
4525
4846
  const text = (name) => Object.hasOwn(TEXT_COMMANDS, name), table = (name) => Object.hasOwn(COMMANDS, name);
4526
4847
  if (first === "profile" && !table(two)) {
4527
4848
  await profileCommand(second, argv.slice(2));
@@ -4551,6 +4872,10 @@ async function main(input) {
4551
4872
  await callPipeCommand(argv.slice(2));
4552
4873
  return -1;
4553
4874
  }
4875
+ if (three && table(three)) {
4876
+ await tableCommand(three, argv.slice(3));
4877
+ return 0;
4878
+ }
4554
4879
  if (two && text(two)) {
4555
4880
  await textCommand(two, argv.slice(2));
4556
4881
  return 0;
@@ -4567,18 +4892,28 @@ async function main(input) {
4567
4892
  await tableCommand(first, argv.slice(1));
4568
4893
  return 0;
4569
4894
  }
4570
- throw unknownCommand(first, two ? second : void 0);
4895
+ throw unknownCommand(first, two ? second : void 0, three ? third : void 0);
4571
4896
  }
4572
4897
  /**
4573
- * A command line that names no command. The first word of a group (`ghostly chat`, `ghostly wallet frob`) is answered
4574
- * with the group's commands, as `ghostly help chat` lists them; before, it was only "Unknown command: chat".
4898
+ * A command line that names no command. The words of a group (`ghostly chat`, `ghostly wallet frob`, `ghostly x401
4899
+ * credential`) are answered with the group's commands, as `ghostly help chat` lists them; before, it was only "Unknown
4900
+ * command: chat". The longest group the words name answers: `x401 credential frob` lists add, list and remove.
4575
4901
  */
4576
- function unknownCommand(first, second) {
4577
- const asked = [first, second].filter(Boolean).join(" ");
4578
- const subs = [...new Set(helpRows().map((row) => commandWords(row.usage)).filter((words) => words[0] === first && words.length > 1).map((words) => words[1]))];
4579
- if (!subs.length) return new CliError("usage", `Unknown command: ${asked} (ghostly help)`);
4580
- const list = `${subs.slice(0, -1).join(", ")}${subs.length > 1 ? " or " : ""}${subs.at(-1)}`;
4581
- return new CliError("usage", second ? `Unknown command: ${asked}: ghostly ${first} takes ${list} (ghostly help ${first})` : `ghostly ${first} takes a command: ${list} (ghostly help ${first})`);
4902
+ function unknownCommand(first, second, third) {
4903
+ const asked = [
4904
+ first,
4905
+ second,
4906
+ third
4907
+ ].filter((word) => !!word);
4908
+ const all = helpRows().map((row) => commandWords(row.usage));
4909
+ for (let depth = Math.min(asked.length, 2); depth >= 1; depth--) {
4910
+ const group = asked.slice(0, depth), name = group.join(" "), extra = asked[depth];
4911
+ const subs = [...new Set(all.filter((words) => words.length > depth && group.every((word, i) => words[i] === word)).map((words) => words[depth]))];
4912
+ if (!subs.length) continue;
4913
+ const list = `${subs.slice(0, -1).join(", ")}${subs.length > 1 ? " or " : ""}${subs.at(-1)}`;
4914
+ return new CliError("usage", extra ? `Unknown command: ${name} ${extra}: ghostly ${name} takes ${list} (ghostly help ${name})` : `ghostly ${name} takes a command: ${list} (ghostly help ${name})`);
4915
+ }
4916
+ return new CliError("usage", `Unknown command: ${asked.slice(0, 2).join(" ")} (ghostly help)`);
4582
4917
  }
4583
4918
  /** Leaves once what was printed reached stdout (a pipe drains asynchronously). */
4584
4919
  function exit(code) {
@@ -4614,4 +4949,4 @@ async function run(argv) {
4614
4949
  //#endregion
4615
4950
  export { run };
4616
4951
 
4617
- //# sourceMappingURL=main-Dt7xrqtb.js.map
4952
+ //# sourceMappingURL=main-BWAwKY4f.js.map