@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/README.md +11 -8
- package/SKILL.md +13 -5
- package/dist/assets/{arkade-3szZ1IpP.js → arkade-CtCpFaF7.js} +2 -2
- package/dist/assets/{arkade-3szZ1IpP.js.map → arkade-CtCpFaF7.js.map} +1 -1
- package/dist/assets/{hyperdhtRelay-DmTrpmba.js → hyperdhtRelay-C0XRWySP.js} +3 -3
- package/dist/assets/{hyperdhtRelay-DmTrpmba.js.map → hyperdhtRelay-C0XRWySP.js.map} +1 -1
- package/dist/assets/{mainline-BueFJx3f.js → mainline-DVYdrus6.js} +2 -2
- package/dist/assets/{mainline-BueFJx3f.js.map → mainline-DVYdrus6.js.map} +1 -1
- package/dist/assets/{server-BGXmSYx_.js → server-D5FBZYW8.js} +412 -74
- package/dist/assets/server-D5FBZYW8.js.map +1 -0
- package/dist/assets/{src-BMUcYQ7b.js → src-aKGjb_yz.js} +107 -22
- package/dist/assets/src-aKGjb_yz.js.map +1 -0
- package/dist/assets/{usdt-DklGBSeq.js → usdt-CUfucwZf.js} +2 -2
- package/dist/assets/{usdt-DklGBSeq.js.map → usdt-CUfucwZf.js.map} +1 -1
- package/dist/ghostly.mjs +478 -135
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/assets/server-BGXmSYx_.js.map +0 -1
- package/dist/assets/src-BMUcYQ7b.js.map +0 -1
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
|
|
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.
|
|
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
|
|
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
|
|
310
|
-
return
|
|
311
|
-
...rest.slice(0,
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
848
|
-
* then a group.
|
|
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
|
|
851
|
-
if (ref.startsWith("group:")) {
|
|
852
|
-
|
|
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
|
-
|
|
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
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
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") ?? "."),
|
|
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
|
-
/**
|
|
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
|
|
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 =
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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:
|
|
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
|
-
|
|
14774
|
-
|
|
14775
|
-
|
|
14776
|
-
|
|
14777
|
-
|
|
14778
|
-
|
|
14779
|
-
|
|
14780
|
-
|
|
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
|
-
|
|
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>
|
|
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
|
-
|
|
15008
|
-
|
|
15009
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
16739
|
-
|
|
16740
|
-
|
|
16741
|
-
|
|
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
|
|
16782
|
-
this.
|
|
16783
|
-
this.
|
|
16784
|
-
|
|
16785
|
-
|
|
16786
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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:${
|
|
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
|
|
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
|
|
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
|
-
|
|
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 &&
|
|
21506
|
+
if (two && text(two)) {
|
|
21164
21507
|
await textCommand(two, argv.slice(2));
|
|
21165
21508
|
return 0;
|
|
21166
21509
|
}
|
|
21167
|
-
if (two &&
|
|
21510
|
+
if (two && table(two)) {
|
|
21168
21511
|
await tableCommand(two, argv.slice(2));
|
|
21169
21512
|
return 0;
|
|
21170
21513
|
}
|
|
21171
|
-
if (
|
|
21514
|
+
if (text(first)) {
|
|
21172
21515
|
await textCommand(first, argv.slice(1));
|
|
21173
21516
|
return 0;
|
|
21174
21517
|
}
|
|
21175
|
-
if (
|
|
21518
|
+
if (table(first)) {
|
|
21176
21519
|
await tableCommand(first, argv.slice(1));
|
|
21177
21520
|
return 0;
|
|
21178
21521
|
}
|