@ghostlytools/cli 1.3.0 → 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 (80) hide show
  1. package/README.md +9 -3
  2. package/dist/assets/{api-CrxVxtY1.js → api-Dq2QXQDk.js} +141 -40
  3. package/dist/assets/api-Dq2QXQDk.js.map +1 -0
  4. package/dist/assets/{api-BiaX6l6f.js → api-TiB5QSG9.js} +2 -2
  5. package/dist/assets/{apiKit-CajiDxdK.js → apiKit-CA2x5GJF.js} +7 -3
  6. package/dist/assets/apiKit-CA2x5GJF.js.map +1 -0
  7. package/dist/assets/{apps-CbfiCSIw.js → apps-BxV9i_Y4.js} +3 -4
  8. package/dist/assets/{apps-CbfiCSIw.js.map → apps-BxV9i_Y4.js.map} +1 -1
  9. package/dist/assets/{apps-t23gkc28.js → apps-C-T8jeji.js} +1 -1
  10. package/dist/assets/{arkade-D4BVJFEQ.js → arkade-C_qDmHnm.js} +2 -2
  11. package/dist/assets/{arkade-D4BVJFEQ.js.map → arkade-C_qDmHnm.js.map} +1 -1
  12. package/dist/assets/{backup-Cg52WeQi.js → backup-Bugnn3Ue.js} +1 -1
  13. package/dist/assets/{backup-BAMi2lAq.js → backup-CnMN2oUK.js} +8 -8
  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-CX1pKB0j.js → buttons-CvNBzh2S.js} +4 -5
  23. package/dist/assets/{buttons-CX1pKB0j.js.map → buttons-CvNBzh2S.js.map} +1 -1
  24. package/dist/assets/{endpoint-Dn7H8qE1.js → endpoint-BOTFMpfS.js} +44 -12
  25. package/dist/assets/endpoint-BOTFMpfS.js.map +1 -0
  26. package/dist/assets/{envelope-BvU0fLEt.js → envelope-WjingYSc.js} +2 -2
  27. package/dist/assets/{envelope-BvU0fLEt.js.map → envelope-WjingYSc.js.map} +1 -1
  28. package/dist/assets/{errors-my6VJR_0.js → errors-DO5z6h_l.js} +29 -1
  29. package/dist/assets/errors-DO5z6h_l.js.map +1 -0
  30. package/dist/assets/groupFiles-IEUW3DQz.js +3202 -0
  31. package/dist/assets/groupFiles-IEUW3DQz.js.map +1 -0
  32. package/dist/assets/{host-Cmd205Dc.js → host-CWt-jtqz.js} +41 -16
  33. package/dist/assets/host-CWt-jtqz.js.map +1 -0
  34. package/dist/assets/{hyperdhtRelay-CEmBpT0Q.js → hyperdhtRelay-CPYwSLLS.js} +2 -3
  35. package/dist/assets/{hyperdhtRelay-CEmBpT0Q.js.map → hyperdhtRelay-CPYwSLLS.js.map} +1 -1
  36. package/dist/assets/{irohWeb--YyEA-hX.js → irohWeb-4oChIOIp.js} +2 -3
  37. package/dist/assets/{irohWeb--YyEA-hX.js.map → irohWeb-4oChIOIp.js.map} +1 -1
  38. package/dist/assets/{main-BzNN6bkx.js → main-DbqcdShN.js} +142 -35
  39. package/dist/assets/main-DbqcdShN.js.map +1 -0
  40. package/dist/assets/{mainline-D5D5C-jT.js → mainline-ZkiOnevs.js} +2 -2
  41. package/dist/assets/{mainline-D5D5C-jT.js.map → mainline-ZkiOnevs.js.map} +1 -1
  42. package/dist/assets/{networkReason-DxZWHw6v.js → networkReason-CetKkTPo.js} +2 -2
  43. package/dist/assets/{networkReason-DxZWHw6v.js.map → networkReason-CetKkTPo.js.map} +1 -1
  44. package/dist/assets/nwc-CgkqyvsE.js +612 -0
  45. package/dist/assets/nwc-CgkqyvsE.js.map +1 -0
  46. package/dist/assets/nwcProvider-4eVz_8HP.js +77 -0
  47. package/dist/assets/nwcProvider-4eVz_8HP.js.map +1 -0
  48. package/dist/assets/{profiles-B6F4oa9K.js → profiles-BHc9pEYg.js} +2 -2
  49. package/dist/assets/{profiles-B6F4oa9K.js.map → profiles-BHc9pEYg.js.map} +1 -1
  50. package/dist/assets/registry-DlAKO0Fy.js +3793 -0
  51. package/dist/assets/registry-DlAKO0Fy.js.map +1 -0
  52. package/dist/assets/registry-OBJBnHVQ.js +2 -0
  53. package/dist/assets/resolve-CouHhCpJ.js +591 -0
  54. package/dist/assets/resolve-CouHhCpJ.js.map +1 -0
  55. package/dist/assets/{server-CBCEYKMN.js → server-Ss0gJ-nw.js} +184 -1181
  56. package/dist/assets/server-Ss0gJ-nw.js.map +1 -0
  57. package/dist/assets/{src-DhfGgAUu.js → src-DDzSERyc.js} +2249 -72
  58. package/dist/assets/src-DDzSERyc.js.map +1 -0
  59. package/dist/assets/{turnRefs-ChEfgbSX.js → turnRefs-B87dtrHM.js} +2 -2
  60. package/dist/assets/{turnRefs-ChEfgbSX.js.map → turnRefs-B87dtrHM.js.map} +1 -1
  61. package/dist/assets/types-C4_KafL_.js +122 -0
  62. package/dist/assets/types-C4_KafL_.js.map +1 -0
  63. package/dist/assets/{usdt-C1Xao2xH.js → usdt-Fj8CQO2z.js} +3 -3
  64. package/dist/assets/{usdt-C1Xao2xH.js.map → usdt-Fj8CQO2z.js.map} +1 -1
  65. package/dist/ghostly.mjs +1 -1
  66. package/package.json +1 -1
  67. package/dist/assets/api-CrxVxtY1.js.map +0 -1
  68. package/dist/assets/apiKit-CajiDxdK.js.map +0 -1
  69. package/dist/assets/backup-BAMi2lAq.js.map +0 -1
  70. package/dist/assets/buttons-CWl2yNJu.js +0 -2
  71. package/dist/assets/endpoint-Dn7H8qE1.js.map +0 -1
  72. package/dist/assets/errors-my6VJR_0.js.map +0 -1
  73. package/dist/assets/groupFiles-BNTSDaoq.js +0 -7685
  74. package/dist/assets/groupFiles-BNTSDaoq.js.map +0 -1
  75. package/dist/assets/groupFiles-DBbjOLuD.js +0 -2088
  76. package/dist/assets/groupFiles-DBbjOLuD.js.map +0 -1
  77. package/dist/assets/host-Cmd205Dc.js.map +0 -1
  78. package/dist/assets/main-BzNN6bkx.js.map +0 -1
  79. package/dist/assets/server-CBCEYKMN.js.map +0 -1
  80. package/dist/assets/src-DhfGgAUu.js.map +0 -1
