@ghostlytools/cli 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ghostly.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as __exportAll } from "./assets/rolldown-runtime-8H4AJuhK.js";
3
- import { $ as SSHSIG_NAMESPACE, $a as readAppRevocations, $n as engineError, An as encodeDnsQuery, Ao as signalHasVideo, At as didJwsAlg, Bn as resolveLightningDestination, Bt as parseDidSignaturePaste, Cn as homeserverWebEndpoint, Co as RTC_CONFIG, Ct as DID_DOCUMENT_MAX_BYTES$1, Dn as dnsProofName, Do as callSignalHeardAt, Dt as checkDidJws, Ec as setLinkTraceSink, En as decodeDnsResponse, Eo as callRtcConfig, Et as DidError, Fa as RUN_RESULTS, Fc as openRelayPayload, Fn as recordsFromWellKnown, Ft as didWebFileUrl, Gc as fromBase64Url$1, Hc as MAX_AVATAR_LENGTH, Ht as staticDidDocument, Ia as STATUS_CARD_LIMITS, Ii as identityStatement, In as recordsName, It as didWebHost, Ja as appFingerprint, Jc as toZ32, Jt as resolveDidDht, Ka as APP_PREFIXES, Kc as randomBytes$1, Ki as irohDescriptor, Kr as sanitizeTypingStatus, La as buttonLabelClash, Ln as txtValue, Lt as normalizeDid, Ma as replySnippet, Mc as RELAY_PAYLOAD_MAX_BYTES, Mn as nip05RootKey, Mt as didWebDocumentUrl, Ni as boundVerifiedIdentity, Nn as normalizeDomain, Nt as didWebFile, On as domainTxtRecord, Oo as extractParamsFromSdp, Ot as checkDidSignature, Pa as ITEM_STATES, Pn as recordsFromTxt, Pt as didWebFileNames, Ra as checkStatusCard, Rn as wellKnownUrl, Rt as parseDid, Sn as pubkyProofPath, So as CALL_SIGNAL_MAX_AGE_MS, St as verifyAtprotoRecordProof, Tn as DOMAIN_FILE_MAX_BYTES, To as buildSdpFromSignal, Uc as sanitizeAvatar, Ur as TYPING_KINDS, Ut as strictBase64Url, Vn as decodeBolt11, Vr as transferEnded, Vt as signingKeys, Wr as TYPING_REFRESH_MS, Xa as checkAppRevokeStatement, Xc as utf8Encode, Ya as appRef, Yc as utf8Decode, Ys as inviteLink, Z as groupName, _ as readAppBundle, _n as isPubkyProofFolder, _t as isAtprotoDid, aa as PLAYABLE_AUDIO, ar as fetchesByItself, at as verifySshSignature, bn as pubkyProofCapability, bt as shortAtprotoDid, c as signAppStore, ca as fileMessageText, ci as checkPushEndpoint, ct as decodeAnyBitcoinAddress, dr as readGroupWantNo, dt as ATPROTO_PROOF_COLLECTION, ea as readImageMeta, eo as signAppObject, er as engineText, et as SshSigError, ft as ATPROTO_PROOF_SCOPE, g as isAppUrl, gn as isPubkyKey, gt as checkAtprotoProofRecord, h as checkAppManifest, hn as PUBKY_PROOF_MAX_BYTES, ho as GhostlyHttpError, ht as atprotoProofRkey, ia as sanitizeMime, in as WALLET_NETWORKS, ir as GROUP_WANT_NO_FRAME, it as sshSignCommand, ji as IdentityCheckUnavailable, jn as isPublicAddress, jt as didSignedMessages, kn as domainWellKnownFile, ko as parseCallSignal, kt as didGhostlyServiceEntry, l as APP_BUNDLE_LIMITS, la as parseVoiceMeta, ln as walletNetworkOf, lr as readGroupHave, lt as ATPROTO_DECLARED_SCOPE, m as assembleAppBundle, ma as RateWindow, mt as atprotoProofRecord, n as appStoreDecision, no as canonicalJson, nr as GROUP_HAVE_FRAME, nt as parseSshSignature, o as checkAppStoreIndex, oa as VOICE_LIMITS, oc as seedSigner, or as groupFileFallback, ot as verifyBitcoinMessage, pi as BUTTONS_CAPABILITY, pt as atprotoDidMethod, qa as APP_STATEMENT_LIMITS, qc as toBase64Url, qi as irohRelayUrl, ra as sanitizeFileName, ro as canonicalJsonBytes, rt as sameSshKey, s as readAppStore, sa as baseMime, sr as isGroupMessageId, st as BitcoinAddressError, to as signAppRevocation, tr as GROUP_FILE_LIMITS, tt as parseSshPublicKey, ua as voicePeaksOf, ur as readGroupWant, ut as ATPROTO_FULL_SCOPE, vn as newPubkyProofFolder, vt as normalizeAtprotoHandle, wn as pubkyHomeserverOf, wo as answersOffer, wt as DID_FILE_MAX_BYTES, xn as pubkyProofFile, xt as AtprotoProofError, yn as normalizePubkyKey, yo as LIMITS, yt as parseAtprotoDidDocument, zn as requestLnurlInvoice, zr as ChatFiles, zt as parseDidDocument } from "./assets/src-BMUcYQ7b.js";
3
+ import { $ as SSHSIG_NAMESPACE, $a as appRef, $c as randomBytes$1, $n as engineError, An as encodeDnsQuery, Ao as RTC_CONFIG, At as didJwsAlg, Ba as cardLine, Bn as resolveLightningDestination, Bt as parseDidSignaturePaste, Cn as homeserverWebEndpoint, Ct as DID_DOCUMENT_MAX_BYTES$1, Dn as dnsProofName, Dt as checkDidJws, En as decodeDnsResponse, Eo as LIMITS, Et as DidError, Fa as PR_CHECKS, Fn as recordsFromWellKnown, Fo as extractParamsFromSdp, Ft as didWebFileUrl, Ha as checkStatusCard, Hc as openRelayPayload, Ht as staticDidDocument, Ia as PR_STATES, Ii as identityStatement, In as recordsName, Io as parseCallSignal, It as didWebHost, Jt as resolveDidDht, Ki as irohDescriptor, Kr as sanitizeTypingStatus, La as RUN_RESULTS, Ln as txtValue, Lo as signalHasVideo, Lt as normalizeDid, Ma as replySnippet, Mn as nip05RootKey, Mo as buildSdpFromSignal, Mt as didWebDocumentUrl, Nc as setLinkTraceSink, Ni as boundVerifiedIdentity, Nn as normalizeDomain, No as callRtcConfig, Nt as didWebFile, On as domainTxtRecord, Ot as checkDidSignature, Pa as ITEM_STATES, Pn as recordsFromTxt, Po as callSignalHeardAt, Pt as didWebFileNames, Qa as appFingerprint, Qc as fromBase64Url$1, Ra as STATUS_CARD_LIMITS, Rn as wellKnownUrl, Rt as parseDid, Sn as pubkyProofPath, So as GhostlyHttpError, St as verifyAtprotoRecordProof, Tn as DOMAIN_FILE_MAX_BYTES, Ur as TYPING_KINDS, Ut as strictBase64Url, Va as cardUrl, Vn as decodeBolt11, Vr as transferEnded, Vt as signingKeys, Wr as TYPING_REFRESH_MS, Xa as APP_PREFIXES, Xc as sanitizeAvatar, Yc as MAX_AVATAR_LENGTH, Z as groupName, Za as APP_STATEMENT_LIMITS, _ as readAppBundle, _n as isPubkyProofFolder, _t as isAtprotoDid, aa as PLAYABLE_AUDIO, ao as signAppObject, ar as fetchesByItself, at as verifySshSignature, bn as pubkyProofCapability, bt as shortAtprotoDid, c as signAppStore, ca as fileMessageText, ci as checkPushEndpoint, co as canonicalJsonBytes, ct as decodeAnyBitcoinAddress, dr as readGroupWantNo, dt as ATPROTO_PROOF_COLLECTION, ea as readImageMeta, el as toBase64Url, eo as checkAppRevokeStatement, er as engineText, et as SshSigError, ft as ATPROTO_PROOF_SCOPE, g as isAppUrl, gn as isPubkyKey, gt as checkAtprotoProofRecord, h as checkAppManifest, hn as PUBKY_PROOF_MAX_BYTES, ht as atprotoProofRkey, ia as sanitizeMime, in as WALLET_NETWORKS, io as readAppRevocations, ir as GROUP_WANT_NO_FRAME, it as sshSignCommand, ji as IdentityCheckUnavailable, jn as isPublicAddress, jo as answersOffer, jt as didSignedMessages, kn as domainWellKnownFile, ko as CALL_SIGNAL_MAX_AGE_MS, kt as didGhostlyServiceEntry, l as APP_BUNDLE_LIMITS, la as parseVoiceMeta, ln as walletNetworkOf, lr as readGroupHave, lt as ATPROTO_DECLARED_SCOPE, m as assembleAppBundle, ma as RateWindow, mt as atprotoProofRecord, n as appStoreDecision, nc as inviteLink, nl as utf8Decode, nr as GROUP_HAVE_FRAME, nt as parseSshSignature, o as checkAppStoreIndex, oa as VOICE_LIMITS, oo as signAppRevocation, or as groupFileFallback, ot as verifyBitcoinMessage, pc as seedSigner, pi as BUTTONS_CAPABILITY, pt as atprotoDidMethod, qi as irohRelayUrl, ra as sanitizeFileName, rl as utf8Encode, rt as sameSshKey, s as readAppStore, sa as baseMime, so as canonicalJson, sr as isGroupMessageId, st as BitcoinAddressError, tl as toZ32, tr as GROUP_FILE_LIMITS, tt as parseSshPublicKey, ua as voicePeaksOf, ur as readGroupWant, ut as ATPROTO_FULL_SCOPE, vn as newPubkyProofFolder, vt as normalizeAtprotoHandle, wn as pubkyHomeserverOf, wt as DID_FILE_MAX_BYTES, xn as pubkyProofFile, xt as AtprotoProofError, yn as normalizePubkyKey, yt as parseAtprotoDidDocument, za as buttonLabelClash, zc as RELAY_PAYLOAD_MAX_BYTES, zn as requestLnurlInvoice, zr as ChatFiles, zt as parseDidDocument } from "./assets/src-aKGjb_yz.js";
4
4
  import { c as fileStore, f as store$1, i as STORES, m as wrap, o as databaseName, p as transact, u as openDb } from "./assets/idb-CTF6LF5B.js";
