@pachca/cli 2026.10.3 → 2026.11.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 (56) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +5 -5
  3. package/README.template.md +5 -5
  4. package/dist/base-command.js +68 -36
  5. package/dist/client.js +2 -1
  6. package/dist/commands/api.js +3 -3
  7. package/dist/commands/auth/login.js +12 -9
  8. package/dist/commands/bots/create.js +13 -7
  9. package/dist/commands/bots/list-company.js +4 -2
  10. package/dist/commands/bots/list-events.js +4 -2
  11. package/dist/commands/bots/list-tokens.js +4 -2
  12. package/dist/commands/bots/list.js +5 -3
  13. package/dist/commands/bots/update-webhook.js +8 -1
  14. package/dist/commands/bots/update.js +14 -7
  15. package/dist/commands/chats/list-company.js +4 -2
  16. package/dist/commands/chats/list.js +4 -2
  17. package/dist/commands/chats/update.js +1 -1
  18. package/dist/commands/doctor.js +60 -35
  19. package/dist/commands/drafts/create.js +6 -3
  20. package/dist/commands/drafts/list.js +4 -2
  21. package/dist/commands/drafts/update.js +7 -4
  22. package/dist/commands/group-tags/list-users.js +4 -2
  23. package/dist/commands/group-tags/list.js +4 -2
  24. package/dist/commands/important-messages/list.js +4 -2
  25. package/dist/commands/members/add.js +1 -1
  26. package/dist/commands/members/list.js +4 -2
  27. package/dist/commands/messages/create.js +7 -5
  28. package/dist/commands/messages/list.js +4 -2
  29. package/dist/commands/messages/update.js +16 -4
  30. package/dist/commands/profile/update-status.js +5 -4
  31. package/dist/commands/reactions/add.js +9 -2
  32. package/dist/commands/reactions/list.js +4 -2
  33. package/dist/commands/reactions/remove.js +1 -28
  34. package/dist/commands/read-member/list-readers.js +4 -2
  35. package/dist/commands/search/list-chats.js +5 -3
  36. package/dist/commands/search/list-messages.js +5 -3
  37. package/dist/commands/search/list-users.js +5 -3
  38. package/dist/commands/security/list.js +5 -3
  39. package/dist/commands/tasks/create.js +3 -29
  40. package/dist/commands/tasks/list.js +4 -2
  41. package/dist/commands/tasks/update.js +4 -2
  42. package/dist/commands/threads/list.js +4 -2
  43. package/dist/commands/upload.js +27 -8
  44. package/dist/commands/users/create.js +5 -2
  45. package/dist/commands/users/list.js +5 -3
  46. package/dist/commands/users/update-status.js +5 -4
  47. package/dist/commands/users/update.js +8 -5
  48. package/dist/commands/views/open.js +1 -0
  49. package/dist/data/changelog.json +150 -0
  50. package/dist/data/endpoints.json +1 -1
  51. package/dist/hooks/init.js +8 -4
  52. package/dist/lib/flags.js +25 -0
  53. package/dist/oauth.js +2 -1
  54. package/dist/profiles.js +22 -4
  55. package/oclif.manifest.json +1 -1
  56. package/package.json +2 -2