package/README.md CHANGED
@@ -127,7 +127,7 @@ The rows are in alphabetical order of their first command, and every command of
127
127
  | `group edit <group> <message> [text… \| --text <text> \| --stdin] [--mention <member>]… [--force]` | Replace the text of a message you sent to the group (`<message>` is the `messageId` `group send` gave). Every member sees it in place, marked edited; mentions whose `@name` is still in the text stay, `--mention` adds more. Answers `{group, messageId, edits, sent, edges}`; `--wait sent` and `--timeout` as for `group send`. The text the message already has is no edit: `sent: false, unchanged: true`. A private group's members whose edge is down get it when it opens, a community's with the catch-up; a member on an older app keeps the old text |
128
128
  | `group hub <group> <member> [--pin \| --exclude \| --auto]` | Past 16 members: pin a member as a hub, keep one from being a hub, or leave it to their app (a daemon offers itself) |
129
129
  | `group invite <group> <chat>`, `group remove <group> <member>`, `group admin <group> <member>` | Membership, for the admin (each prints the group; `--show-secret` for its link) |
130
- | `group leave <group>`, `group forget <group> --yes`, `group accept\|decline <group>` | Membership |
130
+ | `group leave <group> --yes`, `group forget <group> --yes`, `group accept\|decline <group>` | Membership. Leaving deletes the group's history and files on this device, so it asks for `--yes` (`confirm`, exit 5, without it), as `group forget` does; forget also deletes a group you were removed from, which keeps its history. An admin's leave hands the role to a member who is online and names them: `{left, admin: {key, name}}` |
131
131
  | `group list [--show-secret]`, `group show <group> [--show-secret]`, `group history <group>` | Groups, members, history; a message names its author (`member` key, `nick` from the roster, or the name an author no longer in it had). The entry link prints as `<hidden>` without `--show-secret` |
132
132
  | `group react <group> <message> <emoji> [--remove]` | React to a group's message. `refused` when the profile is not a member any more |
133
133
  | `group rename <group> <name>` | A new name, for the admin (1 to 64 characters on one line); the picture stays. Members see it once it reaches them; until then, and on apps from before names, the group keeps the name it had when they got in |
@@ -159,7 +159,7 @@ The rows are in alphabetical order of their first command, and every command of
159
159
  | `status` | The profile, its chats, whether WebRTC and calls run, the last event seq |
160
160
  | `store sign <index> --key <file> [--out <dir>]` | Sign a store index (WISP 1200 · Stores) with the store key file (kept offline; made when missing and the index names no `key`): writes `ghostly-store.json`, canonical, and `ghostly-store.sig` beside the index or in `--out`. Refused, with the reason, when a client would refuse the index (`expires` more than 90 days ahead, a bad field), or when the index already there, signed by the same key, has a higher `sequence` (`rollback`) or the same one with other content (`equivocation`) |
161
161
  | `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]`, `task update <chat\|group> <task> [fields…]` | A task card for a bot's work (WISP 405 · Status Cards): a title, a status (queued, running, blocked, done, failed, cancelled), progress, the step it is on, up to 20 steps or log lines (`--item done:Codec`; a lowercase word before the colon must be an item state: pending, running, done, failed or skipped) and its pull request, shown as a small card that apps without cards read as text. Optional, and ignored by apps from before them: where the pull request stands (`--pr-state draft`, `open`, `merged` or `closed`; an open one puts the task in the board's Review column) and its checks (`--pr-checks passing`, `failing` or `pending`), up to 3 tags of 24 characters (`--tag core --tag web`; an update's tags replace the card's), and `--parent <task>`, a task of yours in the same chat that this one is a part of (sent first; its parts stack under it on the board). `send` prints the task's id and message; `update` merges the fields given over the card (an update's `--item` list replaces the card's: give all of them) and edits its message, at most once per 2.5 s (sooner updates merge into the next). `--json` takes the whole card; `null` in it removes a field. Only the CLI sends cards: the app shows them |