5
5
  import { a as fileBytesOf, i as fileBytes, l as FILE_BYTES_STEP } from "./assets/fileBytes-BcYZhNgF.js";
6
6
  import { createRequire } from "node:module";
@@ -14,11 +14,11 @@ import { schnorr } from "@noble/curves/secp256k1.js";
14
14
  import { wordlist } from "@scure/bip39/wordlists/english.js";
15
15
  import { access, constants, link, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
16
16
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
17
+ import { createHash, createPrivateKey, generateKeyPairSync, randomBytes, timingSafeEqual } from "node:crypto";
17
18
  import { decode, npubEncode } from "nostr-tools/nip19";
18
19
  import { BunkerSigner } from "nostr-tools/nip46";
19
20
  import { SimplePool } from "nostr-tools/pool";
20
21
  import { generateSecretKey } from "nostr-tools/pure";
21
- import { createHash, createPrivateKey, generateKeyPairSync, randomBytes, timingSafeEqual } from "node:crypto";
22
22
  import { createServer as createServer$1 } from "node:http";
23
23
  import { Readable } from "node:stream";
24
24
  import { pipeline } from "node:stream/promises";
@@ -35,7 +35,7 @@ import { IDBFactory, IDBKeyRange as IDBKeyRange$1, IDBTransaction } from "fake-i
35
35
  import { format } from "node:util";
36
36
  var package_default = {
37
37
  name: "@ghostlytools/cli",
38
- version: "1.2.0",
38
+ version: "1.2.1",
39
39
  type: "module",
40
40
  description: "Ghostly for bots and terminals: the app's own engine, headless on Node, behind a daemon and a `ghostly` command with JSON output.",
41
41
  license: "MIT",
@@ -139,7 +139,7 @@ var CliError = class extends Error {
139
139
  /** Anything thrown, as the error a bot sees: the engine's own refusals keep their words. */
140
140
  function asCliError(error) {
141
141
  if (error instanceof CliError) return error;
142
- if (error && typeof error === "object" && "code" in error && "message" in error && typeof error.code === "string" && error.code in EXIT) return new CliError(error.code, String(error.message));
142
+ if (error && typeof error === "object" && "code" in error && "message" in error && typeof error.code === "string" && Object.hasOwn(EXIT, error.code)) return new CliError(error.code, String(error.message));
143
143
  const local = localFileError(error);
144
144
  if (local) return local;
145
145
  const unopened = profileOpenError(error);
@@ -291,7 +291,9 @@ function globalOption(arg) {
291
291
  }
292
292
  /**
293
293
  * Global options given before the command (`ghostly --profile bot send …`) move after it, where every command's
294
- * parser reads them. Stops at `--` and at the first word that is not a global option.
294
+ * parser reads them. Stops at `--` and at the first word that is not a global option. They go before the first word
295
+ * after the command's that starts with a dash (its first option, or the `--` that ends them), else at the end: never
296
+ * between an option and its value, which may itself be `--` (`--pretty profile set --name --`).
295
297
  */
296
298
  function liftGlobals(argv) {
297
299
  const lifted = [];
@@ -306,11 +308,11 @@ function liftGlobals(argv) {
306
308
  i += takesValue ? 2 : 1;
307
309
  }
308
310
  const rest = argv.slice(i);
309
- const end = rest.indexOf("--");
310
- return end === -1 ? [...rest, ...lifted] : [
311
- ...rest.slice(0, end),
311
+ const dash = rest[0] === "--" ? 0 : rest.findIndex((word, i) => i > 0 && word.startsWith("-"));
312
+ return dash === -1 ? [...rest, ...lifted] : [
313
+ ...rest.slice(0, dash),
312
314
  ...lifted,
313
- ...rest.slice(end)
315
+ ...rest.slice(dash)
314
316
  ];
315
317
  }
316
318
  //#endregion
@@ -814,64 +816,104 @@ var CONTACT_KEY = /^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/;
814
816
  * itself (which the contact controls, so it never wins over a label).
815
817
  */
816
818
  function findChat(links, ref) {
819
+ return chatBy(links, ref).link;
820
+ }
821
+ /** `findChat`, and whether only the name the contact gave itself (`theirName`) found the chat. */
822
+ function chatBy(links, ref) {
817
823
  const exact = links.find((link) => link.id === ref);
818
- if (exact) return exact;
824
+ if (exact) return {
825
+ link: exact,
826
+ theirName: false
827
+ };
819
828
  if (CONTACT_KEY.test(ref)) {
820
829
  const byKey = links.filter((link) => link.peerPubKeyZ32 === ref);
821
- if (byKey.length === 1) return byKey[0];
830
+ if (byKey.length === 1) return {
831
+ link: byKey[0],
832
+ theirName: false
833
+ };
822
834
  if (byKey.length > 1) throw new CliError("bad_request", `${JSON.stringify(ref)} names more than one chat: use its id`, { matches: byKey.map((link) => link.id) });
823
835
  throw new CliError("not_found", `No chat with ${JSON.stringify(ref)}`);
824
836
  }
825
837
  const byPrefix = links.filter((link) => link.id.startsWith(ref));
826
- if (byPrefix.length === 1) return byPrefix[0];
838
+ if (byPrefix.length === 1) return {
839
+ link: byPrefix[0],
840
+ theirName: false
841
+ };
827
842
  const lower = ref.toLowerCase();
828
843
  const named = (name) => name?.trim().toLowerCase() === lower;
829
844
  const byLabel = links.filter((link) => named(link.label));
830
845
  const byName = byLabel.length ? byLabel : links.filter((link) => named(link.peerNick));
831
- if (byName.length === 1) return byName[0];
846
+ if (byName.length === 1) return {
847
+ link: byName[0],
848
+ theirName: !byLabel.length
849
+ };
832
850
  if (byPrefix.length > 1 || byName.length > 1) throw new CliError("bad_request", `${JSON.stringify(ref)} names more than one chat: use its id`, { matches: [...byPrefix, ...byName].map((link) => link.id) });
833
851
  throw new CliError("not_found", `No chat ${JSON.stringify(ref)}`);
834
852
  }
853
+ /** Only an invitation so far: this profile is not in the group (yet), and whoever invited chose its name. */
854
+ var invitedOnly = (group) => group.status === void 0 && !!group.invitation;
855
+ /**
856
+ * A group by its id, by a unique prefix of its id, or by its name. A group this profile is in wins over an invitation
857
+ * with the same prefix or name (the one who invites chooses those); an invitation is still found by its id, and by
858
+ * prefix or name where no group of this profile has it.
859
+ */
835
860
  function findGroup(groups, ref) {
836
861
  const exact = groups.find((group) => group.id === ref);
837
862
  if (exact) return exact;
838
- const byPrefix = groups.filter((group) => group.id.startsWith(ref));
863
+ const mineFirst = (found) => {
864
+ const mine = found.filter((group) => !invitedOnly(group));
865
+ return mine.length ? mine : found;
866
+ };
867
+ const byPrefix = mineFirst(groups.filter((group) => group.id.startsWith(ref)));
839
868
  if (byPrefix.length === 1) return byPrefix[0];
840
- const byName = groups.filter((group) => group.name.trim().toLowerCase() === ref.toLowerCase());
869
+ const byName = mineFirst(groups.filter((group) => group.name.trim().toLowerCase() === ref.toLowerCase()));
841
870
  if (byName.length === 1) return byName[0];
842
871
  if (byPrefix.length > 1 || byName.length > 1) throw new CliError("bad_request", `${JSON.stringify(ref)} names more than one group: use its id`, { matches: [...byPrefix, ...byName].map((group) => group.id) });
843
872
  throw new CliError("not_found", `No group ${JSON.stringify(ref)}`);
844
873
  }
845
- var chatOf = (ctx, params) => findChat(state(ctx).links, str(params, "chat", true));
846
874
  /**
847
- * A chat or a group, as `forward` names them: `group:<id>` is a group; anything else a chat first (id, prefix or name),
848
- * then a group. `linkId` is what the engine calls it.
875
+ * A chat or a group, where a command takes either: `group:<id>` is a group; anything else a chat first (`findChat`),
876
+ * then a group. A chat found only by the name its contact gave itself never takes what also names a group: a group's
877
+ * own id is that group, and a prefix or the name of a group this profile is in is refused, with both ids. With neither,
878
+ * the error says what `findChat` would.
849
879
  */
850
- function chatOrGroup(ctx, ref) {
851
- if (ref.startsWith("group:")) {
852
- const group = findGroup(state(ctx).groups, ref.slice(6));
853
- return {
854
- id: group.id,
855
- linkId: `group:${group.id}`,
856
- group: true
857
- };
858
- }
880
+ function findChatOrGroup(links, groups, ref) {
881
+ if (ref.startsWith("group:")) return { group: findGroup(groups, ref.slice(6)) };
882
+ let chat;
859
883
  try {
860
- const link = findChat(state(ctx).links, ref);
861
- return {
862
- id: link.id,
863
- linkId: link.id,
864
- group: false
865
- };
884
+ chat = chatBy(links, ref);
866
885
  } catch (error) {
867
886
  if (!(error instanceof CliError) || error.code !== "not_found") throw error;
868
- const group = findGroup(state(ctx).groups, ref);
869
- return {
870
- id: group.id,
871
- linkId: `group:${group.id}`,
872
- group: true
873
- };
887
+ try {
888
+ return { group: findGroup(groups, ref) };
889
+ } catch (inner) {
890
+ throw inner instanceof CliError && inner.code !== "not_found" ? inner : error;
891
+ }
874
892
  }
893
+ if (!chat.theirName) return { link: chat.link };
894
+ const exact = groups.find((group) => group.id === ref);
895
+ if (exact) return { group: exact };
896
+ const lower = ref.toLowerCase();
897
+ const also = groups.filter((group) => !invitedOnly(group) && (group.id.startsWith(ref) || group.name.trim().toLowerCase() === lower));
898
+ if (also.length) throw new CliError("bad_request", `${JSON.stringify(ref)} names a chat (the name its contact chose) and a group: use the chat's id or group:<id>`, {
899
+ chat: chat.link.id,
900
+ groups: also.map((group) => group.id)
901
+ });
902
+ return { link: chat.link };
903
+ }
904
+ var chatOf = (ctx, params) => findChat(state(ctx).links, str(params, "chat", true));
905
+ /** A chat or a group as `findChatOrGroup` reads it. `linkId` is what the engine calls it. */
906
+ function chatOrGroup(ctx, ref) {
907
+ const found = findChatOrGroup(state(ctx).links, state(ctx).groups, ref);
908
+ return "group" in found ? {
909
+ id: found.group.id,
910
+ linkId: `group:${found.group.id}`,
911
+ group: true
912
+ } : {
913
+ id: found.link.id,
914
+ linkId: found.link.id,
915
+ group: false
916
+ };
875
917
  }
876
918
  var groupOf = (ctx, params) => findGroup(state(ctx).groups, str(params, "group", true));
877
919
  /** Resolves with `check`'s first defined answer: now, or on a later state; `timeout` error after `ms`. */
@@ -2375,24 +2417,8 @@ function filesOf(messages, transfers) {
2375
2417
  transfer: transfers[m.file.id] ?? null
2376
2418
  }));
2377
2419
  }
2378
- /**
2379
- * The chat a file command names, or else a group (`group:<id>`, or a group's id, prefix or name where no chat has it),
2380
- * as `forward` reads them. A name that is neither says what a chat would.
2381
- */
2382
- function chatOrGroupOf(ctx, params) {
2383
- const ref = str(params, "chat", true);
2384
- if (ref.startsWith("group:")) return { group: findGroup(state(ctx).groups, ref.slice(6)) };
2385
- try {
2386
- return { link: chatOf(ctx, params) };
2387
- } catch (error) {
2388
- if (!(error instanceof CliError) || error.code !== "not_found") throw error;
2389
- try {
2390
- return { group: findGroup(state(ctx).groups, ref) };
2391
- } catch {
2392
- throw error;
2393
- }
2394
- }
2395
- }
2420
+ /** The chat or the group a file command names (`findChatOrGroup`). */
2421
+ var chatOrGroupOf = (ctx, params) => findChatOrGroup(state(ctx).links, state(ctx).groups, str(params, "chat", true));
2396
2422
  /**
2397
2423
  * The chat or group a file belongs to: a file id starts with its chat's id (a group's file with `group-<id>-`), so it
2398
2424
  * may be left out; given, it must be the file's own. `where` is what the answer names it by.
@@ -2529,6 +2555,15 @@ function fitName(name) {
2529
2555
  while (stem.length && !fits(stem.join("").trimEnd() + extension)) stem.pop();
2530
2556
  return (stem.join("").trimEnd() || "file") + extension;
2531
2557
  }
2558
+ /**
2559
+ * The name `file save --dir` writes a file under. Its sender chose it, and the folder is one scripts and people run
2560
+ * tools in: a name that starts with a dash is an option to the tool a `*` hands it to ("-rf", "--checkpoint-action=…"),
2561
+ * so it gets a `_` first, as a Windows device name does in `sanitizeFileName`.
2562
+ */
2563
+ function saveName(name) {
2564
+ const clean = sanitizeFileName(name);
2565
+ return fitName(clean.startsWith("-") ? `_${clean}` : clean);
2566
+ }
2532
2567
  var ACTIONS = [
2533
2568
  "accept",
2534
2569
  "decline",
@@ -2717,7 +2752,7 @@ var FILE_METHODS = {
2717
2752
  size: transfer.size
2718
2753
  };
2719
2754
  },
2720
- /** Writes a received (or sent) file to disk: into `dir` under its own name, or to `path`. Never over a file unless `force`. */
2755
+ /** Writes a received (or sent) file to disk: into `dir` under its own name (`saveName`), or to `path`. Never over a file unless `force`. */
2721
2756
  async "file.save"(ctx, params) {
2722
2757
  const fileId = str(params, "file", true);
2723
2758
  if (params.chat !== void 0) fileChat(ctx, params, fileId);
@@ -2730,7 +2765,7 @@ var FILE_METHODS = {
2730
2765
  if (stored.leftOut) throw new CliError("not_found", "Not in this backup: the light backup this profile was restored from left the file out");
2731
2766
  const transfer = state(ctx).transfers[fileId] ?? stored.transfer;
2732
2767
  if (transfer && transfer.state !== "done") throw new CliError("unavailable", `The file is not all here yet (${transfer.transferred} of ${transfer.size} bytes)`);
2733
- const target = str(params, "path") ? resolve(str(params, "path", true)) : join(resolve(str(params, "dir") ?? "."), fitName(sanitizeFileName(stored.metadata.name)));
2768
+ const target = str(params, "path") ? resolve(str(params, "path", true)) : join(resolve(str(params, "dir") ?? "."), saveName(stored.metadata.name));
2734
2769
  const flags = bool(params, "force") ? "w" : "wx";
2735
2770
  let out;
2736
2771
  try {
@@ -2922,12 +2957,74 @@ function checked$1(card) {
2922
2957
  if ("error" in result) throw new CliError("bad_request", `Status card: ${result.error}`, { card });
2923
2958
  return result.card;
2924
2959
  }
2925
- /** The latest message of mine in this chat or group with a card of this kind and id: the one an update edits. */
2960
+ /** Every text in a card's fields (title, step, items, runs, links…), as the contact's app may show it. */
2961
+ function cardTexts(value, out = []) {
2962
+ if (typeof value === "string") out.push(value);
2963
+ else if (Array.isArray(value)) for (const item of value) cardTexts(item, out);
2964
+ else if (isObject(value)) for (const item of Object.values(value)) cardTexts(item, out);
2965
+ return out;
2966
+ }
2967
+ /**
2968
+ * The secret guard `send` has (docs/CLI.md § Safety): a card whose text looks like a seed, a private key or a Cashu
2969
+ * token goes only with `--force`. `fields` are what this command gives (an update's own, not the card it merges over).
2970
+ * A card's text is read as given and as the card carries it (`cardLine`): a line loses its invisible characters on the
2971
+ * way, so a key one of them broke in two is whole again on the card. The fallback text goes as it is, as `send`'s.
2972
+ */
2973
+ function guardSecrets(fields, params, verb) {
2974
+ if (bool(params, "force")) return;
2975
+ const check = (text) => {
2976
+ const secret = findSecret(text);
2977
+ if (secret) throw new CliError("confirm", `The card's text looks like ${secret.kind === "cashu" ? "a Cashu token (money anyone who reads it can take)" : "a secret (a seed or a private key)"}; ${verb} it with --force if you mean to`, { kind: secret.kind });
2978
+ };
2979
+ for (const text of cardTexts([fields, params.run])) {
2980
+ check(text);
2981
+ check(cardLine(text, Infinity) ?? "");
2982
+ }
2983
+ if (typeof params.text === "string") check(params.text);
2984
+ }
2985
+ /** Per card (its chat or group, its kind and its id): the message of mine that carries it, as this process last saw it. */
2986
+ var carriers = /* @__PURE__ */ new WeakMap();
2987
+ var carrierKey = (linkId, kind, id) => `${linkId}\n${kind}\n${id}`;
2988
+ function carriersOf(ctx) {
2989
+ let map = carriers.get(ctx);
2990
+ if (map) return map;
2991
+ carriers.set(ctx, map = /* @__PURE__ */ new Map());
2992
+ const cards = map;
2993
+ ctx.hub.onEvent((event) => {
2994
+ if (event.type !== "message.sent" && event.type !== "group.sent") return;
2995
+ const message = event.message;
2996
+ const card = message?.card;
2997
+ if (typeof message?.id !== "string" || typeof card?.id !== "string" || card.kind !== "task" && card.kind !== "routine" && card.kind !== "usage") return;
2998
+ carried(cards, carrierKey(event.type === "group.sent" ? `group:${String(event.group)}` : String(event.chat), card.kind, card.id), message.id);
2999
+ });
3000
+ return map;
3001
+ }
3002
+ function carried(cards, key, messageId) {
3003
+ cards.delete(key);
3004
+ cards.set(key, messageId);
3005
+ if (cards.size > 1e3) cards.delete(cards.keys().next().value);
3006
+ }
3007
+ /**
3008
+ * The latest message of mine in this chat or group with a card of this kind and id: the one an update edits. A card
3009
+ * this process sent or found before is read by its message's key, so its updates cost the same in a long chat as in a
3010
+ * short one; only a card not seen yet (the first update after a start, a one-shot) is looked for in the whole history.
3011
+ */
2926
3012
  async function cardMessage(ctx, target, kind, id) {
3013
+ const cards = carriersOf(ctx), key = carrierKey(target.linkId, kind, id);
3014
+ const carries = (message) => message?.sender === "me" && message.card?.kind === kind && message.card.id === id;
3015
+ const known = cards.get(key);
3016
+ if (known !== void 0) {
3017
+ const message = await node(ctx).getMessage(target.linkId, known);
3018
+ if (carries(message)) return message;
3019
+ cards.delete(key);
3020
+ }
2927
3021
  const messages = target.group ? await node(ctx).groupMessages({ groupId: target.id }) : await node(ctx).getMessages(target.linkId);
3022
+ if (cards.has(key)) return cardMessage(ctx, target, kind, id);
2928
3023
  for (let i = messages.length - 1; i >= 0; i--) {
2929
3024
  const message = messages[i];
2930
- if (message.sender === "me" && message.card?.kind === kind && message.card.id === id) return message;
3025
+ if (!carries(message)) continue;
3026
+ carried(cards, key, message.id);
3027
+ return message;
2931
3028
  }
2932
3029
  throw new CliError("not_found", `No ${kind} ${JSON.stringify(id)} of yours in this ${target.group ? "group" : "chat"}: send it first with ghostly ${kind} send`);
2933
3030
  }
@@ -2955,6 +3052,7 @@ async function parentThere(ctx, target, card) {
2955
3052
  async function sendCard(ctx, params, kind) {
2956
3053
  const target = chatOrGroup(ctx, str(params, "chat", true));
2957
3054
  const fields = fieldsOf(params);
3055
+ guardSecrets(fields, params, "send");
2958
3056
  const now = Date.now();
2959
3057
  const id = typeof fields.id === "string" && fields.id ? fields.id : `${kind}-${toBase64Url(randomBytes$1(6))}`;
2960
3058
  const card = checked$1(withRun({
@@ -2985,6 +3083,7 @@ async function sendCard(ctx, params, kind) {
2985
3083
  card
2986
3084
  });
2987
3085
  if (result.error || !result.messageId) throw new CliError(result.refused ? "refused" : "unavailable", result.error ?? "Nothing to send");
3086
+ carried(carriersOf(ctx), carrierKey(target.linkId, kind, id), result.messageId);
2988
3087
  const edges = wait === "sent" ? await waitForGroupFrame(ctx, target.id, result.messageId, void 0, ms) : void 0;
2989
3088
  return answer(target, kind, id, result.messageId, {
2990
3089
  card,
@@ -2998,6 +3097,7 @@ async function sendCard(ctx, params, kind) {
2998
3097
  });
2999
3098
  if (result.error) throw new CliError(result.refused ? "refused" : "unavailable", result.error);
3000
3099
  if (!result.messageId) throw new CliError("bad_request", "Nothing to send");
3100
+ carried(carriersOf(ctx), carrierKey(target.linkId, kind, id), result.messageId);
3001
3101
  let message = await node(ctx).getMessage(target.linkId, result.messageId);
3002
3102
  if (wait !== "none") message = await waitForMessage(ctx, target.linkId, result.messageId, wait, ms);
3003
3103
  return answer(target, kind, id, result.messageId, {
@@ -3024,6 +3124,7 @@ async function updateCard(ctx, params, kind) {
3024
3124
  const target = chatOrGroup(ctx, str(params, "chat", true));
3025
3125
  const id = str(params, kind, true);
3026
3126
  const patch = fieldsOf(params);
3127
+ guardSecrets(patch, params, "update");
3027
3128
  const message = await cardMessage(ctx, target, kind, id);
3028
3129
  if ((message.edit?.seq ?? 0) >= STATUS_CARD_LIMITS.edits) throw new CliError("refused", `This ${kind}'s message took ${STATUS_CARD_LIMITS.edits} updates, the most one takes: start a new card with ghostly ${kind} send (a new --id, or the same one: the newest card of an id stands for it)`, {
3029
3130
  messageId: message.id,
@@ -3065,7 +3166,7 @@ async function updateCard(ctx, params, kind) {
3065
3166
  pace.last = Date.now();
3066
3167
  await editCard(ctx, target, message.id, card, text);
3067
3168
  }
3068
- let edited = target.group ? (await node(ctx).groupMessages({ groupId: target.id })).find((m) => m.id === message.id) : await node(ctx).getMessage(target.linkId, message.id);
3169
+ let edited = await node(ctx).getMessage(target.linkId, message.id);
3069
3170
  if (wait === "confirmed" && edited?.edit?.pending) edited = await waitForEdit(ctx, target.linkId, message.id, ms);
3070
3171
  const edges = wait === "sent" && edited?.edit ? await waitForGroupFrame(ctx, target.id, message.id, edited.edit.seq, ms) : void 0;
3071
3172
  return answer(target, kind, id, message.id, {
@@ -3272,6 +3373,7 @@ async function updateButtons(ctx, params) {
3272
3373
  edits: message.edit.seq
3273
3374
  });
3274
3375
  const before = message.card;
3376
+ if (chosen !== void 0 && !before.buttons.some((b) => b.id === chosen)) throw new CliError("bad_request", `That message has no button ${JSON.stringify(chosen)}; its buttons: ${before.buttons.map((b) => b.id).join(", ")}`, { card: before });
3275
3377
  const card = checked({
3276
3378
  ...before,
3277
3379
  ...chosen !== void 0 && { chosen },
@@ -3669,10 +3771,15 @@ function groupJson(group, showSecret = false) {
3669
3771
  * Group administration and pictures (WISP 1100, phase 3): what the app's group menu and profile screen do. The
3670
3772
  * engine enforces who may do what (the admin); these check the arguments and name members.
3671
3773
  */
3672
- /** A member by key, unique key prefix or name; never this profile itself. */
3774
+ /**
3775
+ * A member by key, unique key prefix or name; never this profile itself. A whole key is that member, whatever another
3776
+ * member calls itself; a prefix and a name stand side by side (two members is an error, not a guess).
3777
+ */
3673
3778
  function findMember(group, ref) {
3674
3779
  const lower = ref.toLowerCase();
3675
- const matches = group.members.filter((m) => !m.me && (m.key === ref || m.key.startsWith(ref) || m.nick?.trim().toLowerCase() === lower));
3780
+ const others = group.members.filter((m) => !m.me);
3781
+ const byKey = others.filter((m) => m.key === ref);
3782
+ const matches = byKey.length ? byKey : others.filter((m) => m.key.startsWith(ref) || m.nick?.trim().toLowerCase() === lower);
3676
3783
  if (matches.length === 1) return matches[0];
3677
3784
  throw new CliError(matches.length ? "bad_request" : "not_found", matches.length ? `${JSON.stringify(ref)} names more than one member` : `No member ${JSON.stringify(ref)} in ${group.name}`);
3678
3785
  }
@@ -3789,6 +3896,63 @@ var GROUP_ADMIN_METHODS = {
3789
3896
  }
3790
3897
  };
3791
3898
  //#endregion
3899
+ //#region src/turnRefs.ts
3900
+ /**
3901
+ * The id an agent turn names its message by (docs/CLI.md § Agent turns). A message is stored under an id its sender
3902
+ * chose most of (`peer_<the id they sent>`), so a turn carries this one instead, made here from the chat (or group) and
3903
+ * that id: `t_` and 32 hex digits, in which nobody can write a word. Every command that takes a message's id takes it.
3904
+ */
3905
+ function turnRef(where, messageId) {
3906
+ return "t_" + createHash("sha256").update(`${where}\n${messageId}`).digest("hex").slice(0, 32);
3907
+ }
3908
+ var TURN_REF = /^t_[0-9a-f]{32}$/;
3909
+ var isRef = (value) => typeof value === "string" && TURN_REF.test(value);
3910
+ /**
3911
+ * `params` with each turn's id in `message`, `reply` and `messages` as the id its message is stored under: looked for
3912
+ * among the chat's (or group's) newest messages first, where a turn's almost always is, then in all of them. One that
3913
+ * names no message stays as given, and its command says so as for any unknown id.
3914
+ */
3915
+ async function withStoredIds(ctx, params) {
3916
+ const wanted = new Set([
3917
+ params.message,
3918
+ params.reply,
3919
+ ...Array.isArray(params.messages) ? params.messages : []
3920
+ ].filter(isRef));
3921
+ if (!wanted.size) return params;
3922
+ let target = null;
3923
+ try {
3924
+ if (typeof params.group === "string") {
3925
+ const group = findGroup(state(ctx).groups, params.group);
3926
+ target = {
3927
+ id: group.id,
3928
+ linkId: `group:${group.id}`,
3929
+ group: true
3930
+ };
3931
+ } else if (typeof params.chat === "string") target = chatOrGroup(ctx, params.chat);
3932
+ } catch {}
3933
+ if (!target) return params;
3934
+ const { id, linkId, group } = target;
3935
+ const found = /* @__PURE__ */ new Map();
3936
+ const look = (messages) => {
3937
+ for (const message of messages) {
3938
+ const ref = turnRef(id, message.id);
3939
+ if (wanted.has(ref)) found.set(ref, message.id);
3940
+ }
3941
+ return found.size === wanted.size;
3942
+ };
3943
+ if (!look((await node(ctx).messagePage({
3944
+ linkId,
3945
+ limit: 200
3946
+ })).messages)) look(group ? await node(ctx).groupMessages({ groupId: id }) : await node(ctx).getMessages(linkId));
3947
+ const stored = (value) => isRef(value) ? found.get(value) ?? value : value;
3948
+ return {
3949
+ ...params,
3950
+ ...params.message !== void 0 && { message: stored(params.message) },
3951
+ ...params.reply !== void 0 && { reply: stored(params.reply) },
3952
+ ...Array.isArray(params.messages) && { messages: params.messages.map(stored) }
3953
+ };
3954
+ }
3955
+ //#endregion
3792
3956
  //#region ../browser/src/proofs/bitcoinWallets.ts
3793
3957
  /** What a proof proves and what it does not; shown next to every Bitcoin proof, given or received. */
3794
3958
  var BITCOIN_PROOF_LIMITS = [
@@ -12504,7 +12668,7 @@ var METHODS = {
12504
12668
  async "settings.set"(ctx, params) {
12505
12669
  const patch = {};
12506
12670
  for (const [key, value] of Object.entries(params)) {
12507
- const kind = SETTABLE[key];
12671
+ const kind = Object.hasOwn(SETTABLE, key) ? SETTABLE[key] : void 0;
12508
12672
  if (!kind) throw new CliError("bad_request", `${key} is not a setting the CLI changes (${Object.keys(SETTABLE).join(", ")})`);
12509
12673
  if (kind === "strings" && !(Array.isArray(value) && value.every((v) => typeof v === "string"))) throw new CliError("bad_request", `${key} must be a list of strings`);
12510
12674
  if (kind === "string" && typeof value !== "string") throw new CliError("bad_request", `${key} must be a string`);
@@ -12826,7 +12990,8 @@ var METHODS = {
12826
12990
  },
12827
12991
  async "chat.transport"(ctx, params) {
12828
12992
  const link = chatOf(ctx, params);
12829
- const transport = TRANSPORT_NAMES[str(params, "transport", true)];
12993
+ const name = str(params, "transport", true);
12994
+ const transport = Object.hasOwn(TRANSPORT_NAMES, name) ? TRANSPORT_NAMES[name] : void 0;
12830
12995
  if (!transport) throw new CliError("bad_request", `transport must be one of auto, dht, webrtc, iroh, hyperdht`);
12831
12996
  await node(ctx).setChatTransport({
12832
12997
  linkId: link.id,
@@ -13209,12 +13374,12 @@ function history(messages, params, json = messageJson) {
13209
13374
  };
13210
13375
  }
13211
13376
  Object.keys(METHODS);
13212
- /** Runs one method of the API. */
13377
+ /** Runs one method of the API: one the table lists itself (`constructor`, which every object has, is none). */
13213
13378
  async function callApi(ctx, method, params = {}) {
13214
- const run = METHODS[method];
13379
+ const run = Object.hasOwn(METHODS, method) ? METHODS[method] : void 0;
13215
13380
  if (!run) throw new CliError("not_found", `No method ${method}`);
13216
13381
  if (params === null || typeof params !== "object" || Array.isArray(params)) throw new CliError("bad_request", "params must be an object");
13217
- return run(ctx, params);
13382
+ return run(ctx, await withStoredIds(ctx, params));
13218
13383
  }
13219
13384
  //#endregion
13220
13385
  //#region src/privateFolder.ts
@@ -14318,6 +14483,17 @@ var showSecret = { "show-secret": {
14318
14483
  description: "Print the group's entry link (anyone who has it can join)"
14319
14484
  } };
14320
14485
  var secret = (options) => options["show-secret"] === true ? { showSecret: true } : {};
14486
+ /**
14487
+ * `--for` of the typing commands: the seconds it is kept on, 1 to 600, and never with `--stop`. A usage error (exit 2)
14488
+ * that names the flag: before, the API's own check answered ("for must be a number from 1 to 600", exit 1).
14489
+ */
14490
+ function typingFor(options) {
14491
+ const seconds = options.for;
14492
+ if (seconds === void 0) return void 0;
14493
+ 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)}`);
14494
+ if (options.stop === true) throw new CliError("usage", "--for keeps it on: not with --stop");
14495
+ return seconds;
14496
+ }
14321
14497
  /** A history cursor: a timestamp when it is all digits, else a message id. */
14322
14498
  function cursor(value) {
14323
14499
  if (value === void 0) return void 0;
@@ -14359,6 +14535,10 @@ var cardOptions = {
14359
14535
  text: {
14360
14536
  type: "string",
14361
14537
  description: "The text older apps show instead (default: written from the card)"
14538
+ },
14539
+ force: {
14540
+ type: "boolean",
14541
+ description: "Send even if the card's text looks like a seed, a key or ecash"
14362
14542
  }
14363
14543
  };
14364
14544
  /** `--link label=https://…` or a bare link, as a card's links. */
@@ -14392,7 +14572,26 @@ function itemOf(value) {
14392
14572
  text: value.slice(colon + 1).trim()
14393
14573
  };
14394
14574
  }
14395
- /** A task's fields from its flags; what was not given is left out, so an update changes only what it names. */
14575
+ /** `--progress`, `--left`: a percent, or a usage error that names the flag. */
14576
+ function percent(options, flag) {
14577
+ const value = options[flag];
14578
+ 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)}`);
14579
+ }
14580
+ /** The flags of a task's pull request, and the field of `pr` each one gives. */
14581
+ var PR_FLAGS = [
14582
+ ["pr-url", "url"],
14583
+ ["pr-number", "number"],
14584
+ ["additions", "additions"],
14585
+ ["deletions", "deletions"],
14586
+ ["files", "files"],
14587
+ ["pr-state", "state"],
14588
+ ["pr-checks", "checks"]
14589
+ ];
14590
+ /**
14591
+ * 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
14592
+ * got wrong is a usage error (exit 2) that names the flag: before, it reached the card's own check and failed as
14593
+ * `bad_request` (exit 1) in the card's words ("Status card: done is at most total").
14594
+ */
14396
14595
  function taskFields(options) {
14397
14596
  const fields = { ...cardJson(options.json) };
14398
14597
  const set = (key, value) => {
@@ -14401,6 +14600,7 @@ function taskFields(options) {
14401
14600
  set("id", options.id);
14402
14601
  set("title", options.title);
14403
14602
  set("status", options.status);
14603
+ percent(options, "progress");
14404
14604
  set("progress", options.progress);
14405
14605
  set("step", options.step);
14406
14606
  set("branch", options.branch);
@@ -14409,22 +14609,18 @@ function taskFields(options) {
14409
14609
  if (!match) throw new CliError("usage", `--steps takes done/total, like 2/5, not ${JSON.stringify(options.steps)}`);
14410
14610
  fields.done = Number(match[1]);
14411
14611
  fields.total = Number(match[2]);
14612
+ 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)}`);
14412
14613
  }
14614
+ 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"])}`);
14615
+ 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])}`);
14413
14616
  const pr = {};
14414
- for (const [flag, key] of [
14415
- ["pr-url", "url"],
14416
- ["pr-number", "number"],
14417
- ["additions", "additions"],
14418
- ["deletions", "deletions"],
14419
- ["files", "files"],
14420
- ["pr-state", "state"],
14421
- ["pr-checks", "checks"]
14422
- ]) if (options[flag] !== void 0) pr[key] = options[flag];
14617
+ for (const [flag, key] of PR_FLAGS) if (options[flag] !== void 0) pr[key] = options[flag];
14423
14618
  if (Object.keys(pr).length) fields.pr = {
14424
14619
  ...fields.pr && typeof fields.pr === "object" ? fields.pr : {},
14425
14620
  ...pr
14426
14621
  };
14427
14622
  if (Array.isArray(options.item) && options.item.length) fields.items = options.item.map(itemOf);
14623
+ 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}`);
14428
14624
  if (Array.isArray(options.tag) && options.tag.length) fields.tags = options.tag;
14429
14625
  set("parent", options.parent);
14430
14626
  const links = cardLinks(options.link);
@@ -14441,6 +14637,25 @@ function needs(card, fields, usage) {
14441
14637
  if (missing.length) throw new CliError("usage", `${missing.join(" and ")} ${missing.length > 1 ? "are" : "is"} needed: ghostly ${usage}`);
14442
14638
  return card;
14443
14639
  }
14640
+ /**
14641
+ * A new task's pull request goes by its link: `--pr-state` and the like without `--pr-url` (or `pr.url` in `--json`)
14642
+ * are a usage error that names them. An update's card may have the link already, so only a send asks.
14643
+ */
14644
+ function prLinked(card, options, usage) {
14645
+ const given = PR_FLAGS.filter(([flag]) => options[flag] !== void 0).map(([flag]) => `--${flag}`);
14646
+ if (!given.length || card.pr.url !== void 0) return card;
14647
+ throw new CliError("usage", `${given.join(" and ")} ${given.length > 1 ? "go" : "goes"} with --pr-url: ghostly ${usage}`);
14648
+ }
14649
+ /**
14650
+ * A usage card says how much is left: `--left`, or `--used` with `--limit`, as flags or in `--json`. The one that is
14651
+ * missing is a usage error that names the flag.
14652
+ */
14653
+ function quota(card, usage) {
14654
+ const used = card.used !== void 0;
14655
+ if (used !== (card.limit !== void 0)) throw new CliError("usage", `--${used ? "used" : "limit"} goes with --${used ? "limit" : "used"}: ghostly ${usage}`);
14656
+ if (card.left === void 0 && !used) throw new CliError("usage", `--left is needed (or --used with --limit): ghostly ${usage}`);
14657
+ return card;
14658
+ }
14444
14659
  /** A time a command names: milliseconds, or a date the JavaScript Date reads (`2026-09-30T01:00:00Z`). */
14445
14660
  function cardTime(value, flag) {
14446
14661
  if (value === void 0) return void 0;
@@ -14589,6 +14804,7 @@ function usageWindowOf(value) {
14589
14804
  /** A usage card's fields from its flags (WISP 405 § Usage); `--json` gives any of them, the flags win over it. */
14590
14805
  function usageFields(options) {
14591
14806
  const fields = { ...cardJson(options.json) };
14807
+ percent(options, "left");
14592
14808
  for (const key of [
14593
14809
  "id",
14594
14810
  "left",
@@ -14646,7 +14862,8 @@ var usageOptions = {
14646
14862
  description: "Every 1:1 chat with a contact, instead of one chat"
14647
14863
  },
14648
14864
  json: cardOptions.json,
14649
- text: cardOptions.text
14865
+ text: cardOptions.text,
14866
+ force: cardOptions.force
14650
14867
  };
14651
14868
  //#endregion
14652
14869
  //#region src/commands/calls.ts
@@ -14718,6 +14935,7 @@ var commands$7 = {
14718
14935
  };
14719
14936
  //#endregion
14720
14937
  //#region src/commands/chats.ts
14938
+ var BUTTON_UPDATE = "button update <chat|group> <message> [--chosen <button>] [--close] [--text <text>] [--force] [--wait none|confirmed|sent] [--timeout s]";
14721
14939
  /** Invites, chats and messages: one entry per command, in alphabetical order (test/commands.test.ts checks). */
14722
14940
  var commands$6 = {
14723
14941
  "button press": {
@@ -14746,7 +14964,7 @@ var commands$6 = {
14746
14964
  },
14747
14965
  "button update": {
14748
14966
  method: "button.update",
14749
- usage: "button update <chat|group> <message> [--chosen <button>] [--close] [--text <text>] [--force] [--wait none|confirmed|sent] [--timeout s]",
14967
+ usage: BUTTON_UPDATE,
14750
14968
  summary: "Show the answer on a message's buttons (--chosen), close them, or change its text (the messageId send --button gave)",
14751
14969
  args: ["chat", "message"],
14752
14970
  options: {
@@ -14769,16 +14987,19 @@ var commands$6 = {
14769
14987
  },
14770
14988
  timeout: groupWait.timeout
14771
14989
  },
14772
- params: ({ options }, a) => ({
14773
- chat: a.chat,
14774
- message: a.message,
14775
- chosen: options.chosen,
14776
- close: options.close === true,
14777
- text: options.text,
14778
- force: options.force === true,
14779
- wait: options.wait,
14780
- timeout: options.timeout
14781
- })
14990
+ params: ({ options }, a) => {
14991
+ if (!options.chosen && options.close !== true && !options.text) throw new CliError("usage", `Give --chosen, --close or --text: ghostly ${BUTTON_UPDATE}`);
14992
+ return {
14993
+ chat: a.chat,
14994
+ message: a.message,
14995
+ chosen: options.chosen,
14996
+ close: options.close === true,
14997
+ text: options.text,
14998
+ force: options.force === true,
14999
+ wait: options.wait,
15000
+ timeout: options.timeout
15001
+ };
15002
+ }
14782
15003
  },
14783
15004
  "chat connect": {
14784
15005
  method: "chat.connect",
@@ -14882,10 +15103,13 @@ var commands$6 = {
14882
15103
  type: "string",
14883
15104
  description: "The code the contact reads out"
14884
15105
  } },
14885
- params: ({ options }, { chat }) => ({
14886
- chat,
14887
- code: options.code
14888
- })
15106
+ params: ({ options }, { chat }) => {
15107
+ if (typeof options.code !== "string" || !options.code.trim()) throw new CliError("usage", "--code is needed: ghostly chat verify <chat> --code <code>");
15108
+ return {
15109
+ chat,
15110
+ code: options.code
15111
+ };
15112
+ }
14889
15113
  },
14890
15114
  "chat wait": {
14891
15115
  method: "chat.wait",
@@ -14996,18 +15220,22 @@ var commands$6 = {
14996
15220
  },
14997
15221
  "pin": {
14998
15222
  method: "chat.pin",
14999
- usage: "pin <chat|group> <message> [--remove]",
15223
+ usage: "pin <chat|group> <message> | --remove",
15000
15224
  summary: "Pin a message at the top of a chat or group (one per chat: it replaces the pinned one); --remove unpins",
15001
- args: ["chat", "message"],
15225
+ args: ["chat", "message...?"],
15002
15226
  options: { remove: {
15003
15227
  type: "boolean",
15004
- description: "Unpin the chat's pinned message"
15228
+ description: "Unpin the chat's pinned message (it names no message)"
15005
15229
  } },
15006
- params: ({ options }, a) => ({
15007
- chat: a.chat,
15008
- message: a.message,
15009
- remove: options.remove === true
15010
- })
15230
+ params: ({ options }, a) => {
15231
+ if (a.message === void 0 && options.remove !== true) throw new CliError("usage", "Missing <message>: ghostly pin <chat|group> <message> | --remove");
15232
+ if (a.message?.includes(" ")) throw new CliError("usage", "Too many arguments: ghostly pin <chat|group> <message> | --remove");
15233
+ return {
15234
+ chat: a.chat,
15235
+ message: a.message,
15236
+ remove: options.remove === true
15237
+ };
15238
+ }
15011
15239
  },
15012
15240
  "react": {
15013
15241
  method: "chat.react",
@@ -15031,7 +15259,7 @@ var commands$6 = {
15031
15259
  },
15032
15260
  "routine send": {
15033
15261
  method: "routine.send",
15034
- 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] [--wait none|sent|delivered] [--timeout s]",
15262
+ 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]",
15035
15263
  summary: "Send a routine card (something a bot runs on a schedule: its last run and the next) to a chat or group",
15036
15264
  args: ["chat"],
15037
15265
  options: {
@@ -15048,13 +15276,14 @@ var commands$6 = {
15048
15276
  card: needs(routineFields(options), [["name", "name"], ["schedule", "schedule"]], "routine send <chat|group> --name <name> --schedule \"every day 01:00\""),
15049
15277
  run: runOf(options.run),
15050
15278
  text: options.text,
15279
+ force: options.force === true,
15051
15280
  wait: options.wait,
15052
15281
  timeout: options.timeout
15053
15282
  })
15054
15283
  },
15055
15284
  "routine update": {
15056
15285
  method: "routine.update",
15057
- usage: "routine update <chat|group> <routine> [--run ok|failed|skipped[:summary]] [--next <time>] [--state active|paused] [--schedule ...] [--json json|-|file] [--wait none|confirmed|sent] [--timeout s]",
15286
+ 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]",
15058
15287
  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)",
15059
15288
  args: ["chat", "routine"],
15060
15289
  options: {
@@ -15071,13 +15300,14 @@ var commands$6 = {
15071
15300
  card: routineFields(options),
15072
15301
  run: runOf(options.run),
15073
15302
  text: options.text,
15303
+ force: options.force === true,
15074
15304
  wait: options.wait,
15075
15305
  timeout: options.timeout
15076
15306
  })
15077
15307
  },
15078
15308
  "task send": {
15079
15309
  method: "task.send",
15080
- 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] [--wait none|sent|delivered] [--timeout s]",
15310
+ 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]",
15081
15311
  summary: "Send a task card (a bot's work: status, progress, its PR) to a chat or group; prints its id and message",
15082
15312
  args: ["chat"],
15083
15313
  options: {
@@ -15091,15 +15321,16 @@ var commands$6 = {
15091
15321
  },
15092
15322
  params: ({ options }, { chat }) => ({
15093
15323
  chat,
15094
- card: needs(taskFields(options), [["title", "title"]], "task send <chat|group> --title <title>"),
15324
+ card: prLinked(needs(taskFields(options), [["title", "title"]], "task send <chat|group> --title <title>"), options, "task send <chat|group> --title <title> --pr-url <url>"),
15095
15325
  text: options.text,
15326
+ force: options.force === true,
15096
15327
  wait: options.wait,
15097
15328
  timeout: options.timeout
15098
15329
  })
15099
15330
  },
15100
15331
  "task update": {
15101
15332
  method: "task.update",
15102
- 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] [--wait none|confirmed|sent] [--timeout s]",
15333
+ 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]",
15103
15334
  summary: "Update a task card you sent: the fields given, merged over it (at most one update per 2.5 s; sooner ones merge)",
15104
15335
  args: ["chat", "task"],
15105
15336
  options: {
@@ -15115,6 +15346,7 @@ var commands$6 = {
15115
15346
  task: a.task,
15116
15347
  card: taskFields(options),
15117
15348
  text: options.text,
15349
+ force: options.force === true,
15118
15350
  wait: options.wait,
15119
15351
  timeout: options.timeout
15120
15352
  })
@@ -15147,12 +15379,12 @@ var commands$6 = {
15147
15379
  stop: options.stop === true,
15148
15380
  kind: options.kind,
15149
15381
  status: options.status,
15150
- for: options.for
15382
+ for: typingFor(options)
15151
15383
  })
15152
15384
  },
15153
15385
  "usage send": {
15154
15386
  method: "usage.send",
15155
- 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] [--wait none|sent] [--timeout s]",
15387
+ 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]",
15156
15388
  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",
15157
15389
  args: ["chat?"],
15158
15390
  options: {
@@ -15164,8 +15396,9 @@ var commands$6 = {
15164
15396
  return {
15165
15397
  chat,
15166
15398
  all: options.all === true,
15167
- card: usageFields(options),
15399
+ card: quota(usageFields(options), "usage send <chat|--all> --left <percent>"),
15168
15400
  text: options.text,
15401
+ force: options.force === true,
15169
15402
  wait: options.wait,
15170
15403
  timeout: options.timeout
15171
15404
  };
@@ -15662,7 +15895,7 @@ var commands$3 = {
15662
15895
  stop: options.stop === true,
15663
15896
  kind: options.kind,
15664
15897
  status: options.status,
15665
- for: options.for
15898
+ for: typingFor(options)
15666
15899
  })
15667
15900
  }
15668
15901
  };
@@ -16455,6 +16688,12 @@ var PlaybackQueue = class {
16455
16688
  this.length = 0;
16456
16689
  return ms;
16457
16690
  }
16691
+ /** Drops up to that much from the front, in whole frames (audio whose time went by); returns how much, in milliseconds. */
16692
+ skip(ms) {
16693
+ const frames = Math.min(Math.floor(ms / 20), Math.floor(this.length / this.frame));
16694
+ if (frames > 0) this.take(frames * this.frame);
16695
+ return Math.max(0, frames) * 20;
16696
+ }
16458
16697
  take(bytes) {
16459
16698
  const out = Buffer.allocUnsafe(bytes);
16460
16699
  let filled = 0;
@@ -16682,6 +16921,10 @@ var ReorderBuffer = class {
16682
16921
  }
16683
16922
  }
16684
16923
  };
16924
+ //#endregion
16925
+ //#region src/calls/audio.ts
16926
+ /** The longest stall of the sender whose frames are still sent after it (see `catchUp`). */
16927
+ var MAX_CATCH_UP_MS = 1e3;
16685
16928
  /**
16686
16929
  * One call's audio: every 20 ms a frame from the queue (the program's audio, or silence) is encoded and sent; what
16687
16930
  * comes in is put back in order, decoded, and handed on in 20 ms frames as soon as it is. Nothing waits on a clock
@@ -16699,7 +16942,17 @@ var CallAudio = class {
16699
16942
  timer = null;
16700
16943
  startedAt = 0;
16701
16944
  ticks = 0;
16945
+ /** What was left in the queue after the last frame sent: the program's audio written ahead of the call. */
16946
+ ahead = 0;
16947
+ /** After a stall that was not replayed: how much of its audio is still to drop, down to what queue, and until when. */
16948
+ owed = 0;
16949
+ kept = 0;
16950
+ owedUntil = 0;
16702
16951
  talking = false;
16952
+ /** The last silent frame's packet, how many silent frames in a row gave it, and that packet once it is kept (see `settle`). */
16953
+ lastQuiet = null;
16954
+ same = 0;
16955
+ quiet = null;
16703
16956
  stopped = false;
16704
16957
  /** Frames sent and received, for `call list`. */
16705
16958
  sent = 0;
@@ -16734,16 +16987,40 @@ var CallAudio = class {
16734
16987
  tick() {
16735
16988
  if (this.stopped) return;
16736
16989
  const { frame, silent } = this.queue.next();
16990
+ const still = silent || frame.equals(this.silence);
16737
16991
  let payload;
16738
- try {
16739
- payload = this.options.encoder.encode(frame, this.samples);
16740
- } catch {
16741
- return;
16992
+ if (still && this.quiet) payload = this.quiet;
16993
+ else {
16994
+ try {
16995
+ payload = this.options.encoder.encode(frame, this.samples);
16996
+ } catch {
16997
+ return;
16998
+ }
16999
+ this.settle(still, payload);
16742
17000
  }
16743
17001
  const marker = !silent && !this.talking;
16744
17002
  this.talking = !silent;
16745
17003
  this.options.track.send(this.writer.packet(payload, marker));
16746
17004
  this.sent++;
17005
+ this.ahead = this.queue.queuedMs;
17006
+ }
17007
+ /**
17008
+ * Keeps the packet silence encodes to once the encoder gave it SETTLED_FRAMES times in a row: it is sent again for
17009
+ * the silent frames that follow, without the encoder, until the program's audio is back.
17010
+ */
17011
+ settle(still, payload) {
17012
+ if (!still) {
17013
+ this.lastQuiet = null;
17014
+ this.quiet = null;
17015
+ this.same = 0;
17016
+ return;
17017
+ }
17018
+ if (this.lastQuiet?.equals(payload)) {
17019
+ if (++this.same >= 25) this.quiet = this.lastQuiet;
17020
+ return;
17021
+ }
17022
+ this.lastQuiet = Buffer.from(payload);
17023
+ this.same = 1;
16747
17024
  }
16748
17025
  /** Handles one packet from the track. Exposed for tests. */
16749
17026
  receive(packet) {
@@ -16778,14 +17055,57 @@ var CallAudio = class {
16778
17055
  const due = this.startedAt + (this.ticks + 1) * 20;
16779
17056
  this.timer = setTimeout(() => {
16780
17057
  if (this.stopped) return;
16781
- if (now() - due > 1e3) {
16782
- this.startedAt = now();
16783
- this.ticks = 0;
16784
- } else this.ticks++;
16785
- this.tick();
16786
- this.schedule();
17058
+ if (now() - due < 20) {
17059
+ this.ticks++;
17060
+ this.drop();
17061
+ this.tick();
17062
+ this.schedule();
17063
+ return;
17064
+ }
17065
+ setImmediate(() => {
17066
+ if (this.stopped) return;
17067
+ this.catchUp(now() - due);
17068
+ this.schedule();
17069
+ });
16787
17070
  }, Math.max(0, due - now()));
16788
17071
  }
17072
+ /**
17073
+ * Meets the clock again after the loop was busy: every frame that is due goes out at once, so a loop that turns
17074
+ * slower than a frame for a while (every 40 ms under a large file) keeps up, and a program writing at real time
17075
+ * stays there. The contact's jitter buffer takes the short burst. A stall past MAX_CATCH_UP_MS is not replayed: the
17076
+ * clock starts again and what the program wrote during it is dropped, or it would be heard that much late for the
17077
+ * rest of the call.
17078
+ */
17079
+ catchUp(late) {
17080
+ const now = this.options.now ?? Date.now;
17081
+ if (late > 1e3) {
17082
+ this.owed = late;
17083
+ this.kept = this.ahead;
17084
+ this.owedUntil = now() + MAX_CATCH_UP_MS;
17085
+ this.startedAt = now();
17086
+ this.ticks = 0;
17087
+ this.drop();
17088
+ this.tick();
17089
+ return;
17090
+ }
17091
+ do {
17092
+ this.ticks++;
17093
+ this.drop();
17094
+ this.tick();
17095
+ } while (this.startedAt + (this.ticks + 1) * 20 <= now());
17096
+ }
17097
+ /**
17098
+ * Drops the audio of a stall that was not replayed, as it is read: a long one does not fit the program's socket, so
17099
+ * the rest of it comes over the next turns. What was queued before the stall (a clip written ahead) is kept.
17100
+ */
17101
+ drop() {
17102
+ if (this.owed <= 0) return;
17103
+ if ((this.options.now ?? Date.now)() > this.owedUntil) {
17104
+ this.owed = 0;
17105
+ return;
17106
+ }
17107
+ this.owed -= this.queue.skip(Math.min(this.owed, this.queue.queuedMs - this.kept));
17108
+ }
16789
17109
  };
16790
17110
  //#endregion
16791
17111
  //#region src/calls/media.ts
@@ -17258,6 +17578,8 @@ var CallManager = class {
17258
17578
  this.callable(call.chat);
17259
17579
  call.rate = rateOf(options.rate, call.rate);
17260
17580
  const stack = await this.stack();
17581
+ if (call.ended) throw new CliError("unavailable", "The call ended before it was answered");
17582
+ if (call.state !== "ringing") throw new CliError("refused", "That call is not ringing here");
17261
17583
  call.state = "connecting";
17262
17584
  this.disarm(call);
17263
17585
  try {
@@ -17364,10 +17686,18 @@ var CallManager = class {
17364
17686
  this.calls.set(call.id, call);
17365
17687
  return call;
17366
17688
  }
17367
- /** The call's socket (open from now on: a program may connect before the media is up) and its media. */
17689
+ /**
17690
+ * The call's socket (open from now on: a program may connect before the media is up) and its media. A call that
17691
+ * ended while its socket opened closes it here: `end` found none to close, and nothing lists the call any more.
17692
+ */
17368
17693
  async attach(call, create) {
17369
17694
  const queue = call.queue = new PlaybackQueue(call.rate);
17370
- call.socket = await AudioSocket.open(audioSocketPath(this.host.profileDir, call.id), call.rate, { onAudio: (chunk) => queue.push(chunk) });
17695
+ const socket = await AudioSocket.open(audioSocketPath(this.host.profileDir, call.id), call.rate, { onAudio: (chunk) => queue.push(chunk) });
17696
+ if (call.ended) {
17697
+ await socket.close();
17698
+ return;
17699
+ }
17700
+ call.socket = socket;
17371
17701
  await this.connect(call, create);
17372
17702
  }
17373
17703
  /**
@@ -17375,6 +17705,7 @@ var CallManager = class {
17375
17705
  * the call ended, or asked for a newer connection, while this one was made: then it is closed.
17376
17706
  */
17377
17707
  async connect(call, create) {
17708
+ if (call.ended) return false;
17378
17709
  const attempt = ++call.attempt;
17379
17710
  call.media?.close();
17380
17711
  call.media = null;
@@ -19608,14 +19939,14 @@ async function startRuntime(paths, options = {}) {
19608
19939
  (await import("./assets/ghostly_iroh_web-Dvzh3Y6E.js")).initSync({ module: irohWasmBytes() });
19609
19940
  const [{ registerFileBytes }, { NodeFileBytes }] = await Promise.all([import("./assets/fileBytes-BcYZhNgF.js").then((n) => n.o), import("./assets/fileBytes-Dbifu5I5.js")]);
19610
19941
  registerFileBytes("native", async () => new NodeFileBytes(paths.files), true);
19611
- const [{ EngineServer }, { createHyperEndpoint }] = await Promise.all([import("./assets/server-BGXmSYx_.js").then((n) => n.t), import("./assets/endpoint-pgFkR2AS.js")]);
19942
+ const [{ EngineServer }, { createHyperEndpoint }] = await Promise.all([import("./assets/server-D5FBZYW8.js").then((n) => n.t), import("./assets/endpoint-pgFkR2AS.js")]);
19612
19943
  const network = hyperdhtNetwork();
19613
19944
  const dht = mainlineNetwork();
19614
- const mainline = dht.off ? null : new (await (import("./assets/mainline-BueFJx3f.js"))).Mainline({
19945
+ const mainline = dht.off ? null : new (await (import("./assets/mainline-DVYdrus6.js"))).Mainline({
19615
19946
  bootstrap: dht.bootstrap,
19616
19947
  host: dht.host
19617
19948
  });
19618
- const transport = mainline || pinned ? new (await (import("./assets/mainline-BueFJx3f.js"))).RelaysAndDht(mainline, pinned ? { relays: pinned } : {}, !!pinned) : null;
19949
+ const transport = mainline || pinned ? new (await (import("./assets/mainline-DVYdrus6.js"))).RelaysAndDht(mainline, pinned ? { relays: pinned } : {}, !!pinned) : null;
19619
19950
  const server = new EngineServer({
19620
19951
  ...transport ? { transport } : {},
19621
19952
  irohWeb: true,
@@ -19926,19 +20257,21 @@ function toTurn(event) {
19926
20257
  }
19927
20258
  function turn(event, source, where, message) {
19928
20259
  const file = message.file;
20260
+ const place = where.chat ?? where.group ?? "", messageId = turnRef(place, message.id);
19929
20261
  return {
19930
20262
  seq: event.seq,
19931
- id: `agent.turn:${event.id}`,
20263
+ id: `agent.turn:${source}:${place}:${messageId}`,
19932
20264
  type: "agent.turn",
19933
20265
  at: event.at,
19934
20266
  source,
19935
20267
  ...where,
19936
- messageId: message.id,
20268
+ messageId,
19937
20269
  timestamp: message.timestamp,
19938
20270
  ...message.press ? { press: message.press } : {},
19939
20271
  untrusted: {
19940
20272
  text: message.text,
19941
20273
  name: message.nick ?? null,
20274
+ messageId: message.id,
19942
20275
  ...message.replyTo ? { replyTo: {
19943
20276
  id: message.replyTo.id,
19944
20277
  snippet: message.replyTo.snippet
@@ -20127,7 +20460,14 @@ function requireProfile(g) {
20127
20460
  createProfile(g.home, g.profile);
20128
20461
  return;
20129
20462
  }
20130
- throw new CliError("not_found", `No profile ${g.profile}: make it with \`ghostly profile create ${g.profile}\``);
20463
+ throw noProfile(g);
20464
+ }
20465
+ function noProfile(g) {
20466
+ return new CliError("not_found", `No profile ${g.profile}: make it with \`ghostly profile create ${g.profile}\``);
20467
+ }
20468
+ /** For a command that only reads: a mistyped profile is named as one, and `default` is not made by asking about it. */
20469
+ function knownProfile(g) {
20470
+ if (g.profile !== "default" && !profileExists(g.home, g.profile)) throw noProfile(g);
20131
20471
  }
20132
20472
  /** Said on stderr when the daemon runs another release than this command (it started before an upgrade). */
20133
20473
  function versionWarning(daemon, cli) {
@@ -20591,6 +20931,7 @@ async function daemonCommand(argv) {
20591
20931
  "restart"
20592
20932
  ].includes(sub)) noMoreThan(0, parsed.positionals, sub ? `daemon ${sub}` : "daemon [--detach]");
20593
20933
  if (sub === "status") {
20934
+ knownProfile(g);
20594
20935
  const client = await connectDaemon(g.paths.socket);
20595
20936
  if (!client) {
20596
20937
  print({
@@ -20612,6 +20953,7 @@ async function daemonCommand(argv) {
20612
20953
  return;
20613
20954
  }
20614
20955
  if (sub === "stop") {
20956
+ knownProfile(g);
20615
20957
  const client = await connectDaemon(g.paths.socket);
20616
20958
  if (!client) throw new CliError("not_found", `No daemon runs profile ${g.profile}`);
20617
20959
  print({
@@ -21132,7 +21474,8 @@ async function main(input) {
21132
21474
  }
21133
21475
  const [first, second] = argv;
21134
21476
  const two = second && !second.startsWith("-") ? `${first} ${second}` : "";
21135
- if (first === "profile" && !COMMANDS[two]) {
21477
+ const text = (name) => Object.hasOwn(TEXT_COMMANDS, name), table = (name) => Object.hasOwn(COMMANDS, name);
21478
+ if (first === "profile" && !table(two)) {
21136
21479
  await profileCommand(second, argv.slice(2));
21137
21480
  return 0;
21138
21481
  }
@@ -21160,19 +21503,19 @@ async function main(input) {
21160
21503
  await callPipeCommand(argv.slice(2));
21161
21504
  return -1;
21162
21505
  }
21163
- if (two && TEXT_COMMANDS[two]) {
21506
+ if (two && text(two)) {
21164
21507
  await textCommand(two, argv.slice(2));
21165
21508
  return 0;
21166
21509
  }
21167
- if (two && COMMANDS[two]) {
21510
+ if (two && table(two)) {
21168
21511
  await tableCommand(two, argv.slice(2));
21169
21512
  return 0;
21170
21513
  }
21171
- if (TEXT_COMMANDS[first]) {
21514
+ if (text(first)) {
21172
21515
  await textCommand(first, argv.slice(1));
21173
21516
  return 0;
21174
21517
  }
21175
- if (COMMANDS[first]) {
21518
+ if (table(first)) {
21176
21519
  await tableCommand(first, argv.slice(1));
21177
21520
  return 0;
21178
21521
  }