@pachca/cli 2026.10.2 → 2026.10.4

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 (107) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +29 -29
  3. package/README.template.md +19 -28
  4. package/dist/base-command.js +69 -37
  5. package/dist/client.js +58 -33
  6. package/dist/commands/api.js +4 -4
  7. package/dist/commands/auth/login.js +12 -9
  8. package/dist/commands/auth/logout.js +23 -12
  9. package/dist/commands/bots/create-token.js +0 -3
  10. package/dist/commands/bots/create.js +1 -5
  11. package/dist/commands/bots/delete.js +0 -3
  12. package/dist/commands/bots/get-stream.js +23 -0
  13. package/dist/commands/bots/list-company.js +4 -5
  14. package/dist/commands/bots/list-events.js +4 -5
  15. package/dist/commands/bots/list-scopes.js +0 -3
  16. package/dist/commands/bots/list-tokens.js +4 -2
  17. package/dist/commands/bots/list.js +4 -5
  18. package/dist/commands/bots/recreate-token-self.js +0 -3
  19. package/dist/commands/bots/recreate-token.js +0 -3
  20. package/dist/commands/bots/reissue-token.js +0 -3
  21. package/dist/commands/bots/remove-event.js +0 -3
  22. package/dist/commands/bots/update-webhook.js +0 -3
  23. package/dist/commands/bots/update.js +4 -8
  24. package/dist/commands/chats/archive.js +0 -4
  25. package/dist/commands/chats/create.js +3 -6
  26. package/dist/commands/chats/download-export.js +0 -3
  27. package/dist/commands/chats/list-company.js +4 -5
  28. package/dist/commands/chats/list.js +5 -8
  29. package/dist/commands/chats/remove-avatar.js +0 -3
  30. package/dist/commands/chats/request-export.js +0 -3
  31. package/dist/commands/chats/unarchive.js +0 -3
  32. package/dist/commands/chats/update-avatar.js +0 -3
  33. package/dist/commands/chats/update.js +0 -3
  34. package/dist/commands/custom-properties/list.js +0 -3
  35. package/dist/commands/doctor.js +60 -35
  36. package/dist/commands/drafts/create.js +6 -6
  37. package/dist/commands/drafts/delete.js +0 -3
  38. package/dist/commands/drafts/list.js +4 -2
  39. package/dist/commands/drafts/update.js +7 -4
  40. package/dist/commands/files/uploads.js +0 -3
  41. package/dist/commands/group-tags/create.js +0 -3
  42. package/dist/commands/group-tags/list-users.js +4 -5
  43. package/dist/commands/group-tags/list.js +4 -5
  44. package/dist/commands/important-messages/create.js +64 -0
  45. package/dist/commands/important-messages/delete.js +42 -0
  46. package/dist/commands/important-messages/list.js +91 -0
  47. package/dist/commands/members/add.js +1 -6
  48. package/dist/commands/members/leave.js +0 -3
  49. package/dist/commands/members/list.js +4 -6
  50. package/dist/commands/members/remove.js +0 -3
  51. package/dist/commands/members/update.js +0 -3
  52. package/dist/commands/messages/create.js +6 -9
  53. package/dist/commands/messages/delete.js +0 -3
  54. package/dist/commands/messages/get.js +0 -5
  55. package/dist/commands/messages/list.js +5 -7
  56. package/dist/commands/messages/pin.js +0 -3
  57. package/dist/commands/messages/unfurl.js +0 -3
  58. package/dist/commands/messages/unpin.js +0 -3
  59. package/dist/commands/messages/update.js +5 -8
  60. package/dist/commands/oauth/token-info.js +0 -3
  61. package/dist/commands/profile/delete-avatar.js +0 -3
  62. package/dist/commands/profile/delete-status.js +0 -3
  63. package/dist/commands/profile/get.js +0 -4
  64. package/dist/commands/profile/update-avatar.js +0 -3
  65. package/dist/commands/profile/update-status.js +1 -4
  66. package/dist/commands/reactions/add.js +3 -6
  67. package/dist/commands/reactions/list.js +4 -2
  68. package/dist/commands/reactions/remove.js +3 -6
  69. package/dist/commands/read-member/list-readers.js +4 -5
  70. package/dist/commands/search/list-chats.js +4 -7
  71. package/dist/commands/search/list-messages.js +4 -5
  72. package/dist/commands/search/list-users.js +4 -7
  73. package/dist/commands/security/list.js +4 -7
  74. package/dist/commands/tasks/create.js +4 -7
  75. package/dist/commands/tasks/delete.js +0 -3
  76. package/dist/commands/tasks/get.js +0 -3
  77. package/dist/commands/tasks/list.js +4 -7
  78. package/dist/commands/tasks/update.js +5 -7
  79. package/dist/commands/threads/add.js +0 -4
  80. package/dist/commands/threads/list.js +4 -6
  81. package/dist/commands/upload.js +27 -8
  82. package/dist/commands/users/create.js +0 -5
  83. package/dist/commands/users/delete.js +0 -3
  84. package/dist/commands/users/get-status.js +0 -3
  85. package/dist/commands/users/get.js +0 -3
  86. package/dist/commands/users/list.js +4 -7
  87. package/dist/commands/users/remove-avatar.js +0 -3
  88. package/dist/commands/users/remove-status.js +0 -3
  89. package/dist/commands/users/update-avatar.js +0 -3
  90. package/dist/commands/users/update-status.js +1 -4
  91. package/dist/commands/users/update.js +5 -5
  92. package/dist/commands/views/open.js +1 -5
  93. package/dist/commands/views/submit-response.js +0 -3
  94. package/dist/data/alternatives.json +5 -9
  95. package/dist/data/changelog.json +177 -0
  96. package/dist/data/endpoints.json +1 -1
  97. package/dist/help.js +0 -1
  98. package/dist/hooks/command-not-found.js +0 -1
  99. package/dist/hooks/init.js +13 -5
  100. package/dist/lib/flags.js +25 -0
  101. package/dist/oauth.js +11 -5
  102. package/dist/profiles.js +22 -4
  103. package/dist/token-refresh.js +61 -14
  104. package/oclif.manifest.json +1 -1
  105. package/package.json +2 -2
  106. package/dist/commands/guide.js +0 -90
  107. package/dist/data/workflows.json +0 -1058
