@leemour/max-cli 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -37
- package/dist/bot/transport.d.ts +4 -0
- package/dist/bot/transport.d.ts.map +1 -1
- package/dist/bot/transport.js +43 -3
- package/dist/bot/transport.js.map +1 -1
- package/dist/bot-mcp/server.d.ts.map +1 -1
- package/dist/bot-mcp/server.js +21 -7
- package/dist/bot-mcp/server.js.map +1 -1
- package/dist/client.d.ts +18 -10
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +51 -29
- package/dist/client.js.map +1 -1
- package/dist/commands/account.d.ts.map +1 -1
- package/dist/commands/account.js +7 -5
- package/dist/commands/account.js.map +1 -1
- package/dist/commands/backup.js +1 -1
- package/dist/commands/backup.js.map +1 -1
- package/dist/commands/bot-check.d.ts +2 -2
- package/dist/commands/bot-check.d.ts.map +1 -1
- package/dist/commands/bot-check.js +4 -4
- package/dist/commands/bot-check.js.map +1 -1
- package/dist/commands/bot-comments.d.ts.map +1 -1
- package/dist/commands/bot-comments.js +2 -1
- package/dist/commands/bot-comments.js.map +1 -1
- package/dist/commands/bot-context.d.ts +7 -0
- package/dist/commands/bot-context.d.ts.map +1 -1
- package/dist/commands/bot-context.js +32 -2
- package/dist/commands/bot-context.js.map +1 -1
- package/dist/commands/bot-members.d.ts.map +1 -1
- package/dist/commands/bot-members.js +7 -4
- package/dist/commands/bot-members.js.map +1 -1
- package/dist/commands/bot-people.d.ts.map +1 -1
- package/dist/commands/bot-people.js +3 -3
- package/dist/commands/bot-people.js.map +1 -1
- package/dist/commands/bot-reads.d.ts.map +1 -1
- package/dist/commands/bot-reads.js +11 -6
- package/dist/commands/bot-reads.js.map +1 -1
- package/dist/commands/bot-sends.d.ts.map +1 -1
- package/dist/commands/bot-sends.js +6 -5
- package/dist/commands/bot-sends.js.map +1 -1
- package/dist/commands/bot-setup.d.ts.map +1 -1
- package/dist/commands/bot-setup.js +5 -4
- package/dist/commands/bot-setup.js.map +1 -1
- package/dist/commands/bot.d.ts.map +1 -1
- package/dist/commands/bot.js +7 -3
- package/dist/commands/bot.js.map +1 -1
- package/dist/commands/chats.d.ts.map +1 -1
- package/dist/commands/chats.js +15 -30
- package/dist/commands/chats.js.map +1 -1
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +4 -3
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/complete.d.ts.map +1 -1
- package/dist/commands/complete.js +27 -3
- package/dist/commands/complete.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +29 -27
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/contacts.d.ts.map +1 -1
- package/dist/commands/contacts.js +21 -0
- package/dist/commands/contacts.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +75 -21
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/export.js +1 -1
- package/dist/commands/export.js.map +1 -1
- package/dist/commands/folders.d.ts.map +1 -1
- package/dist/commands/folders.js +3 -2
- package/dist/commands/folders.js.map +1 -1
- package/dist/commands/inbox.d.ts.map +1 -1
- package/dist/commands/inbox.js +1 -1
- package/dist/commands/inbox.js.map +1 -1
- package/dist/commands/messages.d.ts.map +1 -1
- package/dist/commands/messages.js +9 -11
- package/dist/commands/messages.js.map +1 -1
- package/dist/commands/models.d.ts.map +1 -1
- package/dist/commands/models.js +2 -1
- package/dist/commands/models.js.map +1 -1
- package/dist/commands/paging.d.ts +17 -1
- package/dist/commands/paging.d.ts.map +1 -1
- package/dist/commands/paging.js +22 -2
- package/dist/commands/paging.js.map +1 -1
- package/dist/commands/recipients.d.ts.map +1 -1
- package/dist/commands/recipients.js +3 -2
- package/dist/commands/recipients.js.map +1 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +1 -1
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/runs.d.ts.map +1 -1
- package/dist/commands/runs.js +4 -3
- package/dist/commands/runs.js.map +1 -1
- package/dist/commands/sends.d.ts.map +1 -1
- package/dist/commands/sends.js +3 -2
- package/dist/commands/sends.js.map +1 -1
- package/dist/config.d.ts +78 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +143 -72
- package/dist/config.js.map +1 -1
- package/dist/diagnose.d.ts +47 -1
- package/dist/diagnose.d.ts.map +1 -1
- package/dist/diagnose.js +56 -5
- package/dist/diagnose.js.map +1 -1
- package/dist/export.d.ts.map +1 -1
- package/dist/export.js +2 -1
- package/dist/export.js.map +1 -1
- package/dist/generated/operations.generated.d.ts +14 -7
- package/dist/generated/operations.generated.d.ts.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +42 -12
- package/dist/mcp/tools.js.map +1 -1
- package/dist/moderation/check.d.ts +5 -6
- package/dist/moderation/check.d.ts.map +1 -1
- package/dist/moderation/check.js +2 -40
- package/dist/moderation/check.js.map +1 -1
- package/dist/moderation/rules.d.ts +57 -7
- package/dist/moderation/rules.d.ts.map +1 -1
- package/dist/moderation/rules.js +13 -11
- package/dist/moderation/rules.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +9 -3
- package/dist/program.js.map +1 -1
- package/dist/runs/events.d.ts +5 -2
- package/dist/runs/events.d.ts.map +1 -1
- package/dist/runs/events.js +4 -1
- package/dist/runs/events.js.map +1 -1
- package/dist/runs/recording.d.ts +11 -0
- package/dist/runs/recording.d.ts.map +1 -1
- package/dist/runs/recording.js +28 -12
- package/dist/runs/recording.js.map +1 -1
- package/dist/sends/guard.d.ts +2 -2
- package/dist/sends/guard.d.ts.map +1 -1
- package/dist/sends/guard.js +2 -1
- package/dist/sends/guard.js.map +1 -1
- package/dist/sends/journal.d.ts +2 -2
- package/dist/sends/journal.d.ts.map +1 -1
- package/dist/sends/journal.js.map +1 -1
- package/dist/sends/permissions.d.ts.map +1 -1
- package/dist/sends/permissions.js +3 -0
- package/dist/sends/permissions.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +5 -0
- package/dist/server/server.js.map +1 -1
- package/dist/session/store.d.ts +2 -0
- package/dist/session/store.d.ts.map +1 -1
- package/dist/session/store.js +5 -1
- package/dist/session/store.js.map +1 -1
- package/dist/spec/operations/account.d.ts +3 -0
- package/dist/spec/operations/account.d.ts.map +1 -1
- package/dist/spec/operations/account.js +5 -1
- package/dist/spec/operations/account.js.map +1 -1
- package/dist/spec/operations/chats.d.ts +4 -3
- package/dist/spec/operations/chats.d.ts.map +1 -1
- package/dist/spec/operations/chats.js +10 -7
- package/dist/spec/operations/chats.js.map +1 -1
- package/dist/spec/operations/contacts.d.ts +8 -3
- package/dist/spec/operations/contacts.d.ts.map +1 -1
- package/dist/spec/operations/contacts.js +26 -5
- package/dist/spec/operations/contacts.js.map +1 -1
- package/dist/spec/operations/folders.d.ts.map +1 -1
- package/dist/spec/operations/folders.js +1 -0
- package/dist/spec/operations/folders.js.map +1 -1
- package/dist/spec/operations/messages.d.ts +1 -1
- package/dist/spec/operations/messages.d.ts.map +1 -1
- package/dist/spec/operations/messages.js +2 -1
- package/dist/spec/operations/messages.js.map +1 -1
- package/dist/spec/operations/uploads.d.ts +1 -1
- package/dist/spec/operations/uploads.d.ts.map +1 -1
- package/dist/spec/operations/uploads.js +2 -1
- package/dist/spec/operations/uploads.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -1
- package/skills/max-cli/SKILL.md +11 -6
package/dist/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliError } from "@leemour/cli-core";
|
|
2
|
+
import { delayMs } from "./config.js";
|
|
2
3
|
import { namesFrom, POLL_CLOSED, pollSettings, SETTING_FLAGS, toChat, toContact, toFolder, toGroupCard, toGroupMember, toMessage, toPoll, toProfile, toReactions, toSession, } from "./domain/map.js";
|
|
3
4
|
import { heldWindows } from "./export.js";
|
|
4
5
|
import { wireClient } from "./generated/client.generated.js";
|
|
@@ -71,10 +72,12 @@ export class MaxClient {
|
|
|
71
72
|
if (!firstName)
|
|
72
73
|
throw new CliError("validation_error", "MAX needs a first name, and the profile has none — pass --first-name");
|
|
73
74
|
const lastName = change.lastName ?? current.lastName;
|
|
75
|
+
const photoToken = change.photo === undefined ? undefined : await this.#profilePhoto(change.photo);
|
|
74
76
|
const answer = await this.#wire.account.update({
|
|
75
77
|
firstName,
|
|
76
78
|
...(lastName === undefined ? {} : { lastName }),
|
|
77
79
|
...(change.description === undefined ? {} : { description: change.description }),
|
|
80
|
+
...(photoToken === undefined ? {} : { photoToken, avatarType: "USER_AVATAR" }),
|
|
78
81
|
});
|
|
79
82
|
return toProfile(record(answer.profile) ?? {});
|
|
80
83
|
}),
|
|
@@ -272,12 +275,13 @@ export class MaxClient {
|
|
|
272
275
|
throw new CliError("validation_error", "`--offline` reads what was recorded; it cannot read a link");
|
|
273
276
|
const wire = wireLink(link);
|
|
274
277
|
await this.#connectOnce();
|
|
275
|
-
|
|
278
|
+
const answer = await this.#wire.chats.linkInfo({ link: wire }).catch(deadLink(link));
|
|
279
|
+
return toGroupCard(record(answer.chat) ?? {});
|
|
276
280
|
},
|
|
277
281
|
join: (link) => {
|
|
278
282
|
const wire = wireLink(link);
|
|
279
283
|
return this.#changeChat(null, "join", async () => {
|
|
280
|
-
const chat = toGroupCard(record((await this.#wire.chats.join({ link: wire })).chat) ?? {});
|
|
284
|
+
const chat = toGroupCard(record((await this.#wire.chats.join({ link: wire }).catch(deadLink(link))).chat) ?? {});
|
|
281
285
|
return { chatId: chat.id, result: chat };
|
|
282
286
|
});
|
|
283
287
|
},
|
|
@@ -320,13 +324,13 @@ export class MaxClient {
|
|
|
320
324
|
* Not retried, unlike a message: a second attempt with a new `cid` is a second group, and
|
|
321
325
|
* whether MAX deduplicates a creation by `cid` is not measured.
|
|
322
326
|
*/
|
|
323
|
-
create: async (title, people = []) => {
|
|
327
|
+
create: async (title, people = [], { channel = false } = {}) => {
|
|
324
328
|
const userIds = await this.#personIds(people);
|
|
325
329
|
return this.#changeChat(null, "create", async () => {
|
|
326
330
|
const answer = await this.#wire.messages.send({
|
|
327
331
|
message: {
|
|
328
332
|
cid: this.#nextCid(),
|
|
329
|
-
attaches: [{ _type: "CONTROL", event: "new", chatType: "CHAT", title, userIds }],
|
|
333
|
+
attaches: [{ _type: "CONTROL", event: "new", chatType: channel ? "CHANNEL" : "CHAT", title, userIds }],
|
|
330
334
|
},
|
|
331
335
|
notify: true,
|
|
332
336
|
});
|
|
@@ -384,23 +388,6 @@ export class MaxClient {
|
|
|
384
388
|
}),
|
|
385
389
|
remove: (reference, person) => this.#updateMembers(reference, [person], "admins.remove", { operation: "remove", type: "ADMIN" }),
|
|
386
390
|
},
|
|
387
|
-
requests: {
|
|
388
|
-
/** Each with the age of their account, read like a member's — the answer's `{contact}` is the same shape. */
|
|
389
|
-
list: async (reference) => {
|
|
390
|
-
if (this.#offline)
|
|
391
|
-
throw new CliError("validation_error", "`--offline` reads what was recorded; join requests never are");
|
|
392
|
-
const chatId = await this.chats.resolve(reference);
|
|
393
|
-
await this.#connectOnce();
|
|
394
|
-
const answer = await this.#wire.chats.members({ chatId, type: "JOIN_REQUEST", count: JOIN_REQUESTS });
|
|
395
|
-
return asArray(answer.members).map(toGroupMember);
|
|
396
|
-
},
|
|
397
|
-
accept: (reference, people) => this.#updateMembers(reference, people, "requests.accept", {
|
|
398
|
-
operation: "add",
|
|
399
|
-
type: "JOIN_REQUEST",
|
|
400
|
-
showHistory: true,
|
|
401
|
-
}),
|
|
402
|
-
decline: (reference, people) => this.#updateMembers(reference, people, "requests.decline", { operation: "remove", type: "JOIN_REQUEST" }),
|
|
403
|
-
},
|
|
404
391
|
update: async (reference, { title, description }) => {
|
|
405
392
|
if (title === undefined && description === undefined) {
|
|
406
393
|
throw new CliError("validation_error", "nothing to change — give --title, --description or both");
|
|
@@ -501,8 +488,13 @@ export class MaxClient {
|
|
|
501
488
|
throw new CliError("not_found", "MAX has nobody under that number");
|
|
502
489
|
return this.#remember(toContact(contact));
|
|
503
490
|
},
|
|
504
|
-
add: (reference) => this.#contactAction("contact-add", "ADD", reference),
|
|
505
|
-
remove: (reference) => this.#contactAction("contact-remove", "REMOVE", reference),
|
|
491
|
+
add: (reference) => this.#contactAction("contact-add", { action: "ADD" }, reference),
|
|
492
|
+
remove: (reference) => this.#contactAction("contact-remove", { action: "REMOVE" }, reference),
|
|
493
|
+
/** They can no longer write to you. Works for somebody who is not a contact too (measured). */
|
|
494
|
+
block: (reference) => this.#contactAction("contact-block", { action: "BLOCK" }, reference),
|
|
495
|
+
unblock: (reference) => this.#contactAction("contact-unblock", { action: "UNBLOCK" }, reference),
|
|
496
|
+
/** A name of your own for them, kept beside the one they chose; they do not see it. */
|
|
497
|
+
rename: (reference, firstName, lastName) => this.#contactAction("contact-rename", { action: "UPDATE", firstName, ...(lastName === undefined ? {} : { lastName }) }, reference),
|
|
506
498
|
/** Uploads phone numbers to MAX — other people's — under the names they were saved with. */
|
|
507
499
|
import: (entries) => this.#change("contact-import", async () => {
|
|
508
500
|
if (entries.length === 0)
|
|
@@ -769,9 +761,14 @@ export class MaxClient {
|
|
|
769
761
|
*/
|
|
770
762
|
moment: (reference, flag = "--before") => {
|
|
771
763
|
const wanted = reference.trim();
|
|
772
|
-
const
|
|
764
|
+
const ago = delayMs(wanted);
|
|
765
|
+
const time = /^\d+$/.test(wanted)
|
|
766
|
+
? timeOfMessageId(wanted)
|
|
767
|
+
: ago === undefined
|
|
768
|
+
? Date.parse(wanted)
|
|
769
|
+
: Date.now() - ago;
|
|
773
770
|
if (time === undefined || Number.isNaN(time)) {
|
|
774
|
-
throw new CliError("validation_error", `${flag} takes a message id
|
|
771
|
+
throw new CliError("validation_error", `${flag} takes a message id, an ISO 8601 time or 30m, 2h, 1d ago — not "${wanted}"`);
|
|
775
772
|
}
|
|
776
773
|
return time;
|
|
777
774
|
},
|
|
@@ -1607,6 +1604,10 @@ export class MaxClient {
|
|
|
1607
1604
|
`the keyring is probably out of reach (cron, ssh: set XDG_RUNTIME_DIR); ` +
|
|
1608
1605
|
`\`max ${asFirstWord(profile)}doctor\` shows it. Log in again only if the token was removed`);
|
|
1609
1606
|
}
|
|
1607
|
+
if (this.#store.isBot()) {
|
|
1608
|
+
throw new CliError("authentication_error", `profile "${profile}" is a bot — its commands are \`max ${asFirstWord(profile)}bot …\`; ` +
|
|
1609
|
+
`\`max ${asFirstWord(profile)}session start\` would add a personal account to it`);
|
|
1610
|
+
}
|
|
1610
1611
|
// The fix has to carry the profile, or it logs the wrong one in: a name nobody has logged
|
|
1611
1612
|
// in under is the ordinary shape of this failure now that the first word is the profile.
|
|
1612
1613
|
throw new CliError("authentication_error", `no session for profile "${profile}" — run \`max ${asFirstWord(profile)}session start\``);
|
|
@@ -2010,6 +2011,15 @@ export class MaxClient {
|
|
|
2010
2011
|
* Uploads one file and answers what the message attaches (measured 2026-09-24). An upload is never
|
|
2011
2012
|
* retried: a failure here happens before `MSG_SEND`, so nothing was sent.
|
|
2012
2013
|
*/
|
|
2014
|
+
async #profilePhoto(path) {
|
|
2015
|
+
if (!isImage(path))
|
|
2016
|
+
throw new CliError("validation_error", `${path} is not an image MAX takes as a photo`);
|
|
2017
|
+
const bytes = await readUpload(path);
|
|
2018
|
+
const { url } = await this.#wire.uploads.photo({ count: 1, type: 0, uploaderType: 0, profile: true });
|
|
2019
|
+
if (typeof url !== "string")
|
|
2020
|
+
throw new CliError("provider_error", "MAX gave no address to upload the photo to");
|
|
2021
|
+
return uploadPhoto(url, path, bytes);
|
|
2022
|
+
}
|
|
2013
2023
|
async #upload({ path, bytes, kind, voice }) {
|
|
2014
2024
|
const request = { count: 1, type: 0, uploaderType: 0, profile: false };
|
|
2015
2025
|
if (kind === "photo") {
|
|
@@ -2235,7 +2245,7 @@ export class MaxClient {
|
|
|
2235
2245
|
throw new CliError("validation_error", "without a local store a person is named by id — `max contacts lookup` finds one");
|
|
2236
2246
|
}
|
|
2237
2247
|
const id = isId(reference) ? reference.trim() : pickPerson(reference, cache).id;
|
|
2238
|
-
const answer = await this.#wire.contacts.update({ contactId: id,
|
|
2248
|
+
const answer = await this.#wire.contacts.update({ contactId: id, ...wire });
|
|
2239
2249
|
const contact = record(answer.contact);
|
|
2240
2250
|
if (contact)
|
|
2241
2251
|
return this.#remember(toContact(contact));
|
|
@@ -2266,20 +2276,25 @@ export class MaxClient {
|
|
|
2266
2276
|
return this.#login;
|
|
2267
2277
|
}
|
|
2268
2278
|
}
|
|
2269
|
-
/**
|
|
2279
|
+
/**
|
|
2280
|
+
* PyMax `AdminPermission`; the sum is what MAX takes. `read` and `link` measured 2026-09-27 by
|
|
2281
|
+
* switching them in the app for a bot and reading its `permissions` back (`MAX-61`): «Read messages»
|
|
2282
|
+
* sets 1 and 32 together — the Bot API calls them `read_all_messages` and `write` — and «Update chat
|
|
2283
|
+
* link» 128. A bot without `read` sees no message of the group.
|
|
2284
|
+
*/
|
|
2270
2285
|
export const ADMIN_RIGHTS = {
|
|
2286
|
+
read: 1 | 32,
|
|
2271
2287
|
members: 2,
|
|
2272
2288
|
admins: 4,
|
|
2273
2289
|
info: 8,
|
|
2274
2290
|
pin: 16,
|
|
2291
|
+
link: 128,
|
|
2275
2292
|
post: 256,
|
|
2276
2293
|
edit: 512,
|
|
2277
2294
|
delete: 1024,
|
|
2278
2295
|
};
|
|
2279
|
-
/** PyMax asks for this many; paging join requests is not known. */
|
|
2280
2296
|
/** The most chats `max serve` holds from pushes before it logs in again instead. */
|
|
2281
2297
|
const LIVE_CHATS = 10_000;
|
|
2282
|
-
const JOIN_REQUESTS = 100;
|
|
2283
2298
|
/** PyMax's page, the one measured. */
|
|
2284
2299
|
const MEMBERS_PAGE = 50;
|
|
2285
2300
|
const MEMBERS_READ = 5000;
|
|
@@ -2470,6 +2485,13 @@ const asRefusal = (error) => {
|
|
|
2470
2485
|
}
|
|
2471
2486
|
return new CliError("provider_error", error.message, { operation: String(error.opcode) });
|
|
2472
2487
|
};
|
|
2488
|
+
/** A link that was reset, or never led anywhere, is `not.found` from MAX (measured 2026-09-28). */
|
|
2489
|
+
const deadLink = (link) => (error) => {
|
|
2490
|
+
const failure = asCliError(error);
|
|
2491
|
+
if (failure.details.maxError !== "not.found")
|
|
2492
|
+
throw failure;
|
|
2493
|
+
throw new CliError("not_found", `${link.trim()} leads nowhere — the link was reset, or never worked`);
|
|
2494
|
+
};
|
|
2473
2495
|
const LIMIT_WORDS = ["limit.violate", "rate_limit", "rate limit", "too many", "слишком много"];
|
|
2474
2496
|
/** Before any request: a login inside the pause is one more attempt MAX counts (`MAX-38`). */
|
|
2475
2497
|
export const refuseWhilePaused = (state) => {
|