@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.
Files changed (173) hide show
  1. package/README.md +56 -37
  2. package/dist/bot/transport.d.ts +4 -0
  3. package/dist/bot/transport.d.ts.map +1 -1
  4. package/dist/bot/transport.js +43 -3
  5. package/dist/bot/transport.js.map +1 -1
  6. package/dist/bot-mcp/server.d.ts.map +1 -1
  7. package/dist/bot-mcp/server.js +21 -7
  8. package/dist/bot-mcp/server.js.map +1 -1
  9. package/dist/client.d.ts +18 -10
  10. package/dist/client.d.ts.map +1 -1
  11. package/dist/client.js +51 -29
  12. package/dist/client.js.map +1 -1
  13. package/dist/commands/account.d.ts.map +1 -1
  14. package/dist/commands/account.js +7 -5
  15. package/dist/commands/account.js.map +1 -1
  16. package/dist/commands/backup.js +1 -1
  17. package/dist/commands/backup.js.map +1 -1
  18. package/dist/commands/bot-check.d.ts +2 -2
  19. package/dist/commands/bot-check.d.ts.map +1 -1
  20. package/dist/commands/bot-check.js +4 -4
  21. package/dist/commands/bot-check.js.map +1 -1
  22. package/dist/commands/bot-comments.d.ts.map +1 -1
  23. package/dist/commands/bot-comments.js +2 -1
  24. package/dist/commands/bot-comments.js.map +1 -1
  25. package/dist/commands/bot-context.d.ts +7 -0
  26. package/dist/commands/bot-context.d.ts.map +1 -1
  27. package/dist/commands/bot-context.js +32 -2
  28. package/dist/commands/bot-context.js.map +1 -1
  29. package/dist/commands/bot-members.d.ts.map +1 -1
  30. package/dist/commands/bot-members.js +7 -4
  31. package/dist/commands/bot-members.js.map +1 -1
  32. package/dist/commands/bot-people.d.ts.map +1 -1
  33. package/dist/commands/bot-people.js +3 -3
  34. package/dist/commands/bot-people.js.map +1 -1
  35. package/dist/commands/bot-reads.d.ts.map +1 -1
  36. package/dist/commands/bot-reads.js +11 -6
  37. package/dist/commands/bot-reads.js.map +1 -1
  38. package/dist/commands/bot-sends.d.ts.map +1 -1
  39. package/dist/commands/bot-sends.js +6 -5
  40. package/dist/commands/bot-sends.js.map +1 -1
  41. package/dist/commands/bot-setup.d.ts.map +1 -1
  42. package/dist/commands/bot-setup.js +5 -4
  43. package/dist/commands/bot-setup.js.map +1 -1
  44. package/dist/commands/bot.d.ts.map +1 -1
  45. package/dist/commands/bot.js +7 -3
  46. package/dist/commands/bot.js.map +1 -1
  47. package/dist/commands/chats.d.ts.map +1 -1
  48. package/dist/commands/chats.js +15 -30
  49. package/dist/commands/chats.js.map +1 -1
  50. package/dist/commands/check.d.ts.map +1 -1
  51. package/dist/commands/check.js +4 -3
  52. package/dist/commands/check.js.map +1 -1
  53. package/dist/commands/complete.d.ts.map +1 -1
  54. package/dist/commands/complete.js +27 -3
  55. package/dist/commands/complete.js.map +1 -1
  56. package/dist/commands/config.d.ts.map +1 -1
  57. package/dist/commands/config.js +29 -27
  58. package/dist/commands/config.js.map +1 -1
  59. package/dist/commands/contacts.d.ts.map +1 -1
  60. package/dist/commands/contacts.js +21 -0
  61. package/dist/commands/contacts.js.map +1 -1
  62. package/dist/commands/doctor.d.ts.map +1 -1
  63. package/dist/commands/doctor.js +75 -21
  64. package/dist/commands/doctor.js.map +1 -1
  65. package/dist/commands/export.js +1 -1
  66. package/dist/commands/export.js.map +1 -1
  67. package/dist/commands/folders.d.ts.map +1 -1
  68. package/dist/commands/folders.js +3 -2
  69. package/dist/commands/folders.js.map +1 -1
  70. package/dist/commands/inbox.d.ts.map +1 -1
  71. package/dist/commands/inbox.js +1 -1
  72. package/dist/commands/inbox.js.map +1 -1
  73. package/dist/commands/messages.d.ts.map +1 -1
  74. package/dist/commands/messages.js +9 -11
  75. package/dist/commands/messages.js.map +1 -1
  76. package/dist/commands/models.d.ts.map +1 -1
  77. package/dist/commands/models.js +2 -1
  78. package/dist/commands/models.js.map +1 -1
  79. package/dist/commands/paging.d.ts +17 -1
  80. package/dist/commands/paging.d.ts.map +1 -1
  81. package/dist/commands/paging.js +22 -2
  82. package/dist/commands/paging.js.map +1 -1
  83. package/dist/commands/recipients.d.ts.map +1 -1
  84. package/dist/commands/recipients.js +3 -2
  85. package/dist/commands/recipients.js.map +1 -1
  86. package/dist/commands/review.d.ts.map +1 -1
  87. package/dist/commands/review.js +1 -1
  88. package/dist/commands/review.js.map +1 -1
  89. package/dist/commands/runs.d.ts.map +1 -1
  90. package/dist/commands/runs.js +4 -3
  91. package/dist/commands/runs.js.map +1 -1
  92. package/dist/commands/sends.d.ts.map +1 -1
  93. package/dist/commands/sends.js +3 -2
  94. package/dist/commands/sends.js.map +1 -1
  95. package/dist/config.d.ts +78 -7
  96. package/dist/config.d.ts.map +1 -1
  97. package/dist/config.js +143 -72
  98. package/dist/config.js.map +1 -1
  99. package/dist/diagnose.d.ts +47 -1
  100. package/dist/diagnose.d.ts.map +1 -1
  101. package/dist/diagnose.js +56 -5
  102. package/dist/diagnose.js.map +1 -1
  103. package/dist/export.d.ts.map +1 -1
  104. package/dist/export.js +2 -1
  105. package/dist/export.js.map +1 -1
  106. package/dist/generated/operations.generated.d.ts +14 -7
  107. package/dist/generated/operations.generated.d.ts.map +1 -1
  108. package/dist/mcp/tools.d.ts.map +1 -1
  109. package/dist/mcp/tools.js +42 -12
  110. package/dist/mcp/tools.js.map +1 -1
  111. package/dist/moderation/check.d.ts +5 -6
  112. package/dist/moderation/check.d.ts.map +1 -1
  113. package/dist/moderation/check.js +2 -40
  114. package/dist/moderation/check.js.map +1 -1
  115. package/dist/moderation/rules.d.ts +57 -7
  116. package/dist/moderation/rules.d.ts.map +1 -1
  117. package/dist/moderation/rules.js +13 -11
  118. package/dist/moderation/rules.js.map +1 -1
  119. package/dist/program.d.ts.map +1 -1
  120. package/dist/program.js +9 -3
  121. package/dist/program.js.map +1 -1
  122. package/dist/runs/events.d.ts +5 -2
  123. package/dist/runs/events.d.ts.map +1 -1
  124. package/dist/runs/events.js +4 -1
  125. package/dist/runs/events.js.map +1 -1
  126. package/dist/runs/recording.d.ts +11 -0
  127. package/dist/runs/recording.d.ts.map +1 -1
  128. package/dist/runs/recording.js +28 -12
  129. package/dist/runs/recording.js.map +1 -1
  130. package/dist/sends/guard.d.ts +2 -2
  131. package/dist/sends/guard.d.ts.map +1 -1
  132. package/dist/sends/guard.js +2 -1
  133. package/dist/sends/guard.js.map +1 -1
  134. package/dist/sends/journal.d.ts +2 -2
  135. package/dist/sends/journal.d.ts.map +1 -1
  136. package/dist/sends/journal.js.map +1 -1
  137. package/dist/sends/permissions.d.ts.map +1 -1
  138. package/dist/sends/permissions.js +3 -0
  139. package/dist/sends/permissions.js.map +1 -1
  140. package/dist/server/server.d.ts.map +1 -1
  141. package/dist/server/server.js +5 -0
  142. package/dist/server/server.js.map +1 -1
  143. package/dist/session/store.d.ts +2 -0
  144. package/dist/session/store.d.ts.map +1 -1
  145. package/dist/session/store.js +5 -1
  146. package/dist/session/store.js.map +1 -1
  147. package/dist/spec/operations/account.d.ts +3 -0
  148. package/dist/spec/operations/account.d.ts.map +1 -1
  149. package/dist/spec/operations/account.js +5 -1
  150. package/dist/spec/operations/account.js.map +1 -1
  151. package/dist/spec/operations/chats.d.ts +4 -3
  152. package/dist/spec/operations/chats.d.ts.map +1 -1
  153. package/dist/spec/operations/chats.js +10 -7
  154. package/dist/spec/operations/chats.js.map +1 -1
  155. package/dist/spec/operations/contacts.d.ts +8 -3
  156. package/dist/spec/operations/contacts.d.ts.map +1 -1
  157. package/dist/spec/operations/contacts.js +26 -5
  158. package/dist/spec/operations/contacts.js.map +1 -1
  159. package/dist/spec/operations/folders.d.ts.map +1 -1
  160. package/dist/spec/operations/folders.js +1 -0
  161. package/dist/spec/operations/folders.js.map +1 -1
  162. package/dist/spec/operations/messages.d.ts +1 -1
  163. package/dist/spec/operations/messages.d.ts.map +1 -1
  164. package/dist/spec/operations/messages.js +2 -1
  165. package/dist/spec/operations/messages.js.map +1 -1
  166. package/dist/spec/operations/uploads.d.ts +1 -1
  167. package/dist/spec/operations/uploads.d.ts.map +1 -1
  168. package/dist/spec/operations/uploads.js +2 -1
  169. package/dist/spec/operations/uploads.js.map +1 -1
  170. package/dist/version.d.ts +1 -1
  171. package/dist/version.js +1 -1
  172. package/package.json +3 -1
  173. 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
- return toGroupCard(record((await this.#wire.chats.linkInfo({ link: wire })).chat) ?? {});
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 time = /^\d+$/.test(wanted) ? timeOfMessageId(wanted) : Date.parse(wanted);
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 or an ISO 8601 time, not "${wanted}"`);
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, action: wire });
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
- /** PyMax `AdminPermission`; the sum is what MAX takes. */
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) => {