package/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.10.4 (31 октября 2026)
4
+
5
+ - **Исправлено** (api): Справка `--docs` и схема `--spec` показывают `dlp_rule_id` в деталях события `dlp_violation_detected` строкой, как его отдаёт API
6
+ - **Добавлено** (upload): Флаг `--name` задаёт имя файла, которым заканчивается `key`: так данные из stdin получают расширение и уходят в сообщение под этим именем
7
+ - **Исправлено** (upload): Файл, которого нет на диске, команда отклоняет до запроса подписи, в том числе с `--dry-run`
8
+ - **Изменено** (doctor): Проверяет тот же токен, что и остальные команды, в том же порядке: `--token`, `--profile`, `PACHCA_TOKEN`, `PACHCA_PROFILE`, активный профиль, поэтому в CI с одной `PACHCA_TOKEN` команда проходит
9
+ - **Изменено** (doctor): При ошибке хотя бы одной проверки команда завершается с кодом `1` в любом формате вывода, в том числе с `-o json`
10
+ - **Изменено** (doctor): Без файла конфига команда показывает путь, по которому его ищет
11
+ - **Исправлено**: `--dry-run` показывает запрос без входа и без `PACHCA_TOKEN`, а токен в заголовке `Authorization` заменён маской
12
+ - **Исправлено** (files direct-url): С `--dry-run` запрос показан без заголовка `Authorization`, как он и уходит в хранилище
13
+ - **Изменено** (bots update, drafts update): Слово `null` во флаге поля, которое принимает `null`, уходит в API как JSON null: `--kind null` удаляет входящий вебхук бота, а `--parent-message-id null` снимает ответ с черновика
14
+ - **Исправлено** (auth login): Вход через браузер с `--quiet` печатает адрес и код подтверждения, а флаг убирает только строку ожидания и отчёт о входе
15
+ - **Изменено** (auth login): Пример в `--help` и подсказка при `--token -` без потока ввода передают токен из буфера обмена: `pbpaste | pachca auth login --token -`
16
+ - **Исправлено** (messages create, messages update, drafts create, drafts update, tasks create, tasks update): Текст из потока ввода попадает в запрос, только когда поток не пуст, а с пустым потоком, как у скрипта или агента, команда отправляет поля из флагов
17
+ - **Изменено**: Отказ `403` с `role_forbidden` в терминале объясняется словами: право у токена есть, но действие не разрешает роль, поэтому новый вход или токен не помогут. Вывод с `-o json` и в пайпе прежний.
18
+ - **Изменено**: Объяснение отказа `insufficient_scope` в терминале зависит от источника токена: с `--token` или `PACHCA_TOKEN` команда предлагает `pachca oauth token-info -o json`, а если право есть в сохранённом списке профиля, советует обновить его через `pachca auth status --remote`
19
+ - **Изменено**: Когда вход через браузер завершён или отозван, сообщение и подсказка `pachca auth login` называют профиль, с которым шла команда
20
+ - **Исправлено** (api): Эквивалентная команда в справке `--describe` повторяет пример запроса из спецификации и работает при копировании
21
+ - **Исправлено** (api): Пример `pachca api POST /messages` в `--help` передаёт `message[entity_id]` в кавычках и работает при копировании, а описание команды и ошибка `--describe` для неизвестного пути говорят «метод»
22
+ - **Изменено** (views open): Выводит ответ API `{ "success": true }`
23
+ - **Изменено**: Справка команд следует обновлённым описаниям API: удаление входящего вебхука, правило совпадения команд бота, правило упоминаний в тексте сообщения, `duration_ms` и `file_type` у файлов, срок ссылки на поток событий, напоминания вместо задач, поле `scopes` у `bot_token_updated` и другие поля деталей журнала аудита, значения настройки бота у `restricted_bot_tag_denied` в подсказке `--list-tags`, добавление участников в тред, создание треда, `--skip-invite-mentions`, правка без уведомления и список прочитавших
24
+ - **Исправлено** (api): Справка `--docs` и схема `--spec` описывают `scopes` в деталях бота журнала аудита так, как его писал API до 7 октября 2026 года: список прав у `bot_token_created` и объект с `previous` и `new` у `bot_token_updated`
25
+ - **Изменено** (search): С `--all` команды поиска останавливаются по `has_next` и показывают в терминале, сколько результатов загружено из общего числа найденных
26
+ - **Изменено** (api): Справка `--docs` и схема `--spec` методов поиска показывают поле `has_next` в `meta.paginate`
27
+
28
+ ## 2026.10.3 (28 октября 2026)
29
+
30
+ - **Изменено** (guide): Команда убрана вместе со сценариями: как решать задачи в Пачке, агенту рассказывают скиллы (https://dev.pachca.com/guides/mcp/plugin), а флаги каждой команды показывает `pachca <команда> --help`
31
+ - **Изменено**: Из `--help` команд убран блок примеров со сценариями
32
+ - **Изменено** (api): `api ls` и справка `--describe`, `--spec`, `--docs` работают без токена: они читают встроенную спецификацию и в API не обращаются
33
+ - **Добавлено** (important-messages list, important-messages create, important-messages delete): Новые команды — раздел «Важное» владельца токена: список отмеченных сообщений, добавление сообщения и удаление из раздела
34
+ - **Добавлено** (bots get-stream): Новая команда — ссылка на поток событий бота
35
+ - **Добавлено** (users update): Значение `guest` у `--role` вместе с флагом `--chat-ids` переводит сотрудника в гости
36
+ - **Добавлено** (chats create): Команда предупреждает в stderr об идентификаторах из `--group-tag-ids`, которых нет среди тегов пространства
37
+ - **Добавлено**: В выводе команд с сообщениями появилось поле `important`: сообщение в разделе «Важное» владельца токена
38
+ - **Изменено** (auth logout): После входа через браузер команда закрывает вход на сервере и при истёкшем токене: сначала обновляет его, потом отзывает свежий
39
+ - **Изменено** (auth logout): `token_revoked: true` значит, что вход на сервере закрыт, в том числе если его закрыли раньше, а при `false` закрыть вход не удалось, и его нужно отозвать в списке «Токены CLI»
40
+ - **Изменено**: Ответы `500`, `502`, `503`, `504` и сетевую ошибку CLI повторяет до трёх раз только у запросов GET, PUT и DELETE, POST и PATCH после них не повторяются, а `504` с кодом `timeout` (поиск не уложился по времени) приходит ошибкой сразу
41
+ - **Изменено**: Подсказки при опечатке в команде предлагают только существующие команды, без `pachca skill …`
42
+ - **Исправлено**: Описание флага `--no-retry` уточнено: он отключает повторы после `429`, `5xx` и сетевой ошибки
43
+
3
44
  ## 2026.10.2 (21 октября 2026)
4
45
 
5
46
  - **Добавлено** (security list): Флаг `--event-key` принимает события входа по QR-коду: `login_grant_created`, `login_grant_approved` и `login_grant_denied`
package/README.md CHANGED
@@ -14,31 +14,33 @@ npx -y @pachca/cli users list
14
14
  ## Быстрый старт
15
15
 
16
16
  ```
17
- pachca auth login # сохранить токен
17
+ pachca auth login # вход через браузер
18
18
  pachca users list # список сотрудников
19
19
  pachca messages create --entity-id 123 --content "Привет!"
20
20
  ```
21
21
 
22
- Получить токен: https://dev.pachca.com/guides/authorization
22
+ Какой токен выбрать: https://dev.pachca.com/api/authorization
23
23
 
24
24
  ## Использование в агентах (Claude, Cursor и др.)
25
25
 
26
- Если агент устанавливает CLI по просьбе пользователя и токена ещё нет — агент должен запросить его у пользователя:
26
+ Вход запускает сотрудник в своём терминале командой `pachca auth login` и подтверждает в браузере. Агент проверяет вход командой `pachca auth status`. Токен в разговоре агенту не передают.
27
27
 
28
28
  ```
29
- # Агент спрашивает: "Нужен API-токен Пачки."
30
- # "Получите его на https://dev.pachca.com/guides/authorization"
31
- pachca auth login --token <токен от пользователя>
29
+ pachca auth status # есть ли вход и под кем
30
+ pachca api ls # справка по API без входа и без токена
31
+ pachca api POST /messages --describe
32
+ ```
32
33
 
33
- # Дальше команды работают без токена в каждой строке
34
- pachca users list
35
- pachca messages create --entity-id 123 --content "Привет!"
34
+ В CI и у агентов без сотрудника токен приходит из переменной окружения, `auth login` не нужен:
35
+
36
+ ```
37
+ PACHCA_TOKEN=$TOKEN pachca users list --no-input
36
38
  ```
37
39
 
38
- Если токен уже есть в окружении — `auth login` не нужен:
40
+ Готовый токен сохраняют через stdin, а не аргументом: аргумент остаётся в истории оболочки и в списке процессов.
39
41
 
40
42
  ```
41
- PACHCA_TOKEN=$TOKEN pachca users list
43
+ pbpaste | pachca auth login --token -
42
44
  ```
43
45
 
44
46
  ## Профили (несколько токенов)
@@ -52,18 +54,7 @@ pachca auth list
52
54
  pachca auth switch bot-notify
53
55
  ```
54
56
 
55
- Токены хранятся в `~/.config/pachca/config.toml` (chmod 600).
56
- В CI — через переменную окружения PACHCA_TOKEN.
57
-
58
- ## Поиск по сценариям
59
-
60
- Не знаете какую команду использовать? Поищите по задаче:
61
-
62
- ```
63
- pachca guide "отправить файл"
64
- pachca guide "создать беседу и добавить участников"
65
- pachca guide # список всех сценариев
66
- ```
57
+ Токен хранится в хранилище ключей ОС, в `~/.config/pachca/config.toml` только имя, почта и права. Где хранилища нет, токен лежит в этом файле, CLI говорит об этом при входе.
67
58
 
68
59
  <!-- AUTO:COMMANDS -->
69
60
  ## Команды
@@ -97,6 +88,7 @@ pachca guide # список всех сценариев
97
88
  | `pachca bots reissue-token` | Перевыпуск токена бота |
98
89
  | `pachca bots list-company` | Список ботов пространства |
99
90
  | `pachca bots list-events` | История событий |
91
+ | `pachca bots get-stream` | Поток событий |
100
92
  | `pachca bots remove-event` | Удаление события истории |
101
93
 
102
94
  ### chats
@@ -163,6 +155,14 @@ pachca guide # список всех сценариев
163
155
  | `pachca group-tags delete` | Удаление тега |
164
156
  | `pachca group-tags list-users` | Список сотрудников тега |
165
157
 
158
+ ### important-messages
159
+
160
+ | Команда | Описание |
161
+ |---------|---------|
162
+ | `pachca important-messages list` | Список важных сообщений |
163
+ | `pachca important-messages create` | Добавление сообщения в важные |
164
+ | `pachca important-messages delete` | Удаление сообщения из важных |
165
+
166
166
  ### messages
167
167
 
168
168
  | Команда | Описание |
@@ -275,7 +275,7 @@ pachca guide # список всех сценариев
275
275
  | `--no-input` | | Отключить интерактивные промпты |
276
276
  | `--dry-run` | | Показать запрос без отправки |
277
277
  | `--timeout <value>` | | Таймаут запроса в секундах (по умолчанию 30) |
278
- | `--no-retry` | | Отключить авто-retry при 429/503 |
278
+ | `--no-retry` | | Отключить автоповтор при 429, 5xx и сетевых ошибках |
279
279
  | `--plain` | | Плоский вывод: TSV без заголовка, ID первым, без цвета (для скриптов) |
280
280
 
281
281
  <!-- AUTO:FLAGS:END -->
@@ -286,11 +286,11 @@ pachca guide # список всех сценариев
286
286
  # Загрузить файл (получает подпись и загружает автоматически)
287
287
  pachca upload photo.jpg
288
288
 
289
- # Из stdin
290
- cat data.csv | pachca upload -
289
+ # Из stdin: имя с расширением задаёт --name
290
+ cat data.csv | pachca upload - --name data.csv
291
291
  ```
292
292
 
293
- Команда `upload` автоматически получает подпись (`POST /uploads`) и загружает файл на S3. Возвращает `key` — используйте его в `--files` при создании сообщений.
293
+ Команда `upload` автоматически получает подпись (`POST /uploads`) и загружает файл на S3. Возвращает `key` — используйте его в `--files` при создании сообщений. Расширение `name` в файле сообщения должно совпадать с расширением имени в `key`.
294
294
 
295
295
  ## Пагинация
296
296
 
@@ -340,7 +340,7 @@ pachca users list --columns id,email,role
340
340
  pachca users list --columns email --no-header
341
341
 
342
342
  # Пайп — цвет отключается автоматически
343
- pachca users list -o json | jq '.[].name'
343
+ pachca users list -o json | jq -r '.[] | "\(.first_name) \(.last_name)"'
344
344
 
345
345
  # Текст из файла или pipe
346
346
  pachca messages create --entity-id 123 < message.txt
@@ -373,7 +373,7 @@ Escape hatch для нестандартных запросов без сген
373
373
 
374
374
  ```
375
375
  pachca api GET /messages --query chat_id=123
376
- pachca api POST /messages -F message[chat_id]=12345 -f message[content]="Привет"
376
+ pachca api POST /messages -F 'message[entity_id]=12345' -f 'message[content]=Привет'
377
377
  pachca api POST /messages --input payload.json
378
378
  pachca api GET /profile -o yaml --dry-run
379
379
  ```
@@ -14,31 +14,33 @@ npx -y @pachca/cli users list
14
14
  ## Быстрый старт
15
15
 
16
16
  ```
17
- pachca auth login # сохранить токен
17
+ pachca auth login # вход через браузер
18
18
  pachca users list # список сотрудников
19
19
  pachca messages create --entity-id 123 --content "Привет!"
20
20
  ```
21
21
 
22
- Получить токен: https://dev.pachca.com/guides/authorization
22
+ Какой токен выбрать: https://dev.pachca.com/api/authorization
23
23
 
24
24
  ## Использование в агентах (Claude, Cursor и др.)
25
25
 
26
- Если агент устанавливает CLI по просьбе пользователя и токена ещё нет — агент должен запросить его у пользователя:
26
+ Вход запускает сотрудник в своём терминале командой `pachca auth login` и подтверждает в браузере. Агент проверяет вход командой `pachca auth status`. Токен в разговоре агенту не передают.
27
27
 
28
28
  ```
29
- # Агент спрашивает: "Нужен API-токен Пачки."
30
- # "Получите его на https://dev.pachca.com/guides/authorization"
31
- pachca auth login --token <токен от пользователя>
29
+ pachca auth status # есть ли вход и под кем
30
+ pachca api ls # справка по API без входа и без токена
31
+ pachca api POST /messages --describe
32
+ ```
33
+
34
+ В CI и у агентов без сотрудника токен приходит из переменной окружения, `auth login` не нужен:
32
35
 
33
- # Дальше команды работают без токена в каждой строке
34
- pachca users list
35
- pachca messages create --entity-id 123 --content "Привет!"
36
+ ```
37
+ PACHCA_TOKEN=$TOKEN pachca users list --no-input
36
38
  ```
37
39
 
38
- Если токен уже есть в окружении — `auth login` не нужен:
40
+ Готовый токен сохраняют через stdin, а не аргументом: аргумент остаётся в истории оболочки и в списке процессов.
39
41
 
40
42
  ```
41
- PACHCA_TOKEN=$TOKEN pachca users list
43
+ pbpaste | pachca auth login --token -
42
44
  ```
43
45
 
44
46
  ## Профили (несколько токенов)
@@ -52,18 +54,7 @@ pachca auth list
52
54
  pachca auth switch bot-notify
53
55
  ```
54
56
 
55
- Токены хранятся в `~/.config/pachca/config.toml` (chmod 600).
56
- В CI — через переменную окружения PACHCA_TOKEN.
57
-
58
- ## Поиск по сценариям
59
-
60
- Не знаете какую команду использовать? Поищите по задаче:
61
-
62
- ```
63
- pachca guide "отправить файл"
64
- pachca guide "создать беседу и добавить участников"
65
- pachca guide # список всех сценариев
66
- ```
57
+ Токен хранится в хранилище ключей ОС, в `~/.config/pachca/config.toml` только имя, почта и права. Где хранилища нет, токен лежит в этом файле, CLI говорит об этом при входе.
67
58
 
68
59
  <!-- AUTO:COMMANDS -->
69
60
  <!-- AUTO:COMMANDS:END -->
@@ -77,11 +68,11 @@ pachca guide # список всех сценариев
77
68
  # Загрузить файл (получает подпись и загружает автоматически)
78
69
  pachca upload photo.jpg
79
70
 
80
- # Из stdin
81
- cat data.csv | pachca upload -
71
+ # Из stdin: имя с расширением задаёт --name
72
+ cat data.csv | pachca upload - --name data.csv
82
73
  ```
83
74
 
84
- Команда `upload` автоматически получает подпись (`POST /uploads`) и загружает файл на S3. Возвращает `key` — используйте его в `--files` при создании сообщений.
75
+ Команда `upload` автоматически получает подпись (`POST /uploads`) и загружает файл на S3. Возвращает `key` — используйте его в `--files` при создании сообщений. Расширение `name` в файле сообщения должно совпадать с расширением имени в `key`.
85
76
 
86
77
  ## Пагинация
87
78
 
@@ -131,7 +122,7 @@ pachca users list --columns id,email,role
131
122
  pachca users list --columns email --no-header
132
123
 
133
124
  # Пайп — цвет отключается автоматически
134
- pachca users list -o json | jq '.[].name'
125
+ pachca users list -o json | jq -r '.[] | "\(.first_name) \(.last_name)"'
135
126
 
136
127
  # Текст из файла или pipe
137
128
  pachca messages create --entity-id 123 < message.txt
@@ -164,7 +155,7 @@ Escape hatch для нестандартных запросов без сген
164
155
 
165
156
  ```
166
157
  pachca api GET /messages --query chat_id=123
167
- pachca api POST /messages -F message[chat_id]=12345 -f message[content]="Привет"
158
+ pachca api POST /messages -F 'message[entity_id]=12345' -f 'message[content]=Привет'
168
159
  pachca api POST /messages --input payload.json
169
160
  pachca api GET /profile -o yaml --dry-run
170
161
  ```
@@ -1,5 +1,5 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
- import { resolveToken, TokenNotFoundError, ProfileNotFoundError, getAuthMethod, getDefaults, } from './profiles.js';
2
+ import { resolveToken, TokenNotFoundError, ProfileNotFoundError, getAuthMethod, getDefaults, profileFlag, } from './profiles.js';
3
3
  import { ApiError, getExitCode, formatDryRun, request } from './client.js';
4
4
  import { outputData, outputError, outputSuccess } from './output.js';
5
5
  import { defaultOutputFormat, isInteractive } from './utils.js';
@@ -57,7 +57,7 @@ export class BaseCommand extends Command {
57
57
  min: 1,
58
58
  }),
