@ghostlytools/cli 1.2.4 → 1.3.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.
Files changed (100) hide show
  1. package/README.md +25 -9
  2. package/dist/assets/api-Dq2QXQDk.js +4172 -0
  3. package/dist/assets/api-Dq2QXQDk.js.map +1 -0
  4. package/dist/assets/api-TiB5QSG9.js +3 -0
  5. package/dist/assets/apiKit-CA2x5GJF.js +200 -0
  6. package/dist/assets/apiKit-CA2x5GJF.js.map +1 -0
  7. package/dist/assets/apps-BxV9i_Y4.js +786 -0
  8. package/dist/assets/apps-BxV9i_Y4.js.map +1 -0
  9. package/dist/assets/apps-C-T8jeji.js +2 -0
  10. package/dist/assets/{arkade-CdnKGDbg.js → arkade-C_qDmHnm.js} +2 -2
  11. package/dist/assets/{arkade-CdnKGDbg.js.map → arkade-C_qDmHnm.js.map} +1 -1
  12. package/dist/assets/backup-Bugnn3Ue.js +2 -0
  13. package/dist/assets/backup-CnMN2oUK.js +731 -0
  14. package/dist/assets/backup-CnMN2oUK.js.map +1 -0
  15. package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
  16. package/dist/assets/bitcoind-CZchoh43.js +349 -0
  17. package/dist/assets/bitcoind-CZchoh43.js.map +1 -0
  18. package/dist/assets/bitcoindProvider-DGw3xBSa.js +97 -0
  19. package/dist/assets/bitcoindProvider-DGw3xBSa.js.map +1 -0
  20. package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
  21. package/dist/assets/buttons-Bj7Vf7Pn.js +2 -0
  22. package/dist/assets/buttons-CvNBzh2S.js +496 -0
  23. package/dist/assets/buttons-CvNBzh2S.js.map +1 -0
  24. package/dist/assets/{endpoint-pgFkR2AS.js → endpoint-BOTFMpfS.js} +56 -18
  25. package/dist/assets/endpoint-BOTFMpfS.js.map +1 -0
  26. package/dist/assets/envelope-WjingYSc.js +90 -0
  27. package/dist/assets/envelope-WjingYSc.js.map +1 -0
  28. package/dist/assets/errors-DO5z6h_l.js +105 -0
  29. package/dist/assets/errors-DO5z6h_l.js.map +1 -0
  30. package/dist/assets/fedimint_client_wasm_bg-BtHHlFpp.js.map +1 -1
  31. package/dist/assets/{fileBytes-Dbifu5I5.js → fileBytes-CfSXjTFa.js} +2 -2
  32. package/dist/assets/{fileBytes-Dbifu5I5.js.map → fileBytes-CfSXjTFa.js.map} +1 -1
  33. package/dist/assets/fileBytes-CgbibpbV.js +2 -0
  34. package/dist/assets/{fileBytes-BcYZhNgF.js → fileBytes-Di7VZtmU.js} +6 -19
  35. package/dist/assets/fileBytes-Di7VZtmU.js.map +1 -0
  36. package/dist/assets/fileBytesIdb-CFP-DSkZ.js +2 -0
  37. package/dist/assets/{fileBytesIdb-DzSaqI0A.js → fileBytesIdb-D5u6H8X_.js} +4 -4
  38. package/dist/assets/{fileBytesIdb-DzSaqI0A.js.map → fileBytesIdb-D5u6H8X_.js.map} +1 -1
  39. package/dist/assets/{fileBytesOpfs-Dx8RZiV5.js → fileBytesOpfs-Dd1N4ERH.js} +3 -3
  40. package/dist/assets/{fileBytesOpfs-Dx8RZiV5.js.map → fileBytesOpfs-Dd1N4ERH.js.map} +1 -1
  41. package/dist/assets/groupFiles-IEUW3DQz.js +3202 -0
  42. package/dist/assets/groupFiles-IEUW3DQz.js.map +1 -0
  43. package/dist/assets/host-CWt-jtqz.js +4673 -0
  44. package/dist/assets/host-CWt-jtqz.js.map +1 -0
  45. package/dist/assets/hyperdhtRelay-BT_wRxab.js +21 -0
  46. package/dist/assets/hyperdhtRelay-BT_wRxab.js.map +1 -0
  47. package/dist/assets/{hyperdhtRelay-C1p0nkle.js → hyperdhtRelay-CPYwSLLS.js} +25 -10
  48. package/dist/assets/hyperdhtRelay-CPYwSLLS.js.map +1 -0
  49. package/dist/assets/{idb-CTF6LF5B.js → idb-B5L8wuOm.js} +7 -5
  50. package/dist/assets/idb-B5L8wuOm.js.map +1 -0
  51. package/dist/assets/irohWeb-4oChIOIp.js +544 -0
  52. package/dist/assets/irohWeb-4oChIOIp.js.map +1 -0
  53. package/dist/assets/{joinTrace-CO8mlhNO.js → joinTrace-CjRDFy85.js} +2 -7
  54. package/dist/assets/{joinTrace-CO8mlhNO.js.map → joinTrace-CjRDFy85.js.map} +1 -1
  55. package/dist/assets/joinTrace-D-fnt2p3.js +2 -0
  56. package/dist/assets/main-DbqcdShN.js +4409 -0
  57. package/dist/assets/main-DbqcdShN.js.map +1 -0
  58. package/dist/assets/{mainline-BH6s8HVi.js → mainline-ZkiOnevs.js} +11 -6
  59. package/dist/assets/mainline-ZkiOnevs.js.map +1 -0
  60. package/dist/assets/mints-BrlY773j.js +74 -0
  61. package/dist/assets/mints-BrlY773j.js.map +1 -0
  62. package/dist/assets/mints-CqTdA4Gk.js +2 -0
  63. package/dist/assets/networkReason-CetKkTPo.js +154 -0
  64. package/dist/assets/networkReason-CetKkTPo.js.map +1 -0
  65. package/dist/assets/nwc-CgkqyvsE.js +612 -0
  66. package/dist/assets/nwc-CgkqyvsE.js.map +1 -0
  67. package/dist/assets/nwcProvider-4eVz_8HP.js +77 -0
  68. package/dist/assets/nwcProvider-4eVz_8HP.js.map +1 -0
  69. package/dist/assets/privateFolder-Bk9C9oFv.js +38 -0
  70. package/dist/assets/privateFolder-Bk9C9oFv.js.map +1 -0
  71. package/dist/assets/profiles-BHc9pEYg.js +139 -0
  72. package/dist/assets/profiles-BHc9pEYg.js.map +1 -0
  73. package/dist/assets/registry-DlAKO0Fy.js +3793 -0
  74. package/dist/assets/registry-DlAKO0Fy.js.map +1 -0
  75. package/dist/assets/registry-OBJBnHVQ.js +2 -0
  76. package/dist/assets/resolve-CouHhCpJ.js +591 -0
  77. package/dist/assets/resolve-CouHhCpJ.js.map +1 -0
  78. package/dist/assets/{restoredRows-C_84q0Pd.js → restoredRows-Db3oUTko.js} +2 -2
  79. package/dist/assets/{restoredRows-C_84q0Pd.js.map → restoredRows-Db3oUTko.js.map} +1 -1
  80. package/dist/assets/{server-0zfNpABQ.js → server-Ss0gJ-nw.js} +1275 -1404
  81. package/dist/assets/server-Ss0gJ-nw.js.map +1 -0
  82. package/dist/assets/{src-CbLkHfz8.js → src-DDzSERyc.js} +882 -134
  83. package/dist/assets/src-DDzSERyc.js.map +1 -0
  84. package/dist/assets/turnRefs-B87dtrHM.js +323 -0
  85. package/dist/assets/turnRefs-B87dtrHM.js.map +1 -0
  86. package/dist/assets/types-C4_KafL_.js +122 -0
  87. package/dist/assets/types-C4_KafL_.js.map +1 -0
  88. package/dist/assets/{usdt-Qf4UuygS.js → usdt-Fj8CQO2z.js} +3 -3
  89. package/dist/assets/{usdt-Qf4UuygS.js.map → usdt-Fj8CQO2z.js.map} +1 -1
  90. package/dist/ghostly.mjs +4 -22077
  91. package/dist/ghostly.mjs.map +1 -1
  92. package/package.json +1 -1
  93. package/dist/assets/endpoint-pgFkR2AS.js.map +0 -1
  94. package/dist/assets/fileBytes-BcYZhNgF.js.map +0 -1
  95. package/dist/assets/hyperdhtRelay-C1p0nkle.js.map +0 -1
  96. package/dist/assets/idb-CTF6LF5B.js.map +0 -1
  97. package/dist/assets/mainline-BH6s8HVi.js.map +0 -1
  98. package/dist/assets/rolldown-runtime-8H4AJuhK.js +0 -14
  99. package/dist/assets/server-0zfNpABQ.js.map +0 -1
  100. package/dist/assets/src-CbLkHfz8.js.map +0 -1