162
- | `typing <chat> [--kind typing\|recording\|thinking] [--status "<text>"] [--for s] [--stop]` | Show the contact you are writing, recording or thinking, or a short status line in its place ("Transcribing your audio…", 40 characters, no links): live chats only, it holds 6 s there; `--for s` keeps it on that long (up to 600 s; a one-shot stays that long); a new kind or status shows at once; a message to the chat or `--stop` ends it |
162
+ | `typing <chat> [--kind typing\|recording\|thinking] [--status "<text>"] [--for s] [--stop]` | Show the contact you are writing, recording or thinking, or a short status line in its place ("Transcribing your audio…", 40 characters, no links): live chats only, it holds 6 s there; `--for s` keeps it on that long (up to 600 s; a one-shot stays that long once the chat is live, and stops with `live: false` when it is not live within 15 s); a new kind or status shows at once; a message to the chat or `--stop` ends it |
163
163
  | `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]` | How much of a quota the bot has left (WISP 405 § Usage): the percent (or `--used` of `--limit`), what it is of, the account, the window, when it resets, and up to 3 other windows (`--also week=80@2026-10-10T00:00:00Z`). The app shows a small meter on the chat's row and in its header, amber under 20%, red under 5%, muted once 6 h old or past the reset. One card per chat: the first report sends it, later ones edit it in place (quiet, paced as `task update`'s); each report is whole, so a field it leaves out goes. `--all` reports to every 1:1 chat with a contact. See [docs/AI-AGENTS.md](../../docs/AI-AGENTS.md#reporting-usage) for where a bot gets its numbers |
164
164
  | `version` | This CLI's version, `{"version":"…"}` (also `ghostly --version`) |
165
165
  | `wallet create <type> [--network testnet] [--provider id] [--value name=value]… [--stdin] [--invite code] [--api-key key]` | A wallet: `cashu`, `lightning` (a card: its source's form in `--value`), `arkade`, `spark` (on Mainnet, `--api-key` is your Breez API key), `bitcoin` (BDK), `fedimint` (`--invite` is the federation's invite code), `usdt`. With `--stdin`, secret fields come as `name=value` lines on stdin (`api-key=…` too), out of `ps` and the shell's history |
@@ -280,6 +280,8 @@ chat (`chat list`, `chat show`, `chat rename`, …) and `engine getState` print
280
280
  - A message's `timestamp` is its place in the history, which `chat history` and `group history` are ordered by: when
281
281
  this profile sent it, or when it arrived here. A received message also has `sentAt`, the time its sender's clock
282
282
  said, which orders nothing (a clock can be wrong).
283
+ - A message's `from` is `me`, `peer`, or `system` for a group's own line in `group history` (`event`: created, joined,
284
+ renamed, …; the stream says it as `group.event`): nobody wrote it, whatever `member` key it carries.
283
285
  - `seq` grows by one per event in the profile, across restarts; `id` is the same whenever the same fact is reported.
284
286
  Dedupe by `id`; resume with `--since <seq>`, or let `--cursor <file>` remember the last event handled.
285
287
  - Types: `daemon.started`, `chat.created`, `chat.removed`, `chat.renamed`, `chat.pairing` (`stage`: publishing,
@@ -321,7 +323,8 @@ chat (`chat list`, `chat show`, `chat rename`, …) and `engine getState` print
321
323
  - Pins (WISP 400 § Pinned message): `chat.pinned` (with `chat`) and `group.pinned` (with `group`) when someone else pins
322
324
  or unpins: `{messageId, ref, by, removed}`, with `messageId` the message's id here (null when it is not here, or
323
325
  unpinned), `ref` the id both sides know it by, and `by` `peer` or a member key.
324
- - `--type message.received` keeps one type; `--type message.` (or `message.*`) a family.
326
+ - `--type message.received` keeps one type; `--type message` (or `message.`, `message.*`) a family. A family deeper
327
+ down ends in "." (`group.typing.`): `group.message` is that type alone.
325
328
  - `--from <chat|key>` and `--group <group>` (again for more): an allowlist. Only the chats (id, prefix, name, or the
326
329
  contact's `peer` key, which also matches a chat made later) and groups named get through; any other chat's or
327
330
  group's event stops before `--exec`, `--webhook` and stdout (the message stays in the chat; the cursor moves past
@@ -396,6 +399,9 @@ with no framing:
396
399
  rest of it, then plays padded.
397
400
  - Barge-in: `ghostly call flush` (or `call.flush` on the socket API) drops everything queued, at once. Stop writing
398
401
  before you flush.
402
+ - A call that ended unanswered (you hung up while it rang, or it rang out) is followed by the next one in that chat
403
+ no sooner than 2 s later: `call start` refuses sooner with `busy` and `details.retryAfterMs`. The contact's app
404
+ would not ring a call that comes that fast after a missed one.
399
405
  - The socket exists from `call start`/`call answer`, so the program may connect before the media is up. One program
400
406
  at a time: a new connection replaces the old one. When the call ends, the program reads EOF and the socket is
401
407
  removed.
@@ -1,14 +1,14 @@
1
- import { $ as appRef, E as voicePeaksOf, Ft as toBase64Url, L as cardLine, N as STATUS_CARD_LIMITS, Nt as randomBytes$1, P as appCardId, S as baseMime, T as parseVoiceMeta, _t as LIMITS, b as PLAYABLE_AUDIO, it as isAppRef, m as readImageMeta, t as GROUP_FILE_LIMITS, v as sanitizeFileName, x as VOICE_LIMITS, y as sanitizeMime, z as checkStatusCard } from "./groupFiles-DBbjOLuD.js";
2
- import { Da as AWAITING_PEER_MS, Dr as TYPING_REFRESH_MS, Er as TYPING_KINDS, Fi as identityStatement, Ma as GhostlyHttpError, ac as MAX_AVATAR_LENGTH, at as groupName, kr as sanitizeTypingStatus, ls as inviteLink, m as APP_PERMISSIONS, oc as sanitizeAvatar, qr as APP_DATA_MAX_BYTES, y as isAppUrl } from "./src-DhfGgAUu.js";
1
+ import { Bo as LIMITS, Br as TYPING_REFRESH_MS, Hr as sanitizeTypingStatus, Io as GhostlyHttpError, Ji as identityStatement, Qa as cardLine, Xa as appCardId, Ya as STATUS_CARD_LIMITS, Zn as GROUP_FILE_LIMITS, _a as VOICE_LIMITS, _l as randomBytes$1, at as groupName, ba as parseVoiceMeta, do as appRef, eo as checkStatusCard, fa as readImageMeta, ga as PLAYABLE_AUDIO, gc as inviteLink, ha as sanitizeMime, ho as isAppRef, jo as AWAITING_PEER_MS, m as APP_PERMISSIONS, ma as sanitizeFileName, ml as sanitizeAvatar, oi as APP_DATA_MAX_BYTES, pl as MAX_AVATAR_LENGTH, va as baseMime, vc as ownInviteRefusal, vl as toBase64Url, xa as voicePeaksOf, y as isAppUrl, zr as TYPING_KINDS } from "./src-DDzSERyc.js";
3
2
  import { c as fileStore } from "./idb-B5L8wuOm.js";
4
- import { O as waitingGroupFile, T as removeStored, ct as iceServerProblem, i as groupOfFile, k as IDENTITY_PROVIDERS, l as replyRef, lt as iceServerSetting, n as groupFileId, v as paymentNetwork } from "./groupFiles-BNTSDaoq.js";
3
+ import { O as waitingGroupFile, T as removeStored, U as iceServerProblem, W as iceServerSetting, i as groupOfFile, l as replyRef, n as groupFileId, v as paymentNetwork } from "./groupFiles-IEUW3DQz.js";
5
4
  import { f as FILE_BYTES_STEP, o as fileBytes, s as fileBytesOf } from "./fileBytes-Di7VZtmU.js";
6
- import { a as waitForEdit, c as findSecret, i as pressable, n as buttonsCard, o as waitForGroupFrame, s as waitForMessage, t as BUTTON_METHODS } from "./buttons-CX1pKB0j.js";
7
- import { i as ENGINE_READS, n as withStoredIds, r as ENGINE_METHODS } from "./turnRefs-ChEfgbSX.js";
8
- import { t as CliError } from "./errors-my6VJR_0.js";
9
- import { _ as str, a as findChat, c as groupOf, d as messageOf, f as node, g as state, h as sentAt, i as chatOrGroup, l as inviteTaken, m as oneOf, n as bool, o as findChatOrGroup, p as num, r as chatOf, s as findGroup, u as list, v as waitForState } from "./apiKit-CajiDxdK.js";
10
- import { t as BACKUP_METHODS } from "./backup-BAMi2lAq.js";
11
- import { o as checkedBundle } from "./apps-CbfiCSIw.js";
5
+ import { t as IDENTITY_PROVIDERS } from "./registry-DlAKO0Fy.js";
6
+ import { a as waitForEdit, c as findSecret, i as pressable, n as buttonsCard, o as waitForGroupFrame, s as waitForMessage, t as BUTTON_METHODS } from "./buttons-CvNBzh2S.js";
7
+ import { i as ENGINE_READS, n as withStoredIds, r as ENGINE_METHODS } from "./turnRefs-B87dtrHM.js";
8
+ import { t as CliError } from "./errors-DO5z6h_l.js";
9
+ import { _ as str, a as findChat, c as groupOf, d as messageOf, f as node, g as state, h as sentAt, i as chatOrGroup, l as inviteTaken, m as oneOf, n as bool, o as findChatOrGroup, p as num, r as chatOf, s as findGroup, u as list, v as waitForState } from "./apiKit-CA2x5GJF.js";
10
+ import { t as BACKUP_METHODS } from "./backup-CnMN2oUK.js";
11
+ import { o as checkedBundle } from "./apps-BxV9i_Y4.js";
12
12
  import { generateMnemonic } from "@scure/bip39";
13
13
  import { wordlist } from "@scure/bip39/wordlists/english.js";
14
14
  import { openAsBlob, readFileSync, renameSync, writeFileSync } from "node:fs";
@@ -76,6 +76,41 @@ function keepTyping(ctx, target, ms, word = {}) {
76
76
  map.set(key, end);
77
77
  });
78
78
  }