59
59
  'no-retry': Flags.boolean({
60
- description: 'Disable auto-retry on 429/503',
60
+ description: 'Disable auto-retry on 429, 5xx and network errors',
61
61
  default: false,
62
62
  }),
63
63
  json: Flags.boolean({
@@ -181,8 +181,10 @@ export class BaseCommand extends Command {
181
181
  * Make an authenticated API request.
182
182
  */
183
183
  async apiRequest(opts) {
184
- const { token } = this.resolveAuth();
185
184
  const clientFlags = this.getClientFlags();
185
+ // --dry-run sends nothing, so it needs no token: the preview masks the header
186
+ // either way. Resolving first made it fail with «Token not found» before login.
187
+ const token = clientFlags['dry-run'] ? '' : this.resolveAuth().token;
186
188
  const fullOpts = { ...opts, token };
187
189
  if (clientFlags['dry-run']) {
188
190
  const dryRunOutput = formatDryRun(fullOpts, this.getOutputFormat() === 'json');
@@ -235,9 +237,11 @@ export class BaseCommand extends Command {
235
237
  * Explain a scope refusal instead of printing a bare 403.
236
238
  *
237
239
  * An agent cannot walk through a consent screen — the most useful thing it can
238
- * do is hand control back to a human with the reason spelled out. The reason
239
- * differs by how the profile was authorised: an OAuth login already asks for
240
- * every scope, so a refusal there can only come from the role.
240
+ * do is hand control back to a human with the reason spelled out.
241
+ *
242
+ * `insufficient_scope` always means the token itself lacks the scope. A role
243
+ * that does not allow the action comes back as `role_forbidden` instead, see
244
+ * reportRoleRefusal. The fix differs by how the profile was authorised.
241
245
  */
242
246
  reportScopeError(err) {
243
247
  const format = this.getOutputFormat();
@@ -246,44 +250,69 @@ export class BaseCommand extends Command {
246
250
  outputError(err.details, format);
247
251
  this.exit(3);
248
252
  }
249
- let profile;
250
- try {
251
- ({ profile } = resolveToken({
252
- token: this.parsedFlags.token,
253
- profile: this.parsedFlags.profile,
254
- }));
253
+ const resolved = this.tryResolveProfile();
254
+ const profile = resolved?.profile;
255
+ process.stderr.write(`✗ Не хватает права${scope ? ` ${scope}` : ''}.\n`);
256
+ if (!profile) {
257
+ process.stderr.write(` У токена ${scope ? 'этого' : 'нужного'} права нет.\n`);
258
+ process.stderr.write(` Проверить права токена: pachca oauth token-info -o json\n`);
259
+ this.exit(3);
255
260
  }
256
- catch {
257
- // no profile resolved — the token came in from a flag or the environment
261
+ // The scope list in the profile is a copy taken at login. When it still names
262
+ // the scope the server just refused, the copy is stale: the token's scopes were
263
+ // changed after login. Naming the scope needs the server's message to carry it.
264
+ const flag = profileFlag(resolved?.profileName);
265
+ const listedLocally = !!scope && !!profile.scopes?.includes(scope);
266
+ if (listedLocally) {
267
+ process.stderr.write(` В сохранённом списке право есть, но у токена его уже нет: список устарел.\n`);
268
+ process.stderr.write(` Обновить список: pachca auth status --remote${flag}\n`);
258
269
  }
259
- // Two different causes, and the fix differs. Either the scope is not in the
260
- // token, or it is there but the role no longer allows it — the API checks
261
- // both on every call. Which one it is shows in the token's own scope list.
262
- //
263
- // Naming the cause needs the scope itself, and that comes from parsing the
264
- // server's message. When it cannot be identified, fall back to the generic
265
- // wording: guessing a branch here would send people to fix the wrong thing.
266
- const granted = scope ? profile?.scopes : undefined;
267
- const hasScope = !!scope && !!granted && granted.includes(scope);
268
- process.stderr.write(`✗ Не хватает права${scope ? ` ${scope}` : ''}.\n`);
269
- if (granted && hasScope) {
270
- process.stderr.write(` Право у токена есть, но его не даёт текущая роль.\n`);
271
- process.stderr.write(` Обратитесь к администратору пространства.\n`);
270
+ else {
271
+ process.stderr.write(` У токена ${scope ? 'этого' : 'нужного'} права нет.\n`);
272
+ }
273
+ if (getAuthMethod(profile) === 'oauth') {
274
+ process.stderr.write(` При входе через браузер права выдаются по роли на момент подтверждения.\n`);
275
+ process.stderr.write(` Если роль с тех пор расширили, войдите заново: pachca auth login${flag}\n`);
276
+ }
277
+ else {
278
+ process.stderr.write(` Добавьте право токену в Пачке или выпустите новый токен с ним.\n`);
279
+ }
280
+ this.exit(3);
281
+ }
282
+ /**
283
+ * Explain a role refusal: the token carries the scope, but the role of the
284
+ * account behind it does not allow the action. No new login or token changes
285
+ * that, so say who can. The API names no scope here; a generated command
286
+ * knows the one its method requires.
287
+ */
288
+ reportRoleRefusal(err) {
289
+ const format = this.getOutputFormat();
290
+ if (format === 'json' || !process.stderr.isTTY) {
291
+ outputError(err.details, format);
292
+ this.exit(3);
272
293
  }
273
- else if (granted) {
274
- process.stderr.write(` У токена этого права нет.\n`);
275
- process.stderr.write(getAuthMethod(profile) === 'oauth'
276
- ? ` Права выдаются по роли в момент входа — если роль с тех пор изменилась, войдите заново: pachca auth login\n`
277
- : ` Выпустите токен с этим правом в интерфейсе Пачки.\n`);
294
+ const profile = this.tryResolveProfile()?.profile;
295
+ const scope = this.constructor.scope;
296
+ const right = scope ? `Право ${scope}` : 'Право';
297
+ if (profile?.type === 'bot') {
298
+ process.stderr.write(`✗ ${right} есть у токена, но его не даёт роль бота.\n`);
299
+ process.stderr.write(` Это действие выполняют персональным токеном сотрудника с подходящей ролью.\n`);
278
300
  }
279
301
  else {
280
- process.stderr.write(` Либо этого права нет у токена, либо его не даёт ваша роль.\n`);
281
- process.stderr.write(profile
282
- ? ` Проверить права токена: pachca auth status\n`
283
- : ` Проверить права токена: pachca api GET /oauth/token/info\n`);
302
+ process.stderr.write(`✗ ${right} есть у токена, но его не даёт роль.\n`);
303
+ process.stderr.write(` Новый вход или новый токен не помогут: роль меняет администратор пространства.\n`);
284
304
  }
285
305
  this.exit(3);
286
306
  }
307
+ /** The profile the call ran with, or undefined when the token came from --token or PACHCA_TOKEN. */
308
+ tryResolveProfile() {
309
+ try {
310
+ return resolveToken({ token: this.parsedFlags.token, profile: this.parsedFlags.profile });
311
+ }
312
+ catch {
313
+ return undefined;
314
+ }
315
+ }
287
316
  /**
288
317
  * What to do after a 401: the login was revoked, or the saved token no longer works.
289
318
  *
@@ -306,7 +335,7 @@ export class BaseCommand extends Command {
306
335
  ? 'Токен из --token или PACHCA_TOKEN недействителен или отозван.'
307
336
  : 'token from --token or PACHCA_TOKEN is invalid or revoked';
308
337
  }
309
- const flag = resolved.profileName === 'default' ? '' : ` --profile ${resolved.profileName}`;
338
+ const flag = profileFlag(resolved.profileName);
310
339
  if (getAuthMethod(resolved.profile) === 'oauth') {
311
340
  return human
312
341
  ? `Вход завершён или отозван. Войдите заново: pachca auth login${flag}`
@@ -323,6 +352,9 @@ export class BaseCommand extends Command {
323
352
  if (err instanceof ApiError && err.details.type === 'PACHCA_SCOPE_ERROR') {
324
353
  this.reportScopeError(err);
325
354
  }
355
+ if (err instanceof ApiError && err.details.code === 403 && err.details.message === 'role_forbidden') {
356
+ this.reportRoleRefusal(err);
357
+ }
326
358
  if (err instanceof ApiError) {
327
359
  if (err.details.code === 401 && !err.details.hint) {
328
360
  err.details.hint = this.reloginHint();
package/dist/client.js CHANGED
@@ -24,6 +24,11 @@ export function getBaseUrl() {
24
24
  }
25
25
  const MAX_RETRIES = 3;
26
26
  const DEFAULT_TIMEOUT = 30;
27
+ const RETRYABLE_5XX = new Set([500, 502, 503, 504]);
28
+ // A 5xx says nothing about whether the server already applied the request, and
29
+ // the API has no idempotency key: a retried POST /messages sends the message
30
+ // twice. These methods are safe to repeat, the same set as in the SDKs.
31
+ const IDEMPOTENT_METHODS = new Set(['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE']);
27
32
  function buildUrl(apiPath, query) {
28
33
  const base = apiPath.startsWith('http://') || apiPath.startsWith('https://')
29
34
  ? apiPath
@@ -52,7 +57,8 @@ function maskToken(token) {
52
57
  export function formatDryRun(opts, isJson) {
53
58
  const url = buildUrl(opts.path, opts.query);
54
59
  const headers = {
55
- Authorization: `Bearer ${maskToken(opts.token)}`,
60
+ // The real request carries the token; the preview never shows it, and works without one.
61
+ ...(opts.noAuth ? {} : { Authorization: `Bearer ${maskToken(opts.token)}` }),
56
62
  ...(opts.body ? { 'Content-Type': 'application/json' } : {}),
57
63
  ...opts.headers,
58
64
  };
@@ -101,9 +107,11 @@ export async function request(opts, clientFlags) {
101
107
  // means by `--timeout 0`. The env var already ignored 0; make the flag agree.
102
108
  const timeoutSeconds = requestedTimeout > 0 ? requestedTimeout : DEFAULT_TIMEOUT;
103
109
  const noRetry = opts.noRetry ?? clientFlags?.['no-retry'] ?? false;
104
- // Retrying a request the server may already have committed duplicates it.
105
- // Only these methods are safe to repeat blindly.
106
- const safeMethod = ['GET', 'HEAD', 'OPTIONS', 'PUT'].includes(opts.method);
110
+ const method = opts.method.toUpperCase();
111
+ // The same set is retried after a 5xx and after a network error. A repeated
112
+ // DELETE whose first attempt went through answers 404, which the caller sees
113
+ // as an error, but it never deletes anything twice.
114
+ const idempotent = IDEMPOTENT_METHODS.has(method);
107
115
  const fetchHeaders = {
108
116
  ...(opts.noAuth ? {} : { Authorization: `Bearer ${opts.token}` }),
109
117
  Accept: 'application/json',
@@ -178,35 +186,32 @@ export async function request(opts, clientFlags) {
178
186
  continue;
179
187
  }
180
188
  }
181
- // Handle 503 with backoff. Unlike 429, a 503 from a proxy or a draining
182
- // load balancer says nothing about whether the backend already committed
183
- // the write — retrying a POST there duplicates the message.
184
- if (response.status === 503 && safeMethod && !noRetry && attempt < MAX_RETRIES) {
185
- const baseMs = 1000;
186
- const waitMs = baseMs * Math.pow(2, attempt) + Math.random() * baseMs;
187
- if (clientFlags?.verbose) {
188
- process.stderr.write(`${ansis.dim(`⏳ Service unavailable, retrying in ${Math.round(waitMs)}ms...`)}\n`);
189
- }
190
- await sleep(waitMs);
191
- continue;
192
- }
193
- // Parse response body
194
- const contentType = response.headers.get('content-type') || '';
189
+ // Handle 5xx with backoff, for idempotent methods only. Unlike 429, a 5xx
190
+ // from a proxy or a draining load balancer says nothing about whether the
191
+ // backend already committed the write — retrying a POST there duplicates
192
+ // the message.
195
193
  let data;
196
- if (response.status === 204 || response.headers.get('content-length') === '0') {
197
- data = null;
198
- }
199
- else if (opts.isBinary && response.ok) {
200
- // Тело файла нельзя читать как текст: любой байт вне UTF-8 портится
201
- // при декодировании, и сохранённый файл оказывается битым.
202
- data = Buffer.from(await response.arrayBuffer());
203
- }
204
- else if (contentType.includes('application/json')) {
205
- data = await response.json();
206
- }
207
- else {
208
- data = await response.text();
194
+ let bodyRead = false;
195
+ if (RETRYABLE_5XX.has(response.status) && idempotent && !noRetry && attempt < MAX_RETRIES) {
196
+ // A 504 with the code `timeout` is a search the server stopped because
197
+ // the query is too heavy: the same query would time out again, so it
198
+ // goes back to the caller at once. A 504 from a proxy has no such code.
199
+ if (response.status === 504) {
200
+ data = await readBody(response, opts.isBinary).catch(() => null);
201
+ bodyRead = true;
202
+ }
203
+ if (!isQueryTimeout(response.status, data)) {
204
+ const baseMs = 1000;
205
+ const waitMs = baseMs * Math.pow(2, attempt) + Math.random() * baseMs;
206
+ if (clientFlags?.verbose) {
207
+ process.stderr.write(`${ansis.dim(`⏳ Server error ${response.status}, retrying in ${Math.round(waitMs)}ms...`)}\n`);
208
+ }
209
+ await sleep(waitMs);
210
+ continue;
211
+ }
209
212
  }
213
+ if (!bodyRead)
214
+ data = await readBody(response, opts.isBinary);
210
215
  // Handle errors
211
216
  if (!response.ok) {
212
217
  s?.stop('');
@@ -231,8 +236,7 @@ export async function request(opts, clientFlags) {
231
236
  });
232
237
  }
233
238
  lastError = error;
234
- // Only retry network errors for idempotent methods (not POST/DELETE)
235
- if (!safeMethod || noRetry || attempt >= MAX_RETRIES) {
239
+ if (!idempotent || noRetry || attempt >= MAX_RETRIES) {
236
240
  break;
237
241
  }
238
242
  }
@@ -245,6 +249,27 @@ export async function request(opts, clientFlags) {
245
249
  message: lastError?.message || 'Failed to connect to server',
246
250
  });
247
251
  }
252
+ async function readBody(response, isBinary) {
253
+ const contentType = response.headers.get('content-type') || '';
254
+ if (response.status === 204 || response.headers.get('content-length') === '0') {
255
+ return null;
256
+ }
257
+ if (isBinary && response.ok) {
258
+ // Тело файла нельзя читать как текст: любой байт вне UTF-8 портится
259
+ // при декодировании, и сохранённый файл оказывается битым.
260
+ return Buffer.from(await response.arrayBuffer());
261
+ }
262
+ if (contentType.includes('application/json')) {
263
+ return response.json();
264
+ }
265
+ return response.text();
266
+ }
267
+ function isQueryTimeout(status, body) {
268
+ return (status === 504 &&
269
+ isRecord(body) &&
270
+ Array.isArray(body.errors) &&
271
+ body.errors.some((error) => isRecord(error) && error.code === 'timeout'));
272
+ }
248
273
  // Недостающий скоуп по RFC 6750 приходит в параметре scope заголовка
249
274
  // WWW-Authenticate: Bearer error="insufficient_scope", …, scope="profile:read"
250
275
  function scopeFromChallenge(header) {
@@ -4,10 +4,10 @@ import { BaseCommand } from '../base-command.js';
4
4
  import { outputError } from '../output.js';
5
5
  import { buildListRows, detectIntrospectMode, positionals, findEndpoint, renderIntrospect, } from '../api-introspect.js';
6
6
  export default class Api extends BaseCommand {
7
- static description = 'Произвольный запрос к API. Список и справка: `api ls` (список эндпоинтов), `api <МЕТОД> <путь> --describe` / `--spec` / `--docs`';
7
+ static description = 'Произвольный запрос к API. Список и справка: `api ls` (список методов), `api <МЕТОД> <путь> --describe` / `--spec` / `--docs`';
8
8
  static examples = [
9
9
  '<%= config.bin %> api GET /messages --query chat_id=123',
10
- '<%= config.bin %> api POST /messages -F message[chat_id]=12345 -f message[content]="Привет"',
10
+ "<%= config.bin %> api POST /messages -F 'message[entity_id]=12345' -f 'message[content]=Привет'",
11
11
  '<%= config.bin %> api POST /messages --input payload.json',
12
12
  `<%= config.bin %> api POST /messages --data '{"message":{"entity_id":123,"content":"Привет"}}'`,
13
13
  '<%= config.bin %> api GET /profile -o yaml',
@@ -160,7 +160,7 @@ export default class Api extends BaseCommand {
160
160
  const { outputData } = await import('../output.js');
161
161
  outputData(data, { ...this.getOutputOptions(), format: outFormat });
162
162
  }
163
- /** Load the build-time endpoint index (mirrors guide.ts workflows.json loading). */
163
+ /** Load the build-time endpoint index. */
164
164
  async loadEndpoints() {
165
165
  try {
166
166
  const data = await import('../data/endpoints.json', { with: { type: 'json' } });
@@ -195,7 +195,7 @@ export default class Api extends BaseCommand {
195
195
  const entry = findEndpoint(entries, method, path);
196
196
  if (!entry) {
197
197
  outputError({
198
- error: `Эндпоинт не найден: ${method.toUpperCase()} ${path}. Список: pachca api ls`,
198
+ error: `Метод не найден: ${method.toUpperCase()} ${path}. Список: pachca api ls`,
199
199
  type: 'PACHCA_NOT_FOUND_ERROR',
200
200
  code: null,
201
201
  }, errFormat);