@@ -0,0 +1,4409 @@
1
+ import { $a as cardUrl, Ga as ITEM_STATES, Ja as RUN_RESULTS, Ka as PR_CHECKS, Ya as STATUS_CARD_LIMITS, Zn as GROUP_FILE_LIMITS, qa as PR_STATES } from "./src-DDzSERyc.js";
2
+ import { a as SECRET_RESULTS, i as ENGINE_READS, r as ENGINE_METHODS, t as turnRef } from "./turnRefs-B87dtrHM.js";
3
+ import { n as EXIT, r as asCliError, t as CliError } from "./errors-DO5z6h_l.js";
4
+ import { t as CONTACT_KEY } from "./apiKit-CA2x5GJF.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-BHc9pEYg.js";
6
+ import { n as ownSocket } from "./privateFolder-Bk9C9oFv.js";
7
+ import { accessSync, constants, existsSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
8
+ import { dirname, resolve } from "node:path";
9
+ import { spawn } from "node:child_process";
10
+ import { connect } from "node:net";
11
+ import { createInterface } from "node:readline";
12
+ import { format } from "node:util";
13
+ var package_default = {
14
+ name: "@ghostlytools/cli",
15
+ version: "1.3.1",
16
+ type: "module",
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
+ license: "MIT",
19
+ repository: {
20
+ "type": "git",
21
+ "url": "git+https://github.com/MiguelMedeiros/ghostly.git",
22
+ "directory": "packages/cli"
23
+ },
24
+ homepage: "https://ghostly.tools/cli",
25
+ bugs: "https://github.com/MiguelMedeiros/ghostly/issues",
26
+ publishConfig: { "access": "public" },
27
+ bin: { "ghostly": "./dist/ghostly.mjs" },
28
+ files: [
29
+ "dist",
30
+ "examples",
31
+ "README.md",
32
+ "SKILL.md"
33
+ ],
34
+ engines: { "node": ">=22.12" },
35
+ scripts: {
36
+ "build": "vite build",
37
+ "typecheck": "tsc --noEmit -p tsconfig.json",
38
+ "test": "vitest run"
39
+ },
40
+ dependencies: {
41
+ "@arkade-os/sdk": "0.4.78",
42
+ "@atproto/jwk-webcrypto": "^0.3.4",
43
+ "@atproto/oauth-client": "^0.8.9",
44
+ "@bitcoindevkit/bdk-wallet-web": "0.3.0",
45
+ "@bitcoinerlab/descriptors-scure": "^3.2.0",
46
+ "@cashu/cashu-ts": "^4.11.0",
47
+ "@fedimint/core": "0.0.0-canary-eea6a3c98909fa8e736d6dadb1f1243fd3322b27",
48
+ "@fedimint/fedimint-client-wasm-bundler": "0.0.0-canary-eea6a3c98909fa8e736d6dadb1f1243fd3322b27",
49
+ "@fedimint/types": "0.0.0-canary-eea6a3c98909fa8e736d6dadb1f1243fd3322b27",
50
+ "@hyperswarm/dht-relay": "0.4.3",
51
+ "@noble/ciphers": "^2.4.0",
52
+ "@noble/curves": "^2.4.0",
53
+ "@noble/hashes": "2.4.0",
54
+ "@scure/base": "2.4.0",
55
+ "@scure/bip32": "^2.4.0",
56
+ "@scure/bip39": "2.4.0",
57
+ "@scure/btc-signer": "2.4.1",
58
+ "@secondts/bark": "0.25.0",
59
+ "@serenity-kit/opaque": "1.1.0",
60
+ "@synonymdev/pubky": "0.14.0",
61
+ "@tetherto/wdk-wallet-evm": "1.0.0-beta.20",
62
+ "b4a": "1.9.0",
63
+ "bittorrent-dht": "11.0.12",
64
+ "ethers": "6.17.0",
65
+ "eventsource": "^5.1.2",
66
+ "fake-indexeddb": "6.2.5",
67
+ "hyperdht": "6.34.1",
68
+ "keet-identity-key": "3.2.0",
69
+ "mpg123-decoder": "1.0.3",
70
+ "node-datachannel": "^0.33.4",
71
+ "nostr-tools": "2.25.2",
72
+ "opus-decoder": "0.7.12",
73
+ "opusscript": "0.1.1"
74
+ },
75
+ devDependencies: {
76
+ "@ghostly/browser": "*",
77
+ "@ghostly/core": "*",
78
+ "@ghostly/iroh-web": "*",
79
+ "@types/node": "^26.6.4",
80
+ "typescript": "^5.9.3",
81
+ "vite": "^8.3.3",
82
+ "vitest": "^5.0.3"
83
+ }
84
+ };
85
+ //#endregion
86
+ //#region src/args.ts
87
+ /**
88
+ * Options every command takes. They may come anywhere on the line.
89
+ */
90
+ var GLOBAL_OPTIONS = {
91
+ profile: {
92
+ type: "string",
93
+ short: "p",
94
+ description: "Profile to use (default: $GHOSTLY_PROFILE, then the current one)"
95
+ },
96
+ home: {
97
+ type: "string",
98
+ description: "Ghostly's folder (default: $GHOSTLY_HOME, then ~/.ghostly)"
99
+ },
100
+ pretty: {
101
+ type: "boolean",
102
+ description: "Indent the JSON output"
103
+ },
104
+ help: {
105
+ type: "boolean",
106
+ short: "h",
107
+ description: "Show help"
108
+ }
109
+ };
110
+ /**
111
+ * A small, strict parser: `--name value`, `--name=value`, `-x value`, boolean flags, repeatable lists, and `--`
112
+ * before positionals that start with a dash.
113
+ *
114
+ * The value of an option is taken as is even when it starts with `-` (base64url keys and seeds do one time in 64:
115
+ * the ghostly-cli lesson of #208). So is a positional that `idSlot` says takes an id, a key or a draft: there a word
116
+ * that starts with `-`, is longer than two characters and is no option of the command is the value (`--` starts one
117
+ * id in 4096; `-x` stays an option). Any other positional that starts with `-` is refused instead of sent: a
118
+ * mistyped flag must not reach a contact as text. Put `--` before a message that starts with a dash.
119
+ */
120
+ function parseArgs(argv, spec, idSlot = () => false) {
121
+ const all = {
122
+ ...GLOBAL_OPTIONS,
123
+ ...spec
124
+ };
125
+ const byShort = new Map(Object.entries(all).filter(([, s]) => s.short).map(([name, s]) => [s.short, name]));
126
+ const isOption = (arg) => {
127
+ if (!arg.startsWith("--")) return arg.length === 2 && byShort.has(arg.slice(1));
128
+ const name = arg.slice(2).split("=")[0];
129
+ return Object.hasOwn(all, name) || name.startsWith("no-") && all[name.slice(3)]?.type === "boolean";
130
+ };
131
+ const positionals = [];
132
+ const options = {};
133
+ for (let i = 0; i < argv.length; i++) {
134
+ const arg = argv[i];
135
+ if (arg === "--") {
136
+ positionals.push(...argv.slice(i + 1));
137
+ break;
138
+ }
139
+ if (arg.startsWith("-") && arg.length > 2 && idSlot(positionals.length) && !isOption(arg)) {
140
+ positionals.push(arg);
141
+ continue;
142
+ }
143
+ if (arg.startsWith("--") || arg.startsWith("-") && arg.length === 2 && arg !== "-") {
144
+ let name, inline;
145
+ if (arg.startsWith("--")) {
146
+ const eq = arg.indexOf("=");
147
+ name = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
148
+ inline = eq === -1 ? void 0 : arg.slice(eq + 1);
149
+ } else {
150
+ const long = byShort.get(arg.slice(1));
151
+ if (!long) throw new CliError("usage", `Unknown option ${arg}`);
152
+ name = long;
153
+ }
154
+ if (name.startsWith("no-") && all[name.slice(3)]?.type === "boolean" && inline === void 0) {
155
+ options[name.slice(3)] = false;
156
+ continue;
157
+ }
158
+ const option = all[name];
159
+ if (!option) throw new CliError("usage", `Unknown option --${name}`);
160
+ if (option.type === "boolean") {
161
+ if (inline !== void 0 && !["true", "false"].includes(inline)) throw new CliError("usage", `--${name} takes no value`);
162
+ options[name] = inline === void 0 ? true : inline === "true";
163
+ continue;
164
+ }
165
+ let value = inline;
166
+ if (value === void 0 && option.optionalValue && !(i + 1 < argv.length && argv[i + 1] !== "" && Number.isFinite(Number(argv[i + 1])))) {
167
+ options[name] = true;
168
+ continue;
169
+ }
170
+ if (value === void 0) {
171
+ if (i + 1 >= argv.length) throw new CliError("usage", `--${name} needs a value`);
172
+ value = argv[++i];
173
+ }
174
+ if (option.type === "number") {
175
+ const number = Number(value);
176
+ if (!Number.isFinite(number)) throw new CliError("usage", `--${name} takes a number, not ${JSON.stringify(value)}`);
177
+ options[name] = number;
178
+ } else if (option.type === "list") options[name] = [...options[name] ?? [], value];
179
+ else options[name] = value;
180
+ continue;
181
+ }
182
+ if (arg.startsWith("-") && arg !== "-") throw new CliError("usage", `Unknown option ${arg} (put -- before text that starts with a dash)`);
183
+ positionals.push(arg);
184
+ }
185
+ return {
186
+ positionals,
187
+ options
188
+ };
189
+ }
190
+ /** The global option a word names (`--profile`, `--profile=bot`, `-p`), else undefined. */
191
+ function globalOption(arg) {
192
+ const name = globalName(arg);
193
+ return name ? GLOBAL_OPTIONS[name] : void 0;
194
+ }
195
+ /** The name of the global option a word names (`profile` for `-p`, `pretty` for `--no-pretty`), else undefined. */
196
+ function globalName(arg) {
197
+ const eq = arg.indexOf("=");
198
+ const name = arg.startsWith("--") ? arg.slice(2, eq === -1 ? void 0 : eq) : arg.length === 2 && arg.startsWith("-") ? Object.entries(GLOBAL_OPTIONS).find(([, o]) => o.short === arg.slice(1))?.[0] : void 0;
199
+ if (name && Object.hasOwn(GLOBAL_OPTIONS, name)) return name;
200
+ const negated = name?.startsWith("no-") && eq === -1 ? name.slice(3) : void 0;
201
+ return negated && GLOBAL_OPTIONS[negated]?.type === "boolean" ? negated : void 0;
202
+ }
203
+ /**
204
+ * Global options given before the command (`ghostly --profile bot send …`) move after it, where every command's
205
+ * parser reads them. Stops at `--` and at the first word that is not a global option. They go before the first word
206
+ * after the command's that starts with a dash (its first option, or the `--` that ends them), else at the end: never
207
+ * between an option and its value, which may itself be `--` (`--pretty profile set --name --`). One that takes a value
208
+ * and is the last word has none: a usage error.
209
+ */
210
+ function liftGlobals(argv) {
211
+ const lifted = [];
212
+ let i = 0;
213
+ while (i < argv.length) {
214
+ const arg = argv[i];
215
+ const eq = arg.indexOf("=");
216
+ const option = globalOption(arg);
217
+ if (!option || arg === "--") break;
218
+ const takesValue = option.type !== "boolean" && eq === -1;
219
+ if (takesValue && i + 1 >= argv.length) throw new CliError("usage", `--${globalName(arg)} needs a value`);
220
+ lifted.push(...argv.slice(i, i + (takesValue ? 2 : 1)));
221
+ i += takesValue ? 2 : 1;
222
+ }
223
+ const rest = argv.slice(i);
224
+ const dash = rest[0] === "--" ? 0 : rest.findIndex((word, i) => i > 0 && word.startsWith("-"));
225
+ return dash === -1 ? [...rest, ...lifted] : [
226
+ ...rest.slice(0, dash),
227
+ ...lifted,
228
+ ...rest.slice(dash)
229
+ ];
230
+ }
231
+ //#endregion
232
+ //#region src/client.ts
233
+ /**
234
+ * A connection to a profile's daemon over its socket (WISP 1100 § Local control API). `null` from `connectDaemon`
235
+ * means no daemon answers there: a one-shot command runs the profile itself.
236
+ */
237
+ var DaemonClient = class {
238
+ socket;
239
+ nextId = 1;
240
+ pending = /* @__PURE__ */ new Map();
241
+ eventListener = null;
242
+ closedListener = null;
243
+ constructor(socket) {
244
+ this.socket = socket;
245
+ createInterface({
246
+ input: socket,
247
+ crlfDelay: Infinity
248
+ }).on("line", (line) => {
249
+ let message;
250
+ try {
251
+ message = JSON.parse(line);
252
+ } catch {
253
+ return;
254
+ }
255
+ if (message.event) {
256
+ this.eventListener?.(message.event);
257
+ return;
258
+ }
259
+ const waiting = typeof message.id === "number" ? this.pending.get(message.id) : void 0;
260
+ if (!waiting) return;
261
+ this.pending.delete(message.id);
262
+ if (message.error) waiting.reject(new CliError(message.error.code, message.error.message, message.error.details));
263
+ else waiting.resolve(message.result);
264
+ });
265
+ socket.on("close", () => {
266
+ for (const waiting of this.pending.values()) waiting.reject(new CliError("unavailable", "The daemon went away"));
267
+ this.pending.clear();
268
+ this.closedListener?.();
269
+ });
270
+ socket.on("error", () => {});
271
+ }
272
+ call(method, params = {}) {
273
+ const id = this.nextId++;
274
+ return new Promise((resolve, reject) => {
275
+ this.pending.set(id, {
276
+ resolve,
277
+ reject
278
+ });
279
+ this.socket.write(JSON.stringify({
280
+ id,
281
+ method,
282
+ params
283
+ }) + "\n");
284
+ });
285
+ }
286
+ /** Events after `since` (replayed from the journal), then every new one, until the connection closes. */
287
+ async subscribe(since, listener, onClosed) {
288
+ this.eventListener = listener;
289
+ this.closedListener = onClosed ?? null;
290
+ return this.call("events.subscribe", since === void 0 ? {} : { since });
291
+ }
292
+ close() {
293
+ this.socket.end();
294
+ }
295
+ /** Closes at once: what the daemon still sends is not read. */
296
+ destroy() {
297
+ this.socket.destroy();
298
+ }
299
+ };
300
+ function connectDaemon(path, timeoutMs = 2e3) {
301
+ if (!ownSocket(path)) return Promise.resolve(null);
302
+ return new Promise((resolve) => {
303
+ const socket = connect(path);
304
+ const timer = setTimeout(() => {
305
+ socket.destroy();
306
+ resolve(null);
307
+ }, timeoutMs);
308
+ socket.once("connect", () => {
309
+ clearTimeout(timer);
310
+ resolve(new DaemonClient(socket));
311
+ });
312
+ socket.once("error", () => {
313
+ clearTimeout(timer);
314
+ resolve(null);
315
+ });
316
+ });
317
+ }
318
+ //#endregion
319
+ //#region src/commands/shared.ts
320
+ var wait = {
321
+ wait: {
322
+ type: "string",
323
+ description: "none, sent or delivered (default: none with a daemon, sent without)"
324
+ },
325
+ timeout: {
326
+ type: "number",
327
+ description: "Seconds to wait (default 30)"
328
+ }
329
+ };
330
+ /** A group has no receipts: `sent` waits until an edge took the frame (a member's, or one of my hubs'). */
331
+ var groupWait = {
332
+ wait: {
333
+ type: "string",
334
+ description: "none or sent: until an edge took it (default none)"
335
+ },
336
+ timeout: {
337
+ type: "number",
338
+ description: "Seconds to wait (default 30)"
339
+ }
340
+ };
341
+ var net = {
342
+ type: "string",
343
+ description: "mainnet or testnet (default testnet)"
344
+ };
345
+ var card = {
346
+ type: "string",
347
+ description: "A Lightning card (its id; default: the network's default)"
348
+ };
349
+ var memo = {
350
+ type: "string",
351
+ description: "What it is for (up to 140 characters)"
352
+ };
353
+ var confirmReal = {
354
+ type: "boolean",
355
+ description: "Confirm a Mainnet (real money) spend"
356
+ };
357
+ /** A path as this command's working directory means it: the daemon runs elsewhere. */
358
+ function here(path) {
359
+ return typeof path === "string" ? resolve(path) : void 0;
360
+ }
361
+ /** A whole number of sats from the command line. */
362
+ function sats(value) {
363
+ const n = Number(value);
364
+ if (!value || !Number.isSafeInteger(n) || n <= 0) throw new CliError("usage", `Not an amount in sats: ${JSON.stringify(value)}`);
365
+ return n;
366
+ }
367
+ /** `name=value` options as an object. */
368
+ function pairs(values) {
369
+ if (!Array.isArray(values) || !values.length) return void 0;
370
+ const out = {};
371
+ for (const item of values) {
372
+ const eq = item.indexOf("=");
373
+ if (eq < 1) throw new CliError("usage", `--value takes name=value, not ${JSON.stringify(item)}`);
374
+ out[item.slice(0, eq)] = item.slice(eq + 1);
375
+ }
376
+ return out;
377
+ }
378
+ var rate = {
379
+ type: "number",
380
+ description: "The audio's sample rate: 48000 (default), 24000, 16000, 12000 or 8000"
381
+ };
382
+ var force = {
383
+ type: "boolean",
384
+ description: "Send even if the text looks like a seed, a key or ecash"
385
+ };
386
+ var reply = {
387
+ type: "string",
388
+ description: "Reply to this message (its id, from history or an event)"
389
+ };
390
+ /** Buttons under a message (WISP 406 · Message Buttons): the text is the question. */
391
+ var buttonOptions = {
392
+ button: {
393
+ type: "list",
394
+ description: "A button under the message, id:Label (again for each, 1 to 6; the label may hold colons). A press comes back as button.pressed"
395
+ },
396
+ style: {
397
+ type: "list",
398
+ description: "A button's look, id=primary, neutral or danger"
399
+ },
400
+ once: {
401
+ type: "boolean",
402
+ description: "Each person answers once: after a press their app offers the buttons no more"
403
+ },
404
+ id: {
405
+ type: "string",
406
+ description: "The question's id (default: made up, ask-…)"
407
+ }
408
+ };
409
+ /** The group's entry link lets anyone join: printed only when asked for. */
410
+ var showSecret = { "show-secret": {
411
+ type: "boolean",
412
+ description: "Print the group's entry link (anyone who has it can join)"
413
+ } };
414
+ /** A pending chat's invite code lets whoever reads it take the chat: printed only when asked for. */
415
+ var showInvite = { "show-secret": {
416
+ type: "boolean",
417
+ description: "Print a pending chat's invite code (whoever has it can take the chat)"
418
+ } };
419
+ var secret = (options) => options["show-secret"] === true ? { showSecret: true } : {};
420
+ /**
421
+ * `--for` of the typing commands: the seconds it is kept on, 1 to 600, and never with `--stop`. A usage error (exit 2)
422
+ * that names the flag: before, the API's own check answered ("for must be a number from 1 to 600", exit 1).
423
+ */
424
+ function typingFor(options) {
425
+ const seconds = options.for;
426
+ if (seconds === void 0) return void 0;
427
+ if (typeof seconds !== "number" || !(seconds >= 1 && seconds <= 600)) throw new CliError("usage", `--for takes a number of seconds from 1 to 600, not ${JSON.stringify(seconds)}`);
428
+ if (options.stop === true) throw new CliError("usage", "--for keeps it on: not with --stop");
429
+ return seconds;
430
+ }
431
+ /**
432
+ * A time given as digits: milliseconds. Unix seconds (from 1e9, 2001, up to 1e11, which as milliseconds would be 1970
433
+ * to 1973) are refused with the milliseconds they mean: before, they were read as January 1970 without a word.
434
+ */
435
+ function milliseconds(text, flag) {
436
+ const ms = Number(text);
437
+ if (ms >= 1e9 && ms < 1e11) throw new CliError("usage", `--${flag} takes milliseconds, not seconds: ${text}000`);
438
+ return ms;
439
+ }
440
+ /**
441
+ * A history cursor: a time when it is all digits (`0` is from the start), else a message id. Message ids are never all
442
+ * digits (`me_…`, `peer_…`): before, fewer than 10 digits were read as an id ("No message 0").
443
+ */
444
+ function cursor(value, flag) {
445
+ if (value === void 0) return void 0;
446
+ const text = String(value).trim();
447
+ return /^\d+$/.test(text) ? milliseconds(text, flag) : text;
448
+ }
449
+ /**
450
+ * A status card's fields given as JSON (WISP 405 · Status Cards): inline (`{...}`), `-` for stdin, or a file's path. The
451
+ * flags given beside it win over it.
452
+ */
453
+ function cardJson(value) {
454
+ if (value === void 0) return {};
455
+ const text = String(value).trim();
456
+ let raw;
457
+ try {
458
+ raw = text.startsWith("{") ? text : readFileSync(text === "-" ? 0 : resolve(text), "utf8");
459
+ } catch (error) {
460
+ throw new CliError("usage", `--json takes a card as JSON, - for stdin, or a file: ${error instanceof Error ? error.message : String(error)}`);
461
+ }
462
+ let parsed;
463
+ try {
464
+ parsed = JSON.parse(raw);
465
+ } catch {
466
+ throw new CliError("usage", "--json is not valid JSON");
467
+ }
468
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new CliError("usage", "--json is a JSON object of the card's fields");
469
+ return parsed;
470
+ }
471
+ /** Options every status card command takes. */
472
+ var cardOptions = {
473
+ json: {
474
+ type: "string",
475
+ description: "The card's fields as JSON: inline, - for stdin, or a file (flags win over it)"
476
+ },
477
+ link: {
478
+ type: "list",
479
+ description: "A link on the card: https://… or label=https://… (up to 4)"
480
+ },
481
+ text: {
482
+ type: "string",
483
+ description: "The text older apps show instead (default: written from the card)"
484
+ },
485
+ force: {
486
+ type: "boolean",
487
+ description: "Send even if the card's text looks like a seed, a key or ecash"
488
+ }
489
+ };
490
+ /** `--link label=https://…` or a bare link, as a card's links. */
491
+ function cardLinks(values) {
492
+ if (!Array.isArray(values) || !values.length) return void 0;
493
+ return values.map((value) => {
494
+ const eq = value.indexOf("=");
495
+ return eq > 0 && !value.slice(0, eq).includes(":") ? {
496
+ label: value.slice(0, eq),
497
+ url: value.slice(eq + 1)
498
+ } : { url: value };
499
+ });
500
+ }
501
+ var listed = (words) => `${words.slice(0, -1).join(", ")} or ${words.at(-1)}`;
502
+ /**
503
+ * `--item state:text` as a task's item. The word before the first colon is the state when it is one lowercase word (and
504
+ * the colon does not start `://`): one of `ITEM_STATES`, or a usage error that lists them, so `queued:Publishing` is
505
+ * refused rather than shown as that text. Anything else (`Step 2: build`, `Note: x`, a link, no colon) is the whole
506
+ * text, pending; `pending:fix: tests` keeps a colon after a lowercase word.
507
+ */
508
+ function itemOf(value) {
509
+ const colon = value.indexOf(":");
510
+ const word = colon > 0 ? value.slice(0, colon) : "";
511
+ if (!/^[a-z]+$/.test(word) || value.startsWith("//", colon + 1)) return {
512
+ state: "pending",
513
+ text: value
514
+ };
515
+ if (!ITEM_STATES.includes(word)) throw new CliError("usage", `--item takes state:text, and ${JSON.stringify(word)} is not a state: use ${listed(ITEM_STATES)} (pending:<text> keeps a colon in the text), not ${JSON.stringify(value)}`);
516
+ return {
517
+ state: word,
518
+ text: value.slice(colon + 1).trim()
519
+ };
520
+ }
521
+ /** `--progress`, `--left`: a percent, or a usage error that names the flag. */
522
+ function percent(options, flag) {
523
+ const value = options[flag];
524
+ if (value !== void 0 && !(typeof value === "number" && value >= 0 && value <= 100)) throw new CliError("usage", `--${flag} takes a percent from 0 to 100, not ${JSON.stringify(value)}`);
525
+ }
526
+ /** The flags of a task's pull request, and the field of `pr` each one gives. */
527
+ var PR_FLAGS = [
528
+ ["pr-url", "url"],
529
+ ["pr-number", "number"],
530
+ ["additions", "additions"],
531
+ ["deletions", "deletions"],
532
+ ["files", "files"],
533
+ ["pr-state", "state"],
534
+ ["pr-checks", "checks"]
535
+ ];
536
+ /**
537
+ * A task's fields from its flags; what was not given is left out, so an update changes only what it names. What a flag
538
+ * got wrong is a usage error (exit 2) that names the flag: before, it reached the card's own check and failed as
539
+ * `bad_request` (exit 1) in the card's words ("Status card: done is at most total").
540
+ */
541
+ function taskFields(options) {
542
+ const fields = { ...cardJson(options.json) };
543
+ const set = (key, value) => {
544
+ if (value !== void 0) fields[key] = value;
545
+ };
546
+ set("id", options.id);
547
+ set("title", options.title);
548
+ set("status", options.status);
549
+ percent(options, "progress");
550
+ if (options.progress !== void 0 && options.steps !== void 0) throw new CliError("usage", "--progress or --steps, not both");
551
+ set("progress", options.progress);
552
+ set("step", options.step);
553
+ set("branch", options.branch);
554
+ if (options.steps !== void 0) {
555
+ const match = /^(\d+)\s*\/\s*(\d+)$/.exec(String(options.steps));
556
+ if (!match) throw new CliError("usage", `--steps takes done/total, like 2/5, not ${JSON.stringify(options.steps)}`);
557
+ fields.done = Number(match[1]);
558
+ fields.total = Number(match[2]);
559
+ if (Number(match[2]) < 1 || Number(match[1]) > Number(match[2])) throw new CliError("usage", `--steps takes done/total with done at most total, like 2/5, not ${JSON.stringify(options.steps)}`);
560
+ }
561
+ if (options["pr-url"] !== void 0 && !cardUrl(options["pr-url"])) throw new CliError("usage", `--pr-url takes an https link of at most ${STATUS_CARD_LIMITS.url} characters, not ${JSON.stringify(options["pr-url"])}`);
562
+ for (const [flag, words] of [["pr-state", PR_STATES], ["pr-checks", PR_CHECKS]]) if (options[flag] !== void 0 && !words.includes(options[flag])) throw new CliError("usage", `--${flag} takes ${listed(words)}, not ${JSON.stringify(options[flag])}`);
563
+ const pr = {};
564
+ for (const [flag, key] of PR_FLAGS) if (options[flag] !== void 0) pr[key] = options[flag];
565
+ if (Object.keys(pr).length) fields.pr = {
566
+ ...fields.pr && typeof fields.pr === "object" ? fields.pr : {},
567
+ ...pr
568
+ };
569
+ if (Array.isArray(options.item) && options.item.length) fields.items = options.item.map(itemOf);
570
+ if (Array.isArray(options.tag) && options.tag.length > STATUS_CARD_LIMITS.tags) throw new CliError("usage", `--tag is given at most ${STATUS_CARD_LIMITS.tags} times, not ${options.tag.length}`);
571
+ if (Array.isArray(options.tag) && options.tag.length) fields.tags = options.tag;
572
+ set("parent", options.parent);
573
+ const links = cardLinks(options.link);
574
+ if (links) fields.links = links;
575
+ return fields;
576
+ }
577
+ /**
578
+ * The fields a new card cannot go without, given as a flag or in `--json`: a usage error (exit 2) that names the
579
+ * flags. Before, a missing `--title` reached the card's own check and failed as `bad_request` (exit 1) with
580
+ * "Status card: title is text".
581
+ */
582
+ function needs(card, fields, usage) {
583
+ const missing = fields.filter(([key]) => typeof card[key] !== "string" || !card[key].trim()).map(([, flag]) => `--${flag}`);
584
+ if (missing.length) throw new CliError("usage", `${missing.join(" and ")} ${missing.length > 1 ? "are" : "is"} needed: ghostly ${usage}`);
585
+ return card;
586
+ }
587
+ /**
588
+ * A new task's pull request goes by its link: `--pr-state` and the like without `--pr-url` (or `pr.url` in `--json`)
589
+ * are a usage error that names them. An update's card may have the link already, so only a send asks.
590
+ */
591
+ function prLinked(card, options, usage) {
592
+ const given = PR_FLAGS.filter(([flag]) => options[flag] !== void 0).map(([flag]) => `--${flag}`);
593
+ if (!given.length || card.pr.url !== void 0) return card;
594
+ throw new CliError("usage", `${given.join(" and ")} ${given.length > 1 ? "go" : "goes"} with --pr-url: ghostly ${usage}`);
595
+ }
596
+ /**
597
+ * A usage card says how much is left: `--left`, or `--used` with `--limit`, as flags or in `--json`. The one that is
598
+ * missing is a usage error that names the flag.
599
+ */
600
+ function quota(card, usage) {
601
+ const used = card.used !== void 0;
602
+ if (used !== (card.limit !== void 0)) throw new CliError("usage", `--${used ? "used" : "limit"} goes with --${used ? "limit" : "used"}: ghostly ${usage}`);
603
+ if (card.left === void 0 && !used) throw new CliError("usage", `--left is needed (or --used with --limit): ghostly ${usage}`);
604
+ return card;
605
+ }
606
+ /** A time a command names: milliseconds, or a date the JavaScript Date reads (`2026-09-30T01:00:00Z`). */
607
+ function cardTime(value, flag) {
608
+ if (value === void 0) return void 0;
609
+ const text = String(value).trim();
610
+ const ms = /^\d+$/.test(text) ? milliseconds(text, flag) : Date.parse(text);
611
+ if (!Number.isSafeInteger(ms) || ms <= 0) throw new CliError("usage", `--${flag} takes a time in milliseconds or a date like 2026-09-30T01:00:00Z, not ${JSON.stringify(value)}`);
612
+ return ms;
613
+ }
614
+ /** `--run ok`, `--run failed:"CI flaked"`: a run of a routine, recorded as its last. */
615
+ function runOf(value) {
616
+ if (value === void 0) return void 0;
617
+ const text = String(value), colon = text.indexOf(":");
618
+ const result = colon === -1 ? text : text.slice(0, colon), summary = colon === -1 ? "" : text.slice(colon + 1).trim();
619
+ if (!RUN_RESULTS.includes(result)) throw new CliError("usage", `--run takes ok, failed or skipped, and :a summary after it if you like, not ${JSON.stringify(value)}`);
620
+ return {
621
+ result,
622
+ ...summary && { summary }
623
+ };
624
+ }
625
+ /** A routine's fields from its flags; what was not given is left out. */
626
+ function routineFields(options) {
627
+ const fields = { ...cardJson(options.json) };
628
+ for (const key of [
629
+ "id",
630
+ "name",
631
+ "schedule",
632
+ "cron",
633
+ "state"
634
+ ]) if (options[key] !== void 0) fields[key] = options[key];
635
+ const next = cardTime(options.next, "next");
636
+ if (next !== void 0) fields.nextRunAt = next;
637
+ const links = cardLinks(options.link);
638
+ if (links) fields.links = links;
639
+ return fields;
640
+ }
641
+ /** Options a routine command takes, beside `cardOptions`. */
642
+ var routineOptions = {
643
+ name: {
644
+ type: "string",
645
+ description: "What the routine is (120 characters)"
646
+ },
647
+ schedule: {
648
+ type: "string",
649
+ description: "When it runs, as people say it: \"every day 01:00\" (80 characters)"
650
+ },
651
+ cron: {
652
+ type: "string",
653
+ description: "Its cron line, shown beside the schedule (never run)"
654
+ },
655
+ state: {
656
+ type: "string",
657
+ description: "active (default on send) or paused"
658
+ },
659
+ next: {
660
+ type: "string",
661
+ description: "Its next run: milliseconds or a date like 2026-09-30T01:00:00Z"
662
+ },
663
+ run: {
664
+ type: "string",
665
+ description: "Record a run now: ok, failed or skipped, with :a summary if you like (it becomes the last run)"
666
+ },
667
+ ...cardOptions
668
+ };
669
+ /** Options a task command takes, beside `cardOptions`. */
670
+ var taskOptions = {
671
+ title: {
672
+ type: "string",
673
+ description: "What the task is (120 characters)"
674
+ },
675
+ status: {
676
+ type: "string",
677
+ description: "queued, running (default on send), blocked, done, failed or cancelled"
678
+ },
679
+ progress: {
680
+ type: "number",
681
+ description: "Percent done, 0 to 100"
682
+ },
683
+ steps: {
684
+ type: "string",
685
+ description: "Steps done of total, like 2/5 (progress is worked out from them)"
686
+ },
687
+ step: {
688
+ type: "string",
689
+ description: "What it is doing now (200 characters)"
690
+ },
691
+ item: {
692
+ type: "list",
693
+ description: "A step or log line, state:text with state pending, running, done, failed or skipped (text alone: pending). A lowercase word before the first colon must be one of them; pending:fix: x keeps the colon. The list replaces the card's (up to 20)"
694
+ },
695
+ branch: {
696
+ type: "string",
697
+ description: "The branch it works on"
698
+ },
699
+ "pr-url": {
700
+ type: "string",
701
+ description: "The pull request's https link"
702
+ },
703
+ "pr-number": {
704
+ type: "number",
705
+ description: "The pull request's number"
706
+ },
707
+ additions: {
708
+ type: "number",
709
+ description: "Lines the pull request adds"
710
+ },
711
+ deletions: {
712
+ type: "number",
713
+ description: "Lines the pull request removes"
714
+ },
715
+ files: {
716
+ type: "number",
717
+ description: "Files the pull request changes"
718
+ },
719
+ "pr-state": {
720
+ type: "string",
721
+ description: "Where the pull request stands: draft, open (ready for review), merged or closed"
722
+ },
723
+ "pr-checks": {
724
+ type: "string",
725
+ description: "The pull request's checks: passing, failing or pending"
726
+ },
727
+ tag: {
728
+ type: "list",
729
+ description: "A short label, like an area or a repository (24 characters); up to 3. An update's tags replace the card's: give all of them"
730
+ },
731
+ parent: {
732
+ type: "string",
733
+ description: "The id of the task of yours in this chat that this one is a part of"
734
+ },
735
+ ...cardOptions
736
+ };
737
+ /**
738
+ * `--also week=80` or `--also week=80@2026-10-10T00:00:00Z`: another window of a usage card, its percent left and, after
739
+ * `@`, when it resets.
740
+ */
741
+ function usageWindowOf(value) {
742
+ const match = /^([^=]+)=(\d+(?:\.\d+)?)(?:@(.+))?$/.exec(value.trim());
743
+ if (!match) throw new CliError("usage", `--also takes window=percent or window=percent@time, like week=80@2026-10-10T00:00:00Z, not ${JSON.stringify(value)}`);
744
+ const resetsAt = cardTime(match[3], "also");
745
+ return {
746
+ window: match[1].trim(),
747
+ left: Number(match[2]),
748
+ ...resetsAt !== void 0 && { resetsAt }
749
+ };
750
+ }
751
+ /** A usage card's fields from its flags (WISP 405 § Usage); `--json` gives any of them, the flags win over it. */
752
+ function usageFields(options) {
753
+ const fields = { ...cardJson(options.json) };
754
+ percent(options, "left");
755
+ if (options.left !== void 0 && (options.used !== void 0 || options.limit !== void 0)) throw new CliError("usage", "--left or --used with --limit, not both");
756
+ for (const key of [
757
+ "id",
758
+ "left",
759
+ "used",
760
+ "limit",
761
+ "label",
762
+ "account",
763
+ "window"
764
+ ]) if (options[key] !== void 0) fields[key] = options[key];
765
+ const resets = cardTime(options.resets, "resets");
766
+ if (resets !== void 0) fields.resetsAt = resets;
767
+ if (Array.isArray(options.also) && options.also.length) fields.windows = options.also.map(usageWindowOf);
768
+ return fields;
769
+ }
770
+ /** Options `usage send` takes. */
771
+ var usageOptions = {
772
+ left: {
773
+ type: "number",
774
+ description: "Percent of the quota left, 0 to 100"
775
+ },
776
+ used: {
777
+ type: "number",
778
+ description: "How much is used, with --limit (instead of --left; the percent is worked out)"
779
+ },
780
+ limit: {
781
+ type: "number",
782
+ description: "The quota, with --used"
783
+ },
784
+ label: {
785
+ type: "string",
786
+ description: "What the quota is of, like Claude (24 characters)"
787
+ },
788
+ account: {
789
+ type: "string",
790
+ description: "Which of your accounts it is, short (24 characters)"
791
+ },
792
+ window: {
793
+ type: "string",
794
+ description: "The quota's window, as people say it: \"5 h\", \"week\" (16 characters)"
795
+ },
796
+ resets: {
797
+ type: "string",
798
+ description: "When the window starts again: milliseconds or a date like 2026-10-07T18:00:00Z"
799
+ },
800
+ also: {
801
+ type: "list",
802
+ description: "Another window, window=percent[@time], like week=80@2026-10-10T00:00:00Z (up to 3; shown in the details)"
803
+ },
804
+ id: {
805
+ type: "string",
806
+ description: "The card's id (default usage: one card per chat)"
807
+ },
808
+ all: {
809
+ type: "boolean",
810
+ description: "Every 1:1 chat with a contact, instead of one chat"
811
+ },
812
+ json: cardOptions.json,
813
+ text: cardOptions.text,
814
+ force: cardOptions.force
815
+ };
816
+ //#endregion
817
+ //#region src/commands/apps.ts
818
+ /** The key file a command signs with: always named, never a default, never on the command line itself. */
819
+ function keyFile(value, usage) {
820
+ if (typeof value !== "string" || !value) throw new CliError("usage", `--key <file> is needed: ghostly ${usage}`);
821
+ return value;
822
+ }
823
+ /** `app send`'s message: JSON on the command line, or `-` for stdin. */
824
+ function appData(value) {
825
+ if (value === void 0) throw new CliError("usage", "The message is needed, as JSON or - for stdin: ghostly app send <chat> <ref|name> <json|->");
826
+ const text = value.trim() === "-" ? readFileSync(0, "utf8") : value;
827
+ try {
828
+ return JSON.parse(text);
829
+ } catch {
830
+ throw new CliError("usage", "The message is not valid JSON: quote it for the shell, like '{\"k\":\"move\"}', or pass - and write it on stdin");
831
+ }
832
+ }
833
+ var key = {
834
+ type: "string",
835
+ description: "The key file (made, owner-only, when it is missing)"
836
+ };
837
+ /**
838
+ * The publisher tools of WISP 1200 (Apps · Publishing): they run here, on files, with no profile or daemon. And the
839
+ * apps a profile serves as a bot (`app serve`, `app served`, `app unserve`), which are the profile's. One entry per
840
+ * command, in alphabetical order (test/commands.test.ts checks).
841
+ */
842
+ var commands$8 = {
843
+ "app close": {
844
+ method: "app.close",
845
+ usage: "app close <chat> <ref|name>",
846
+ summary: "Close a served app in a chat: the contact's app hears it ended",
847
+ args: ["chat", "ref"],
848
+ params: (_, a) => ({
849
+ chat: a.chat,
850
+ ref: a.ref
851
+ })
852
+ },
853
+ "app init": {
854
+ method: "app.init",
855
+ usage: "app init <dir> [--name <name>] [--title <title>] [--force]",
856
+ summary: "Start an app (WISP 1200): a small working one in <dir>, its ghostly-app.json, an index.html that says hello to the contact and a README, ready for app publish",
857
+ args: ["dir"],
858
+ options: {
859
+ name: {
860
+ type: "string",
861
+ description: "The app's name, lowercase letters, digits and hyphens (default: the folder's)"
862
+ },
863
+ title: {
864
+ type: "string",
865
+ description: "The title people see, up to 40 characters (default: from the name)"
866
+ },
867
+ force: {
868
+ type: "boolean",
869
+ description: "Replace the three files when they are there"
870
+ }
871
+ },
872
+ run: async ({ options }, a) => (await import("./apps-C-T8jeji.js")).initApp(a.dir, {
873
+ name: options.name,
874
+ title: options.title,
875
+ force: options.force === true
876
+ })
877
+ },
878
+ "app open": {
879
+ method: "app.open",
880
+ usage: "app open <chat> <ref|name> [--no-card]",
881
+ summary: "Open a served app in a chat, as a bot (needs the daemon): the contact gets the app's card, unless their app is open there already, and the two sides can talk once both opened it",
882
+ args: ["chat", "ref"],
883
+ options: { "no-card": {
884
+ type: "boolean",
885
+ description: "Send no app card, only say the app is open"
886
+ } },
887
+ params: ({ options }, a) => ({
888
+ chat: a.chat,
889
+ ref: a.ref,
890
+ noCard: options["no-card"] === true
891
+ })
892
+ },
893
+ "app publish": {
894
+ method: "app.publish",
895
+ usage: "app publish <dir> --key <file> [--out <file>]",
896
+ summary: "Bundle and sign an app (WISP 1200): <dir>/ghostly-app.json and every other file in <dir> become one .ghostlyapp, its sequence one more than the bundle at --out (default <dir>/app.ghostlyapp)",
897
+ args: ["dir"],
898
+ options: {
899
+ key: {
900
+ ...key,
901
+ description: "The publisher key file (made, owner-only, on first use; back it up)"
902
+ },
903
+ out: {
904
+ type: "string",
905
+ description: "Where the bundle goes (default <dir>/app.ghostlyapp); the bundle there is the version this one follows"
906
+ },
907
+ sequence: {
908
+ type: "number",
909
+ description: "This version's sequence, higher than the bundle at --out's (default: one more than it)"
910
+ }
911
+ },
912
+ run: async ({ options }, a) => (await import("./apps-C-T8jeji.js")).publishApp(a.dir, keyFile(options.key, "app publish <dir> --key <file> [--out <file>] [--sequence n]"), options.out, options.sequence)
913
+ },
914
+ "app revoke": {
915
+ method: "app.revoke",
916
+ usage: "app revoke <dir> --key <file> (--digest <digest>... | --up-to <sequence>) [--reason <text>] [--bundle <file>]",
917
+ summary: "Revoke versions of your app (WISP 1200 · Revocation): a ghostly-revoke/1 statement signed with its publisher key, added to <dir>/ghostly-revoke.json",
918
+ args: ["dir"],
919
+ options: {
920
+ key: {
921
+ type: "string",
922
+ description: "The app's publisher key file (never made here)"
923
+ },
924
+ digest: {
925
+ type: "list",
926
+ description: "A version's digest, base64url as app verify prints it (again for each, up to 64)"
927
+ },
928
+ "up-to": {
929
+ type: "number",
930
+ description: "Every version with this sequence or a lower one"
931
+ },
932
+ reason: {
933
+ type: "string",
934
+ description: "Why, one line of up to 200 characters, shown to people"
935
+ },
936
+ bundle: {
937
+ type: "string",
938
+ description: "The bundle naming the app (default <dir>/app.ghostlyapp)"
939
+ }
940
+ },
941
+ run: async ({ options }, a) => (await import("./apps-C-T8jeji.js")).revokeApp(a.dir, keyFile(options.key, "app revoke <dir> --key <file> (--digest <digest>... | --up-to <sequence>) [--reason <text>]"), {
942
+ digests: options.digest,
943
+ upTo: options["up-to"],
944
+ reason: options.reason,
945
+ bundle: options.bundle
946
+ })
947
+ },
948
+ "app send": {
949
+ method: "app.send",
950
+ usage: "app send <chat> <ref|name> <json|->",
951
+ summary: "Send one message of a served app's own protocol to the contact's app (needs the daemon, and both sides open): any JSON value up to 32 KiB, or - to read it from stdin; live only, never kept for later",
952
+ args: [
953
+ "chat",
954
+ "ref",
955
+ "data..."
956
+ ],
957
+ params: (_, a) => ({
958
+ chat: a.chat,
959
+ ref: a.ref,
960
+ data: appData(a.data)
961
+ })
962
+ },
963
+ "app serve": {
964
+ method: "app.serve",
965
+ usage: "app serve <bundle|url> --grant chat [--grant name] [--url <url>]",
966
+ summary: "Serve an app as a bot (WISP 1200): its signed .ghostlyapp is checked as a client would, and the profile speaks the app's messages in chats itself; each permission the app asks needs its --grant",
967
+ args: ["bundle"],
968
+ options: {
969
+ grant: {
970
+ type: "list",
971
+ description: "A permission the app asks and you allow: chat (talk to the contact's app) or name (your name in that chat); again for each"
972
+ },
973
+ url: {
974
+ type: "string",
975
+ description: "Where a contact's app reads the bundle, for the card (default: the URL it was served from, else the manifest's first source)"
976
+ }
977
+ },
978
+ params: ({ options }, a) => ({
979
+ bundle: /^[a-z][a-z0-9+.-]*:\/\//i.test(a.bundle ?? "") ? a.bundle : here(a.bundle),
980
+ grant: options.grant,
981
+ url: options.url
982
+ })
983
+ },
984
+ "app served": {
985
+ method: "app.served",
986
+ usage: "app served",
987
+ summary: "The apps this profile serves as a bot, and whether apps/1 is offered to contacts now"
988
+ },
989
+ "app unserve": {
990
+ method: "app.unserve",
991
+ usage: "app unserve <ref|name>",
992
+ summary: "Stop serving an app, by its reference or its name",
993
+ args: ["ref"],
994
+ params: (_, a) => ({ ref: a.ref })
995
+ },
996
+ "app verify": {
997
+ method: "app.verify",
998
+ usage: "app verify <bundle|url>",
999
+ 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",
1000
+ args: ["bundle"],
1001
+ run: async (_, a) => (await import("./apps-C-T8jeji.js")).verifyApp(a.bundle)
1002
+ },
1003
+ "store sign": {
1004
+ method: "store.sign",
1005
+ usage: "store sign <index> --key <file> [--out <dir>]",
1006
+ summary: "Sign a store index (WISP 1200 · Stores): writes ghostly-store.json, canonical, and ghostly-store.sig beside it or in --out",
1007
+ args: ["index"],
1008
+ options: {
1009
+ key: {
1010
+ ...key,
1011
+ description: "The store key file (made, owner-only, when it is missing and the index names no key; keep it offline)"
1012
+ },
1013
+ out: {
1014
+ type: "string",
1015
+ description: "The folder to write ghostly-store.json and ghostly-store.sig in (default: the index's)"
1016
+ }
1017
+ },
1018
+ run: async ({ options }, a) => (await import("./apps-C-T8jeji.js")).signStore(a.index, keyFile(options.key, "store sign <index> --key <file> [--out <dir>]"), options.out)
1019
+ }
1020
+ };
1021
+ //#endregion
1022
+ //#region src/commands/calls.ts
1023
+ /** Voice calls: one entry per command, in alphabetical order (test/commands.test.ts checks). */
1024
+ var commands$7 = {
1025
+ "call answer": {
1026
+ method: "call.answer",
1027
+ usage: "call answer [<chat|call>] [--rate 48000]",
1028
+ summary: "Answer a call that rings (the only one, or the one named)",
1029
+ args: ["call...?"],
1030
+ options: { rate },
1031
+ params: ({ options }, a) => ({
1032
+ call: a.call,
1033
+ rate: options.rate
1034
+ })
1035
+ },
1036
+ "call auto": {
1037
+ method: "call.auto",
1038
+ usage: "call auto [on|off] [--from <chat>]... [--rate 48000]",
1039
+ summary: "Answer calls by themselves: from anyone, or the chats named (kept in the profile)",
1040
+ args: ["mode...?"],
1041
+ options: {
1042
+ from: {
1043
+ type: "list",
1044
+ description: "Only calls from this chat (again for more)"
1045
+ },
1046
+ rate
1047
+ },
1048
+ params: ({ options }, a) => {
1049
+ if (a.mode !== void 0 && a.mode !== "on" && a.mode !== "off") throw new CliError("usage", "ghostly call auto [on|off] [--from <chat>]... [--rate n]");
1050
+ if (a.mode !== "on" && (options.from !== void 0 || options.rate !== void 0)) throw new CliError("usage", "--from and --rate go with on: ghostly call auto on --from <chat>");
1051
+ if (a.mode === void 0) return {};
1052
+ return a.mode === "on" ? {
1053
+ on: true,
1054
+ from: options.from,
1055
+ rate: options.rate
1056
+ } : { on: false };
1057
+ }
1058
+ },
1059
+ "call flush": {
1060
+ method: "call.flush",
1061
+ usage: "call flush [<chat|call>]",
1062
+ summary: "Drop the audio queued and not played yet (barge-in)",
1063
+ args: ["call...?"],
1064
+ params: (_, a) => ({ call: a.call })
1065
+ },
1066
+ "call hangup": {
1067
+ method: "call.hangup",
1068
+ usage: "call hangup [<chat|call>]",
1069
+ summary: "Hang up, or decline a call that rings",
1070
+ args: ["call...?"],
1071
+ params: (_, a) => ({ call: a.call })
1072
+ },
1073
+ "call list": {
1074
+ method: "call.list",
1075
+ usage: "call list",
1076
+ summary: "Calls on now, their audio sockets, and auto-answer"
1077
+ },
1078
+ "call start": {
1079
+ method: "call.start",
1080
+ usage: "call start <chat> [--rate 48000]",
1081
+ summary: "Call a contact (voice); the result names the call's audio socket (daemon)",
1082
+ args: ["chat"],
1083
+ options: { rate },
1084
+ params: ({ options }, a) => ({
1085
+ chat: a.chat,
1086
+ rate: options.rate
1087
+ })
1088
+ }
1089
+ };
1090
+ //#endregion
1091
+ //#region src/commands/chats.ts
1092
+ var BUTTON_UPDATE = "button update <chat|group> <message> [--chosen <button>] [--close] [--text <text>] [--force] [--wait none|confirmed|sent] [--timeout s]";
1093
+ /** Invites, chats and messages: one entry per command, in alphabetical order (test/commands.test.ts checks). */
1094
+ var commands$6 = {
1095
+ "button press": {
1096
+ method: "button.press",
1097
+ usage: "button press <chat|group> <message> <button> [--wait none|sent|delivered] [--timeout s]",
1098
+ summary: "Press a button of someone else's message, as a tap in the app does (its id from the message's card)",
1099
+ args: [
1100
+ "chat",
1101
+ "message",
1102
+ "button"
1103
+ ],
1104
+ options: {
1105
+ wait: {
1106
+ type: "string",
1107
+ description: "none, sent (a one-shot's default in a chat) or delivered; a group takes none or sent"
1108
+ },
1109
+ timeout: groupWait.timeout
1110
+ },
1111
+ params: ({ options }, a) => ({
1112
+ chat: a.chat,
1113
+ message: a.message,
1114
+ button: a.button,
1115
+ wait: options.wait,
1116
+ timeout: options.timeout
1117
+ })
1118
+ },
1119
+ "button update": {
1120
+ method: "button.update",
1121
+ usage: BUTTON_UPDATE,
1122
+ summary: "Show the answer on a message's buttons (--chosen), close them, or change its text (the messageId send --button gave)",
1123
+ args: ["chat", "message"],
1124
+ options: {
1125
+ chosen: {
1126
+ type: "string",
1127
+ description: "The button to show as the answer (its id)"
1128
+ },
1129
+ close: {
1130
+ type: "boolean",
1131
+ description: "Close the buttons: no more presses"
1132
+ },
1133
+ text: {
1134
+ type: "string",
1135
+ description: "A new text for the message (default: the one it has); people see it marked edited"
1136
+ },
1137
+ force,
1138
+ wait: {
1139
+ type: "string",
1140
+ description: "none (default), confirmed (a chat's contact took it) or sent (a group's edge took it)"
1141
+ },
1142
+ timeout: groupWait.timeout
1143
+ },
1144
+ params: ({ options }, a) => {
1145
+ if (!options.chosen && options.close !== true && !options.text) throw new CliError("usage", `Give --chosen, --close or --text: ghostly ${BUTTON_UPDATE}`);
1146
+ return {
1147
+ chat: a.chat,
1148
+ message: a.message,
1149
+ chosen: options.chosen,
1150
+ close: options.close === true,
1151
+ text: options.text,
1152
+ force: options.force === true,
1153
+ wait: options.wait,
1154
+ timeout: options.timeout
1155
+ };
1156
+ }
1157
+ },
1158
+ "chat connect": {
1159
+ method: "chat.connect",
1160
+ usage: "chat connect <chat>",
1161
+ summary: "Reconnect a chat now (ends a --hold)",
1162
+ args: ["chat"],
1163
+ params: (_, { chat }) => ({ chat })
1164
+ },
1165
+ "chat disconnect": {
1166
+ method: "chat.disconnect",
1167
+ usage: "chat disconnect <chat> [--hold <minutes>]",
1168
+ summary: "Close a chat's live session (the contact may redial; --hold keeps it on the DHT that long, 0 ends it)",
1169
+ args: ["chat"],
1170
+ options: { hold: {
1171
+ type: "number",
1172
+ description: "Stay off the direct link this many minutes (up to 10080): DHT only, taken back after"
1173
+ } },
1174
+ params: ({ options }, { chat }) => ({
1175
+ chat,
1176
+ hold: options.hold
1177
+ })
1178
+ },
1179
+ "chat history": {
1180
+ method: "chat.history",
1181
+ usage: "chat history <chat> [--limit n] [--before id|ms] [--after id|ms]",
1182
+ summary: "Messages, oldest first (a page)",
1183
+ args: ["chat"],
1184
+ options: {
1185
+ limit: {
1186
+ type: "number",
1187
+ description: "Messages per page (default 50)"
1188
+ },
1189
+ before: {
1190
+ type: "string",
1191
+ description: "Only before this message id or timestamp"
1192
+ },
1193
+ after: {
1194
+ type: "string",
1195
+ description: "Only after this message id or timestamp"
1196
+ }
1197
+ },
1198
+ params: ({ options }, { chat }) => ({
1199
+ chat,
1200
+ limit: options.limit,
1201
+ before: cursor(options.before, "before"),
1202
+ after: cursor(options.after, "after")
1203
+ })
1204
+ },
1205
+ "chat list": {
1206
+ method: "chat.list",
1207
+ usage: "chat list [--show-secret]",
1208
+ summary: "Chats, newest first (a pending invite code only with --show-secret)",
1209
+ options: showInvite,
1210
+ params: ({ options }) => secret(options)
1211
+ },
1212
+ "chat remove": {
1213
+ method: "chat.remove",
1214
+ usage: "chat remove <chat> --yes",
1215
+ summary: "Delete a chat, its keys and history on this device",
1216
+ args: ["chat"],
1217
+ options: { yes: {
1218
+ type: "boolean",
1219
+ description: "Confirm"
1220
+ } },
1221
+ params: ({ options }, { chat }) => ({
1222
+ chat,
1223
+ yes: options.yes === true
1224
+ })
1225
+ },
1226
+ "chat rename": {
1227
+ method: "chat.rename",
1228
+ usage: "chat rename <chat> <name...>",
1229
+ summary: "Name a chat on this side (empty: the contact's name)",
1230
+ args: ["chat", "name...?"],
1231
+ params: (_, { chat, name }) => ({
1232
+ chat,
1233
+ name: name ?? ""
1234
+ })
1235
+ },
1236
+ "chat show": {
1237
+ method: "chat.get",
1238
+ usage: "chat show <chat> [--show-secret]",
1239
+ summary: "One chat and its connection (its pending invite code only with --show-secret)",
1240
+ args: ["chat"],
1241
+ options: showInvite,
1242
+ params: ({ options }, { chat }) => ({
1243
+ chat,
1244
+ ...secret(options)
1245
+ })
1246
+ },
1247
+ "chat transport": {
1248
+ method: "chat.transport",
1249
+ usage: "chat transport <chat> <auto|dht|webrtc|iroh|hyperdht>",
1250
+ summary: "Choose what carries a chat",
1251
+ args: ["chat", "transport"],
1252
+ params: (_, a) => ({
1253
+ chat: a.chat,
1254
+ transport: a.transport
1255
+ })
1256
+ },
1257
+ "chat verify": {
1258
+ method: "chat.verify",
1259
+ usage: "chat verify <chat> --code <code>",
1260
+ summary: "Mark a contact verified after comparing codes",
1261
+ args: ["chat"],
1262
+ options: { code: {
1263
+ type: "string",
1264
+ description: "The code the contact reads out"
1265
+ } },
1266
+ params: ({ options }, { chat }) => {
1267
+ if (typeof options.code !== "string" || !options.code.trim()) throw new CliError("usage", "--code is needed: ghostly chat verify <chat> --code <code>");
1268
+ return {
1269
+ chat,
1270
+ code: options.code
1271
+ };
1272
+ }
1273
+ },
1274
+ "chat wait": {
1275
+ method: "chat.wait",
1276
+ usage: "chat wait <chat> [--until live|text|paired] [--timeout s]",
1277
+ summary: "Wait for a chat to reach a state",
1278
+ args: ["chat"],
1279
+ options: {
1280
+ until: {
1281
+ type: "string",
1282
+ description: "live (default), text or paired"
1283
+ },
1284
+ timeout: {
1285
+ type: "number",
1286
+ description: "Seconds (default 60)"
1287
+ }
1288
+ },
1289
+ params: ({ options }, { chat }) => ({
1290
+ chat,
1291
+ until: options.until,
1292
+ timeout: options.timeout
1293
+ })
1294
+ },
1295
+ "forward": {
1296
+ method: "chat.forward",
1297
+ usage: "forward <chat|group> <message>... --to <chat|group>... [--force] [--wait none|sent] [--timeout s]",
1298
+ summary: "Forward messages (texts, files from the bytes here) to up to 5 chats and groups, as new messages of yours",
1299
+ args: ["chat", "message..."],
1300
+ options: {
1301
+ to: {
1302
+ type: "list",
1303
+ description: "A chat or a group to forward to (id, prefix or name; group:<id> for a group; a dash-leading id as it is); up to 5"
1304
+ },
1305
+ force: {
1306
+ type: "boolean",
1307
+ description: "Forward even if a text looks like a seed, a key or ecash"
1308
+ },
1309
+ wait: {
1310
+ type: "string",
1311
+ description: "none or sent: each text on its way (in a group, taken by an edge), each file's transfer done (default none)"
1312
+ },
1313
+ timeout: groupWait.timeout
1314
+ },
1315
+ params: ({ options }, a) => {
1316
+ if (!Array.isArray(options.to) || !options.to.length) throw new CliError("usage", "Missing --to: ghostly forward <chat|group> <message>... --to <chat|group>... [--force] [--wait none|sent] [--timeout s]");
1317
+ return {
1318
+ chat: a.chat,
1319
+ messages: (a.message ?? "").split(" ").filter(Boolean),
1320
+ to: options.to,
1321
+ force: options.force === true,
1322
+ wait: options.wait,
1323
+ timeout: options.timeout
1324
+ };
1325
+ }
1326
+ },
1327
+ "invite create": {
1328
+ method: "invite.create",
1329
+ usage: "invite create [--label <name>]",
1330
+ summary: "A new chat's ghostly1 invite and its link",
1331
+ options: { label: {
1332
+ type: "string",
1333
+ description: "A name for the chat on this side"
1334
+ } },
1335
+ params: ({ options }) => ({ label: options.label })
1336
+ },
1337
+ "invite join": {
1338
+ method: "invite.join",
1339
+ usage: "invite join <invite-or-link> [--label <name>]",
1340
+ summary: "Join a chat from its invite",
1341
+ args: ["invite"],
1342
+ options: { label: {
1343
+ type: "string",
1344
+ description: "A name for the chat on this side"
1345
+ } },
1346
+ params: ({ options }, { invite }) => ({
1347
+ invite,
1348
+ label: options.label
1349
+ })
1350
+ },
1351
+ "message delete": {
1352
+ method: "chat.delete",
1353
+ usage: "message delete <chat> <message>",
1354
+ summary: "Forget a message on this device (the contact keeps theirs)",
1355
+ args: ["chat", "message"],
1356
+ params: (_, a) => ({
1357
+ chat: a.chat,
1358
+ message: a.message
1359
+ })
1360
+ },
1361
+ "message details": {
1362
+ method: "chat.details",
1363
+ usage: "message details <chat> <message>",
1364
+ summary: "How a message travelled",
1365
+ args: ["chat", "message"],
1366
+ params: (_, a) => ({
1367
+ chat: a.chat,
1368
+ message: a.message
1369
+ })
1370
+ },
1371
+ "message retry": {
1372
+ method: "chat.retry",
1373
+ usage: "message retry <chat> <message>",
1374
+ summary: "Send a failed message again",
1375
+ args: ["chat", "message"],
1376
+ params: (_, a) => ({
1377
+ chat: a.chat,
1378
+ message: a.message
1379
+ })
1380
+ },
1381
+ "pin": {
1382
+ method: "chat.pin",
1383
+ usage: "pin <chat|group> <message> | --remove",
1384
+ summary: "Pin a message at the top of a chat or group (one per chat: it replaces the pinned one); --remove unpins",
1385
+ args: ["chat", "message...?"],
1386
+ options: { remove: {
1387
+ type: "boolean",
1388
+ description: "Unpin the chat's pinned message (it names no message)"
1389
+ } },
1390
+ params: ({ options }, a) => {
1391
+ if (a.message === void 0 && options.remove !== true) throw new CliError("usage", "Missing <message>: ghostly pin <chat|group> <message> | --remove");
1392
+ if (a.message?.includes(" ")) throw new CliError("usage", "Too many arguments: ghostly pin <chat|group> <message> | --remove");
1393
+ return {
1394
+ chat: a.chat,
1395
+ message: a.message,
1396
+ remove: options.remove === true
1397
+ };
1398
+ }
1399
+ },
1400
+ "react": {
1401
+ method: "chat.react",
1402
+ usage: "react <chat> <message> <emoji> [--remove]",
1403
+ summary: "React to a message with one emoji (it replaces yours); --remove takes yours back",
1404
+ args: [
1405
+ "chat",
1406
+ "message",
1407
+ "emoji...?"
1408
+ ],
1409
+ options: { remove: {
1410
+ type: "boolean",
1411
+ description: "Take your reaction back"
1412
+ } },
1413
+ params: ({ options }, a) => ({
1414
+ chat: a.chat,
1415
+ message: a.message,
1416
+ emoji: a.emoji,
1417
+ remove: options.remove === true
1418
+ })
1419
+ },
1420
+ "routine send": {
1421
+ method: "routine.send",
1422
+ usage: "routine send <chat|group> --name <name> --schedule \"every day 01:00\" [--cron \"0 1 * * *\"] [--state active|paused] [--next <time>] [--run ok|failed|skipped[:summary]] [--link url]... [--id id] [--json json|-|file] [--force] [--wait none|sent|delivered] [--timeout s]",
1423
+ summary: "Send a routine card (something a bot runs on a schedule: its last run and the next) to a chat or group",
1424
+ args: ["chat"],
1425
+ options: {
1426
+ id: {
1427
+ type: "string",
1428
+ description: "The routine's id, kept across updates (default: made up)"
1429
+ },
1430
+ ...routineOptions,
1431
+ ...groupWait,
1432
+ wait: wait.wait
1433
+ },
1434
+ params: ({ options }, { chat }) => ({
1435
+ chat,
1436
+ card: needs(routineFields(options), [["name", "name"], ["schedule", "schedule"]], "routine send <chat|group> --name <name> --schedule \"every day 01:00\""),
1437
+ run: runOf(options.run),
1438
+ text: options.text,
1439
+ force: options.force === true,
1440
+ wait: options.wait,
1441
+ timeout: options.timeout
1442
+ })
1443
+ },
1444
+ "routine update": {
1445
+ method: "routine.update",
1446
+ usage: "routine update <chat|group> <routine> [--run ok|failed|skipped[:summary]] [--next <time>] [--state active|paused] [--schedule ...] [--json json|-|file] [--force] [--wait none|confirmed|sent] [--timeout s]",
1447
+ summary: "Update a routine card you sent: record a run, the next one, a pause (at most one update per 2.5 s; sooner ones merge)",
1448
+ args: ["chat", "routine"],
1449
+ options: {
1450
+ ...routineOptions,
1451
+ wait: {
1452
+ type: "string",
1453
+ description: "none (default), confirmed (a chat's contact took it) or sent (a group's edge took it)"
1454
+ },
1455
+ timeout: groupWait.timeout
1456
+ },
1457
+ params: ({ options }, a) => ({
1458
+ chat: a.chat,
1459
+ routine: a.routine,
1460
+ card: routineFields(options),
1461
+ run: runOf(options.run),
1462
+ text: options.text,
1463
+ force: options.force === true,
1464
+ wait: options.wait,
1465
+ timeout: options.timeout
1466
+ })
1467
+ },
1468
+ "task send": {
1469
+ method: "task.send",
1470
+ usage: "task send <chat|group> --title <title> [--status s] [--progress n | --steps a/b] [--step \"...\"] [--item state:text]... [--pr-url url --pr-number n --additions n --deletions n --files n --pr-state s --pr-checks c] [--branch b] [--tag label]... [--parent task] [--link url]... [--id id] [--json json|-|file] [--force] [--wait none|sent|delivered] [--timeout s]",
1471
+ summary: "Send a task card (a bot's work: status, progress, its PR) to a chat or group; prints its id and message",
1472
+ args: ["chat"],
1473
+ options: {
1474
+ id: {
1475
+ type: "string",
1476
+ description: "The task's id, kept across updates (default: made up)"
1477
+ },
1478
+ ...taskOptions,
1479
+ ...groupWait,
1480
+ wait: wait.wait
1481
+ },
1482
+ params: ({ options }, { chat }) => ({
1483
+ chat,
1484
+ card: prLinked(needs(taskFields(options), [["title", "title"]], "task send <chat|group> --title <title>"), options, "task send <chat|group> --title <title> --pr-url <url>"),
1485
+ text: options.text,
1486
+ force: options.force === true,
1487
+ wait: options.wait,
1488
+ timeout: options.timeout
1489
+ })
1490
+ },
1491
+ "task update": {
1492
+ method: "task.update",
1493
+ usage: "task update <chat|group> <task> [--status s] [--progress n | --steps a/b] [--step \"...\"] [--item state:text]... [--pr-url ...] [--pr-state s] [--pr-checks c] [--tag label]... [--parent task] [--json json|-|file] [--force] [--wait none|confirmed|sent] [--timeout s]",
1494
+ summary: "Update a task card you sent: the fields given, merged over it (at most one update per 2.5 s; sooner ones merge)",
1495
+ args: ["chat", "task"],
1496
+ options: {
1497
+ ...taskOptions,
1498
+ wait: {
1499
+ type: "string",
1500
+ description: "none (default), confirmed (a chat's contact took it) or sent (a group's edge took it)"
1501
+ },
1502
+ timeout: groupWait.timeout
1503
+ },
1504
+ params: ({ options }, a) => ({
1505
+ chat: a.chat,
1506
+ task: a.task,
1507
+ card: taskFields(options),
1508
+ text: options.text,
1509
+ force: options.force === true,
1510
+ wait: options.wait,
1511
+ timeout: options.timeout
1512
+ })
1513
+ },
1514
+ "typing": {
1515
+ method: "chat.typing",
1516
+ usage: "typing <chat> [--kind typing|recording|thinking] [--status \"<text>\"] [--for s] [--stop]",
1517
+ summary: "Show the contact you are writing, recording or thinking (live chats; it fades after 6 s unless said again, or kept with --for; a send or --stop ends it)",
1518
+ args: ["chat"],
1519
+ options: {
1520
+ stop: {
1521
+ type: "boolean",
1522
+ description: "Say you stopped"
1523
+ },
1524
+ kind: {
1525
+ type: "string",
1526
+ description: "typing (default), recording (a voice note) or thinking"
1527
+ },
1528
+ status: {
1529
+ type: "string",
1530
+ description: "A short line shown instead, e.g. \"Transcribing your audio…\" (40 characters, no links)"
1531
+ },
1532
+ for: {
1533
+ type: "number",
1534
+ description: "Keep saying it for this many seconds (up to 600), until a send or --stop"
1535
+ }
1536
+ },
1537
+ params: ({ options }, { chat }) => ({
1538
+ chat,
1539
+ stop: options.stop === true,
1540
+ kind: options.kind,
1541
+ status: options.status,
1542
+ for: typingFor(options)
1543
+ })
1544
+ },
1545
+ "usage send": {
1546
+ method: "usage.send",
1547
+ usage: "usage send [<chat|group>] [--all] --left <percent> [--window \"5 h\"] [--resets <time>] [--label Claude] [--account <name>] [--also week=80@<time>]... [--used n --limit n] [--id id] [--json json|-|file] [--force] [--wait none|sent] [--timeout s]",
1548
+ summary: "Say how much of your quota is left (a bot's usage card, one per chat, updated in place); --all for every 1:1 chat",
1549
+ args: ["chat?"],
1550
+ options: {
1551
+ ...usageOptions,
1552
+ ...groupWait
1553
+ },
1554
+ params: ({ options }, { chat }) => {
1555
+ if (!chat && options.all !== true) throw new CliError("usage", "Name a chat or group, or give --all: ghostly usage send <chat|--all> --left <percent>");
1556
+ return {
1557
+ chat,
1558
+ all: options.all === true,
1559
+ card: quota(usageFields(options), "usage send <chat|--all> --left <percent>"),
1560
+ text: options.text,
1561
+ force: options.force === true,
1562
+ wait: options.wait,
1563
+ timeout: options.timeout
1564
+ };
1565
+ }
1566
+ }
1567
+ };
1568
+ //#endregion
1569
+ //#region src/commands/files.ts
1570
+ /** Files: one entry per command, in alphabetical order (test/commands.test.ts checks). */
1571
+ var commands$5 = {
1572
+ "file accept": {
1573
+ method: "file.action",
1574
+ usage: "file accept [<chat>] <file>",
1575
+ summary: "Take a file the contact offers, or download a group's file that did not come by itself (<chat> may be the group)",
1576
+ args: ["chat?", "file"],
1577
+ params: (_, a) => ({
1578
+ chat: a.chat,
1579
+ file: a.file,
1580
+ action: "accept"
1581
+ })
1582
+ },
1583
+ "file cancel": {
1584
+ method: "file.action",
1585
+ usage: "file cancel [<chat>] <file>",
1586
+ summary: "Cancel a transfer",
1587
+ args: ["chat?", "file"],
1588
+ params: (_, a) => ({
1589
+ chat: a.chat,
1590
+ file: a.file,
1591
+ action: "cancel"
1592
+ })
1593
+ },
1594
+ "file decline": {
1595
+ method: "file.action",
1596
+ usage: "file decline [<chat>] <file>",
1597
+ summary: "Refuse a file the contact offers",
1598
+ args: ["chat?", "file"],
1599
+ params: (_, a) => ({
1600
+ chat: a.chat,
1601
+ file: a.file,
1602
+ action: "decline"
1603
+ })
1604
+ },
1605
+ "file list": {
1606
+ method: "file.list",
1607
+ usage: "file list <chat|group>",
1608
+ summary: "A chat's or a group's files and their transfers",
1609
+ args: ["chat"],
1610
+ params: (_, { chat }) => ({ chat })
1611
+ },
1612
+ "file pause": {
1613
+ method: "file.action",
1614
+ usage: "file pause [<chat>] <file>",
1615
+ summary: "Pause a transfer",
1616
+ args: ["chat?", "file"],
1617
+ params: (_, a) => ({
1618
+ chat: a.chat,
1619
+ file: a.file,
1620
+ action: "pause"
1621
+ })
1622
+ },
1623
+ "file request": {
1624
+ method: "file.action",
1625
+ usage: "file request [<chat>] <file>",
1626
+ summary: "Ask again for a file that stopped arriving: it goes on from what is here (a group's file: asked of whoever holds it)",
1627
+ args: ["chat?", "file"],
1628
+ params: (_, a) => ({
1629
+ chat: a.chat,
1630
+ file: a.file,
1631
+ action: "request"
1632
+ })
1633
+ },
1634
+ "file resend": {
1635
+ method: "file.action",
1636
+ usage: "file resend [<chat>] <file>",
1637
+ summary: "Send again a file that stopped moving or failed: it goes on from what the contact holds",
1638
+ args: ["chat?", "file"],
1639
+ params: (_, a) => ({
1640
+ chat: a.chat,
1641
+ file: a.file,
1642
+ action: "resend"
1643
+ })
1644
+ },
1645
+ "file resume": {
1646
+ method: "file.action",
1647
+ usage: "file resume [<chat>] <file>",
1648
+ summary: "Resume a transfer",
1649
+ args: ["chat?", "file"],
1650
+ params: (_, a) => ({
1651
+ chat: a.chat,
1652
+ file: a.file,
1653
+ action: "resume"
1654
+ })
1655
+ },
1656
+ "file save": {
1657
+ method: "file.save",
1658
+ usage: "file save [<chat>] <file> [--dir d | --path p] [--force] [--wait [--timeout s]]",
1659
+ summary: "Write a received file to disk (never over a file without --force; --wait: once it is all here)",
1660
+ args: ["chat?", "file"],
1661
+ options: {
1662
+ dir: {
1663
+ type: "string",
1664
+ description: "Into this folder, under its own name"
1665
+ },
1666
+ path: {
1667
+ type: "string",
1668
+ description: "To this path"
1669
+ },
1670
+ force: {
1671
+ type: "boolean",
1672
+ description: "Replace a file there"
1673
+ },
1674
+ wait: {
1675
+ type: "boolean",
1676
+ description: "Wait for the transfer to finish first"
1677
+ },
1678
+ timeout: {
1679
+ type: "number",
1680
+ description: "Seconds --wait waits (default 300)"
1681
+ }
1682
+ },
1683
+ params: ({ options }, a) => {
1684
+ if (options.path === "") throw new CliError("usage", "--path needs a file");
1685
+ if (options.dir === "") throw new CliError("usage", "--dir needs a folder");
1686
+ if (options.dir !== void 0 && options.path !== void 0) throw new CliError("usage", "--path names the file: not with --dir");
1687
+ if (options.timeout !== void 0 && options.wait !== true) throw new CliError("usage", "--timeout is how long --wait waits: not without --wait");
1688
+ return {
1689
+ chat: a.chat,
1690
+ file: a.file,
1691
+ dir: options.path === void 0 ? here(options.dir ?? ".") : void 0,
1692
+ path: here(options.path),
1693
+ force: options.force === true,
1694
+ wait: options.wait === true ? true : void 0,
1695
+ timeout: options.timeout
1696
+ };
1697
+ }
1698
+ },
1699
+ "file send": {
1700
+ method: "file.send",
1701
+ usage: "file send <chat|group> <path> [--name n] [--mime t] [--voice [ms] [--peaks 0,40,…]] [--reply <message>]",
1702
+ summary: `Send a file (or, with --voice, a voice note), as a reply with --reply; to a group, every member's app fetches it (up to ${GROUP_FILE_LIMITS.maxBytes / 1024 / 1024} MiB)`,
1703
+ args: ["chat", "path"],
1704
+ options: {
1705
+ name: {
1706
+ type: "string",
1707
+ description: "The name the contact (or the group) sees"
1708
+ },
1709
+ mime: {
1710
+ type: "string",
1711
+ description: "Its type (default: from the extension)"
1712
+ },
1713
+ voice: {
1714
+ type: "number",
1715
+ optionalValue: true,
1716
+ description: "A voice note; its length in milliseconds (default: measured from the sound)"
1717
+ },
1718
+ peaks: {
1719
+ type: "string",
1720
+ description: "Loudness bars 0-255, comma-separated (default: measured from the sound)"
1721
+ },
1722
+ reply
1723
+ },
1724
+ params: ({ options }, a) => ({
1725
+ chat: a.chat,
1726
+ path: here(a.path),
1727
+ name: options.name,
1728
+ mime: options.mime,
1729
+ voice: options.voice,
1730
+ peaks: typeof options.peaks === "string" ? options.peaks.split(",") : void 0,
1731
+ reply: options.reply
1732
+ })
1733
+ },
1734
+ "file wait": {
1735
+ method: "file.wait",
1736
+ usage: "file wait [<chat>] <file> [--timeout s]",
1737
+ summary: "Wait until a transfer is done (exit 0) or failed (exit 1, with its error)",
1738
+ args: ["chat?", "file"],
1739
+ options: { timeout: {
1740
+ type: "number",
1741
+ description: "Seconds (default 300)"
1742
+ } },
1743
+ params: ({ options }, a) => ({
1744
+ chat: a.chat,
1745
+ file: a.file,
1746
+ timeout: options.timeout
1747
+ })
1748
+ }
1749
+ };
1750
+ //#endregion
1751
+ //#region src/commands/general.ts
1752
+ /** The profile's status, its picture, the event journal: one entry per command, in alphabetical order (test/commands.test.ts checks). */
1753
+ var commands$4 = {
1754
+ "events": {
1755
+ method: "events.replay",
1756
+ usage: "events [--since seq]",
1757
+ summary: "Events the journal holds, without following",
1758
+ options: { since: {
1759
+ type: "number",
1760
+ description: "Only events after this seq"
1761
+ } },
1762
+ params: ({ options }) => ({ since: options.since ?? 0 })
1763
+ },
1764
+ "profile picture": {
1765
+ method: "profile.picture",
1766
+ usage: "profile picture <jpeg> | --clear",
1767
+ summary: "The picture contacts see (a square JPEG, 128 px is what the app sends)",
1768
+ args: ["path...?"],
1769
+ options: { clear: {
1770
+ type: "boolean",
1771
+ description: "Remove the picture"
1772
+ } },
1773
+ params: ({ options }, a) => {
1774
+ if (a.path === void 0 && options.clear !== true) throw new CliError("usage", "Missing <jpeg>: ghostly profile picture <jpeg> | --clear");
1775
+ if (a.path !== void 0 && options.clear === true) throw new CliError("usage", "--clear removes the picture: not with a <jpeg>");
1776
+ return {
1777
+ path: here(a.path),
1778
+ clear: options.clear === true
1779
+ };
1780
+ }
1781
+ },
1782
+ "status": {
1783
+ method: "status",
1784
+ usage: "status",
1785
+ summary: "The profile, its chats and whether a daemon runs it"
1786
+ }
1787
+ };
1788
+ //#endregion
1789
+ //#region src/commands/groups.ts
1790
+ /** Groups: one entry per command, in alphabetical order (test/commands.test.ts checks). */
1791
+ var commands$3 = {
1792
+ "group accept": {
1793
+ method: "group.accept",
1794
+ usage: "group accept <group>",
1795
+ summary: "Accept an invitation to a group",
1796
+ args: ["group"],
1797
+ params: (_, { group }) => ({ group })
1798
+ },
1799
+ "group admin": {
1800
+ method: "group.admin",
1801
+ usage: "group admin <group> <member> [--show-secret]",
1802
+ summary: "Make a member the admin (admin)",
1803
+ args: ["group", "member"],
1804
+ options: showSecret,
1805
+ params: ({ options }, a) => ({
1806
+ group: a.group,
1807
+ member: a.member,
1808
+ ...secret(options)
1809
+ })
1810
+ },
1811
+ "group create": {
1812
+ method: "group.create",
1813
+ usage: "group create <name...> [--mesh]",
1814
+ summary: "A community (a link anyone can open) or, with --mesh, a private group of up to 32",
1815
+ args: ["name..."],
1816
+ options: { mesh: {
1817
+ type: "boolean",
1818
+ description: "A private mesh of contacts instead of a community"
1819
+ } },
1820
+ params: ({ options }, { name }) => ({
1821
+ name,
1822
+ profile: options.mesh ? "mesh" : "community"
1823
+ })
1824
+ },
1825
+ "group decline": {
1826
+ method: "group.decline",
1827
+ usage: "group decline <group>",
1828
+ summary: "Decline an invitation to a group",
1829
+ args: ["group"],
1830
+ params: (_, { group }) => ({ group })
1831
+ },
1832
+ "group forget": {
1833
+ method: "group.forget",
1834
+ usage: "group forget <group> --yes",
1835
+ summary: "Leave and delete a group's history here",
1836
+ args: ["group"],
1837
+ options: { yes: {
1838
+ type: "boolean",
1839
+ description: "Confirm"
1840
+ } },
1841
+ params: ({ options }, { group }) => ({
1842
+ group,
1843
+ yes: options.yes === true
1844
+ })
1845
+ },
1846
+ "group history": {
1847
+ method: "group.history",
1848
+ usage: "group history <group> [--limit n] [--before id|ms] [--after id|ms]",
1849
+ summary: "A group's messages (a page)",
1850
+ args: ["group"],
1851
+ options: {
1852
+ limit: {
1853
+ type: "number",
1854
+ description: "Messages per page (default 50)"
1855
+ },
1856
+ before: {
1857
+ type: "string",
1858
+ description: "Only before this message id or timestamp"
1859
+ },
1860
+ after: {
1861
+ type: "string",
1862
+ description: "Only after this message id or timestamp"
1863
+ }
1864
+ },
1865
+ params: ({ options }, { group }) => ({
1866
+ group,
1867
+ limit: options.limit,
1868
+ before: cursor(options.before, "before"),
1869
+ after: cursor(options.after, "after")
1870
+ })
1871
+ },
1872
+ "group hub": {
1873
+ method: "group.hub",
1874
+ usage: "group hub <group> <member> [--pin | --exclude | --auto] [--show-secret]",
1875
+ summary: "Pin a member as a hub of a private group past 16 members, exclude one, or leave it to their app (admin)",
1876
+ args: ["group", "member"],
1877
+ options: {
1878
+ ...showSecret,
1879
+ pin: {
1880
+ type: "boolean",
1881
+ description: "Always a hub while online"
1882
+ },
1883
+ exclude: {
1884
+ type: "boolean",
1885
+ description: "Never a hub"
1886
+ },
1887
+ auto: {
1888
+ type: "boolean",
1889
+ description: "A hub if their app stays online (the default)"
1890
+ }
1891
+ },
1892
+ params: ({ options }, a) => {
1893
+ const roles = [
1894
+ "pin",
1895
+ "exclude",
1896
+ "auto"
1897
+ ].filter((role) => options[role] === true);
1898
+ if (roles.length > 1) throw new CliError("usage", "--pin, --exclude or --auto: one of them");
1899
+ return {
1900
+ group: a.group,
1901
+ member: a.member,
1902
+ role: roles[0] ?? "auto",
1903
+ ...secret(options)
1904
+ };
1905
+ }
1906
+ },
1907
+ "group invite": {
1908
+ method: "group.invite",
1909
+ usage: "group invite <group> <chat> [--show-secret]",
1910
+ summary: "Invite a contact into a group you administer",
1911
+ args: ["group", "chat"],
1912
+ options: showSecret,
1913
+ params: ({ options }, a) => ({
1914
+ group: a.group,
1915
+ chat: a.chat,
1916
+ ...secret(options)
1917
+ })
1918
+ },
1919
+ "group join": {
1920
+ method: "group.join",
1921
+ usage: "group join <link>",
1922
+ summary: "Join a group by its link",
1923
+ args: ["link"],
1924
+ params: (_, { link }) => ({ link })
1925
+ },
1926
+ "group leave": {
1927
+ method: "group.leave",
1928
+ usage: "group leave <group> --yes",
1929
+ summary: "Leave a group; its history and files go from this device",
1930
+ args: ["group"],
1931
+ options: { yes: {
1932
+ type: "boolean",
1933
+ description: "Confirm"
1934
+ } },
1935
+ params: ({ options }, { group }) => ({
1936
+ group,
1937
+ yes: options.yes === true
1938
+ })
1939
+ },
1940
+ "group link": {
1941
+ method: "group.link",
1942
+ usage: "group link <group> [--off] [--reset]",
1943
+ summary: "Turn the group's link on (--reset: a new one), or --off",
1944
+ args: ["group"],
1945
+ options: {
1946
+ off: {
1947
+ type: "boolean",
1948
+ description: "Turn the link off"
1949
+ },
1950
+ reset: {
1951
+ type: "boolean",
1952
+ description: "Replace it: the old link stops working"
1953
+ }
1954
+ },
1955
+ params: ({ options }, a) => {
1956
+ if (options.off === true && options.reset === true) throw new CliError("usage", "--off turns the link off: not with --reset");
1957
+ return {
1958
+ group: a.group,
1959
+ on: options.off !== true,
1960
+ reset: options.reset === true
1961
+ };
1962
+ }
1963
+ },
1964
+ "group list": {
1965
+ method: "group.list",
1966
+ usage: "group list [--show-secret]",
1967
+ summary: "Groups and invitations",
1968
+ options: showSecret,
1969
+ params: ({ options }) => secret(options)
1970
+ },
1971
+ "group picture": {
1972
+ method: "group.picture",
1973
+ usage: "group picture <group> <jpeg> | --clear",
1974
+ summary: "Set or clear the group's picture (admin; a square JPEG)",
1975
+ args: ["group", "path...?"],
1976
+ options: { clear: {
1977
+ type: "boolean",
1978
+ description: "Remove the picture"
1979
+ } },
1980
+ params: ({ options }, a) => {
1981
+ if (a.path === void 0 && options.clear !== true) throw new CliError("usage", "Missing <jpeg>: ghostly group picture <group> <jpeg> | --clear");
1982
+ if (a.path !== void 0 && options.clear === true) throw new CliError("usage", "--clear removes the picture: not with a <jpeg>");
1983
+ return {
1984
+ group: a.group,
1985
+ path: here(a.path),
1986
+ clear: options.clear === true
1987
+ };
1988
+ }
1989
+ },
1990
+ "group react": {
1991
+ method: "group.react",
1992
+ usage: "group react <group> <message> <emoji> [--remove]",
1993
+ summary: "React to a group's message with one emoji; --remove takes yours back",
1994
+ args: [
1995
+ "group",
1996
+ "message",
1997
+ "emoji...?"
1998
+ ],
1999
+ options: { remove: {
2000
+ type: "boolean",
2001
+ description: "Take your reaction back"
2002
+ } },
2003
+ params: ({ options }, a) => ({
2004
+ group: a.group,
2005
+ message: a.message,
2006
+ emoji: a.emoji,
2007
+ remove: options.remove === true
2008
+ })
2009
+ },
2010
+ "group remove": {
2011
+ method: "group.remove",
2012
+ usage: "group remove <group> <member> [--show-secret]",
2013
+ summary: "Remove a member (admin)",
2014
+ args: ["group", "member"],
2015
+ options: showSecret,
2016
+ params: ({ options }, a) => ({
2017
+ group: a.group,
2018
+ member: a.member,
2019
+ ...secret(options)
2020
+ })
2021
+ },
2022
+ "group rename": {
2023
+ method: "group.rename",
2024
+ usage: "group rename <group> <name...> [--show-secret]",
2025
+ summary: "Rename the group (admin; the picture stays)",
2026
+ args: ["group", "name..."],
2027
+ options: showSecret,
2028
+ params: ({ options }, a) => ({
2029
+ group: a.group,
2030
+ name: a.name,
2031
+ ...secret(options)
2032
+ })
2033
+ },
2034
+ "group rotate": {
2035
+ method: "group.rotate",
2036
+ usage: "group rotate <group> [--show-secret]",
2037
+ summary: "A fresh group secret, members unchanged (admin)",
2038
+ args: ["group"],
2039
+ options: showSecret,
2040
+ params: ({ options }, a) => ({
2041
+ group: a.group,
2042
+ ...secret(options)
2043
+ })
2044
+ },
2045
+ "group show": {
2046
+ method: "group.get",
2047
+ usage: "group show <group> [--show-secret]",
2048
+ summary: "A group and its members (its entry link only with --show-secret)",
2049
+ args: ["group"],
2050
+ options: showSecret,
2051
+ params: ({ options }, { group }) => ({
2052
+ group,
2053
+ ...secret(options)
2054
+ })
2055
+ },
2056
+ "group typing": {
2057
+ method: "group.typing",
2058
+ usage: "group typing <group> [--kind typing|recording|thinking] [--status \"<text>\"] [--for s] [--stop]",
2059
+ summary: "Show the members you are writing, recording or thinking (private groups; it fades after 6 s unless said again, or kept with --for; a send or --stop ends it)",
2060
+ args: ["group"],
2061
+ options: {
2062
+ stop: {
2063
+ type: "boolean",
2064
+ description: "Say you stopped"
2065
+ },
2066
+ kind: {
2067
+ type: "string",
2068
+ description: "typing (default), recording (a voice note) or thinking"
2069
+ },
2070
+ status: {
2071
+ type: "string",
2072
+ description: "A short line shown instead, e.g. \"Reading the thread…\" (40 characters, no links)"
2073
+ },
2074
+ for: {
2075
+ type: "number",
2076
+ description: "Keep saying it for this many seconds (up to 600), until a send or --stop"
2077
+ }
2078
+ },
2079
+ params: ({ options }, { group }) => ({
2080
+ group,
2081
+ stop: options.stop === true,
2082
+ kind: options.kind,
2083
+ status: options.status,
2084
+ for: typingFor(options)
2085
+ })
2086
+ }
2087
+ };
2088
+ //#endregion
2089
+ //#region src/commands/identities.ts
2090
+ /** Identity proofs: one entry per command, in alphabetical order (test/commands.test.ts checks). */
2091
+ var commands$2 = {
2092
+ "identity cancel": {
2093
+ method: "identity.cancel",
2094
+ usage: "identity cancel <draft>",
2095
+ summary: "Drop a proof not finished",
2096
+ args: ["draft"],
2097
+ params: (_, a) => ({ draft: a.draft })
2098
+ },
2099
+ "identity complete": {
2100
+ method: "identity.complete",
2101
+ usage: "identity complete <draft> [--evidence-file f | --stdin]",
2102
+ summary: "Finish a proof: what the tool printed, or nothing for a published record",
2103
+ args: ["draft"],
2104
+ options: {
2105
+ "evidence-file": {
2106
+ type: "string",
2107
+ description: "The tool's output, from this file"
2108
+ },
2109
+ stdin: {
2110
+ type: "boolean",
2111
+ description: "The tool's output, from stdin"
2112
+ }
2113
+ },
2114
+ params: ({ options }, a) => {
2115
+ if (options["evidence-file"] !== void 0 && options.stdin === true) throw new CliError("usage", "--evidence-file or --stdin, not both");
2116
+ return {
2117
+ draft: a.draft,
2118
+ evidenceFile: here(options["evidence-file"]),
2119
+ stdin: options.stdin === true
2120
+ };
2121
+ }
2122
+ },
2123
+ "identity contact": {
2124
+ method: "identity.contact",
2125
+ usage: "identity contact <chat>",
2126
+ summary: "What a contact shared, as this device checked it",
2127
+ args: ["chat"],
2128
+ params: (_, a) => ({ chat: a.chat })
2129
+ },
2130
+ "identity list": {
2131
+ method: "identity.list",
2132
+ usage: "identity list",
2133
+ summary: "This profile's identity proofs"
2134
+ },
2135
+ "identity providers": {
2136
+ method: "identity.providers",
2137
+ usage: "identity providers",
2138
+ summary: "Kinds of identity proof, and which signers work here"
2139
+ },
2140
+ "identity recheck": {
2141
+ method: "identity.recheck",
2142
+ usage: "identity recheck <chat> <id>",
2143
+ summary: "Check a contact's proof again",
2144
+ args: ["chat", "id"],
2145
+ params: (_, a) => ({
2146
+ chat: a.chat,
2147
+ id: a.id
2148
+ })
2149
+ },
2150
+ "identity remove": {
2151
+ method: "identity.remove",
2152
+ usage: "identity remove <id>",
2153
+ summary: "Remove a proof: withdrawn from every chat, revoked",
2154
+ args: ["id"],
2155
+ params: (_, a) => ({ id: a.id })
2156
+ },
2157
+ "identity share": {
2158
+ method: "identity.share",
2159
+ usage: "identity share <chat> <id>",
2160
+ summary: "Show a proof to a contact",
2161
+ args: ["chat", "id"],
2162
+ params: (_, a) => ({
2163
+ chat: a.chat,
2164
+ id: a.id
2165
+ })
2166
+ },
2167
+ "identity withdraw": {
2168
+ method: "identity.withdraw",
2169
+ usage: "identity withdraw <chat> <id>",
2170
+ summary: "Withdraw a proof from a contact",
2171
+ args: ["chat", "id"],
2172
+ params: (_, a) => ({
2173
+ chat: a.chat,
2174
+ id: a.id
2175
+ })
2176
+ }
2177
+ };
2178
+ //#endregion
2179
+ //#region src/commands/payments.ts
2180
+ /** Wallets and payments: one entry per command, in alphabetical order (test/commands.test.ts checks). */
2181
+ var commands$1 = {
2182
+ "chat accept": {
2183
+ method: "chat.accept",
2184
+ usage: "chat accept <chat> <method> [--off] [--networks mainnet,testnet]",
2185
+ summary: "Which ways of paying a chat takes",
2186
+ args: ["chat", "method"],
2187
+ options: {
2188
+ off: {
2189
+ type: "boolean",
2190
+ description: "Stop taking it"
2191
+ },
2192
+ networks: {
2193
+ type: "string",
2194
+ description: "Comma-separated networks it is taken on"
2195
+ }
2196
+ },
2197
+ params: ({ options }, a) => ({
2198
+ chat: a.chat,
2199
+ method: a.method,
2200
+ on: options.off !== true,
2201
+ networks: typeof options.networks === "string" ? options.networks.split(",").map((n) => n.trim()).filter(Boolean) : void 0
2202
+ })
2203
+ },
2204
+ "chat pay": {
2205
+ method: "chat.pay",
2206
+ usage: "chat pay <chat> <sats> [--memo t] [--network testnet] [--confirm-real]",
2207
+ summary: "Send ecash to a contact (Mainnet needs --confirm-real)",
2208
+ args: ["chat", "amount"],
2209
+ options: {
2210
+ memo,
2211
+ network: net,
2212
+ "confirm-real": confirmReal
2213
+ },
2214
+ params: ({ options }, a) => ({
2215
+ chat: a.chat,
2216
+ amount: sats(a.amount),
2217
+ memo: options.memo,
2218
+ network: options.network,
2219
+ confirmReal: options["confirm-real"] === true
2220
+ })
2221
+ },
2222
+ "chat pay-request": {
2223
+ method: "chat.payRequest",
2224
+ usage: "chat pay-request <chat> <payment> [--via lightning] [--max-fee sats] [--card <id>] [--confirm-real]",
2225
+ summary: "Pay a contact's request",
2226
+ args: ["chat", "payment"],
2227
+ options: {
2228
+ via: {
2229
+ type: "string",
2230
+ description: "lightning: pay its invoice instead of ecash"
2231
+ },
2232
+ "max-fee": {
2233
+ type: "number",
2234
+ description: "Refuse a fee above this"
2235
+ },
2236
+ card,
2237
+ "confirm-real": confirmReal
2238
+ },
2239
+ params: ({ options }, a) => ({
2240
+ chat: a.chat,
2241
+ payment: a.payment,
2242
+ via: options.via,
2243
+ maxFee: options["max-fee"],
2244
+ card: options.card,
2245
+ confirmReal: options["confirm-real"] === true
2246
+ })
2247
+ },
2248
+ "chat request": {
2249
+ method: "chat.request",
2250
+ usage: "chat request <chat> <sats> [--memo t] [--method m] [--rail cashu|lightning] [--network testnet] [--card <id>]",
2251
+ summary: "Ask a contact to pay",
2252
+ args: ["chat", "amount"],
2253
+ options: {
2254
+ memo,
2255
+ method: {
2256
+ type: "string",
2257
+ description: "cashu, arkade, usdt, bitcoin, spark, …"
2258
+ },
2259
+ rail: {
2260
+ type: "string",
2261
+ description: "cashu or lightning"
2262
+ },
2263
+ network: net,
2264
+ card
2265
+ },
2266
+ params: ({ options }, a) => ({
2267
+ chat: a.chat,
2268
+ amount: sats(a.amount),
2269
+ memo: options.memo,
2270
+ method: options.method,
2271
+ rail: options.rail,
2272
+ network: options.network,
2273
+ card: options.card
2274
+ })
2275
+ },
2276
+ "lightning default": {
2277
+ method: "lightning.default",
2278
+ usage: "lightning default <card> [--network testnet]",
2279
+ summary: "The Lightning card that receives by default",
2280
+ args: ["card"],
2281
+ options: { network: net },
2282
+ params: ({ options }, { card: c }) => ({
2283
+ card: c,
2284
+ network: options.network
2285
+ })
2286
+ },
2287
+ "lightning rename": {
2288
+ method: "lightning.rename",
2289
+ usage: "lightning rename <card> <name...> [--network testnet]",
2290
+ summary: "Name a Lightning card",
2291
+ args: ["card", "name..."],
2292
+ options: { network: net },
2293
+ params: ({ options }, a) => ({
2294
+ card: a.card,
2295
+ name: a.name,
2296
+ network: options.network
2297
+ })
2298
+ },
2299
+ "pay": {
2300
+ method: "pay",
2301
+ usage: "pay <invoice|lightning-address|lnurl> [--amount sats] [--network n] [--max-fee sats] [--card <id>] [--comment t] [--confirm-real]",
2302
+ summary: "Pay over Lightning from Testnet, or --network mainnet with --confirm-real",
2303
+ args: ["target"],
2304
+ options: {
2305
+ amount: {
2306
+ type: "number",
2307
+ description: "Sats, for an address or LNURL"
2308
+ },
2309
+ network: net,
2310
+ "max-fee": {
2311
+ type: "number",
2312
+ description: "Refuse a fee above this"
2313
+ },
2314
+ card,
2315
+ comment: {
2316
+ type: "string",
2317
+ description: "For the address's owner"
2318
+ },
2319
+ "confirm-real": confirmReal
2320
+ },
2321
+ params: ({ options }, { target }) => ({
2322
+ target,
2323
+ amount: options.amount,
2324
+ network: options.network,
2325
+ maxFee: options["max-fee"],
2326
+ card: options.card,
2327
+ comment: options.comment,
2328
+ confirmReal: options["confirm-real"] === true
2329
+ })
2330
+ },
2331
+ "payment check": {
2332
+ method: "payment.check",
2333
+ usage: "payment check <chat> <payment>",
2334
+ summary: "Paid from another wallet: the contact's app looks now",
2335
+ args: ["chat", "payment"],
2336
+ params: (_, a) => ({
2337
+ chat: a.chat,
2338
+ payment: a.payment
2339
+ })
2340
+ },
2341
+ "payment list": {
2342
+ method: "payment.list",
2343
+ usage: "payment list [--chat <chat>]",
2344
+ summary: "Payments and requests, newest first",
2345
+ options: { chat: {
2346
+ type: "string",
2347
+ description: "Only this chat's"
2348
+ } },
2349
+ params: ({ options }) => ({ chat: options.chat })
2350
+ },
2351
+ "payment reclaim": {
2352
+ method: "payment.reclaim",
2353
+ usage: "payment reclaim <payment>",
2354
+ summary: "Take back ecash the contact has not taken",
2355
+ args: ["payment"],
2356
+ params: (_, a) => ({ payment: a.payment })
2357
+ },
2358
+ "wallet add-mint": {
2359
+ method: "wallet.mint.add",
2360
+ usage: "wallet add-mint <url> [--primary]",
2361
+ summary: "Add a Cashu mint (a local or test mint is Testnet's)",
2362
+ args: ["url"],
2363
+ options: { primary: {
2364
+ type: "boolean",
2365
+ description: "Make it the one invoices are created at"
2366
+ } },
2367
+ params: ({ options }, { url }) => ({
2368
+ url,
2369
+ primary: options.primary === true
2370
+ })
2371
+ },
2372
+ "wallet address": {
2373
+ method: "wallet.address",
2374
+ usage: "wallet address <bitcoin|arkade|spark|usdt> [--network testnet]",
2375
+ summary: "An address to be paid on",
2376
+ args: ["type"],
2377
+ options: { network: net },
2378
+ params: ({ options }, { type }) => ({
2379
+ type,
2380
+ network: options.network
2381
+ })
2382
+ },
2383
+ "wallet create": {
2384
+ method: "wallet.create",
2385
+ usage: "wallet create <type> [--network testnet] [--provider <id>] [--value name=value]... [--stdin] [--invite <code>] [--api-key <key>]",
2386
+ summary: "A wallet: cashu, lightning, arkade, spark, bitcoin, fedimint, usdt (bark: app only)",
2387
+ args: ["type"],
2388
+ options: {
2389
+ network: net,
2390
+ provider: {
2391
+ type: "string",
2392
+ description: "Lightning or on-chain source (wallet list shows them)"
2393
+ },
2394
+ value: {
2395
+ type: "list",
2396
+ description: "A field of the source's form, name=value"
2397
+ },
2398
+ stdin: {
2399
+ type: "boolean",
2400
+ description: "Secret fields as name=value lines on stdin (api-key=… for the API key), kept out of ps"
2401
+ },
2402
+ invite: {
2403
+ type: "string",
2404
+ description: "Fedimint: the federation's invite"
2405
+ },
2406
+ "api-key": {
2407
+ type: "string",
2408
+ description: "Spark on Mainnet: your Breez API key (better on stdin: --stdin)"
2409
+ }
2410
+ },
2411
+ params: ({ options }, { type }) => ({
2412
+ type,
2413
+ network: options.network,
2414
+ provider: options.provider,
2415
+ invite: options.invite,
2416
+ apiKey: options["api-key"],
2417
+ values: pairs(options.value),
2418
+ stdin: options.stdin === true ? true : void 0
2419
+ })
2420
+ },
2421
+ "wallet faucet": {
2422
+ method: "wallet.faucet",
2423
+ usage: "wallet faucet <type> [--card <id>]",
2424
+ summary: "Test coins from a Testnet wallet's faucet",
2425
+ args: ["type"],
2426
+ options: { card },
2427
+ params: ({ options }, { type }) => ({
2428
+ type,
2429
+ network: "testnet",
2430
+ card: options.card
2431
+ })
2432
+ },
2433
+ "wallet history": {
2434
+ method: "wallet.history",
2435
+ usage: "wallet history [--network testnet] [--limit n]",
2436
+ summary: "A network's wallet history, newest first",
2437
+ options: {
2438
+ network: net,
2439
+ limit: {
2440
+ type: "number",
2441
+ description: "Entries (default 50)"
2442
+ }
2443
+ },
2444
+ params: ({ options }) => ({
2445
+ network: options.network,
2446
+ limit: options.limit
2447
+ })
2448
+ },
2449
+ "wallet list": {
2450
+ method: "wallet.list",
2451
+ usage: "wallet list [--network mainnet|testnet]",
2452
+ summary: "Wallets with their balances, and what New can make",
2453
+ options: { network: net },
2454
+ params: ({ options }) => ({ network: options.network })
2455
+ },
2456
+ "wallet receive": {
2457
+ method: "wallet.receive",
2458
+ usage: "wallet receive <sats> [--network testnet] [--card <id>]",
2459
+ summary: "A Lightning invoice to be paid",
2460
+ args: ["amount"],
2461
+ options: {
2462
+ network: net,
2463
+ card
2464
+ },
2465
+ params: ({ options }, { amount }) => ({
2466
+ amount: sats(amount),
2467
+ network: options.network,
2468
+ card: options.card
2469
+ })
2470
+ },
2471
+ "wallet redeem": {
2472
+ method: "wallet.redeem",
2473
+ usage: "wallet redeem [<cashu-token>]",
2474
+ summary: "Take the ecash of a Cashu token (its mint must be one of yours); the token on stdin keeps it out of ps",
2475
+ args: ["token?"],
2476
+ params: (_, { token }) => ({ token })
2477
+ },
2478
+ "wallet remove": {
2479
+ method: "wallet.remove",
2480
+ usage: "wallet remove <type> [--network testnet] [--card <id>] [--accept-loss]",
2481
+ summary: "Remove a wallet; refused while it holds money or waits for some, unless --accept-loss",
2482
+ args: ["type"],
2483
+ options: {
2484
+ network: net,
2485
+ card,
2486
+ "accept-loss": {
2487
+ type: "boolean",
2488
+ description: "What it holds on this device is lost without its backup"
2489
+ }
2490
+ },
2491
+ params: ({ options }, { type }) => ({
2492
+ type,
2493
+ network: options.network,
2494
+ card: options.card,
2495
+ acceptLoss: options["accept-loss"] === true
2496
+ })
2497
+ }
2498
+ };
2499
+ //#endregion
2500
+ //#region src/commands/services.ts
2501
+ /** Shared local services: one entry per command, in alphabetical order (test/commands.test.ts checks). */
2502
+ var commands = {
2503
+ "service add": {
2504
+ method: "service.add",
2505
+ usage: "service add <name> <http://127.0.0.1:port>",
2506
+ summary: "A web app on this machine that contacts may be given",
2507
+ args: ["name", "target"],
2508
+ params: (_, a) => ({
2509
+ name: a.name,
2510
+ target: a.target
2511
+ })
2512
+ },
2513
+ "service close": {
2514
+ method: "service.close",
2515
+ usage: "service close <chat> <service>",
2516
+ summary: "Close a contact's app opened here",
2517
+ args: ["chat", "service"],
2518
+ params: (_, a) => ({
2519
+ chat: a.chat,
2520
+ service: a.service
2521
+ })
2522
+ },
2523
+ "service enable": {
2524
+ method: "service.enable",
2525
+ usage: "service enable <service> [--off]",
2526
+ summary: "Turn a shared app on or off",
2527
+ args: ["service"],
2528
+ options: { off: {
2529
+ type: "boolean",
2530
+ description: "Turn it off"
2531
+ } },
2532
+ params: ({ options }, a) => ({
2533
+ service: a.service,
2534
+ off: options.off === true
2535
+ })
2536
+ },
2537
+ "service list": {
2538
+ method: "service.list",
2539
+ usage: "service list",
2540
+ summary: "Web apps this profile shares"
2541
+ },
2542
+ "service open": {
2543
+ method: "service.open",
2544
+ usage: "service open <chat> <service> [--port p]",
2545
+ summary: "A contact's app on a loopback port here (daemon)",
2546
+ args: ["chat", "service"],
2547
+ options: { port: {
2548
+ type: "number",
2549
+ description: "Local port (default: any free one)"
2550
+ } },
2551
+ params: ({ options }, a) => ({
2552
+ chat: a.chat,
2553
+ service: a.service,
2554
+ port: options.port
2555
+ })
2556
+ },
2557
+ "service peer": {
2558
+ method: "service.peer",
2559
+ usage: "service peer <chat>",
2560
+ summary: "What a contact shares with you",
2561
+ args: ["chat"],
2562
+ params: (_, a) => ({ chat: a.chat })
2563
+ },
2564
+ "service remove": {
2565
+ method: "service.remove",
2566
+ usage: "service remove <service>",
2567
+ summary: "Stop sharing a web app with anyone",
2568
+ args: ["service"],
2569
+ params: (_, a) => ({ service: a.service })
2570
+ },
2571
+ "service share": {
2572
+ method: "service.share",
2573
+ usage: "service share <service> <chat> [--off]",
2574
+ summary: "Give (or take back) a contact access to a web app",
2575
+ args: ["service", "chat"],
2576
+ options: { off: {
2577
+ type: "boolean",
2578
+ description: "Take it back"
2579
+ } },
2580
+ params: ({ options }, a) => ({
2581
+ service: a.service,
2582
+ chat: a.chat,
2583
+ off: options.off === true
2584
+ })
2585
+ }
2586
+ };
2587
+ //#endregion
2588
+ //#region src/commands/index.ts
2589
+ /**
2590
+ * The commands that are one API method each, one file per area of the app, each in alphabetical order. A new
2591
+ * command goes in its area's file at its place; a new area is a new file and its two lines here. The order here
2592
+ * is the order `ghostly help` lists them in.
2593
+ */
2594
+ var COMMANDS = {
2595
+ ...commands$4,
2596
+ ...commands$6,
2597
+ ...commands$3,
2598
+ ...commands$5,
2599
+ ...commands$1,
2600
+ ...commands$2,
2601
+ ...commands,
2602
+ ...commands$7,
2603
+ ...commands$8
2604
+ };
2605
+ /** Commands that take text (argument or stdin), with the secret guard and delivery waits. */
2606
+ var TEXT_COMMANDS = {
2607
+ "send": {
2608
+ method: "chat.send",
2609
+ target: "chat",
2610
+ args: ["chat", "text...?"],
2611
+ usage: "send <chat> [text...] [--reply <message>] [--button id:Label]... [--style id=primary|neutral|danger]... [--once] [--id id] [--stdin] [--force] [--wait none|sent|delivered]",
2612
+ summary: "Send a message (text from arguments or stdin); --button puts buttons under it, each label its own (case aside)",
2613
+ options: {
2614
+ stdin: {
2615
+ type: "boolean",
2616
+ description: "Read the text from stdin"
2617
+ },
2618
+ force,
2619
+ reply,
2620
+ ...buttonOptions,
2621
+ ...wait
2622
+ }
2623
+ },
2624
+ "edit": {
2625
+ method: "chat.edit",
2626
+ target: "chat",
2627
+ message: true,
2628
+ args: [
2629
+ "chat",
2630
+ "message",
2631
+ "text...?"
2632
+ ],
2633
+ usage: "edit <chat> <message> [text... | --text <text> | --stdin] [--force] [--wait none|confirmed]",
2634
+ summary: "Replace the text of a message you sent (the id send gave; in a group, group edit)",
2635
+ options: {
2636
+ stdin: {
2637
+ type: "boolean",
2638
+ description: "Read the new text from stdin"
2639
+ },
2640
+ text: {
2641
+ type: "string",
2642
+ description: "The new text, as one argument"
2643
+ },
2644
+ force,
2645
+ wait: {
2646
+ type: "string",
2647
+ description: "none or confirmed (default: none with a daemon, confirmed without)"
2648
+ },
2649
+ timeout: wait.timeout
2650
+ }
2651
+ },
2652
+ "group send": {
2653
+ method: "group.send",
2654
+ target: "group",
2655
+ args: ["group", "text...?"],
2656
+ usage: "group send <group> [text...] [--mention <member>]... [--reply <message>] [--button id:Label]... [--style id=primary|neutral|danger]... [--once] [--id id] [--stdin] [--force] [--wait none|sent] [--timeout s]",
2657
+ summary: "Send to a group; mention members written as @name in the text; --button puts buttons under it",
2658
+ options: {
2659
+ stdin: {
2660
+ type: "boolean",
2661
+ description: "Read the text from stdin"
2662
+ },
2663
+ force,
2664
+ reply,
2665
+ mention: {
2666
+ type: "list",
2667
+ description: "A member (key, key prefix, name or everyone) named as @name in the text"
2668
+ },
2669
+ ...buttonOptions,
2670
+ ...groupWait
2671
+ }
2672
+ },
2673
+ "group edit": {
2674
+ method: "group.edit",
2675
+ target: "group",
2676
+ message: true,
2677
+ args: [
2678
+ "group",
2679
+ "message",
2680
+ "text...?"
2681
+ ],
2682
+ usage: "group edit <group> <message> [text... | --text <text> | --stdin] [--mention <member>]... [--force] [--wait none|sent] [--timeout s]",
2683
+ summary: "Replace the text of a message you sent to a group (the id group send gave)",
2684
+ options: {
2685
+ stdin: {
2686
+ type: "boolean",
2687
+ description: "Read the new text from stdin"
2688
+ },
2689
+ text: {
2690
+ type: "string",
2691
+ description: "The new text, as one argument"
2692
+ },
2693
+ force,
2694
+ mention: {
2695
+ type: "list",
2696
+ description: "A member newly named as @name in the text (those the message named stay)"
2697
+ },
2698
+ ...groupWait
2699
+ }
2700
+ }
2701
+ };
2702
+ /**
2703
+ * Positionals that name something by its id (a chat or a group also by a prefix or its name). The ids are the
2704
+ * engine's and the contacts', and drafts, payments and groups are base64url, which starts with `-` one time in 64:
2705
+ * in these a word that starts with a dash is the value. A mistyped flag there names nothing and fails as not found;
2706
+ * text, names and paths stay strict.
2707
+ */
2708
+ var ID_ARGS = /* @__PURE__ */ new Set([
2709
+ "chat",
2710
+ "message",
2711
+ "group",
2712
+ "member",
2713
+ "payment",
2714
+ "file",
2715
+ "draft",
2716
+ "id",
2717
+ "card",
2718
+ "service",
2719
+ "call"
2720
+ ]);
2721
+ /** A leading positional that may be left out (`name?`); a trailing one that may (`text...?`) shifts nothing. */
2722
+ var leadingOptional = (name) => name.endsWith("?") && !name.endsWith("...?");
2723
+ /**
2724
+ * Which of a command's positionals take an id, by index (a trailing `...` one covers the rest). An optional `name?`
2725
+ * shifts the others, so it counts only where it and what may come in its place are all ids.
2726
+ */
2727
+ function idSlot(command) {
2728
+ const names = (command.args ?? []).map((name) => name.replace(/\?$/, ""));
2729
+ const shift = (command.args ?? []).filter(leadingOptional).length;
2730
+ const rest = names.findIndex((name) => name.endsWith("..."));
2731
+ const at = (index) => {
2732
+ const name = rest !== -1 && index >= rest ? names[rest].slice(0, -3) : names[index];
2733
+ return name !== void 0 && ID_ARGS.has(name);
2734
+ };
2735
+ return (index) => at(index) && (shift === 0 || index + shift >= names.length || at(index + shift));
2736
+ }
2737
+ /**
2738
+ * The positionals of a command, by name; missing required ones are a usage error. A leading `name?` is optional: it
2739
+ * is given only when every required one is there too (`file accept [<chat>] <file>`). A trailing `name...` takes the
2740
+ * rest and needs at least one word; `name...?` may take none (text from stdin, `chat rename`'s name back to the
2741
+ * contact's).
2742
+ */
2743
+ function positionals(command, values) {
2744
+ let names = command.args ?? [];
2745
+ const optional = names.filter(leadingOptional);
2746
+ if (optional.length) {
2747
+ const skip = Math.max(0, optional.length - Math.max(0, values.length - (names.length - optional.length)));
2748
+ names = [...optional.slice(skip).map((name) => name.slice(0, -1)), ...names.filter((name) => !leadingOptional(name))];
2749
+ }
2750
+ const out = {};
2751
+ for (let i = 0; i < names.length; i++) {
2752
+ const name = names[i].replace(/\?$/, "");
2753
+ if (name.endsWith("...")) {
2754
+ const rest = values.slice(i);
2755
+ if (!rest.length && !names[i].endsWith("?")) throw new CliError("usage", `Missing <${name.slice(0, -3)}>: ghostly ${command.usage}`);
2756
+ out[name.slice(0, -3)] = rest.length ? rest.join(" ") : void 0;
2757
+ return out;
2758
+ }
2759
+ if (values[i] === void 0) throw new CliError("usage", `Missing <${name}>: ghostly ${command.usage}`);
2760
+ out[name] = values[i];
2761
+ }
2762
+ noMoreThan(names.length, values, command.usage);
2763
+ return out;
2764
+ }
2765
+ /** Words past a usage's arguments are refused, never dropped: `profile use my bot` would select `my`. */
2766
+ function noMoreThan(count, values, usage) {
2767
+ if (values.length > count) throw new CliError("usage", `Too many arguments: ghostly ${usage}`);
2768
+ }
2769
+ //#endregion
2770
+ //#region src/listen.ts
2771
+ /**
2772
+ * Who `listen` passes on, checked before any hook or output: chats by id (a contact key is matched to its chat as
2773
+ * events come, so a chat made after the start counts too), groups by id. An event of a chat or group not on it is
2774
+ * dropped; one that names neither (`daemon.started`, `events.gap`, `identity.approval`) is this profile's own and
2775
+ * passes.
2776
+ */
2777
+ var Allowlist = class {
2778
+ chats;
2779
+ keys;
2780
+ groups;
2781
+ peerOf;
2782
+ peers = /* @__PURE__ */ new Map();
2783
+ /** `peerOf`: a chat's contact key (null when it has none yet, or the chat is gone). */
2784
+ constructor(chats, keys, groups, peerOf) {
2785
+ this.chats = chats;
2786
+ this.keys = keys;
2787
+ this.groups = groups;
2788
+ this.peerOf = peerOf;
2789
+ }
2790
+ async allows(event) {
2791
+ if ("group" in event) return typeof event.group === "string" && this.groups.has(event.group);
2792
+ if (!("chat" in event)) return true;
2793
+ const chat = event.chat;
2794
+ if (typeof chat !== "string") return false;
2795
+ if (this.chats.has(chat)) return true;
2796
+ if (!this.keys.size) return false;
2797
+ let key = this.peers.get(chat);
2798
+ if (!key) {
2799
+ key = await this.peerOf(chat).catch(() => null) || void 0;
2800
+ if (key) this.peers.set(chat, key);
2801
+ }
2802
+ return !!key && this.keys.has(key);
2803
+ }
2804
+ };
2805
+ /**
2806
+ * `--from` and `--group` as ids, resolved once at start: a contact key as that key (whatever any chat is named), else a
2807
+ * chat by id, prefix or name (`findChat`); a group by id, prefix or name. A name that later changes (or a contact who
2808
+ * renames themselves) moves nothing.
2809
+ */
2810
+ async function allowlist(from, groups, call, peerOf) {
2811
+ const chats = /* @__PURE__ */ new Set(), keys = /* @__PURE__ */ new Set(), groupIds = /* @__PURE__ */ new Set();
2812
+ for (const ref of from) if (CONTACT_KEY.test(ref)) keys.add(ref);
2813
+ else chats.add((await call("chat.get", { chat: ref })).id);
2814
+ for (const ref of groups) groupIds.add((await call("group.get", { group: ref })).id);
2815
+ return new Allowlist(chats, keys, groupIds, peerOf);
2816
+ }
2817
+ /** The turn an event starts, or null: only a contact's message, and a group message that mentions this profile. */
2818
+ function toTurn(event) {
2819
+ const message = event.message;
2820
+ if (!message || typeof message.text !== "string") return null;
2821
+ if (event.type === "message.received" && typeof event.chat === "string") return turn(event, "message.received", { chat: event.chat }, message);
2822
+ if (event.type === "group.message" && typeof event.group === "string" && message.mentioned) return turn(event, "group.message", {
2823
+ group: event.group,
2824
+ ...message.member ? { member: message.member } : {}
2825
+ }, message);
2826
+ if (event.type === "group.mentioned" && typeof event.group === "string" && message.mentioned) return turn(event, "group.mentioned", {
2827
+ group: event.group,
2828
+ ...message.member ? { member: message.member } : {}
2829
+ }, message);
2830
+ return null;
2831
+ }
2832
+ function turn(event, source, where, message) {
2833
+ const file = message.file;
2834
+ const place = where.chat ?? where.group ?? "", messageId = turnRef(place, message.id);
2835
+ return {
2836
+ seq: event.seq,
2837
+ id: `agent.turn:${source}:${place}:${messageId}`,
2838
+ type: "agent.turn",
2839
+ at: event.at,
2840
+ source,
2841
+ ...where,
2842
+ messageId,
2843
+ timestamp: message.timestamp,
2844
+ ...message.press ? { press: message.press } : {},
2845
+ untrusted: {
2846
+ text: message.text,
2847
+ name: message.nick ?? null,
2848
+ messageId: message.id,
2849
+ ...message.replyTo ? { replyTo: {
2850
+ id: message.replyTo.id,
2851
+ snippet: message.replyTo.snippet
2852
+ } } : {},
2853
+ ...file ? { file: {
2854
+ id: file.id,
2855
+ name: file.name,
2856
+ size: file.size,
2857
+ mime: file.mime,
2858
+ voice: !!file.voice
2859
+ } } : {}
2860
+ }
2861
+ };
2862
+ }
2863
+ /** Only loopback: events carry message text, and a webhook is a local bridge (WISP 1100 § Security). */
2864
+ function checkWebhook(url) {
2865
+ let parsed;
2866
+ try {
2867
+ parsed = new URL(url);
2868
+ } catch {
2869
+ throw new CliError("usage", `Not a URL: ${url}`);
2870
+ }
2871
+ if (!["http:", "https:"].includes(parsed.protocol)) throw new CliError("usage", "The webhook must be http:// or https://");
2872
+ if (![
2873
+ "127.0.0.1",
2874
+ "localhost",
2875
+ "[::1]"
2876
+ ].includes(parsed.hostname)) throw new CliError("usage", "The webhook must be on this machine (127.0.0.1, localhost or [::1])");
2877
+ return parsed;
2878
+ }
2879
+ function readCursor(path) {
2880
+ if (!path || !existsSync(path)) return void 0;
2881
+ const value = Number(readFileSync(path, "utf8").trim());
2882
+ return Number.isInteger(value) && value >= 0 ? value : void 0;
2883
+ }
2884
+ /**
2885
+ * Whether a cursor can be kept at `path`, asked before anything starts: its folder is there and takes a file. Before, a
2886
+ * folder that was not there was found at the first event: the listener ran, said "event N failed: ENOENT" for each one
2887
+ * and never wrote the cursor, so the next start began at the journal's end and skipped what came while it was down.
2888
+ * Throws what the file system says (`asCliError` names the path and the code: not_found, refused).
2889
+ */
2890
+ function checkCursor(path) {
2891
+ if (!path) return;
2892
+ const folder = dirname(resolve(path));
2893
+ if (!statSync(folder).isDirectory()) throw new CliError("not_found", `No such file or folder: ${folder}`);
2894
+ accessSync(folder, constants.W_OK | constants.X_OK);
2895
+ }
2896
+ function writeCursor(path, seq) {
2897
+ rmSync(path + ".tmp", { force: true });
2898
+ writeFileSync(path + ".tmp", String(seq) + "\n", {
2899
+ mode: 384,
2900
+ flag: "wx"
2901
+ });
2902
+ renameSync(path + ".tmp", path);
2903
+ }
2904
+ /**
2905
+ * `--type`: one type (`message.received`, exactly: `group.message` is not its `group.message.edited`), or a family, a
2906
+ * word with no "." (`message`, `chat`) or one ending in "." or "*" (`group.typing.`). Before, a family needed its "." and
2907
+ * `--type chat` let nothing through. A family is whole words: `message` is not `messages.x`.
2908
+ */
2909
+ function matches(event, types) {
2910
+ if (!types.length) return true;
2911
+ return types.some((type) => {
2912
+ if (type === "*") return true;
2913
+ const family = /[.*]$/.test(type) ? type.replace(/\.?\*$/, "").replace(/\.$/, "") : type.includes(".") ? null : type;
2914
+ return event.type === type || family !== null && event.type.startsWith(family + ".");
2915
+ });
2916
+ }
2917
+ /**
2918
+ * Every event type a daemon of this version emits (events.ts, appEvents.ts, holds.ts, identities.ts, calls/manager.ts,
2919
+ * main.ts) and `--turns`' own. A test reads them back from the source: a type added there and not here fails it.
2920
+ */
2921
+ var EVENT_TYPES = [
2922
+ "agent.turn",
2923
+ "app.closed",
2924
+ "app.message",
2925
+ "app.opened",
2926
+ "button.pressed",
2927
+ "call.connected",
2928
+ "call.ended",
2929
+ "call.incoming",
2930
+ "call.outgoing",
2931
+ "call.resumed",
2932
+ "call.stalled",
2933
+ "chat.announced",
2934
+ "chat.connection",
2935
+ "chat.created",
2936
+ "chat.held",
2937
+ "chat.joined",
2938
+ "chat.pairing",
2939
+ "chat.pinned",
2940
+ "chat.released",
2941
+ "chat.removed",
2942
+ "chat.renamed",
2943
+ "daemon.started",
2944
+ "events.gap",
2945
+ "file.done",
2946
+ "file.failed",
2947
+ "file.offered",
2948
+ "file.stage",
2949
+ "group.created",
2950
+ "group.deleted",
2951
+ "group.event",
2952
+ "group.members",
2953
+ "group.mentioned",
2954
+ "group.message",
2955
+ "group.message.edited",
2956
+ "group.pinned",
2957
+ "group.reaction",
2958
+ "group.removed",
2959
+ "group.sent",
2960
+ "group.status",
2961
+ "group.typing.started",
2962
+ "group.typing.stopped",
2963
+ "identity.approval",
2964
+ "identity.progress",
2965
+ "identity.received",
2966
+ "identity.status",
2967
+ "message.deleted",
2968
+ "message.delivery",
2969
+ "message.edited",
2970
+ "message.reaction",
2971
+ "message.received",
2972
+ "message.sent",
2973
+ "payment.created",
2974
+ "payment.updated",
2975
+ "typing.started",
2976
+ "typing.stopped"
2977
+ ];
2978
+ /** The families, the first word of each type: what a `--type` must start with. */
2979
+ var EVENT_FAMILIES = [...new Set(EVENT_TYPES.map((type) => type.split(".")[0]))];
2980
+ /**
2981
+ * Checks each `--type` before anything starts. Before, `--type bogus` (or a typo such as `message.recieved`) was taken
2982
+ * and the listener stayed silent for good. A first word that is no family is a usage error naming them; a type, or a
2983
+ * family below the first word, that no event has is said on stderr (`warn`) and kept: a newer daemon may emit it.
2984
+ */
2985
+ function checkTypes(types, warn) {
2986
+ for (const type of types) {
2987
+ if (type === "*") continue;
2988
+ const family = type.split(/[.*]/)[0];
2989
+ if (!EVENT_FAMILIES.includes(family)) throw new CliError("usage", `No event is of type ${type}: --type takes a type, or a family (${EVENT_FAMILIES.join(", ")})`);
2990
+ if (!EVENT_TYPES.some((known) => matches({
2991
+ seq: 0,
2992
+ id: "",
2993
+ type: known,
2994
+ at: 0
2995
+ }, [type]))) warn(`ghostly: no event is of type ${type}; the ${family} family has ${EVENT_TYPES.filter((known) => known.startsWith(family + ".")).join(", ")}`);
2996
+ }
2997
+ }
2998
+ /** The environment a hook runs in: this one's, less the backup passphrase (a hook, and what it runs, never needs it). */
2999
+ function hookEnv(env) {
3000
+ const rest = { ...env };
3001
+ delete rest.GHOSTLY_BACKUP_PASSPHRASE;
3002
+ return rest;
3003
+ }
3004
+ /** Runs `command` through the shell with the event on stdin (never in its arguments); resolves with its exit code. */
3005
+ function runExec(command, event) {
3006
+ return new Promise((resolve) => {
3007
+ const child = spawn(command, {
3008
+ shell: true,
3009
+ stdio: [
3010
+ "pipe",
3011
+ "inherit",
3012
+ "inherit"
3013
+ ],
3014
+ env: {
3015
+ ...hookEnv(process.env),
3016
+ GHOSTLY_EVENT_TYPE: event.type,
3017
+ GHOSTLY_EVENT_ID: event.id,
3018
+ GHOSTLY_EVENT_SEQ: String(event.seq)
3019
+ }
3020
+ });
3021
+ child.on("error", () => resolve(127));
3022
+ child.on("close", (code) => resolve(code ?? 1));
3023
+ child.stdin.on("error", () => {});
3024
+ child.stdin.end(JSON.stringify(event) + "\n");
3025
+ });
3026
+ }
3027
+ async function post(url, event) {
3028
+ for (let attempt = 0; attempt < 3; attempt++) {
3029
+ try {
3030
+ if ((await fetch(url, {
3031
+ method: "POST",
3032
+ redirect: "manual",
3033
+ headers: {
3034
+ "content-type": "application/json",
3035
+ "x-ghostly-event": event.type,
3036
+ "x-ghostly-seq": String(event.seq)
3037
+ },
3038
+ body: JSON.stringify(event),
3039
+ signal: AbortSignal.timeout(1e4)
3040
+ })).ok) return true;
3041
+ } catch {}
3042
+ await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** attempt));
3043
+ }
3044
+ return false;
3045
+ }
3046
+ /**
3047
+ * A handler that takes events in order, one at a time. A step that throws (the output closed, the allowlist's lookup,
3048
+ * the cursor's disk) is said on stderr and passed: the cursor moves on and the next event is handled.
3049
+ */
3050
+ function eventHandler(options) {
3051
+ const webhook = options.webhook ? checkWebhook(options.webhook) : null;
3052
+ let chain = Promise.resolve();
3053
+ let behind = 0;
3054
+ const handle = (event) => {
3055
+ const seq = event.seq, type = event.type;
3056
+ behind++;
3057
+ chain = chain.then(async () => {
3058
+ const skip = () => {
3059
+ if (options.cursor) writeCursor(options.cursor, event.seq);
3060
+ };
3061
+ if (!matches(event, options.types)) return skip();
3062
+ if (options.allow && !await options.allow.allows(event)) return skip();
3063
+ if (options.turns) {
3064
+ const next = toTurn(event);
3065
+ if (!next) return skip();
3066
+ event = next;
3067
+ }
3068
+ const full = options.print && options.write(JSON.stringify(event)) === false;
3069
+ if (options.exec) {
3070
+ const code = await runExec(options.exec, event);
3071
+ if (code !== 0) process.stderr.write(`ghostly: --exec exited ${code} on event ${event.seq} (${event.type})\n`);
3072
+ }
3073
+ if (webhook && !await post(webhook, event)) process.stderr.write(`ghostly: the webhook did not take event ${event.seq} (${event.type})\n`);
3074
+ if (full) await options.drained?.();
3075
+ if (options.cursor) writeCursor(options.cursor, event.seq);
3076
+ }).catch((error) => {
3077
+ try {
3078
+ process.stderr.write(`ghostly: event ${seq} (${type}) failed: ${error instanceof Error ? error.message : String(error)}\n`);
3079
+ if (options.cursor) writeCursor(options.cursor, seq);
3080
+ } catch {}
3081
+ }).finally(() => {
3082
+ behind--;
3083
+ });
3084
+ };
3085
+ return Object.assign(handle, {
3086
+ behind: () => behind,
3087
+ idle: () => chain
3088
+ });
3089
+ }
3090
+ /**
3091
+ * Bounds what a listener holds when its output or hooks fall behind (a stdout reader that hung, a slow --exec): past
3092
+ * `MAX_BEHIND` events not handled it stops taking events (`pause`: the subscription closes) and drops what still
3093
+ * arrives; once every event taken is handled, `resume` opens it again after the last one taken. The daemon's journal
3094
+ * keeps what came meanwhile (and says `events.gap` past what it keeps), so a consumer that hangs for a day does not
3095
+ * grow the listener by the day's traffic.
3096
+ */
3097
+ var Feed = class {
3098
+ handle;
3099
+ since;
3100
+ pause;
3101
+ resume;
3102
+ held = false;
3103
+ /** `since`: the last event taken, where a new subscription starts. */
3104
+ constructor(handle, since, pause, resume) {
3105
+ this.handle = handle;
3106
+ this.since = since;
3107
+ this.pause = pause;
3108
+ this.resume = resume;
3109
+ }
3110
+ get paused() {
3111
+ return this.held;
3112
+ }
3113
+ take = (event) => {
3114
+ if (this.held) return;
3115
+ this.since = event.seq;
3116
+ this.handle(event);
3117
+ if (this.handle.behind() < 1e3) return;
3118
+ this.held = true;
3119
+ this.pause();
3120
+ this.handle.idle().then(() => {
3121
+ this.held = false;
3122
+ this.resume(this.since);
3123
+ });
3124
+ };
3125
+ };
3126
+ //#endregion
3127
+ //#region src/secretInput.ts
3128
+ /**
3129
+ * Secrets a command takes (a Cashu token, an API key, a card's form) come on stdin: on the command line they show in
3130
+ * `ps` to every user of the machine while the command runs, and stay in the shell's history. Still taken there, with
3131
+ * a warning. Fills `params` in place; the warning to print on stderr, if any. `tty`: stdin is a terminal, where a
3132
+ * read would wait on the keyboard without a word.
3133
+ */
3134
+ async function secretsFromStdin(method, params, read, tty = false) {
3135
+ const warning = (what) => `${what} on the command line shows in ps and the shell's history: give it on stdin instead`;
3136
+ if (method === "wallet.redeem") {
3137
+ if (params.token !== void 0) return warning("A token");
3138
+ const missing = () => new CliError("usage", "Missing <cashu-token>: give it as an argument or on stdin (… | ghostly wallet redeem)");
3139
+ if (tty) throw missing();
3140
+ const token = (await read()).trim();
3141
+ if (!token) throw missing();
3142
+ params.token = token;
3143
+ return;
3144
+ }
3145
+ if (method === "wallet.create") {
3146
+ const onLine = params.apiKey !== void 0;
3147
+ if (params.stdin === true) {
3148
+ const values = { ...params.values };
3149
+ for (const line of (await read()).split(/\r?\n/)) {
3150
+ if (!line.trim()) continue;
3151
+ const eq = line.indexOf("=");
3152
+ if (eq < 1) throw new CliError("usage", "--stdin takes name=value lines (api-key=… for the API key)");
3153
+ const name = line.slice(0, eq).trim(), value = line.slice(eq + 1);
3154
+ if (name === "api-key") params.apiKey = value;
3155
+ else values[name] = value;
3156
+ }
3157
+ if (Object.keys(values).length) params.values = values;
3158
+ }
3159
+ delete params.stdin;
3160
+ return onLine ? warning("An API key") : void 0;
3161
+ }
3162
+ }
3163
+ //#endregion
3164
+ //#region src/main.ts
3165
+ var VERSION = package_default.version;
3166
+ /**
3167
+ * The engine's modules (the API, the host, the backup, the buttons) are loaded only where a command needs them here: a
3168
+ * command a running daemon answers loads none of them, and starts about as fast as Node does. Statically imported, they
3169
+ * cost every command ~100 ms before its socket was opened.
3170
+ */
3171
+ var engineApi = () => import("./api-TiB5QSG9.js");
3172
+ var engineHost = () => import("./host-CWt-jtqz.js");
3173
+ var pretty = false;
3174
+ function print(value) {
3175
+ process.stdout.write(JSON.stringify(value, null, pretty ? 2 : void 0) + "\n");
3176
+ }
3177
+ /** Done, with something to know (a voice note sent without its waveform): said on stderr, the JSON stays the answer. */
3178
+ function printAnswer(result) {
3179
+ if (typeof result?.warning === "string") {
3180
+ process.stderr.write(`ghostly: ${result.warning}\n`);
3181
+ delete result.warning;
3182
+ }
3183
+ print(result);
3184
+ }
3185
+ function globals(parsed) {
3186
+ const home = ghostlyHome(parsed.options.home);
3187
+ const profile = currentProfile(home, parsed.options.profile);
3188
+ return {
3189
+ home,
3190
+ profile,
3191
+ paths: profilePaths(home, profile),
3192
+ pretty: parsed.options.pretty === true
3193
+ };
3194
+ }
3195
+ /** The profile must exist; `default` is made on first use, any other with `profile create`. */
3196
+ function requireProfile(g) {
3197
+ if (profileExists(g.home, g.profile)) return;
3198
+ if (g.profile === "default") {
3199
+ createProfile(g.home, g.profile);
3200
+ return;
3201
+ }
3202
+ throw noProfile(g);
3203
+ }
3204
+ function noProfile(g) {
3205
+ return new CliError("not_found", `No profile ${g.profile}: make it with \`ghostly profile create ${g.profile}\``);
3206
+ }
3207
+ /** For a command that only reads: a mistyped profile is named as one, and `default` is not made by asking about it. */
3208
+ function knownProfile(g) {
3209
+ if (g.profile !== "default" && !profileExists(g.home, g.profile)) throw noProfile(g);
3210
+ }
3211
+ /** Said on stderr when the daemon runs another release than this command (it started before an upgrade). */
3212
+ function versionWarning(daemon, cli) {
3213
+ if (typeof daemon !== "string" || daemon === cli) return null;
3214
+ return `ghostly: the daemon runs ${daemon} and this command is ${cli}: \`ghostly daemon restart\` runs the new code`;
3215
+ }
3216
+ async function warnVersion(client) {
3217
+ const warning = versionWarning((await client.call("status").catch(() => null))?.version, VERSION);
3218
+ if (warning) process.stderr.write(warning + "\n");
3219
+ }
3220
+ /**
3221
+ * A command about one 1:1 chat: a one-shot for it leaves the groups' sessions unstarted. A fresh process starts an edge
3222
+ * per group member, and their first polls spent the relays' 30 requests a minute in seconds: the chat's message then
3223
+ * waited for the next minute (60 s and more instead of 3 to 5).
3224
+ */
3225
+ function chatOnly(method, params) {
3226
+ if (method.startsWith("task.") || method.startsWith("routine.") || method.startsWith("button.")) return false;
3227
+ if (method === "usage.send") return params.all === true;
3228
+ return method.startsWith("chat.") || params.chat !== void 0 && params.group === void 0 && !method.startsWith("group.");
3229
+ }
3230
+ /**
3231
+ * Calls that only read what the profile stores: a one-shot for one opens it offline (`RuntimeOptions.offline`).
3232
+ * Opened online, `status` or `chat list` started every chat, put the profile's records on the relays again and waited
3233
+ * for its leave packet to be taken: 0.25 s became 3.3 s with a relay that takes 1.5 s, and 10 s with one that does not
3234
+ * answer, and each read spent ~5 relay requests per chat of the relays' per-address budget.
3235
+ */
3236
+ var READS = /* @__PURE__ */ new Set([
3237
+ "status",
3238
+ "profile.get",
3239
+ "settings.get",
3240
+ "events.replay",
3241
+ "chat.list",
3242
+ "chat.get",
3243
+ "chat.history",
3244
+ "chat.details",
3245
+ "group.list",
3246
+ "group.get",
3247
+ "group.history",
3248
+ "file.list",
3249
+ "wallet.list",
3250
+ "wallet.history",
3251
+ "payment.list",
3252
+ "identity.list",
3253
+ "identity.providers",
3254
+ "identity.contact",
3255
+ "service.list",
3256
+ "service.peer",
3257
+ "call.list",
3258
+ "app.served"
3259
+ ]);
3260
+ /** Whether a one-shot for this call opens the profile offline; test/readOnlyCommands.test.ts names every table command. */
3261
+ function readOnly(method, params) {
3262
+ if (method === "engine.call") return typeof params.method === "string" && ENGINE_READS.includes(params.method);
3263
+ if (method === "call.auto") return params.on === void 0;
3264
+ return READS.has(method);
3265
+ }
3266
+ async function session(g, options) {
3267
+ requireProfile(g);
3268
+ const client = await connectDaemon(g.paths.socket);
3269
+ if (client) await warnVersion(client);
3270
+ if (client) return {
3271
+ mode: "daemon",
3272
+ call: (m, p) => client.call(m, p),
3273
+ close: async () => client.close()
3274
+ };
3275
+ const [{ openHost }, { callApi }] = await Promise.all([engineHost(), engineApi()]);
3276
+ const host = await openHost(g.paths, "one-shot", VERSION, options);
3277
+ return {
3278
+ mode: "one-shot",
3279
+ call: (m, p) => callApi(host.ctx, m, p ?? {}),
3280
+ close: () => host.close()
3281
+ };
3282
+ }
3283
+ /**
3284
+ * How a one-shot opens the profile: `deferGroups` when the command is about one 1:1 chat (`chatOnly`) and needs
3285
+ * nothing of the groups, `offline` when it only reads (`readOnly`).
3286
+ */
3287
+ async function withSession(g, work, options = {}) {
3288
+ const s = await session(g, options);
3289
+ try {
3290
+ return await work(s);
3291
+ } finally {
3292
+ await s.close();
3293
+ }
3294
+ }
3295
+ async function readStdin() {
3296
+ const chunks = [];
3297
+ for await (const chunk of process.stdin) chunks.push(chunk);
3298
+ return Buffer.concat(chunks).toString("utf8");
3299
+ }
3300
+ var o = (type, description) => ({
3301
+ type,
3302
+ description
3303
+ });
3304
+ /** Commands with their own flow (main.ts), as help shows them. */
3305
+ var SPECIAL = [
3306
+ [
3307
+ "profile create <name> [--use] [--name <shown name>]",
3308
+ "Make a profile (its own keys, chats and wallets)",
3309
+ {
3310
+ use: o("boolean", "Make it the current profile"),
3311
+ name: o("string", "The name contacts see")
3312
+ }
3313
+ ],
3314
+ ["profile list", "Profiles, and which one is current"],
3315
+ ["profile use <name>", "Make a profile the current one"],
3316
+ ["profile show", "The name contacts see, and whether it is shared"],
3317
+ [
3318
+ "profile set [--name <name>] [--share-profile | --no-share-profile]",
3319
+ "Change the name contacts see",
3320
+ {
3321
+ name: o("string", "The name contacts see"),
3322
+ "share-profile": o("boolean", "Share the name and picture with contacts (--no-share-profile: do not)")
3323
+ }
3324
+ ],
3325
+ [
3326
+ "profile backup --out <file> [--passphrase-file f | --no-passphrase] [--light]",
3327
+ "A backup of the profile, encrypted with a passphrase (from a file or GHOSTLY_BACKUP_PASSPHRASE); --no-passphrase makes one anyone can read",
3328
+ {
3329
+ out: o("string", "The backup file to write"),
3330
+ "passphrase-file": o("string", "The passphrase, from this file (else GHOSTLY_BACKUP_PASSPHRASE)"),
3331
+ passphrase: {
3332
+ ...o("boolean", "Do not encrypt. The file then holds the profile's keys and wallet secrets in the clear"),
3333
+ negatedOnly: true
3334
+ },
3335
+ light: o("boolean", "Leave out files over 1 MB (voice messages over 4 MB); their messages stay")
3336
+ }
3337
+ ],
3338
+ [
3339
+ "profile restore <file> <new profile> [--passphrase-file f] [--use]",
3340
+ "A backup into a new profile (a backup made with --no-passphrase needs none)",
3341
+ {
3342
+ "passphrase-file": o("string", "The passphrase, from this file (else GHOSTLY_BACKUP_PASSPHRASE)"),
3343
+ use: o("boolean", "Make it the current profile")
3344
+ }
3345
+ ],
3346
+ [
3347
+ "daemon [--detach] [--timeout s]",
3348
+ "Keep the profile online (foreground; --detach runs it in the background)",
3349
+ {
3350
+ detach: o("boolean", "Run in the background (log in the profile folder)"),
3351
+ timeout: o("number", "Seconds --detach waits for it to start (default 60)")
3352
+ }
3353
+ ],
3354
+ ["daemon status", "Whether a daemon runs the profile, its version, and its socket (for the socket API)"],
3355
+ [
3356
+ "daemon stop [--timeout s]",
3357
+ "Stop the profile's daemon",
3358
+ { timeout: o("number", "Seconds to wait for it to stop (default 20)") }
3359
+ ],
3360
+ [
3361
+ "daemon restart [--timeout s]",
3362
+ "Stop the profile's daemon and start it again in the background (after an upgrade: the new code)",
3363
+ { timeout: o("number", "Seconds to wait for each step") }
3364
+ ],
3365
+ [
3366
+ "listen [--since seq] [--cursor file] [--type t]... [--turns] [--from <chat|key>]... [--group <group>]... [--exec cmd] [--webhook url] [--print]",
3367
+ "Stream events as JSON lines (starts the profile here if no daemon runs it)",
3368
+ {
3369
+ since: o("number", "Replay events after this seq first"),
3370
+ cursor: o("string", "A file that keeps the last seq handled (read at start, written after each event)"),
3371
+ type: o("list", "Only events of this type (message.received) or family (message, or group.typing.); repeat for more"),
3372
+ turns: o("boolean", "Only agent turns: each message received, and each group message that mentions you, as one agent.turn event"),
3373
+ from: o("list", "Only this chat's events (id, prefix, name or the contact's key; again for more)"),
3374
+ group: o("list", "Only this group's events (again for more); with --from or --group, nobody else's event gets through"),
3375
+ exec: o("string", "Run this command per event, the event as JSON on stdin"),
3376
+ webhook: o("string", "POST each event to this URL"),
3377
+ print: o("boolean", "Print events too when --exec or --webhook handles them")
3378
+ }
3379
+ ],
3380
+ [
3381
+ "settings get [--show-secret]",
3382
+ "The profile's settings",
3383
+ { "show-secret": o("boolean", "Show credentials too") }
3384
+ ],
3385
+ ["settings set <key> <json-value>", "Change one: relays, irohRelays, hyperdhtRelay, readRelays, iceServers, publicProfiles, online, shareProfile, sendTyping, autoDownloads, serveFiles, nick"],
3386
+ [
3387
+ "engine <method> [json-params | -] [--confirm-real] [--show-secret]",
3388
+ "Any call of the app's engine, with its own parameters",
3389
+ {
3390
+ "confirm-real": o("boolean", "Confirm a Mainnet (real money) spend"),
3391
+ "show-secret": o("boolean", "Print a call's secrets")
3392
+ }
3393
+ ],
3394
+ ["engine --list", "The engine's calls"],
3395
+ ["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)"],
3396
+ [
3397
+ "identity add <provider> [subject] [--signer id] [--field name=value]... [--days n]",
3398
+ "Make an identity proof (a tool's or a published one finishes with identity complete)",
3399
+ {
3400
+ signer: o("string", "Which signer makes the proof (identity providers lists them)"),
3401
+ field: o("list", "A field of the provider's form, name=value"),
3402
+ days: o("number", "How long the proof holds"),
3403
+ timeout: o("number", "Seconds to wait for an approval")
3404
+ }
3405
+ ],
3406
+ ["version", "This CLI's version (also ghostly --version)"]
3407
+ ];
3408
+ function help() {
3409
+ const rows = [
3410
+ ...SPECIAL.slice(0, 7).map(([usage, summary]) => [usage, summary]),
3411
+ ...Object.values(COMMANDS).filter((c) => c.method !== "events.replay").map((c) => [c.usage, c.summary]),
3412
+ ...Object.values(TEXT_COMMANDS).map((c) => [c.usage, c.summary]),
3413
+ ...SPECIAL.slice(7).map(([usage, summary]) => [usage, summary]),
3414
+ [COMMANDS.events.usage, COMMANDS.events.summary]
3415
+ ];
3416
+ const width = 58;
3417
+ return [
3418
+ `ghostly ${VERSION}: Ghostly for bots and terminals, the app's own engine without a screen.`,
3419
+ "",
3420
+ "Usage: ghostly <command> [options] (JSON on stdout; errors as {\"error\":{code,message}} with an exit code)",
3421
+ "",
3422
+ ...rows.map(([usage, summary]) => ` ${usage.length > width ? usage + "\n " + " ".repeat(width) : usage.padEnd(width)} ${summary}`),
3423
+ "",
3424
+ "Options for every command:",
3425
+ ...Object.entries(GLOBAL_OPTIONS).map(([name, o]) => ` --${name}${o.short ? `, -${o.short}` : ""}`.padEnd(24) + o.description),
3426
+ "",
3427
+ "A <chat> is its id, a unique prefix of it, or its name; a <group> the same. Exit codes: 0 done, 1 failed,",
3428
+ "2 usage, 3 not found, 4 timed out, 5 needs --force/--yes/--confirm-real. See packages/cli/README.md."
3429
+ ].join("\n");
3430
+ }
3431
+ /** What `ghostly help help` answers. */
3432
+ var HELP_USAGE = [
3433
+ "Usage: ghostly help [<command>...]",
3434
+ "",
3435
+ " Help for every command, or for one command or a group of them (also ghostly <command> --help)",
3436
+ "",
3437
+ "Options for every command: --profile, -p <name> --home <dir> --pretty --help, -h"
3438
+ ].join("\n");
3439
+ /** Every command's help row: usage, summary and, for the table and text commands, their options. */
3440
+ function helpRows() {
3441
+ return [
3442
+ ...SPECIAL.map(([usage, summary, options]) => ({
3443
+ usage,
3444
+ summary,
3445
+ options
3446
+ })),
3447
+ ...Object.values(COMMANDS),
3448
+ ...Object.values(TEXT_COMMANDS)
3449
+ ];
3450
+ }
3451
+ /** The words that name a command in its usage line: up to the first argument or option. */
3452
+ function commandWords(usage) {
3453
+ const words = [];
3454
+ for (const word of usage.split(" ")) {
3455
+ if (!/^[a-z][a-z-]*$/.test(word)) break;
3456
+ words.push(word);
3457
+ }
3458
+ return words;
3459
+ }
3460
+ /**
3461
+ * Help for one command (`ghostly help file send`, `ghostly file send --help`): its usage, what it does and its
3462
+ * options; for a group (`ghostly help file`), the group's commands.
3463
+ */
3464
+ function commandHelp(words) {
3465
+ if (words[0] === "help") return HELP_USAGE;
3466
+ const named = (list) => helpRows().filter((row) => {
3467
+ const own = commandWords(row.usage);
3468
+ return list.every((word, i) => own[i] === word);
3469
+ });
3470
+ let rows = named(words);
3471
+ while (!rows.length && words.length > 1) rows = named(words = words.slice(0, -1));
3472
+ const asked = words.join(" ");
3473
+ if (!rows.length) throw new CliError("usage", `Unknown command: ${asked} (ghostly help)`);
3474
+ const exact = rows.filter((row) => commandWords(row.usage).join(" ") === asked);
3475
+ const lines = [];
3476
+ for (const row of exact) {
3477
+ lines.push(`Usage: ghostly ${row.usage}`, "", ` ${row.summary}`);
3478
+ const options = Object.entries(row.options ?? {}).filter(([, o]) => o.description);
3479
+ if (options.length) {
3480
+ lines.push("", "Options:");
3481
+ const labels = options.map(([name, o]) => ` --${o.negatedOnly ? "no-" : ""}${name}${o.type === "boolean" ? "" : ` <${o.type === "number" ? "n" : "value"}>`}`);
3482
+ const width = Math.max(26, ...labels.map((label) => label.length + 2));
3483
+ options.forEach(([, o], i) => lines.push(labels[i].padEnd(width) + o.description));
3484
+ }
3485
+ if (Object.values(TEXT_COMMANDS).includes(row)) lines.push("", " -- ends the options: everything after it is the text, options included (\"-- -1 --wait sent\" sends", " all of it). Put options before --.");
3486
+ lines.push("");
3487
+ }
3488
+ const rest = rows.filter((row) => !exact.includes(row));
3489
+ if (rest.length) {
3490
+ const width = Math.min(58, Math.max(...rest.map((row) => row.usage.length)));
3491
+ const count = `${rest.length}${exact.length ? " more" : ""} command${rest.length === 1 ? "" : "s"}`;
3492
+ lines.push(`ghostly ${asked}: ${count} (ghostly help ${asked} <command> for one)`, "");
3493
+ for (const row of rest) lines.push(` ${row.usage.length > width ? row.usage + "\n " + " ".repeat(width) : row.usage.padEnd(width)} ${row.summary}`);
3494
+ lines.push("");
3495
+ }
3496
+ lines.push("Options for every command: --profile, -p <name> --home <dir> --pretty --help, -h");
3497
+ return lines.join("\n");
3498
+ }
3499
+ async function profileCommand(sub, argv) {
3500
+ const parsed = parseArgs(argv, {
3501
+ create: {
3502
+ use: {
3503
+ type: "boolean",
3504
+ description: ""
3505
+ },
3506
+ name: {
3507
+ type: "string",
3508
+ description: ""
3509
+ }
3510
+ },
3511
+ backup: {
3512
+ out: {
3513
+ type: "string",
3514
+ description: ""
3515
+ },
3516
+ "passphrase-file": {
3517
+ type: "string",
3518
+ description: ""
3519
+ },
3520
+ passphrase: {
3521
+ type: "boolean",
3522
+ description: ""
3523
+ },
3524
+ light: {
3525
+ type: "boolean",
3526
+ description: ""
3527
+ }
3528
+ },
3529
+ restore: {
3530
+ "passphrase-file": {
3531
+ type: "string",
3532
+ description: ""
3533
+ },
3534
+ use: {
3535
+ type: "boolean",
3536
+ description: ""
3537
+ }
3538
+ },
3539
+ set: {
3540
+ name: {
3541
+ type: "string",
3542
+ description: ""
3543
+ },
3544
+ "share-profile": {
3545
+ type: "boolean",
3546
+ description: ""
3547
+ }
3548
+ }
3549
+ }[sub ?? ""] ?? {});
3550
+ const g = globals(parsed);
3551
+ pretty = g.pretty;
3552
+ switch (sub) {
3553
+ case "create": {
3554
+ const [name] = parsed.positionals;
3555
+ if (!name || parsed.positionals.length > 1) throw new CliError("usage", "ghostly profile create <name>");
3556
+ const paths = createProfile(g.home, name);
3557
+ if (parsed.options.use) selectProfile(g.home, name);
3558
+ let shown = null;
3559
+ if (typeof parsed.options.name === "string") shown = await withSession({
3560
+ ...g,
3561
+ profile: name,
3562
+ paths
3563
+ }, (s) => s.call("profile.set", { name: parsed.options.name }));
3564
+ print({
3565
+ created: name,
3566
+ folder: paths.dir,
3567
+ current: currentProfile(g.home) === name,
3568
+ ...shown ? { profile: shown } : {}
3569
+ });
3570
+ return;
3571
+ }
3572
+ case "list": {
3573
+ noMoreThan(0, parsed.positionals, "profile list");
3574
+ const current = currentProfile(g.home);
3575
+ print({
3576
+ profiles: listProfiles(g.home).map((name) => ({
3577
+ name,
3578
+ current: name === current,
3579
+ running: lockOwner(profilePaths(g.home, name)) !== null
3580
+ })),
3581
+ home: g.home
3582
+ });
3583
+ return;
3584
+ }
3585
+ case "use": {
3586
+ const [name] = parsed.positionals;
3587
+ if (!name) throw new CliError("usage", "ghostly profile use <name>");
3588
+ noMoreThan(1, parsed.positionals, "profile use <name>");
3589
+ selectProfile(g.home, name);
3590
+ print({ current: name });
3591
+ return;
3592
+ }
3593
+ case "show":
3594
+ noMoreThan(0, parsed.positionals, "profile show");
3595
+ print(await withSession(g, (s) => s.call("profile.get"), { offline: true }));
3596
+ return;
3597
+ case "set": {
3598
+ noMoreThan(0, parsed.positionals, "profile set [--name <name>] [--share-profile | --no-share-profile]");
3599
+ const params = {};
3600
+ if (parsed.options.name !== void 0) params.name = parsed.options.name;
3601
+ if (parsed.options["share-profile"] !== void 0) params.shareProfile = parsed.options["share-profile"];
3602
+ if (!Object.keys(params).length) throw new CliError("usage", "ghostly profile set [--name <name>] [--share-profile | --no-share-profile]");
3603
+ print(await withSession(g, (s) => s.call("profile.set", params)));
3604
+ return;
3605
+ }
3606
+ case "backup": {
3607
+ noMoreThan(0, parsed.positionals, "profile backup --out <file> [--passphrase-file f | --no-passphrase] [--light]");
3608
+ const out = parsed.options.out;
3609
+ if (typeof out !== "string") throw new CliError("usage", "ghostly profile backup --out <file> [--passphrase-file f | --no-passphrase] [--light] (or GHOSTLY_BACKUP_PASSPHRASE)");
3610
+ const unsealed = parsed.options.passphrase === false;
3611
+ if (parsed.options.passphrase === true) throw new CliError("usage", "The passphrase comes from --passphrase-file or GHOSTLY_BACKUP_PASSPHRASE, never the command line");
3612
+ if (unsealed && (parsed.options["passphrase-file"] !== void 0 || process.env.GHOSTLY_BACKUP_PASSPHRASE)) throw new CliError("usage", "A passphrase is set (--passphrase-file or GHOSTLY_BACKUP_PASSPHRASE) and --no-passphrase was given: choose one");
3613
+ const light = parsed.options.light === true ? { light: true } : {};
3614
+ const params = unsealed ? {
3615
+ path: resolve(out),
3616
+ noPassphrase: true,
3617
+ ...light
3618
+ } : {
3619
+ path: resolve(out),
3620
+ passphrase: backupPassphrase(parsed.options["passphrase-file"]),
3621
+ ...light
3622
+ };
3623
+ const made = await withSession(g, (s) => s.call("profile.backup", params), { offline: true });
3624
+ if (unsealed) process.stderr.write("Not encrypted: this file holds the profile's keys, chats and wallet secrets in the clear. Anyone who gets it gets everything in it, including any money in its wallets.\n");
3625
+ print(made);
3626
+ return;
3627
+ }
3628
+ case "restore": {
3629
+ const [file, name] = parsed.positionals;
3630
+ if (!file || !name) throw new CliError("usage", "ghostly profile restore <file> <new profile> [--passphrase-file f] [--use]");
3631
+ noMoreThan(2, parsed.positionals, "profile restore <file> <new profile> [--passphrase-file f] [--use]");
3632
+ const { backupFileProtection, restoreProfile } = await import("./backup-Bugnn3Ue.js");
3633
+ const protection = await backupFileProtection(file);
3634
+ const paths = await restoreProfile(g.home, checkProfileName(name), file, protection === "none" ? void 0 : backupPassphrase(parsed.options["passphrase-file"], RESTORE_PASSPHRASE));
3635
+ if (parsed.options.use) selectProfile(g.home, name);
3636
+ print({
3637
+ restored: name,
3638
+ folder: paths.dir,
3639
+ protection
3640
+ });
3641
+ return;
3642
+ }
3643
+ default: throw new CliError("usage", "ghostly profile create|list|use|show|set|picture|backup|restore");
3644
+ }
3645
+ }
3646
+ /**
3647
+ * A clean stop (peers told goodbye, the store folded), once; a second signal, or a stop that takes longer than
3648
+ * 20 s, leaves at once. The store's journal makes that safe.
3649
+ */
3650
+ function stopper(work) {
3651
+ let started = false;
3652
+ return () => {
3653
+ if (started) process.exit(1);
3654
+ started = true;
3655
+ setTimeout(() => process.exit(1), 2e4).unref();
3656
+ work().then(() => process.exit(0), () => process.exit(1));
3657
+ };
3658
+ }
3659
+ /**
3660
+ * Ctrl-C, a service manager's stop, and the terminal or SSH session it ran in closing (SIGHUP, which would otherwise
3661
+ * end it on the spot): each a clean stop.
3662
+ */
3663
+ function onStopSignals(stop) {
3664
+ for (const signal of [
3665
+ "SIGINT",
3666
+ "SIGTERM",
3667
+ "SIGHUP"
3668
+ ]) process.on(signal, stop);
3669
+ }
3670
+ var BACKUP_PASSPHRASE = "The backup passphrase comes from --passphrase-file or GHOSTLY_BACKUP_PASSPHRASE (at least 12 characters). For a backup that is not encrypted, say so with --no-passphrase";
3671
+ /** Restore takes no --no-passphrase: a backup made without one says so itself. */
3672
+ var RESTORE_PASSPHRASE = "This backup is encrypted: its passphrase comes from --passphrase-file or GHOSTLY_BACKUP_PASSPHRASE";
3673
+ /** The backup passphrase: from a file or the environment, never the command line (it would sit in shell history). */
3674
+ function backupPassphrase(file, missing = BACKUP_PASSPHRASE) {
3675
+ const value = typeof file === "string" ? readFileSync(file, "utf8").replace(/\r?\n$/, "") : process.env.GHOSTLY_BACKUP_PASSPHRASE;
3676
+ if (!value) throw new CliError("usage", missing);
3677
+ return value;
3678
+ }
3679
+ async function runDaemon(g) {
3680
+ requireProfile(g);
3681
+ if (await connectDaemon(g.paths.socket)) throw new CliError("busy", `A daemon already runs profile ${g.profile}`);
3682
+ const { keepServing, openHost, serve } = await engineHost();
3683
+ const host = await openHost(g.paths, "daemon", VERSION);
3684
+ let served;
3685
+ try {
3686
+ served = await serve(host);
3687
+ } catch (error) {
3688
+ await host.close();
3689
+ throw error;
3690
+ }
3691
+ keepServing();
3692
+ const stop = stopper(async () => {
3693
+ await served.close();
3694
+ await host.close();
3695
+ });
3696
+ host.ctx.stop = stop;
3697
+ onStopSignals(stop);
3698
+ host.ctx.hub.emit("daemon.started", `daemon.started:${process.pid}:${Date.now()}`, {
3699
+ pid: process.pid,
3700
+ version: VERSION
3701
+ });
3702
+ print({
3703
+ daemon: "ready",
3704
+ profile: g.profile,
3705
+ pid: process.pid,
3706
+ socket: g.paths.socket,
3707
+ lastSeq: host.ctx.hub.lastSeq
3708
+ });
3709
+ }
3710
+ async function daemonCommand(argv) {
3711
+ const sub = argv[0] && !argv[0].startsWith("-") ? argv[0] : void 0;
3712
+ const parsed = parseArgs(sub ? argv.slice(1) : argv, {
3713
+ detach: {
3714
+ type: "boolean",
3715
+ description: ""
3716
+ },
3717
+ timeout: {
3718
+ type: "number",
3719
+ description: ""
3720
+ }
3721
+ });
3722
+ const g = globals(parsed);
3723
+ pretty = g.pretty;
3724
+ if (!sub || [
3725
+ "status",
3726
+ "stop",
3727
+ "restart"
3728
+ ].includes(sub)) noMoreThan(0, parsed.positionals, sub === "status" ? "daemon status" : sub ? `daemon ${sub} [--timeout s]` : "daemon [--detach] [--timeout s]");
3729
+ if (sub === "status") {
3730
+ knownProfile(g);
3731
+ const client = await connectDaemon(g.paths.socket);
3732
+ if (!client) {
3733
+ print({
3734
+ running: false,
3735
+ profile: g.profile,
3736
+ socket: g.paths.socket
3737
+ });
3738
+ return;
3739
+ }
3740
+ try {
3741
+ print({
3742
+ running: true,
3743
+ ...await client.call("status"),
3744
+ socket: g.paths.socket
3745
+ });
3746
+ } finally {
3747
+ client.close();
3748
+ }
3749
+ return;
3750
+ }
3751
+ if (sub === "stop") {
3752
+ knownProfile(g);
3753
+ const stopFor = seconds(parsed, 20);
3754
+ const client = await connectDaemon(g.paths.socket);
3755
+ if (!client) throw new CliError("not_found", `No daemon runs profile ${g.profile}`);
3756
+ print({
3757
+ stopped: true,
3758
+ pid: await stopDaemon(g, client, stopFor)
3759
+ });
3760
+ return;
3761
+ }
3762
+ if (sub === "restart") {
3763
+ const stopFor = seconds(parsed, 20), startFor = seconds(parsed, 60);
3764
+ const client = await connectDaemon(g.paths.socket);
3765
+ const before = client ? (await client.call("status").catch(() => null))?.version ?? null : null;
3766
+ print({
3767
+ restarted: true,
3768
+ stopped: client ? await stopDaemon(g, client, stopFor) : null,
3769
+ before,
3770
+ version: VERSION,
3771
+ ...await startDetached(g, startFor)
3772
+ });
3773
+ return;
3774
+ }
3775
+ if (sub) throw new CliError("usage", "ghostly daemon [--detach] [--timeout s] | daemon status | daemon stop | daemon restart");
3776
+ const startFor = seconds(parsed, 60);
3777
+ if (!parsed.options.detach) {
3778
+ await runDaemon(g);
3779
+ return;
3780
+ }
3781
+ print(await startDetached(g, startFor));
3782
+ }
3783
+ /** `--timeout <s>`, or `fallback` without one: seconds above 0 (0 or less would not wait at all). */
3784
+ function seconds(parsed, fallback) {
3785
+ const value = parsed.options.timeout;
3786
+ if (value === void 0) return fallback;
3787
+ if (typeof value !== "number" || !(value > 0)) throw new CliError("usage", "--timeout takes a number of seconds above 0");
3788
+ return value;
3789
+ }
3790
+ /**
3791
+ * Asks the daemon to stop and waits until its process is gone, not only until it let go of the profile (it releases
3792
+ * the lock a moment before it exits); its pid.
3793
+ */
3794
+ async function stopDaemon(g, client, seconds) {
3795
+ const { pid } = await client.call("daemon.stop");
3796
+ client.close();
3797
+ const until = Date.now() + seconds * 1e3;
3798
+ const running = () => lockOwner(g.paths) === pid || processAlive(pid);
3799
+ while (running() && Date.now() < until) await new Promise((resolve) => setTimeout(resolve, 100));
3800
+ if (running()) throw new CliError("timeout", `The daemon (process ${pid}) has not stopped yet`);
3801
+ return pid;
3802
+ }
3803
+ /** Throws busy, with its pid, when a process other than `except` holds the profile. */
3804
+ function heldBy(g, except) {
3805
+ const owner = lockOwner(g.paths);
3806
+ if (owner !== null && owner !== except) throw new CliError("busy", `Profile ${g.profile} is in use by process ${owner}`, { pid: owner });
3807
+ }
3808
+ function processAlive(pid) {
3809
+ try {
3810
+ process.kill(pid, 0);
3811
+ return true;
3812
+ } catch (error) {
3813
+ return error.code === "EPERM";
3814
+ }
3815
+ }
3816
+ /** A daemon in the background, running this command's code, once its socket answers. */
3817
+ async function startDetached(g, seconds) {
3818
+ requireProfile(g);
3819
+ if (await connectDaemon(g.paths.socket)) throw new CliError("busy", `A daemon already runs profile ${g.profile}`);
3820
+ heldBy(g);
3821
+ const log = openSync(g.paths.log, "a", 384);
3822
+ const child = spawn(process.execPath, [
3823
+ ...process.execArgv,
3824
+ process.argv[1],
3825
+ "daemon",
3826
+ "--home",
3827
+ g.home,
3828
+ "--profile",
3829
+ g.profile
3830
+ ], {
3831
+ detached: true,
3832
+ stdio: [
3833
+ "ignore",
3834
+ log,
3835
+ log
3836
+ ]
3837
+ });
3838
+ child.unref();
3839
+ const until = Date.now() + seconds * 1e3;
3840
+ let client = null;
3841
+ while (!client && Date.now() < until) {
3842
+ if (child.exitCode !== null) break;
3843
+ await new Promise((resolve) => setTimeout(resolve, 200));
3844
+ client = await connectDaemon(g.paths.socket, 500);
3845
+ }
3846
+ if (!client) {
3847
+ heldBy(g, child.pid);
3848
+ 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`, {
3849
+ pid: child.pid,
3850
+ log: g.paths.log
3851
+ });
3852
+ throw new CliError("engine", `The daemon did not start; see ${g.paths.log}`);
3853
+ }
3854
+ client.close();
3855
+ return {
3856
+ daemon: "started",
3857
+ profile: g.profile,
3858
+ pid: child.pid,
3859
+ socket: g.paths.socket,
3860
+ log: g.paths.log
3861
+ };
3862
+ }
3863
+ async function listenCommand(argv) {
3864
+ const parsed = parseArgs(argv, {
3865
+ since: {
3866
+ type: "number",
3867
+ description: ""
3868
+ },
3869
+ cursor: {
3870
+ type: "string",
3871
+ description: ""
3872
+ },
3873
+ type: {
3874
+ type: "list",
3875
+ description: ""
3876
+ },
3877
+ exec: {
3878
+ type: "string",
3879
+ description: ""
3880
+ },
3881
+ webhook: {
3882
+ type: "string",
3883
+ description: ""
3884
+ },
3885
+ print: {
3886
+ type: "boolean",
3887
+ description: ""
3888
+ },
3889
+ from: {
3890
+ type: "list",
3891
+ description: ""
3892
+ },
3893
+ group: {
3894
+ type: "list",
3895
+ description: ""
3896
+ },
3897
+ turns: {
3898
+ type: "boolean",
3899
+ description: ""
3900
+ }
3901
+ });
3902
+ noMoreThan(0, parsed.positionals, SPECIAL.find(([usage]) => usage.startsWith("listen "))[0]);
3903
+ const g = globals(parsed);
3904
+ pretty = false;
3905
+ if (parsed.options.webhook) checkWebhook(String(parsed.options.webhook));
3906
+ const types = parsed.options.type ?? [];
3907
+ const turns = parsed.options.turns === true;
3908
+ if (turns && types.length) throw new CliError("usage", "--turns takes the place of --type: a turn is a message received, or a group message that mentions you");
3909
+ checkTypes(types, (line) => process.stderr.write(line + "\n"));
3910
+ const from = parsed.options.from ?? [], groups = parsed.options.group ?? [];
3911
+ const cursor = parsed.options.cursor;
3912
+ checkCursor(cursor);
3913
+ const since = parsed.options.since ?? readCursor(cursor);
3914
+ const start = async (call, peerOf) => {
3915
+ return eventHandler({
3916
+ types,
3917
+ turns,
3918
+ exec: parsed.options.exec,
3919
+ webhook: parsed.options.webhook,
3920
+ cursor,
3921
+ print: parsed.options.print === true || !parsed.options.exec && !parsed.options.webhook,
3922
+ write: (line) => process.stdout.write(line + "\n"),
3923
+ drained: () => new Promise((resolve) => {
3924
+ const done = () => {
3925
+ process.stdout.off("drain", done);
3926
+ process.stdout.off("close", done);
3927
+ resolve();
3928
+ };
3929
+ process.stdout.on("drain", done);
3930
+ process.stdout.on("close", done);
3931
+ }),
3932
+ allow: from.length || groups.length ? await allowlist(from, groups, call, peerOf) : void 0
3933
+ });
3934
+ };
3935
+ requireProfile(g);
3936
+ const client = await connectDaemon(g.paths.socket);
3937
+ if (client) await warnVersion(client);
3938
+ if (client) {
3939
+ const handle = await start((m, p) => client.call(m, p), async (chat) => {
3940
+ const c = await connectDaemon(g.paths.socket, 1e3);
3941
+ if (!c) return null;
3942
+ try {
3943
+ return (await c.call("chat.get", { chat })).peer || null;
3944
+ } finally {
3945
+ c.close();
3946
+ }
3947
+ }).catch((error) => {
3948
+ client.close();
3949
+ throw error;
3950
+ });
3951
+ let current = client;
3952
+ const reconnect = (wait) => void (async () => {
3953
+ while (!current && !feed.paused) {
3954
+ await new Promise((resolve) => setTimeout(resolve, wait));
3955
+ wait = 1e3;
3956
+ if (current || feed.paused) return;
3957
+ const next = await connectDaemon(g.paths.socket, 1e3);
3958
+ if (next) {
3959
+ current = next;
3960
+ await follow(next).catch(() => {
3961
+ if (current === next) current = null;
3962
+ });
3963
+ }
3964
+ }
3965
+ })();
3966
+ const feed = new Feed(handle, since, () => {
3967
+ const c = current;
3968
+ current = null;
3969
+ c?.destroy();
3970
+ }, () => reconnect(0));
3971
+ const follow = async (c) => {
3972
+ await c.subscribe(feed.since, feed.take, () => {
3973
+ if (current !== c) return;
3974
+ current = null;
3975
+ reconnect(1e3);
3976
+ });
3977
+ };
3978
+ await follow(client);
3979
+ process.on("SIGINT", () => {
3980
+ current?.close();
3981
+ process.exit(0);
3982
+ });
3983
+ process.on("SIGTERM", () => {
3984
+ current?.close();
3985
+ process.exit(0);
3986
+ });
3987
+ return;
3988
+ }
3989
+ const [{ keepServing, openHost, serve }, { callApi }] = await Promise.all([engineHost(), engineApi()]);
3990
+ const host = await openHost(g.paths, "daemon", VERSION);
3991
+ const call = (m, p) => callApi(host.ctx, m, p);
3992
+ const handle = await start(call, async (chat) => (await call("chat.get", { chat })).peer || null).catch(async (error) => {
3993
+ await host.close();
3994
+ throw error;
3995
+ });
3996
+ const served = await serve(host).catch(async (error) => {
3997
+ await host.close();
3998
+ throw error;
3999
+ });
4000
+ keepServing();
4001
+ const stop = stopper(async () => {
4002
+ await served.close();
4003
+ await host.close();
4004
+ });
4005
+ host.ctx.stop = stop;
4006
+ onStopSignals(stop);
4007
+ let off = null, left = [];
4008
+ const open = (from) => {
4009
+ for (;;) {
4010
+ const events = left.length ? left : host.ctx.hub.replay(from ?? host.ctx.hub.lastSeq);
4011
+ left = [];
4012
+ if (!events.length) break;
4013
+ for (let i = 0; i < events.length; i++) {
4014
+ feed.take(events[i]);
4015
+ if (feed.paused) {
4016
+ left = events.slice(i + 1);
4017
+ return;
4018
+ }
4019
+ }
4020
+ from = feed.since;
4021
+ }
4022
+ off = host.ctx.hub.onEvent(feed.take);
4023
+ };
4024
+ const feed = new Feed(handle, since, () => {
4025
+ off?.();
4026
+ off = null;
4027
+ }, open);
4028
+ open(since);
4029
+ host.ctx.hub.emit("daemon.started", `daemon.started:${process.pid}:${Date.now()}`, {
4030
+ pid: process.pid,
4031
+ version: VERSION,
4032
+ listen: true
4033
+ });
4034
+ }
4035
+ async function engineCommand(argv) {
4036
+ const parsed = parseArgs(argv, {
4037
+ "confirm-real": {
4038
+ type: "boolean",
4039
+ description: ""
4040
+ },
4041
+ "show-secret": {
4042
+ type: "boolean",
4043
+ description: ""
4044
+ },
4045
+ list: {
4046
+ type: "boolean",
4047
+ description: ""
4048
+ }
4049
+ });
4050
+ const g = globals(parsed);
4051
+ pretty = g.pretty;
4052
+ if (parsed.options.list) {
4053
+ noMoreThan(0, parsed.positionals, "engine --list");
4054
+ print({ methods: [...ENGINE_METHODS, ...ENGINE_READS].sort() });
4055
+ return;
4056
+ }
4057
+ const [method, raw] = parsed.positionals;
4058
+ if (!method || parsed.positionals.length > 2) throw new CliError("usage", "ghostly engine <method> [json-params | -]");
4059
+ if (!ENGINE_METHODS.includes(method) && !ENGINE_READS.includes(method)) throw new CliError("not_found", `The engine has no call ${method} (ghostly engine --list)`);
4060
+ const showSecret = parsed.options["show-secret"] === true;
4061
+ if (SECRET_RESULTS.has(method) && !showSecret) throw new CliError("confirm", `${method} answers with secrets: add --show-secret to print them`);
4062
+ let params;
4063
+ const text = raw === "-" ? await readStdin() : raw;
4064
+ if (text !== void 0 && text.trim()) try {
4065
+ params = JSON.parse(text);
4066
+ } catch {
4067
+ throw new CliError("usage", "The parameters must be JSON (an object)");
4068
+ }
4069
+ const confirmReal = parsed.options["confirm-real"] === true;
4070
+ if (confirmReal && params && typeof params === "object") params.confirmedReal = true;
4071
+ let result = await withSession(g, (s) => s.call("engine.call", {
4072
+ method,
4073
+ params,
4074
+ confirmReal
4075
+ }), { offline: ENGINE_READS.includes(method) });
4076
+ if (method === "getState" && !showSecret && result && typeof result === "object") {
4077
+ const { hideStateSecrets } = await engineApi();
4078
+ result = hideStateSecrets(result);
4079
+ }
4080
+ print(result);
4081
+ }
4082
+ async function settingsCommand(sub, argv) {
4083
+ const parsed = parseArgs(argv, { "show-secret": {
4084
+ type: "boolean",
4085
+ description: ""
4086
+ } });
4087
+ const g = globals(parsed);
4088
+ pretty = g.pretty;
4089
+ if (sub === "get") {
4090
+ noMoreThan(0, parsed.positionals, "settings get [--show-secret]");
4091
+ print(await withSession(g, (s) => s.call("settings.get", { showSecret: parsed.options["show-secret"] === true }), { offline: true }));
4092
+ return;
4093
+ }
4094
+ if (sub === "set") {
4095
+ const [key, value] = parsed.positionals;
4096
+ if (!key || value === void 0 || parsed.positionals.length > 2) throw new CliError("usage", "ghostly settings set <key> <json-value>");
4097
+ let parsedValue;
4098
+ try {
4099
+ parsedValue = JSON.parse(value);
4100
+ } catch {
4101
+ parsedValue = value;
4102
+ }
4103
+ if ((await engineApi()).SETTABLE[key] === "string" && typeof parsedValue !== "string") parsedValue = value;
4104
+ printAnswer(await withSession(g, (s) => s.call("settings.set", { [key]: parsedValue })));
4105
+ return;
4106
+ }
4107
+ throw new CliError("usage", "ghostly settings get | settings set <key> <json-value>");
4108
+ }
4109
+ async function textCommand(name, argv) {
4110
+ const spec = TEXT_COMMANDS[name];
4111
+ const parsed = parseArgs(argv, spec.options, idSlot(spec));
4112
+ const g = globals(parsed);
4113
+ pretty = g.pretty;
4114
+ const [target, ...rest] = parsed.positionals;
4115
+ const [message, ...words] = spec.message ? rest : [void 0, ...rest];
4116
+ if (!target || spec.message && !message) throw new CliError("usage", `ghostly ${spec.usage}`);
4117
+ let text = words.join(" ");
4118
+ if (parsed.options.text !== void 0) {
4119
+ if (text || parsed.options.stdin) throw new CliError("usage", "Give the text once: as arguments, --text or stdin");
4120
+ text = String(parsed.options.text);
4121
+ } else if (parsed.options.stdin || !text && !process.stdin.isTTY) {
4122
+ if (text) throw new CliError("usage", "Give the text as arguments or on stdin, not both");
4123
+ text = (await readStdin()).replace(/\n$/, "");
4124
+ }
4125
+ if (!text.trim()) throw new CliError("usage", `No text: ghostly ${spec.usage}`);
4126
+ const params = {
4127
+ [spec.target]: target,
4128
+ text,
4129
+ force: parsed.options.force === true,
4130
+ ...message !== void 0 && { message }
4131
+ };
4132
+ if (spec.target === "group") params.mentions = parsed.options.mention ?? [];
4133
+ if (parsed.options.reply !== void 0) params.reply = parsed.options.reply;
4134
+ if (!spec.message) Object.assign(params, (await import("./buttons-Bj7Vf7Pn.js")).buttonsOf(parsed.options));
4135
+ print(await withSession(g, (s) => {
4136
+ if (spec.target === "chat") {
4137
+ params.wait = parsed.options.wait ?? (s.mode === "one-shot" ? spec.message ? "confirmed" : "sent" : "none");
4138
+ if (parsed.options.timeout !== void 0) params.timeout = parsed.options.timeout;
4139
+ } else {
4140
+ if (parsed.options.wait !== void 0) params.wait = parsed.options.wait;
4141
+ if (parsed.options.timeout !== void 0) params.timeout = parsed.options.timeout;
4142
+ }
4143
+ return s.call(spec.method, params);
4144
+ }, { deferGroups: spec.target === "chat" }));
4145
+ }
4146
+ /**
4147
+ * `identity add`: a proof. An in-app signer finishes in this one command (what it waits on is printed to stderr as it
4148
+ * comes); a tool or a published record answers with the statement, and `identity complete` finishes it later, in the
4149
+ * same daemon.
4150
+ */
4151
+ async function identityAddCommand(argv) {
4152
+ const parsed = parseArgs(argv, {
4153
+ signer: {
4154
+ type: "string",
4155
+ description: ""
4156
+ },
4157
+ field: {
4158
+ type: "list",
4159
+ description: ""
4160
+ },
4161
+ days: {
4162
+ type: "number",
4163
+ description: ""
4164
+ },
4165
+ timeout: {
4166
+ type: "number",
4167
+ description: ""
4168
+ }
4169
+ });
4170
+ const g = globals(parsed);
4171
+ pretty = g.pretty;
4172
+ const [provider, ...subject] = parsed.positionals;
4173
+ if (!provider) throw new CliError("usage", "ghostly identity add <provider> [subject] [--signer id] [--field name=value]... [--days n]");
4174
+ const fields = {};
4175
+ for (const item of parsed.options.field ?? []) {
4176
+ const eq = item.indexOf("=");
4177
+ if (eq < 1) throw new CliError("usage", `--field takes name=value, not ${JSON.stringify(item)}`);
4178
+ fields[item.slice(0, eq)] = item.slice(eq + 1);
4179
+ }
4180
+ const params = {
4181
+ provider,
4182
+ subject: subject.join(" ") || void 0,
4183
+ signer: parsed.options.signer,
4184
+ fields,
4185
+ days: parsed.options.days,
4186
+ timeout: parsed.options.timeout
4187
+ };
4188
+ const tell = (event) => {
4189
+ if (event.type === "identity.approval" || event.type === "identity.progress") process.stderr.write(JSON.stringify(event) + "\n");
4190
+ };
4191
+ requireProfile(g);
4192
+ const client = await connectDaemon(g.paths.socket);
4193
+ if (client) {
4194
+ await warnVersion(client);
4195
+ const watcher = await connectDaemon(g.paths.socket);
4196
+ await watcher?.subscribe(void 0, tell);
4197
+ try {
4198
+ print(await client.call("identity.add", params));
4199
+ } finally {
4200
+ client.close();
4201
+ watcher?.close();
4202
+ }
4203
+ return;
4204
+ }
4205
+ const [{ openHost }, { callApi }] = await Promise.all([engineHost(), engineApi()]);
4206
+ const host = await openHost(g.paths, "one-shot", VERSION);
4207
+ const off = host.ctx.hub.onEvent(tell);
4208
+ try {
4209
+ const result = await callApi(host.ctx, "identity.add", params);
4210
+ if (!result.done) {
4211
+ await callApi(host.ctx, "identity.cancel", { draft: result.draft }).catch(() => {});
4212
+ throw new CliError("unavailable", "A proof made in two steps waits in the daemon between them: start `ghostly daemon --detach`, then run this again");
4213
+ }
4214
+ print(result);
4215
+ } finally {
4216
+ off();
4217
+ await host.close();
4218
+ }
4219
+ }
4220
+ async function tableCommand(name, argv) {
4221
+ const command = COMMANDS[name];
4222
+ const parsed = parseArgs(argv, command.options ?? {}, idSlot(command));
4223
+ const g = globals(parsed);
4224
+ pretty = g.pretty;
4225
+ const args = positionals(command, parsed.positionals);
4226
+ if (command.run) {
4227
+ printAnswer(await command.run(parsed, args));
4228
+ return;
4229
+ }
4230
+ const params = command.params?.(parsed, args) ?? {};
4231
+ for (const key of Object.keys(params)) if (params[key] === void 0) delete params[key];
4232
+ if (command.method === "identity.complete") {
4233
+ if (params.evidenceFile) params.evidence = readFileSync(params.evidenceFile, "utf8");
4234
+ else if (params.stdin) params.evidence = await readStdin();
4235
+ delete params.evidenceFile;
4236
+ delete params.stdin;
4237
+ }
4238
+ const secretWarning = await secretsFromStdin(command.method, params, readStdin, process.stdin.isTTY === true);
4239
+ if (secretWarning) process.stderr.write(`ghostly: ${secretWarning}\n`);
4240
+ printAnswer(await withSession(g, async (s) => {
4241
+ const result = await s.call(command.method, params);
4242
+ if (command.method === "invite.join" && s.mode === "one-shot") await s.call("chat.wait", {
4243
+ chat: result.chat,
4244
+ until: "paired",
4245
+ timeout: 60
4246
+ }).catch(() => {});
4247
+ return result;
4248
+ }, {
4249
+ deferGroups: chatOnly(command.method, params),
4250
+ offline: readOnly(command.method, params)
4251
+ }));
4252
+ }
4253
+ /**
4254
+ * `call pipe`: a call's audio socket bridged to stdin and stdout, for shell pipelines. stdout carries audio only (the
4255
+ * contact's, raw PCM); what the command says goes to stderr. It ends when the call does.
4256
+ */
4257
+ async function callPipeCommand(argv) {
4258
+ const parsed = parseArgs(argv, {}, idSlot({ args: ["call...?"] }));
4259
+ const g = globals(parsed);
4260
+ const ref = parsed.positionals.join(" ") || void 0;
4261
+ const client = await connectDaemon(g.paths.socket);
4262
+ if (!client) throw new CliError("unavailable", `No daemon runs profile ${g.profile}: calls need one (ghostly daemon --detach)`);
4263
+ let call;
4264
+ try {
4265
+ call = await client.call("call.get", ref ? { call: ref } : {});
4266
+ } finally {
4267
+ client.close();
4268
+ }
4269
+ const audio = call.audio;
4270
+ if (!audio) throw new CliError("unavailable", "That call has no audio yet: answer it first");
4271
+ const socket = connect(audio.socket);
4272
+ await new Promise((resolve, reject) => {
4273
+ socket.once("connect", resolve);
4274
+ socket.once("error", reject);
4275
+ });
4276
+ process.stderr.write(`ghostly: call ${call.call}: s16le mono ${audio.rate} Hz on stdin/stdout\n`);
4277
+ process.stdin.pipe(socket, { end: false });
4278
+ socket.pipe(process.stdout);
4279
+ socket.on("close", () => exit(0));
4280
+ socket.on("error", () => exit(1));
4281
+ }
4282
+ /**
4283
+ * The command's words when the line asks for help (`help <words>`, or `--help`/`--help=true`/`-h` before any `--`), else
4284
+ * null. `--help=false` asks for none: the command runs.
4285
+ */
4286
+ function helpAsked(argv) {
4287
+ const end = argv.indexOf("--");
4288
+ const head = end === -1 ? argv : argv.slice(0, end);
4289
+ const words = (list) => {
4290
+ const out = [];
4291
+ for (const word of list) {
4292
+ if (word.startsWith("-")) break;
4293
+ out.push(word);
4294
+ }
4295
+ return out;
4296
+ };
4297
+ if (head[0] === "help") return words(head.slice(1));
4298
+ if (head.some((word) => word === "--help" || word === "--help=true" || word === "-h")) return words(head);
4299
+ return null;
4300
+ }
4301
+ async function main(input) {
4302
+ const argv = liftGlobals(input);
4303
+ const helpFor = argv.length === 0 || globalOption(argv[0]) ? [] : helpAsked(argv);
4304
+ if (helpFor) {
4305
+ process.stdout.write((helpFor.length ? commandHelp(helpFor) : help()) + "\n");
4306
+ return 0;
4307
+ }
4308
+ if (argv[0] === "--version" || argv[0] === "version") {
4309
+ const parsed = parseArgs(argv.slice(1), {});
4310
+ noMoreThan(0, parsed.positionals, "version");
4311
+ pretty = parsed.options.pretty === true;
4312
+ print({ version: VERSION });
4313
+ return 0;
4314
+ }
4315
+ const [first, second] = argv;
4316
+ const two = second && !second.startsWith("-") ? `${first} ${second}` : "";
4317
+ const text = (name) => Object.hasOwn(TEXT_COMMANDS, name), table = (name) => Object.hasOwn(COMMANDS, name);
4318
+ if (first === "profile" && !table(two)) {
4319
+ await profileCommand(second, argv.slice(2));
4320
+ return 0;
4321
+ }
4322
+ if (first === "daemon") {
4323
+ await daemonCommand(argv.slice(1));
4324
+ return -1;
4325
+ }
4326
+ if (first === "listen") {
4327
+ await listenCommand(argv.slice(1));
4328
+ return -1;
4329
+ }
4330
+ if (first === "engine") {
4331
+ await engineCommand(argv.slice(1));
4332
+ return 0;
4333
+ }
4334
+ if (first === "settings") {
4335
+ await settingsCommand(second, argv.slice(2));
4336
+ return 0;
4337
+ }
4338
+ if (two === "identity add") {
4339
+ await identityAddCommand(argv.slice(2));
4340
+ return 0;
4341
+ }
4342
+ if (two === "call pipe") {
4343
+ await callPipeCommand(argv.slice(2));
4344
+ return -1;
4345
+ }
4346
+ if (two && text(two)) {
4347
+ await textCommand(two, argv.slice(2));
4348
+ return 0;
4349
+ }
4350
+ if (two && table(two)) {
4351
+ await tableCommand(two, argv.slice(2));
4352
+ return 0;
4353
+ }
4354
+ if (text(first)) {
4355
+ await textCommand(first, argv.slice(1));
4356
+ return 0;
4357
+ }
4358
+ if (table(first)) {
4359
+ await tableCommand(first, argv.slice(1));
4360
+ return 0;
4361
+ }
4362
+ throw unknownCommand(first, two ? second : void 0);
4363
+ }
4364
+ /**
4365
+ * A command line that names no command. The first word of a group (`ghostly chat`, `ghostly wallet frob`) is answered
4366
+ * with the group's commands, as `ghostly help chat` lists them; before, it was only "Unknown command: chat".
4367
+ */
4368
+ function unknownCommand(first, second) {
4369
+ const asked = [first, second].filter(Boolean).join(" ");
4370
+ const subs = [...new Set(helpRows().map((row) => commandWords(row.usage)).filter((words) => words[0] === first && words.length > 1).map((words) => words[1]))];
4371
+ if (!subs.length) return new CliError("usage", `Unknown command: ${asked} (ghostly help)`);
4372
+ const list = `${subs.slice(0, -1).join(", ")}${subs.length > 1 ? " or " : ""}${subs.at(-1)}`;
4373
+ return new CliError("usage", second ? `Unknown command: ${asked}: ghostly ${first} takes ${list} (ghostly help ${first})` : `ghostly ${first} takes a command: ${list} (ghostly help ${first})`);
4374
+ }
4375
+ /** Leaves once what was printed reached stdout (a pipe drains asynchronously). */
4376
+ function exit(code) {
4377
+ process.stdout.write("", () => process.exit(code));
4378
+ }
4379
+ /**
4380
+ * Stdout is the command's answer and nothing else: one JSON object, or a stream of JSON lines. What the engine notes
4381
+ * as it works (a relay left alone for a minute, a wallet moved to its network's key) is written with `console.info`
4382
+ * and the like, which Node sends to stdout: a script reading the answer (`ghostly wallet list | jq`) then failed on a
4383
+ * line that is not JSON. Those notes go to stderr, where a daemon's log and a terminal still show them.
4384
+ */
4385
+ function notesToStderr() {
4386
+ for (const level of [
4387
+ "log",
4388
+ "info",
4389
+ "debug"
4390
+ ]) console[level] = (...args) => {
4391
+ process.stderr.write(`${format(...args)}\n`);
4392
+ };
4393
+ }
4394
+ /** Runs the command line; `-1` from `main` means the process stays (a daemon, a listener). */
4395
+ async function run(argv) {
4396
+ notesToStderr();
4397
+ try {
4398
+ const code = await main(argv);
4399
+ if (code >= 0) exit(code);
4400
+ } catch (error) {
4401
+ const failure = asCliError(error);
4402
+ print({ error: failure.toJSON() });
4403
+ exit(EXIT[failure.code]);
4404
+ }
4405
+ }
4406
+ //#endregion
4407
+ export { run };
4408
+
4409
+ //# sourceMappingURL=main-DbqcdShN.js.map