79
+ /** How long a one-shot's `--for` waits for its chat or group to be live: a fresh one-shot connects first. */
80
+ var ONE_SHOT_LIVE_GRACE_MS = 15e3;
81
+ /**
82
+ * A one-shot's `--for` (it holds the profile while it runs): kept the whole time once the chat or group is live, as a
83
+ * daemon keeps it; ended when it is not live within `ONE_SHOT_LIVE_GRACE_MS`, since nobody would see it, and `--stop`
84
+ * or a send from another shell could not end it.
85
+ */
86
+ async function keepOneShot(ctx, target, kept, live) {
87
+ let stop = () => {};
88
+ const cameLive = new Promise((resolve) => {
89
+ if (live(state(ctx))) return resolve(true);
90
+ const timer = setTimeout(() => {
91
+ off();
92
+ resolve(false);
93
+ }, ONE_SHOT_LIVE_GRACE_MS);
94
+ const off = ctx.hub.onState((next) => {
95
+ if (live(next)) {
96
+ clearTimeout(timer);
97
+ off();
98
+ resolve(true);
99
+ }
100
+ });
101
+ stop = () => {
102
+ clearTimeout(timer);
103
+ off();
104
+ resolve(true);
105
+ };
106
+ });
107
+ if (await Promise.race([kept.then(() => true), cameLive])) {
108
+ stop();
109
+ await kept;
110
+ return;
111
+ }
112
+ endTyping(ctx, target, false);
113
+ }
79
114
  /** Ends a kept `--for` in the chat or group; `tell`: and says this side stopped. Whether one was kept. */