@@ -79,6 +79,7 @@ export default class MessagesList extends BaseCommand {
79
79
  let nextCursor = undefined;
80
80
  let pages = 0;
81
81
  const seenCursors = new Set();
82
+ let total;
82
83
  while (pages < 500) {
83
84
  const query = {
84
85
  'chat_id': flags['chat-id'],
@@ -96,7 +97,7 @@ export default class MessagesList extends BaseCommand {
96
97
  const paginate = meta?.paginate;
97
98
  nextCursor = paginate?.next_page;
98
99
  pages++;
99
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
100
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
100
101
  const hasNext = paginate?.has_next;
101
102
  if (typeof hasNext === 'boolean') {
102
103
  if (!hasNext)
@@ -106,7 +107,8 @@ export default class MessagesList extends BaseCommand {
106
107
  break;
107
108
  }
108
109
  if (process.stderr.isTTY) {
109
- const total = paginate?.total;
110
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
111
+ total ??= meta?.total;
110
112
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
111
113
  process.stderr.write(`\r Загружено: ${progress}...`);
112
114
  }
@@ -16,7 +16,7 @@ export default class MessagesUpdate extends BaseCommand {
16
16
  static flags = {
17
17
  ...BaseCommand.baseFlags,
18
18
  'content': Flags.string({
19
- description: "Текст сообщения. Поддерживает упоминания: `@nickname` или `<@user_id>` (будет автоматически преобразовано в `@nickname`). У мульти-гостя `<@user_id>` недоступного ему сотрудника остаётся в тексте как есть. До 40 000 байт в UTF-8, для кириллицы это около 20 000 символов.",
19
+ description: "Текст сообщения. Поддерживает упоминания: `@nickname` или `<@user_id>` (будет автоматически преобразовано в `@nickname`). Ник берут из ответа метода, а `user_id` — из данных: ник, составленный из имени, никого не упоминает. У мульти-гостя `<@user_id>` недоступного ему сотрудника остаётся в тексте как есть. До 40 000 байт в UTF-8, для кириллицы это около 20 000 символов.",
20
20
  }),
21
21
  'files': Flags.string({
22
22
  description: "Прикрепляемые файлы. Переданный набор заменяет прежний: чтобы оставить уже приложенный файл, передайте его снова с тем же `key`. Такой файл остаётся прежним вложением с тем же `id` и названием, а его поля из запроса не применяются. Чтобы показать файл под другим названием, загрузите его заново.",
@@ -25,10 +25,10 @@ export default class MessagesUpdate extends BaseCommand {
25
25
  description: "Массив строк, каждая из которых представлена массивом кнопок. Максимум 100 кнопок у сообщения, до 8 кнопок в строке. Для удаления кнопок пришлите пустой массив.",
26
26
  }),
27
27
  'display-avatar-url': Flags.string({
28
- description: "Ссылка на специальную аватарку отправителя для этого сообщения. Использование этого поля возможно только с access_token бота.",
28
+ description: "Ссылка на специальную аватарку отправителя для этого сообщения. Использование этого поля возможно только с access_token бота. До 255 байт в UTF-8. (макс. 255 символов)",
29
29
  }),
30
30
  'display-name': Flags.string({
31
- description: "Полное специальное имя отправителя для этого сообщения. Использование этого поля возможно только с access_token бота.",
31
+ description: "Полное специальное имя отправителя для этого сообщения. Использование этого поля возможно только с access_token бота. До 255 байт в UTF-8, для кириллицы это около 127 символов. (макс. 255 символов)",
32
32
  }),
33
33
  };
34
34
  async run() {
@@ -40,7 +40,19 @@ export default class MessagesUpdate extends BaseCommand {
40
40
  for await (const chunk of process.stdin) {
41
41
  chunks.push(chunk);
42
42
  }
43
- flags['content'] = Buffer.concat(chunks).toString('utf-8').trimEnd();
43
+ const piped = Buffer.concat(chunks).toString('utf-8').trimEnd();
44
+ if (piped)
45
+ flags['content'] = piped;
46
+ }
47
+ const validationErrors = [];
48
+ if (flags['display-avatar-url'] && String(flags['display-avatar-url']).length > 255) {
49
+ validationErrors.push({ message: `--display-avatar-url: максимум 255 символов (передано: ${String(flags['display-avatar-url']).length})`, flag: 'display-avatar-url' });
50
+ }
51
+ if (flags['display-name'] && String(flags['display-name']).length > 255) {
52
+ validationErrors.push({ message: `--display-name: максимум 255 символов (передано: ${String(flags['display-name']).length})`, flag: 'display-name' });
53
+ }
54
+ if (validationErrors.length > 0) {
55
+ this.validationError(validationErrors);
44
56
  }
45
57
  const body = { message: {
46
58
  content: flags['content'],
@@ -1,6 +1,7 @@
1
1
  // Auto-generated from openapi.yaml — DO NOT EDIT
2
2
  import { Flags } from '@oclif/core';
3
3
  import { BaseCommand } from '../../base-command.js';
4
+ import { nullableString } from '../../lib/flags.js';
4
5
  export default class ProfileUpdateStatus extends BaseCommand {
5
6
  static description = "Новый свой статус";
6
7
  static scope = "profile_status:write";
@@ -16,15 +17,15 @@ export default class ProfileUpdateStatus extends BaseCommand {
16
17
  'title': Flags.string({
17
18
  description: "Текст статуса. Обязателен, если статуса ещё нет. (макс. 50 символов)",
18
19
  }),
19
- 'expires-at': Flags.string({
20
- description: "Срок жизни статуса (ISO-8601). Смещение вида `+03:00` учитывается, время без смещения считается UTC.",
20
+ 'expires-at': nullableString({
21
+ description: "Срок жизни статуса (ISO-8601). Смещение вида `+03:00` учитывается, время без смещения считается UTC. `null` убирает срок: статус не снимется сам. Чтобы передать null, укажите `--expires-at null`.",
21
22
  }),
22
23
  'is-away': Flags.boolean({
23
24
  description: "Режим «Нет на месте»",
24
25
  allowNo: true,
25
26
  }),
26
- 'away-message': Flags.string({
27
- description: "Текст сообщения при режиме «Нет на месте». Отображается в профиле и при личных сообщениях/упоминаниях. (макс. 1024 символов)",
27
+ 'away-message': nullableString({
28
+ description: "Текст сообщения при режиме «Нет на месте». Отображается в профиле и при личных сообщениях/упоминаниях. Поддерживает Markdown и упоминания `@nickname` и `<@user_id>`, но сохраняется и возвращается обычным текстом: разметка убирается, а упоминание сотрудника заменяется на `@` с его именем и фамилией. Предел 1024 символа действует и на присланный текст, и на получившийся, сверх него ответ `400` с кодом `too_long`. `null` или пустая строка убирает текст. Чтобы передать null, укажите `--away-message null`. (макс. 1024 символов)",
28
29
  }),
29
30
  };
30
31
  async run() {
@@ -18,7 +18,7 @@ export default class ReactionsAdd extends BaseCommand {
18
18
  static flags = {
19
19
  ...BaseCommand.baseFlags,
20
20
  'code': Flags.string({
21
- description: "Emoji символ реакции. Без `name` нужен именно символ: текстовые записи вроде `+1` или `:+1:` отклоняются как неизвестный эмодзи. С `name` подходит любое непустое значение: оно сохраняется как передано.",
21
+ description: "Emoji символ реакции. Без `name` нужен именно символ: текстовые записи вроде `+1` или `:+1:` отклоняются как неизвестный эмодзи. С `name` подходит любое непустое значение до 16 символов: оно сохраняется как передано. (макс. 16 символов)",
22
22
  }),
23
23
  'name': Flags.string({
24
24
  description: "Имя кастомного эмодзи пространства. С ним реакцией ставится этот эмодзи, а если его в пространстве нет, ответ `404`.",
@@ -28,7 +28,7 @@ export default class ReactionsAdd extends BaseCommand {
28
28
  const { args, flags } = await this.parse(ReactionsAdd);
29
29
  this.parsedFlags = flags;
30
30
  const missingRequired = [
31
- { flag: 'code', label: "Emoji символ реакции. Без `name` нужен именно символ: текстовые записи вроде `+1` или `:+1:` отклоняются как неизвестный эмодзи. С `name` подходит любое непустое значение: оно сохраняется как передано.", type: 'string' },
31
+ { flag: 'code', label: "Emoji символ реакции. Без `name` нужен именно символ: текстовые записи вроде `+1` или `:+1:` отклоняются как неизвестный эмодзи. С `name` подходит любое непустое значение до 16 символов: оно сохраняется как передано.", type: 'string' },
32
32
  ].filter((f) => flags[f.flag] === undefined || flags[f.flag] === null);
33
33
  if (missingRequired.length > 0) {
34
34
  if (this.isInteractive()) {
@@ -53,6 +53,13 @@ export default class ReactionsAdd extends BaseCommand {
53
53
  this.validationError(missingRequired.map((f) => ({ message: `Обязательный флаг --${f.flag} не передан`, flag: f.flag })), { hint: "Обязательные: --code <string>. pachca introspect reactions add" });
54
54
  }
55
55
  }
56
+ const validationErrors = [];
57
+ if (flags['code'] && String(flags['code']).length > 16) {
58
+ validationErrors.push({ message: `--code: максимум 16 символов (передано: ${String(flags['code']).length})`, flag: 'code' });
59
+ }
60
+ if (validationErrors.length > 0) {
61
+ this.validationError(validationErrors);
62
+ }
56
63
  const body = {
57
64
  code: flags['code'],
58
65
  name: flags['name'],
@@ -35,6 +35,7 @@ export default class ReactionsList extends BaseCommand {
35
35
  let nextCursor = undefined;
36
36
  let pages = 0;
37
37
  const seenCursors = new Set();
38
+ let total;
38
39
  while (pages < 500) {
39
40
  const query = {
40
41
  limit: flags.limit,
@@ -49,7 +50,7 @@ export default class ReactionsList extends BaseCommand {
49
50
  const paginate = meta?.paginate;
50
51
  nextCursor = paginate?.next_page;
51
52
  pages++;
52
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
53
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
53
54
  const hasNext = paginate?.has_next;
54
55
  if (typeof hasNext === 'boolean') {
55
56
  if (!hasNext)
@@ -59,7 +60,8 @@ export default class ReactionsList extends BaseCommand {
59
60
  break;
60
61
  }
61
62
  if (process.stderr.isTTY) {
62
- const total = paginate?.total;
63
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
64
+ total ??= meta?.total;
63
65
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
64
66
  process.stderr.write(`\r Загружено: ${progress}...`);
65
67
  }
@@ -7,7 +7,6 @@ export default class ReactionsRemove extends BaseCommand {
7
7
  static scope = "reactions:write";
8
8
  static apiMethod = "DELETE";
9
9
  static apiPath = "/messages/{id}/reactions";
10
- static requiredFlags = ["code"];
11
10
  static args = {
12
11
  id: Args.integer({
13
12
  description: "Идентификатор сообщения",
@@ -17,7 +16,7 @@ export default class ReactionsRemove extends BaseCommand {
17
16
  static flags = {
18
17
  ...BaseCommand.baseFlags,
19
18
  'code': Flags.string({
20
- description: "Emoji символ реакции. Без `name` нужен именно символ: текстовые записи вроде `+1` или `:+1:` отклоняются как неизвестный эмодзи.",
19
+ description: "Emoji символ реакции. Обязателен без `name`: нужен именно символ, текстовые записи вроде `+1` или `:+1:` отклоняются как неизвестный эмодзи. С `name` не используется.",
21
20
  }),
22
21
  'name': Flags.string({
23
22
  description: "Имя реакции, как в её поле `name`. С ним реакция ищется по имени, так снимается и кастомный эмодзи пространства.",
@@ -30,32 +29,6 @@ export default class ReactionsRemove extends BaseCommand {
30
29
  async run() {
31
30
  const { args, flags } = await this.parse(ReactionsRemove);
32
31
  this.parsedFlags = flags;
33
- const missingRequired = [
34
- { flag: 'code', label: "Emoji символ реакции. Без `name` нужен именно символ: текстовые записи вроде `+1` или `:+1:` отклоняются как неизвестный эмодзи.", type: 'string' },
35
- ].filter((f) => flags[f.flag] === undefined || flags[f.flag] === null);
36
- if (missingRequired.length > 0) {
37
- if (this.isInteractive()) {
38
- for (const field of missingRequired) {
39
- const value = await clack.text({ message: field.label, validate: (v) => v.length === 0 ? 'Обязательное поле' : undefined });
40
- if (clack.isCancel(value)) {
41
- process.stderr.write('Отменено.\n');
42
- this.exit(0);
43
- }
44
- if (field.type === 'integer') {
45
- flags[field.flag] = Number.parseInt(value, 10);
46
- }
47
- else if (field.type === 'boolean') {
48
- flags[field.flag] = value === 'true';
49
- }
50
- else {
51
- flags[field.flag] = value;
52
- }
53
- }
54
- }
55
- else {
56
- this.validationError(missingRequired.map((f) => ({ message: `Обязательный флаг --${f.flag} не передан`, flag: f.flag })), { hint: "Обязательные: --code <string>. pachca introspect reactions remove" });
57
- }
58
- }
59
32
  if (!flags.force) {
60
33
  if (!this.isInteractive()) {
61
34
  this.validationError([{ message: 'Деструктивная операция требует флага --force', flag: 'force' }], { type: 'PACHCA_DESTRUCTIVE_OP_ERROR', hint: "pachca reactions remove <id> --force" });
@@ -34,6 +34,7 @@ export default class ReadMemberListReaders extends BaseCommand {
34
34
  let nextCursor = undefined;
35
35
  let pages = 0;
36
36
  const seenCursors = new Set();
37
+ let total;
37
38
  while (pages < 500) {
38
39
  const query = {
39
40
  limit: flags.limit,
@@ -48,7 +49,7 @@ export default class ReadMemberListReaders extends BaseCommand {
48
49
  const paginate = meta?.paginate;
49
50
  nextCursor = paginate?.next_page;
50
51
  pages++;
51
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
52
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
52
53
  const hasNext = paginate?.has_next;
53
54
  if (typeof hasNext === 'boolean') {
54
55
  if (!hasNext)
@@ -58,7 +59,8 @@ export default class ReadMemberListReaders extends BaseCommand {
58
59
  break;
59
60
  }
60
61
  if (process.stderr.isTTY) {
61
- const total = paginate?.total;
62
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
63
+ total ??= meta?.total;
62
64
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
63
65
  process.stderr.write(`\r Загружено: ${progress}...`);
64
66
  }
@@ -11,7 +11,7 @@ export default class SearchListChats extends BaseCommand {
11
11
  static flags = {
12
12
  ...BaseCommand.baseFlags,
13
13
  'query': Flags.string({
14
- description: "Текст поискового запроса. Запрос длиннее 500 знаков обрезается.",
14
+ description: "Текст поискового запроса. Запрос длиннее 256 знаков обрезается.",
15
15
  }),
16
16
  'order': Flags.string({
17
17
  description: "Направление сортировки",
@@ -55,6 +55,7 @@ export default class SearchListChats extends BaseCommand {
55
55
  let nextCursor = undefined;
56
56
  let pages = 0;
57
57
  const seenCursors = new Set();
58
+ let total;
58
59
  while (pages < 500) {
59
60
  const query = {
60
61
  query: flags['query'],
@@ -76,7 +77,7 @@ export default class SearchListChats extends BaseCommand {
76
77
  const paginate = meta?.paginate;
77
78
  nextCursor = paginate?.next_page;
78
79
  pages++;
79
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
80
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
80
81
  const hasNext = paginate?.has_next;
81
82
  if (typeof hasNext === 'boolean') {
82
83
  if (!hasNext)
@@ -86,7 +87,8 @@ export default class SearchListChats extends BaseCommand {
86
87
  break;
87
88
  }
88
89
  if (process.stderr.isTTY) {
89
- const total = paginate?.total;
90
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
91
+ total ??= meta?.total;
90
92
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
91
93
  process.stderr.write(`\r Загружено: ${progress}...`);
92
94
  }
@@ -11,7 +11,7 @@ export default class SearchListMessages extends BaseCommand {
11
11
  static flags = {
12
12
  ...BaseCommand.baseFlags,
13
13
  'query': Flags.string({
14
- description: "Текст поискового запроса. Без него сообщения отбираются только фильтрами. Запрос длиннее 500 знаков обрезается.",
14
+ description: "Текст поискового запроса. Без него сообщения отбираются только фильтрами. Запрос длиннее 256 знаков обрезается.",
15
15
  }),
16
16
  'order': Flags.string({
17
17
  description: "Направление сортировки",
@@ -58,6 +58,7 @@ export default class SearchListMessages extends BaseCommand {
58
58
  let nextCursor = undefined;
59
59
  let pages = 0;
60
60
  const seenCursors = new Set();
61
+ let total;
61
62
  while (pages < 500) {
62
63
  const query = {
63
64
  query: flags['query'],
@@ -80,7 +81,7 @@ export default class SearchListMessages extends BaseCommand {
80
81
  const paginate = meta?.paginate;
81
82
  nextCursor = paginate?.next_page;
82
83
  pages++;
83
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
84
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
84
85
  const hasNext = paginate?.has_next;
85
86
  if (typeof hasNext === 'boolean') {
86
87
  if (!hasNext)
@@ -90,7 +91,8 @@ export default class SearchListMessages extends BaseCommand {
90
91
  break;
91
92
  }
92
93
  if (process.stderr.isTTY) {
93
- const total = paginate?.total;
94
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
95
+ total ??= meta?.total;
94
96
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
95
97
  process.stderr.write(`\r Загружено: ${progress}...`);
96
98
  }
@@ -11,7 +11,7 @@ export default class SearchListUsers extends BaseCommand {
11
11
  static flags = {
12
12
  ...BaseCommand.baseFlags,
13
13
  'query': Flags.string({
14
- description: "Текст поискового запроса. Запрос длиннее 500 знаков обрезается.",
14
+ description: "Текст поискового запроса. Запрос длиннее 256 знаков обрезается.",
15
15
  }),
16
16
  'sort': Flags.string({
17
17
  description: "Сортировка результатов",
@@ -50,6 +50,7 @@ export default class SearchListUsers extends BaseCommand {
50
50
  let nextCursor = undefined;
51
51
  let pages = 0;
52
52
  const seenCursors = new Set();
53
+ let total;
53
54
  while (pages < 500) {
54
55
  const query = {
55
56
  query: flags['query'],
@@ -70,7 +71,7 @@ export default class SearchListUsers extends BaseCommand {
70
71
  const paginate = meta?.paginate;
71
72
  nextCursor = paginate?.next_page;
72
73
  pages++;
73
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
74
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
74
75
  const hasNext = paginate?.has_next;
75
76
  if (typeof hasNext === 'boolean') {
76
77
  if (!hasNext)
@@ -80,7 +81,8 @@ export default class SearchListUsers extends BaseCommand {
80
81
  break;
81
82
  }
82
83
  if (process.stderr.isTTY) {
83
- const total = paginate?.total;
84
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
85
+ total ??= meta?.total;
84
86
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
85
87
  process.stderr.write(`\r Загружено: ${progress}...`);
86
88
  }
@@ -22,7 +22,7 @@ export default class SecurityList extends BaseCommand {
22
22
  options: ["user_login", "user_logout", "user_2fa_fail", "user_2fa_success", "user_2fa_disabled", "login_grant_created", "login_grant_approved", "login_grant_denied", "user_created", "user_deleted", "user_role_changed", "user_updated", "tag_created", "tag_deleted", "user_added_to_tag", "user_removed_from_tag", "chat_created", "chat_renamed", "chat_permission_changed", "user_chat_join", "user_chat_leave", "tag_added_to_chat", "tag_removed_from_chat", "message_updated", "message_deleted", "message_created", "reaction_created", "reaction_deleted", "thread_created", "access_token_created", "access_token_updated", "access_token_destroy", "kms_encrypt", "kms_decrypt", "audit_events_accessed", "company_chats_accessed", "company_bots_accessed", "dlp_violation_detected", "search_users_api", "search_chats_api", "search_messages_api", "bot_scopes_updated", "bot_webhook_settings_updated", "bot_token_recreated", "bot_deleted", "bot_oauth_client_updated", "bot_oauth_client_disabled", "bot_oauth_client_secret_rotated", "bot_oauth_client_secret_issued", "bot_token_created", "bot_token_updated", "bot_token_reissued", "bot_token_deleted", "oauth_authorization_granted", "oauth_authorization_revoked", "oauth_token_revoked", "oauth_device_authorization_approved", "oauth_device_authorization_denied", "video_call_started", "video_call_finished", "video_call_recording_ready", "exchange_disabled"],
23
23
  }),
24
24
  'actor-id': Flags.string({
25
- description: "Идентификатор пользователя, выполнившего действие",
25
+ description: "Идентификатор актора события, как в поле `actor_id` записи. Смену роли, добавление в чат и исключение из чата, назначение и снятие тегов фильтр находит по сотруднику, которого они коснулись.",
26
26
  }),
27
27
  'actor-type': Flags.string({
28
28
  description: "Тип актора",
@@ -53,6 +53,7 @@ export default class SecurityList extends BaseCommand {
53
53
  let nextCursor = undefined;
54
54
  let pages = 0;
55
55
  const seenCursors = new Set();
56
+ let total;
56
57
  while (pages < 500) {
57
58
  const query = {
58
59
  'start_time': flags['start-time'],
@@ -74,7 +75,7 @@ export default class SecurityList extends BaseCommand {
74
75
  const paginate = meta?.paginate;
75
76
  nextCursor = paginate?.next_page;
76
77
  pages++;
77
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
78
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
78
79
  const hasNext = paginate?.has_next;
79
80
  if (typeof hasNext === 'boolean') {
80
81
  if (!hasNext)
@@ -84,7 +85,8 @@ export default class SecurityList extends BaseCommand {
84
85
  break;
85
86
  }
86
87
  if (process.stderr.isTTY) {
87
- const total = paginate?.total;
88
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
89
+ total ??= meta?.total;
88
90
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
89
91
  process.stderr.write(`\r Загружено: ${progress}...`);
90
92
  }
@@ -1,14 +1,12 @@
1
1
  // Auto-generated from openapi.yaml — DO NOT EDIT
2
2
  import { Flags } from '@oclif/core';
3
3
  import { BaseCommand } from '../../base-command.js';
4
- import * as clack from '@clack/prompts';
5
4
  export default class TasksCreate extends BaseCommand {
6
5
  static description = "Новое напоминание";
7
6
  static scope = "tasks:create";
8
7
  static apiMethod = "POST";
9
8
  static apiPath = "/tasks";
10
9
  static defaultColumns = ["id", "content", "created_at", "kind", "due_at"];
11
- static requiredFlags = ["kind"];
12
10
  static args = {};
13
11
  static flags = {
14
12
  ...BaseCommand.baseFlags,
@@ -47,33 +45,9 @@ export default class TasksCreate extends BaseCommand {
47
45
  for await (const chunk of process.stdin) {
48
46
  chunks.push(chunk);
49
47
  }
50
- flags['content'] = Buffer.concat(chunks).toString('utf-8').trimEnd();
51
- }
52
- const missingRequired = [
53
- { flag: 'kind', label: "Тип", type: 'string' },
54
- ].filter((f) => flags[f.flag] === undefined || flags[f.flag] === null);
55
- if (missingRequired.length > 0) {
56
- if (this.isInteractive()) {
57
- for (const field of missingRequired) {
58
- const value = await clack.text({ message: field.label, validate: (v) => v.length === 0 ? 'Обязательное поле' : undefined });
59
- if (clack.isCancel(value)) {
60
- process.stderr.write('Отменено.\n');
61
- this.exit(0);
62
- }
63
- if (field.type === 'integer') {
64
- flags[field.flag] = Number.parseInt(value, 10);
65
- }
66
- else if (field.type === 'boolean') {
67
- flags[field.flag] = value === 'true';
68
- }
69
- else {
70
- flags[field.flag] = value;
71
- }
72
- }
73
- }
74
- else {
75
- this.validationError(missingRequired.map((f) => ({ message: `Обязательный флаг --${f.flag} не передан`, flag: f.flag })), { hint: "Обязательные: --kind <string>. pachca introspect tasks create" });
76
- }
48
+ const piped = Buffer.concat(chunks).toString('utf-8').trimEnd();
49
+ if (piped)
50
+ flags['content'] = piped;
77
51
  }
78
52
  const body = { task: {
79
53
  kind: flags['kind'],
@@ -43,6 +43,7 @@ export default class TasksList extends BaseCommand {
43
43
  let nextCursor = undefined;
44
44
  let pages = 0;
45
45
  const seenCursors = new Set();
46
+ let total;
46
47
  while (pages < 500) {
47
48
  const query = {
48
49
  status: flags['status'],
@@ -61,7 +62,7 @@ export default class TasksList extends BaseCommand {
61
62
  const paginate = meta?.paginate;
62
63
  nextCursor = paginate?.next_page;
63
64
  pages++;
64
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
65
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
65
66
  const hasNext = paginate?.has_next;
66
67
  if (typeof hasNext === 'boolean') {
67
68
  if (!hasNext)
@@ -71,7 +72,8 @@ export default class TasksList extends BaseCommand {
71
72
  break;
72
73
  }
73
74
  if (process.stderr.isTTY) {
74
- const total = paginate?.total;
75
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
76
+ total ??= meta?.total;
75
77
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
76
78
  process.stderr.write(`\r Загружено: ${progress}...`);
77
79
  }
@@ -38,7 +38,7 @@ export default class TasksUpdate extends BaseCommand {
38
38
  allowNo: true,
39
39
  }),
40
40
  'done-at': Flags.string({
41
- description: "Дата и время выполнения напоминания (ISO-8601). Смещение вида `+03:00` учитывается, время без смещения считается UTC. Значение, которое не удалось разобрать, отметку о выполнении снимает: ответ придёт успешный, а в нём `done_at` будет `null`.",
41
+ description: "Дата и время выполнения напоминания (ISO-8601). Смещение вида `+03:00` учитывается, время без смещения считается UTC. Значение, которое не удалось разобрать, отметку о выполнении снимает: ответ придёт успешный, а в нём `status` будет `undone`.",
42
42
  }),
43
43
  'custom-properties': Flags.string({
44
44
  description: "Задаваемые дополнительные поля",
@@ -53,7 +53,9 @@ export default class TasksUpdate extends BaseCommand {
53
53
  for await (const chunk of process.stdin) {
54
54
  chunks.push(chunk);
55
55
  }
56
- flags['content'] = Buffer.concat(chunks).toString('utf-8').trimEnd();
56
+ const piped = Buffer.concat(chunks).toString('utf-8').trimEnd();
57
+ if (piped)
58
+ flags['content'] = piped;
57
59
  }
58
60
  const body = { task: {
59
61
  kind: flags['kind'],
@@ -36,6 +36,7 @@ export default class ThreadsList extends BaseCommand {
36
36
  let nextCursor = undefined;
37
37
  let pages = 0;
38
38
  const seenCursors = new Set();
39
+ let total;
39
40
  while (pages < 500) {
40
41
  const query = {
41
42
  'last_message_at_after': flags['last-message-at-after'],
@@ -52,7 +53,7 @@ export default class ThreadsList extends BaseCommand {
52
53
  const paginate = meta?.paginate;
53
54
  nextCursor = paginate?.next_page;
54
55
  pages++;
55
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
56
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
56
57
  const hasNext = paginate?.has_next;
57
58
  if (typeof hasNext === 'boolean') {
58
59
  if (!hasNext)
@@ -62,7 +63,8 @@ export default class ThreadsList extends BaseCommand {
62
63
  break;
63
64
  }
64
65
  if (process.stderr.isTTY) {
65
- const total = paginate?.total;
66
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
67
+ total ??= meta?.total;
66
68
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
67
69
  process.stderr.write(`\r Загружено: ${progress}...`);
68
70
  }
@@ -1,4 +1,4 @@
1
- import { Args } from '@oclif/core';
1
+ import { Args, Flags } from '@oclif/core';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import { BaseCommand } from '../base-command.js';
@@ -6,7 +6,8 @@ export default class Upload extends BaseCommand {
6
6
  static description = 'Загрузить файл (получает подпись и загружает автоматически)';
7
7
  static examples = [
8
8
  '<%= config.bin %> upload photo.jpg',
9
- 'cat data.csv | <%= config.bin %> upload -',
9
+ 'cat data.csv | <%= config.bin %> upload - --name data.csv',
10
+ '<%= config.bin %> upload ./build/out.pdf --name report.pdf',
10
11
  ];
11
12
  static scope = 'uploads:write';
12
13
  static args = {
@@ -17,10 +18,20 @@ export default class Upload extends BaseCommand {
17
18
  };
18
19
  static flags = {
19
20
  ...BaseCommand.baseFlags,
21
+ name: Flags.string({
22
+ description: 'Имя файла, которым заканчивается key. Расширение name при отправке файла в сообщении должно совпадать с расширением этого имени. По умолчанию это имя загружаемого файла, а у данных из stdin имя stdin без расширения',
23
+ }),
20
24
  };
21
25
  async run() {
22
26
  const { args, flags } = await this.parse(Upload);
23
27
  this.parsedFlags = flags;
28
+ const fromStdin = args.file === '-';
29
+ if (flags.name !== undefined && !isPlainFileName(flags.name)) {
30
+ this.validationError([{ message: 'Имя файла в --name не может быть пустым и содержать / или \\', flag: 'name' }], { type: 'PACHCA_USAGE_ERROR' });
31
+ }
32
+ if (!fromStdin && !fs.existsSync(args.file)) {
33
+ this.validationError([{ message: `Файл не найден: ${args.file}` }], { type: 'PACHCA_USAGE_ERROR' });
34
+ }
24
35
  // Step 1: Get upload params
25
36
  const { data: uploadData } = await this.apiRequest({
26
37
  method: 'POST',
@@ -30,19 +41,18 @@ export default class Upload extends BaseCommand {
30
41
  // Step 2: Read file
31
42
  let fileBlob;
32
43
  let fileName;
33
- if (args.file === '-') {
44
+ // The key ends in this name, and the API checks the extension of `name` in
45
+ // a message's `files` against it. Data from stdin has no name of its own.
46
+ if (fromStdin) {
34
47
  const chunks = [];
35
48
  for await (const chunk of process.stdin)
36
49
  chunks.push(chunk);
37
50
  fileBlob = new Blob([Buffer.concat(chunks)]);
38
- fileName = 'stdin';
51
+ fileName = flags.name ?? 'stdin';
39
52
  }
40
53
  else {
41
- if (!fs.existsSync(args.file)) {
42
- this.validationError([{ message: `Файл не найден: ${args.file}` }], { type: 'PACHCA_USAGE_ERROR' });
43
- }
44
54
  fileBlob = new Blob([fs.readFileSync(args.file)]);
45
- fileName = path.basename(args.file);
55
+ fileName = flags.name ?? path.basename(args.file);
46
56
  }
47
57
  // Step 3: Replace ${filename} in key
48
58
  const key = params.key.replace('${filename}', fileName);
@@ -67,7 +77,16 @@ export default class Upload extends BaseCommand {
67
77
  formData,
68
78
  noAuth: true,
69
79
  });
80
+ // Said after the upload succeeds, so a failed one does not report a file as uploaded.
81
+ if (fromStdin && flags.name === undefined && !flags.quiet) {
82
+ process.stderr.write('⚠ Файл из stdin загружен под именем stdin без расширения: в name файла в сообщении расширения тоже не должно быть.\n' +
83
+ ' Имя с расширением задаёт флаг --name, например --name report.csv\n');
84
+ }
70
85
  // Output the key for use in messages
71
86
  this.output({ key });
72
87
  }
73
88
  }
89
+ /** A bare file name: the key keeps its folder, so a path separator would move the file out of it. */
90
+ function isPlainFileName(name) {
91
+ return name.trim() !== '' && !/[\\/]/.test(name);
92
+ }
@@ -25,13 +25,13 @@ export default class UsersCreate extends BaseCommand {
25
25
  description: "Телефон (макс. 255 символов)",
26
26
  }),
27
27
  'nickname': Flags.string({
28
- description: "Имя пользователя (макс. 255 символов)",
28
+ description: "Имя пользователя: латинские и кириллические буквы, цифры, `_`, `-` и `.`, иначе ответ `400` с кодом `invalid`. Ники `here` и `all` зарезервированы: на них ответ `400` с кодом `use_of_system_words`. (макс. 255 символов)",
29
29
  }),
30
30
  'department': Flags.string({
31
31
  description: "Департамент (макс. 255 символов)",
32
32
  }),
33
33
  'title': Flags.string({
34
- description: "Должность",
34
+ description: "Должность (макс. 255 символов)",
35
35
  }),
36
36
  'role': Flags.string({
37
37
  description: "Уровень доступа",
@@ -102,6 +102,9 @@ export default class UsersCreate extends BaseCommand {
102
102
  if (flags['department'] && String(flags['department']).length > 255) {
103
103
  validationErrors.push({ message: `--department: максимум 255 символов (передано: ${String(flags['department']).length})`, flag: 'department' });
104
104
  }
105
+ if (flags['title'] && String(flags['title']).length > 255) {
106
+ validationErrors.push({ message: `--title: максимум 255 символов (передано: ${String(flags['title']).length})`, flag: 'title' });
107
+ }
105
108
  if (validationErrors.length > 0) {
106
109
  this.validationError(validationErrors);
107
110
  }