@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.
- package/CHANGELOG.md +41 -0
- package/README.md +29 -29
- package/README.template.md +19 -28
- package/dist/base-command.js +69 -37
- package/dist/client.js +58 -33
- package/dist/commands/api.js +4 -4
- package/dist/commands/auth/login.js +12 -9
- package/dist/commands/auth/logout.js +23 -12
- package/dist/commands/bots/create-token.js +0 -3
- package/dist/commands/bots/create.js +1 -5
- package/dist/commands/bots/delete.js +0 -3
- package/dist/commands/bots/get-stream.js +23 -0
- package/dist/commands/bots/list-company.js +4 -5
- package/dist/commands/bots/list-events.js +4 -5
- package/dist/commands/bots/list-scopes.js +0 -3
- package/dist/commands/bots/list-tokens.js +4 -2
- package/dist/commands/bots/list.js +4 -5
- package/dist/commands/bots/recreate-token-self.js +0 -3
- package/dist/commands/bots/recreate-token.js +0 -3
- package/dist/commands/bots/reissue-token.js +0 -3
- package/dist/commands/bots/remove-event.js +0 -3
- package/dist/commands/bots/update-webhook.js +0 -3
- package/dist/commands/bots/update.js +4 -8
- package/dist/commands/chats/archive.js +0 -4
- package/dist/commands/chats/create.js +3 -6
- package/dist/commands/chats/download-export.js +0 -3
- package/dist/commands/chats/list-company.js +4 -5
- package/dist/commands/chats/list.js +5 -8
- package/dist/commands/chats/remove-avatar.js +0 -3
- package/dist/commands/chats/request-export.js +0 -3
- package/dist/commands/chats/unarchive.js +0 -3
- package/dist/commands/chats/update-avatar.js +0 -3
- package/dist/commands/chats/update.js +0 -3
- package/dist/commands/custom-properties/list.js +0 -3
- package/dist/commands/doctor.js +60 -35
- package/dist/commands/drafts/create.js +6 -6
- package/dist/commands/drafts/delete.js +0 -3
- package/dist/commands/drafts/list.js +4 -2
- package/dist/commands/drafts/update.js +7 -4
- package/dist/commands/files/uploads.js +0 -3
- package/dist/commands/group-tags/create.js +0 -3
- package/dist/commands/group-tags/list-users.js +4 -5
- package/dist/commands/group-tags/list.js +4 -5
- package/dist/commands/important-messages/create.js +64 -0
- package/dist/commands/important-messages/delete.js +42 -0
- package/dist/commands/important-messages/list.js +91 -0
- package/dist/commands/members/add.js +1 -6
- package/dist/commands/members/leave.js +0 -3
- package/dist/commands/members/list.js +4 -6
- package/dist/commands/members/remove.js +0 -3
- package/dist/commands/members/update.js +0 -3
- package/dist/commands/messages/create.js +6 -9
- package/dist/commands/messages/delete.js +0 -3
- package/dist/commands/messages/get.js +0 -5
- package/dist/commands/messages/list.js +5 -7
- package/dist/commands/messages/pin.js +0 -3
- package/dist/commands/messages/unfurl.js +0 -3
- package/dist/commands/messages/unpin.js +0 -3
- package/dist/commands/messages/update.js +5 -8
- package/dist/commands/oauth/token-info.js +0 -3
- package/dist/commands/profile/delete-avatar.js +0 -3
- package/dist/commands/profile/delete-status.js +0 -3
- package/dist/commands/profile/get.js +0 -4
- package/dist/commands/profile/update-avatar.js +0 -3
- package/dist/commands/profile/update-status.js +1 -4
- package/dist/commands/reactions/add.js +3 -6
- package/dist/commands/reactions/list.js +4 -2
- package/dist/commands/reactions/remove.js +3 -6
- package/dist/commands/read-member/list-readers.js +4 -5
- package/dist/commands/search/list-chats.js +4 -7
- package/dist/commands/search/list-messages.js +4 -5
- package/dist/commands/search/list-users.js +4 -7
- package/dist/commands/security/list.js +4 -7
- package/dist/commands/tasks/create.js +4 -7
- package/dist/commands/tasks/delete.js +0 -3
- package/dist/commands/tasks/get.js +0 -3
- package/dist/commands/tasks/list.js +4 -7
- package/dist/commands/tasks/update.js +5 -7
- package/dist/commands/threads/add.js +0 -4
- package/dist/commands/threads/list.js +4 -6
- package/dist/commands/upload.js +27 -8
- package/dist/commands/users/create.js +0 -5
- package/dist/commands/users/delete.js +0 -3
- package/dist/commands/users/get-status.js +0 -3
- package/dist/commands/users/get.js +0 -3
- package/dist/commands/users/list.js +4 -7
- package/dist/commands/users/remove-avatar.js +0 -3
- package/dist/commands/users/remove-status.js +0 -3
- package/dist/commands/users/update-avatar.js +0 -3
- package/dist/commands/users/update-status.js +1 -4
- package/dist/commands/users/update.js +5 -5
- package/dist/commands/views/open.js +1 -5
- package/dist/commands/views/submit-response.js +0 -3
- package/dist/data/alternatives.json +5 -9
- package/dist/data/changelog.json +177 -0
- package/dist/data/endpoints.json +1 -1
- package/dist/help.js +0 -1
- package/dist/hooks/command-not-found.js +0 -1
- package/dist/hooks/init.js +13 -5
- package/dist/lib/flags.js +25 -0
- package/dist/oauth.js +11 -5
- package/dist/profiles.js +22 -4
- package/dist/token-refresh.js +61 -14
- package/oclif.manifest.json +1 -1
- package/package.json +2 -2
- package/dist/commands/guide.js +0 -90
- 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
|
-
|
|
22
|
+
Какой токен выбрать: https://dev.pachca.com/api/authorization
|
|
23
23
|
|
|
24
24
|
## Использование в агентах (Claude, Cursor и др.)
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Вход запускает сотрудник в своём терминале командой `pachca auth login` и подтверждает в браузере. Агент проверяет вход командой `pachca auth status`. Токен в разговоре агенту не передают.
|
|
27
27
|
|
|
28
28
|
```
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
pachca
|
|
29
|
+
pachca auth status # есть ли вход и под кем
|
|
30
|
+
pachca api ls # справка по API без входа и без токена
|
|
31
|
+
pachca api POST /messages --describe
|
|
32
|
+
```
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
В CI и у агентов без сотрудника токен приходит из переменной окружения, `auth login` не нужен:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
PACHCA_TOKEN=$TOKEN pachca users list --no-input
|
|
36
38
|
```
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
Готовый токен сохраняют через stdin, а не аргументом: аргумент остаётся в истории оболочки и в списке процессов.
|
|
39
41
|
|
|
40
42
|
```
|
|
41
|
-
|
|
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
|
-
|
|
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` | | Отключить
|
|
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 '.[].
|
|
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[
|
|
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
|
```
|
package/README.template.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
|
-
|
|
22
|
+
Какой токен выбрать: https://dev.pachca.com/api/authorization
|
|
23
23
|
|
|
24
24
|
## Использование в агентах (Claude, Cursor и др.)
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Вход запускает сотрудник в своём терминале командой `pachca auth login` и подтверждает в браузере. Агент проверяет вход командой `pachca auth status`. Токен в разговоре агенту не передают.
|
|
27
27
|
|
|
28
28
|
```
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
pachca
|
|
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
|
-
|
|
40
|
+
Готовый токен сохраняют через stdin, а не аргументом: аргумент остаётся в истории оболочки и в списке процессов.
|
|
39
41
|
|
|
40
42
|
```
|
|
41
|
-
|
|
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
|
-
|
|
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 '.[].
|
|
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[
|
|
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
|
```
|
package/dist/base-command.js
CHANGED
|
@@ -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
|
|
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.
|
|
239
|
-
*
|
|
240
|
-
*
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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(
|
|
281
|
-
process.stderr.write(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
//
|
|
106
|
-
|
|
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
|
|
182
|
-
// load balancer says nothing about whether the
|
|
183
|
-
// the write — retrying a POST there duplicates
|
|
184
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/commands/api.js
CHANGED
|
@@ -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` (список
|
|
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
|
-
|
|
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
|
|
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:
|
|
198
|
+
error: `Метод не найден: ${method.toUpperCase()} ${path}. Список: pachca api ls`,
|
|
199
199
|
type: 'PACHCA_NOT_FOUND_ERROR',
|
|
200
200
|
code: null,
|
|
201
201
|
}, errFormat);
|