80
115
  function endTyping(ctx, target, tell) {
81
116
  const end = keepersOf(ctx).get(keyOf(target));
@@ -755,9 +790,12 @@ var FILE_METHODS = {
755
790
  const fileId = str(params, "file", true);
756
791
  const { linkId, where } = fileChat(ctx, params, fileId);
757
792
  const action = oneOf(params, "action", ACTIONS, "accept");
793
+ const refuseAllHere = async () => {
794
+ if ((state(ctx).transfers[fileId] ?? (await fileStore.get(fileId).catch(() => void 0))?.transfer)?.state === "done") throw new CliError("refused", action === "accept" || action === "request" || action === "resume" ? "Nothing to ask for: the file is all here" : "Nothing to stop: the file is all here");
795
+ };
758
796
  if ("group" in where) {
759
797
  if (action === "resend") throw new CliError("bad_request", "A group's file is downloaded (file accept) or asked for again (file request)");
760
- if ((state(ctx).transfers[fileId] ?? (await fileStore.get(fileId).catch(() => void 0))?.transfer)?.state === "done") throw new CliError("refused", action === "accept" || action === "request" || action === "resume" ? "Nothing to ask for: the file is all here" : "Nothing to stop: the file is all here");
798
+ await refuseAllHere();
761
799
  try {
762
800
  await node(ctx).fileAction({
763
801
  linkId,
@@ -780,7 +818,7 @@ var FILE_METHODS = {
780
818
  const transfer = state(ctx).transfers[fileId];
781
819
  if (!transfer || transfer.state === "done") throw new CliError("refused", outgoing ? "Nothing to send again: it arrived whole" : "Nothing to ask for: the file is all here");
782
820
  if (transfer.state === "failed" && (!outgoing || !transfer.retry)) throw new CliError("refused", `It ended: ${transfer.error ?? "failed"}`);
783
- }
821
+ } else await refuseAllHere();
784
822
  await node(ctx).fileAction({
785
823
  linkId,
786
824
  fileId,
@@ -842,7 +880,7 @@ var FILE_METHODS = {
842
880
  const stored = await fileStore.get(fileId);
843
881
  if (!stored?.metadata) throw new CliError("not_found", `No file ${fileId}`);
844
882
  if (stored.leftOut) throw new CliError("not_found", "Not in this backup: the light backup this profile was restored from left the file out");
845
- const transfer = state(ctx).transfers[fileId] ?? stored.transfer;
883
+ const transfer = stored.direction === "out" ? void 0 : state(ctx).transfers[fileId] ?? stored.transfer;
846
884
  if (transfer && transfer.state !== "done") throw new CliError("unavailable", `The file is not all here yet (${transfer.transferred} of ${transfer.size} bytes)`);
847
885
  const target = str(params, "path") ? resolve(str(params, "path", true)) : join(resolve(str(params, "dir") ?? "."), saveName(stored.metadata.name));
848
886
  const flags = bool(params, "force") ? "w" : "wx";
@@ -850,7 +888,9 @@ var FILE_METHODS = {
850
888
  try {
851
889
  out = await open(target, flags, 384);
852
890
  } catch (error) {
853
- if (error.code === "EEXIST") throw new CliError("confirm", `${target} exists: pass force (--force) to replace it`);
891
+ const code = error.code;
892
+ if ((code === "EEXIST" || code === "EISDIR") && (await stat(target).catch(() => void 0))?.isDirectory()) throw new CliError("bad_request", `${target} is a folder: name a file, or use dir (--dir ${target})`);
893
+ if (code === "EEXIST") throw new CliError("confirm", `${target} exists: pass force (--force) to replace it`);
854
894
  throw error;
855
895
  }
856
896
  let written = 0, complete = false;
@@ -1431,7 +1471,7 @@ function messageJson(message) {
1431
1471
  return {
1432
1472
  id: message.id,
1433
1473
  chat: message.linkId,
1434
- from: message.sender,
1474
+ from: message.event ? "system" : message.sender,
1435
1475
  text: message.text,
1436
1476
  timestamp: message.timestamp,
1437
1477
  ...message.sentAt !== void 0 ? { sentAt: message.sentAt } : {},
@@ -2613,6 +2653,33 @@ var CHAT_METHODS = [
2613
2653
  * them as unavailable; creating one is refused before the engine is asked.
2614
2654
  */
2615
2655
  var NODE_GAPS = { bark: "Bark's SDK ships a browser build only: its WebAssembly needs a browser window. Use it from the app." };
2656
+ /** Sources the engine lists that live in a browser page (`window.webln`): a headless Ghostly never offers them. */
2657
+ var BROWSER_SOURCES = /* @__PURE__ */ new Set(["webln"]);
2658
+ /** The sources of an offer a headless Ghostly can connect. */
2659
+ var nodeSources = (offer) => offer?.providers?.filter((p) => !BROWSER_SOURCES.has(p.id));
2660
+ /**
2661
+ * What `wallet create` was given checked against the offer's own form, before the engine is asked: a source, a field
2662
+ * or a flag left out, or one the wallet does not take, is the person's value, said naming the flag. Before, the engine
2663
+ * refused it in the app's words ("Enter connection uri", "Nothing was saved; try again") as `engine`, or the flag was
2664
+ * dropped without a word.
2665
+ */
2666
+ function createChecked(type, n, offer, provider, values, params) {
2667
+ if (type !== "lightning" && type !== "bitcoin") {
2668
+ if (provider || Object.keys(values).length) throw new CliError("usage", `--provider and --value name a lightning or bitcoin source: not with ${type}`);
2669
+ if (offer?.needs === "invite" && !str(params, "invite")?.trim()) throw new CliError("bad_request", `${type} needs --invite <fed11…>`);
2670
+ if (offer?.needs === "apiKey" && !str(params, "apiKey")?.trim()) throw new CliError("bad_request", `${type} on ${n} needs --api-key <key> (or api-key=… on --stdin)`);
2671
+ return;
2672
+ }
2673
+ const sources = nodeSources(offer);
2674
+ if (!sources?.length) return;
2675
+ if (!provider) throw new CliError("bad_request", `${type} needs --provider: one of ${sources.map((p) => p.id).join(", ")} (ghostly wallet list)`);
2676
+ const source = sources.find((p) => p.id === provider);
2677
+ if (!source) throw new CliError("bad_request", `No ${type} source ${JSON.stringify(provider)} on ${n} (ghostly wallet list)`);
2678
+ const unknown = Object.keys(values).find((name) => !source.fields.some((f) => f.name === name));
2679
+ if (unknown) throw new CliError("bad_request", `${provider} takes no --value ${unknown}: ${source.fields.length ? `its fields are ${source.fields.map((f) => f.name).join(", ")}` : "it has no fields"}`);
2680
+ const missing = source.fields.find((f) => !f.optional && !f.defaults?.[n] && !values[f.name]?.trim());
2681
+ if (missing) throw new CliError("bad_request", `${provider} needs --value ${missing.name}=<${missing.options?.map((o) => o.value).join("|") ?? missing.placeholder ?? missing.label.toLowerCase()}>`);
2682
+ }
2616
2683
  var network = (params, fallback = "testnet") => oneOf(params, "network", NETWORKS, fallback);
2617
2684
  /** `confirmedReal` for the engine: only when the caller confirmed real money, and only then. */
2618
2685
  var real = (params) => bool(params, "confirmReal") ? { confirmedReal: true } : {};
@@ -2775,7 +2842,7 @@ var WALLET_METHODS = Object.fromEntries(Object.entries({
2775
2842
  exists: !!o.exists,
2776
2843
  several: !!o.several,
2777
2844
  needs: o.needs ?? null,
2778
- ...o.providers ? { providers: o.providers.map((p) => ({
2845
+ ...o.providers ? { providers: nodeSources(o).map((p) => ({
2779
2846
  id: p.id,
2780
2847
  label: p.label,
2781
2848
  fields: p.fields.map((f) => ({
@@ -2799,14 +2866,13 @@ var WALLET_METHODS = Object.fromEntries(Object.entries({
2799
2866
  const raw = params.values ?? {};
2800
2867
  if (!raw || typeof raw !== "object" || Array.isArray(raw) || Object.values(raw).some((v) => typeof v !== "string")) throw new CliError("bad_request", "values must be an object of strings");
2801
2868
  Object.assign(values, raw);
2802
- const provider = str(params, "provider");
2803
- const offered = state(ctx).wallet.offers?.find((o) => o.type === type && o.network === n)?.providers;
2804
- if (provider && offered && !offered.some((p) => p.id === provider)) throw new CliError("bad_request", `No ${type} source ${JSON.stringify(provider)} on ${n} (ghostly wallet list)`);
2805
- if (type === "bitcoin" && (provider ?? "bdk") === "bdk" && !values.mnemonic) values.mnemonic = generateMnemonic(wordlist, 128);
2869
+ const provider = str(params, "provider") ?? (type === "bitcoin" ? "bdk" : void 0);
2870
+ if (type === "bitcoin" && provider === "bdk" && !values.mnemonic) values.mnemonic = generateMnemonic(wordlist, 128);
2871
+ createChecked(type, n, state(ctx).wallet.offers?.find((o) => o.type === type && o.network === n), provider, values, params);
2806
2872
  return walletJson(await node(ctx).walletCreate({
2807
2873
  type,
2808
2874
  network: n,
2809
- ...provider ? { providerId: provider } : type === "bitcoin" ? { providerId: "bdk" } : {},
2875
+ ...provider ? { providerId: provider } : {},
2810
2876
  ...Object.keys(values).length ? { values } : {},
2811
2877
  ...str(params, "invite") ? { invite: str(params, "invite") } : {},
2812
2878
  ...str(params, "apiKey") ? { apiKey: str(params, "apiKey") } : {}
@@ -3159,6 +3225,20 @@ var SETTABLE = {
3159
3225
  autoDownloads: "boolean",
3160
3226
  serveFiles: "boolean"
3161
3227
  };
3228
+ /**
3229
+ * A relay address the engine refuses (node.ts `updateSettings`, irohWeb.ts, hyperdhtRelay.ts): the person's value, so
3230
+ * `bad_request` named by its key, in a terminal's words ("give", not the app's "Enter"). Before, `engine` (exit 1). A
3231
+ * wrong scheme ("Use an https:// relay address") too.
3232
+ */
3233
+ function relayRefused(error, patch) {
3234
+ if (!(error instanceof Error) || !/^(?:Enter (?:at least one|a) relay address|Not a relay address|Use an? \S+ relay address)\b/.test(error.message)) return error;
3235
+ const keys = [
3236
+ "relays",
3237
+ "irohRelays",
3238
+ "hyperdhtRelay"
3239
+ ].filter((key) => Object.hasOwn(patch, key));
3240
+ return new CliError("bad_request", `${keys.join(", ") || "relays"}: ${error.message.replace(/^Enter /, "give ").replace(/^(Not|Use) /, (word) => word.toLowerCase())}`);
3241
+ }
3162
3242
  var TRANSPORT_NAMES = {
3163
3243
  auto: "auto",
3164
3244
  dht: "dht",
@@ -3320,7 +3400,9 @@ var METHODS = {
3320
3400
  }
3321
3401
  patch[key] = value;
3322
3402
  }
3323
- await node(ctx).updateSettings({ settings: patch });
3403
+ await node(ctx).updateSettings({ settings: patch }).catch((error) => {
3404
+ throw relayRefused(error, patch);
3405
+ });
3324
3406
  return {
3325
3407
  ...redactSettings(state(ctx).settings),
3326
3408
  ...warning ? { warning } : {}
@@ -3357,8 +3439,9 @@ var METHODS = {
3357
3439
  try {
3358
3440
  ({linkId} = await node(ctx).joinLink({ inviteCode: invite }));
3359
3441
  } catch (error) {
3442
+ const own = ownInviteRefusal(error);
3443
+ if (own) throw new CliError("refused", "This is your own invite. Share it with a contact; they join with it.", own.linkId ? { chat: own.linkId } : void 0);
3360
3444
  const message = error instanceof Error ? error.message : String(error);
3361
- if (/own invite/i.test(message)) throw new CliError("refused", message);
3362
3445
  if (/does not look like/i.test(message)) throw new CliError("bad_request", message);
3363
3446
  throw error;
3364
3447
  }
@@ -3375,18 +3458,18 @@ var METHODS = {
3375
3458
  return { chats: state(ctx).links.map((link) => chatJson(link, showSecret)).sort((a, b) => b.lastMessageAt - a.lastMessageAt || b.createdAt - a.createdAt) };
3376
3459
  },
3377
3460
  async "chat.get"(ctx, params) {
3378
- const link = chatOf(ctx, params);
3461
+ const link = chatNotGroup(ctx, params, "group show");
3379
3462
  return {
3380
3463
  ...chatDetailsJson(link, bool(params, "showSecret")),
3381
3464
  heldUntil: holdOf(ctx, link.id)
3382
3465
  };
3383
3466
  },
3384
3467
  async "chat.history"(ctx, params) {
3385
- const link = chatOf(ctx, params);
3468
+ const link = chatNotGroup(ctx, params, "group history");
3386
3469
  return historyOf(ctx, link.id, params, (m) => chatMessageJson(m, link));
3387
3470
  },
3388
3471
  async "chat.send"(ctx, params) {
3389
- const link = chatOf(ctx, params);
3472
+ const link = chatNotGroup(ctx, params, "group send", "[text...]");
3390
3473
  const text = str(params, "text", true);
3391
3474
  checkTextSize(text);
3392
3475
  if (!bool(params, "force")) {
@@ -3430,7 +3513,7 @@ var METHODS = {
3430
3513
  * once the chat is live and both apps offer edit/1; `--wait confirmed` waits for the contact to confirm it.
3431
3514
  */
3432
3515
  async "chat.edit"(ctx, params) {
3433
- const link = chatOf(ctx, params);
3516
+ const link = chatNotGroup(ctx, params, "group edit", "<message> <text...>");
3434
3517
  const messageId = str(params, "message", true);
3435
3518
  const text = str(params, "text", true);
3436
3519
  if (!bool(params, "force")) {
@@ -3461,10 +3544,11 @@ var METHODS = {
3461
3544
  * WISP 401 § Typing: said on the live session only, and a start only while `sendTyping` is on; it holds 6 s there.
3462
3545
  * `kind` (typing, recording, thinking) and `status` (a short line, e.g. "Transcribing your audio…") say what this
3463
3546
  * side is doing; a new one reaches the contact at once.
3464
- * `for`: kept that many seconds (a one-shot stays that long), until a message to the chat or a stop.
3547
+ * `for`: kept that many seconds (a one-shot stays that long once the chat is live, and stops when it is not live
3548
+ * within `ONE_SHOT_LIVE_GRACE_MS`), until a message to the chat or a stop.
3465
3549
  */
3466
3550
  async "chat.typing"(ctx, params) {
3467
- const link = chatOf(ctx, params);
3551
+ const link = chatNotGroup(ctx, params, "group typing");
3468
3552
  const typing = !bool(params, "stop");
3469
3553
  const { kind, status } = typingWord(params, typing);
3470
3554
  const word = {
@@ -3484,20 +3568,21 @@ var METHODS = {
3484
3568
  });
3485
3569
  const until = seconds ? Date.now() + seconds * 1e3 : null;
3486
3570
  const kept = seconds ? keepTyping(ctx, { linkId: link.id }, seconds * 1e3, word) : null;
3487
- if (kept && ctx.mode === "one-shot") await kept;
3571
+ const open = (s) => s.links.find((l) => l.id === link.id)?.dataLink === "open";
3572
+ if (kept && ctx.mode === "one-shot") await keepOneShot(ctx, { linkId: link.id }, kept, open);
3488
3573
  return {
3489
3574
  chat: link.id,
3490
3575
  typing,
3491
3576
  ...typing ? { kind } : {},
3492
3577
  ...status ? { status } : {},
3493
- live: link.dataLink === "open",
3578
+ live: open(state(ctx)),
3494
3579
  sendTyping: state(ctx).settings.sendTyping !== false,
3495
3580
  ...until ? { until } : {}
3496
3581
  };
3497
3582
  },
3498
3583
  /** WISP 400 § Reactions: one emoji per person per message, a new one replaces mine; `remove` takes mine back. */
3499
3584
  async "chat.react"(ctx, params) {
3500
- const link = notAGroup(ctx, params, () => chatOf(ctx, params), "group react", "<message> <emoji>");
3585
+ const link = chatNotGroup(ctx, params, "group react", "<message> <emoji>");
3501
3586
  return {
3502
3587
  chat: link.id,
3503
3588
  ...await react(ctx, link.id, params)
@@ -3594,7 +3679,10 @@ var METHODS = {
3594
3679
  async "chat.retry"(ctx, params) {
3595
3680
  const link = chatOf(ctx, params);
3596
3681
  const messageId = str(params, "message", true);
3597
- if (!await node(ctx).getMessage(link.id, messageId)) throw new CliError("not_found", `No message ${messageId} in this chat`);
3682
+ const message = await node(ctx).getMessage(link.id, messageId);
3683
+ if (!message) throw new CliError("not_found", `No message ${messageId} in this chat`);
3684
+ if (message.sender !== "me") throw new CliError("not_found", `No message ${messageId} of yours in this chat: only your own messages are sent again`);
3685
+ if (message.delivery === "delivered") throw new CliError("refused", "Nothing to send again: it was delivered");
3598
3686
  await node(ctx).retryMessage({
3599
3687
  linkId: link.id,
3600
3688
  messageId
@@ -3627,7 +3715,7 @@ var METHODS = {
3627
3715
  return details;
3628
3716
  },
3629
3717
  async "chat.rename"(ctx, params) {
3630
- const link = chatOf(ctx, params);
3718
+ const link = chatNotGroup(ctx, params, "group rename", "<name...>");
3631
3719
  await node(ctx).renameLink({
3632
3720
  linkId: link.id,
3633
3721
  label: str(params, "name") ?? ""
@@ -3635,7 +3723,7 @@ var METHODS = {
3635
3723
  return chatJson(findChat(state(ctx).links, link.id));
3636
3724
  },
3637
3725
  async "chat.remove"(ctx, params) {
3638
- const link = chatOf(ctx, params);
3726
+ const link = chatNotGroup(ctx, params, "group forget", "--yes");
3639
3727
  if (!bool(params, "yes")) throw new CliError("confirm", "Removing a chat deletes its keys and history on this device; pass yes (--yes) to go on");
3640
3728
  await node(ctx).removeLink({ linkId: link.id });
3641
3729
  return { removed: link.id };
@@ -3866,21 +3954,30 @@ var METHODS = {
3866
3954
  sayTyping(ctx, target, typing, word);
3867
3955
  const until = seconds ? Date.now() + seconds * 1e3 : null;
3868
3956
  const kept = seconds ? keepTyping(ctx, target, seconds * 1e3, word) : null;
3869
- if (kept && ctx.mode === "one-shot") await kept;
3957
+ const reached = (s) => (s.groups.find((g) => g.id === group.id) ?? group).members.filter((m) => !m.me && m.online && !m.viaHub).length;
3958
+ if (kept && ctx.mode === "one-shot") await keepOneShot(ctx, target, kept, (s) => reached(s) > 0);
3870
3959
  return {
3871
3960
  group: group.id,
3872
3961
  typing,
3873
3962
  ...typing ? { kind } : {},
3874
3963
  ...status ? { status } : {},
3875
- reached: group.members.filter((m) => !m.me && m.online && !m.viaHub).length,
3964
+ reached: reached(state(ctx)),
3876
3965
  sendTyping: state(ctx).settings.sendTyping !== false,
3877
3966
  ...until ? { until } : {}
3878
3967
  };
3879
3968
  },
3880
3969
  async "group.leave"(ctx, params) {
3881
3970
  const group = groupOf(ctx, params);
3882
- await node(ctx).leaveGroup({ groupId: group.id });
3883
- return { left: group.id };
3971
+ if (!bool(params, "yes")) throw new CliError("confirm", "Leaving a group deletes its history and files on this device; pass yes (--yes) to go on");
3972
+ const { admin } = await node(ctx).leaveGroup({ groupId: group.id });
3973
+ const next = admin ? group.members.find((m) => m.key === admin) : void 0;
3974
+ return {
3975
+ left: group.id,
3976
+ ...admin ? { admin: {
3977
+ key: admin,
3978
+ name: next?.nick ?? null
3979
+ } } : {}
3980
+ };
3884
3981
  },
3885
3982
  async "group.forget"(ctx, params) {
3886
3983
  const group = groupOf(ctx, params);
@@ -3992,9 +4089,13 @@ function notAGroup(ctx, params, chat, command, rest) {
3992
4089
  throw error;
3993
4090
  }
3994
4091
  const shown = /^[\w.:-]+$/.test(ref) ? ref : JSON.stringify(ref);
3995
- throw new CliError("not_found", `${JSON.stringify(ref)} is a group, not a chat: ghostly ${command} ${shown} ${rest}`, { group: group.id });
4092
+ throw new CliError("not_found", `${JSON.stringify(ref)} is a group, not a chat: ghostly ${command} ${shown}${rest ? ` ${rest}` : ""}`, { group: group.id });
3996
4093
  }
3997
4094
  }
4095
+ /** The chat a chat command with a group twin names: a group's name says so, with `command`, the twin (see `notAGroup`). */
4096
+ function chatNotGroup(ctx, params, command, rest = "") {
4097
+ return notAGroup(ctx, params, () => chatOf(ctx, params), command, rest);
4098
+ }
3998
4099
  /**
3999
4100
  * An edit the engine refused. A message that is not in the chat or group at all is `not_found` (exit 3), naming the id
4000
4101
  * to give, as `button update` and `task update` do; before, a mistyped or lost id read "Only your own text messages
@@ -4068,4 +4169,4 @@ async function callApi(ctx, method, params = {}) {
4068
4169
  //#endregion
4069
4170
  export { flushCardUpdates as _, hideStateSecrets as a, paymentJson as c, nodeLocalFetch as d, chatJson as f, resumeHolds as g, groupMessageJson as h, groupSendRefused as i, reportAppFrames as l, groupJson as m, SETTABLE as n, mentionsFor as o, chatMessageJson as p, callApi as r, redactSettings as s, API_METHODS as t, servedApps as u };
4070
4171
 
4071
- //# sourceMappingURL=api-CrxVxtY1.js.map
4172
+ //# sourceMappingURL=api-Dq2QXQDk.js.map