@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.11.0 (4 ноября 2026)
4
+
5
+ - **Изменено** (tasks create): Работает без `--kind` и не спрашивает тип в терминале: по умолчанию создаётся простое напоминание
6
+ - **Изменено** (reactions remove): Снимает реакцию по `--name` без `--code`
7
+ - **Добавлено** (profile update-status, users update-status): `--expires-at null` убирает срок статуса, а `--away-message null` убирает текст «Нет на месте»
8
+ - **Добавлено** (bots create, bots update, bots update-webhook, messages update, reactions add, users create, users update): Длина значений проверяется до запроса: `--outgoing-url`, `--challenge-key`, `--display-avatar-url`, `--display-name` и `--title` до 255 символов, а `--code` у `reactions add` до 16
9
+ - **Добавлено**: В выводе команд с сообщениями и черновиками есть поле `voice_content.transcript_status`: статус расшифровки голосового сообщения
10
+ - **Изменено** (api): Справка `--docs` и схема `--spec` показывают необязательными поля деталей журнала аудита, которых нет в записи без значения, а список скоупов без `files:write`, `webhooks:read` и `webhooks:write`
11
+ - **Изменено**: Справка команд следует уточнённым описаниям API: ник сотрудника и бота, пустой бот, события, команды и `--ignore-self-messages` бота, поиск сотрудников и ботов, длина поискового запроса, подпись сообщения бота, название чата, упоминания в черновике, текст «Нет на месте», смена почты, приостановка владельца, `--actor-id` у `pachca security list` и `--done-at` у `pachca tasks update`
12
+
13
+ ## 2026.10.4 (31 октября 2026)
14
+
15
+ - **Исправлено** (api): Справка `--docs` и схема `--spec` показывают `dlp_rule_id` в деталях события `dlp_violation_detected` строкой, как его отдаёт API
16
+ - **Добавлено** (upload): Флаг `--name` задаёт имя файла, которым заканчивается `key`: так данные из stdin получают расширение и уходят в сообщение под этим именем
17
+ - **Исправлено** (upload): Файл, которого нет на диске, команда отклоняет до запроса подписи, в том числе с `--dry-run`
18
+ - **Изменено** (doctor): Проверяет тот же токен, что и остальные команды, в том же порядке: `--token`, `--profile`, `PACHCA_TOKEN`, `PACHCA_PROFILE`, активный профиль, поэтому в CI с одной `PACHCA_TOKEN` команда проходит
19
+ - **Изменено** (doctor): При ошибке хотя бы одной проверки команда завершается с кодом `1` в любом формате вывода, в том числе с `-o json`
20
+ - **Изменено** (doctor): Без файла конфига команда показывает путь, по которому его ищет
21
+ - **Исправлено**: `--dry-run` показывает запрос без входа и без `PACHCA_TOKEN`, а токен в заголовке `Authorization` заменён маской
22
+ - **Исправлено** (files direct-url): С `--dry-run` запрос показан без заголовка `Authorization`, как он и уходит в хранилище
23
+ - **Изменено** (bots update, drafts update): Слово `null` во флаге поля, которое принимает `null`, уходит в API как JSON null: `--kind null` удаляет входящий вебхук бота, а `--parent-message-id null` снимает ответ с черновика
24
+ - **Исправлено** (auth login): Вход через браузер с `--quiet` печатает адрес и код подтверждения, а флаг убирает только строку ожидания и отчёт о входе
25
+ - **Изменено** (auth login): Пример в `--help` и подсказка при `--token -` без потока ввода передают токен из буфера обмена: `pbpaste | pachca auth login --token -`
26
+ - **Исправлено** (messages create, messages update, drafts create, drafts update, tasks create, tasks update): Текст из потока ввода попадает в запрос, только когда поток не пуст, а с пустым потоком, как у скрипта или агента, команда отправляет поля из флагов
27
+ - **Изменено**: Отказ `403` с `role_forbidden` в терминале объясняется словами: право у токена есть, но действие не разрешает роль, поэтому новый вход или токен не помогут. Вывод с `-o json` и в пайпе прежний.
28
+ - **Изменено**: Объяснение отказа `insufficient_scope` в терминале зависит от источника токена: с `--token` или `PACHCA_TOKEN` команда предлагает `pachca oauth token-info -o json`, а если право есть в сохранённом списке профиля, советует обновить его через `pachca auth status --remote`
29
+ - **Изменено**: Когда вход через браузер завершён или отозван, сообщение и подсказка `pachca auth login` называют профиль, с которым шла команда
30
+ - **Исправлено** (api): Эквивалентная команда в справке `--describe` повторяет пример запроса из спецификации и работает при копировании
31
+ - **Исправлено** (api): Пример `pachca api POST /messages` в `--help` передаёт `message[entity_id]` в кавычках и работает при копировании, а описание команды и ошибка `--describe` для неизвестного пути говорят «метод»
32
+ - **Изменено** (views open): Выводит ответ API `{ "success": true }`
33
+ - **Изменено**: Справка команд следует обновлённым описаниям API: удаление входящего вебхука, правило совпадения команд бота, правило упоминаний в тексте сообщения, `duration_ms` и `file_type` у файлов, срок ссылки на поток событий, напоминания вместо задач, поле `scopes` у `bot_token_updated` и другие поля деталей журнала аудита, значения настройки бота у `restricted_bot_tag_denied` в подсказке `--list-tags`, добавление участников в тред, создание треда, `--skip-invite-mentions`, правка без уведомления и список прочитавших
34
+ - **Исправлено** (api): Справка `--docs` и схема `--spec` описывают `scopes` в деталях бота журнала аудита так, как его писал API до 7 октября 2026 года: список прав у `bot_token_created` и объект с `previous` и `new` у `bot_token_updated`
35
+ - **Изменено** (search): С `--all` команды поиска останавливаются по `has_next` и показывают в терминале, сколько результатов загружено из общего числа найденных
36
+ - **Изменено** (api): Справка `--docs` и схема `--spec` методов поиска показывают поле `has_next` в `meta.paginate`
37
+
3
38
  ## 2026.10.3 (28 октября 2026)
4
39
 
5
40
  - **Изменено** (guide): Команда убрана вместе со сценариями: как решать задачи в Пачке, агенту рассказывают скиллы (https://dev.pachca.com/guides/mcp/plugin), а флаги каждой команды показывает `pachca <команда> --help`
package/README.md CHANGED
@@ -286,11 +286,11 @@ pachca auth switch bot-notify
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
  ```
@@ -68,11 +68,11 @@ pachca auth switch bot-notify
68
68
  # Загрузить файл (получает подпись и загружает автоматически)
69
69
  pachca upload photo.jpg
70
70
 
71
- # Из stdin
72
- cat data.csv | pachca upload -
71
+ # Из stdin: имя с расширением задаёт --name
72
+ cat data.csv | pachca upload - --name data.csv
73
73
  ```
74
74
 
75
- Команда `upload` автоматически получает подпись (`POST /uploads`) и загружает файл на S3. Возвращает `key` — используйте его в `--files` при создании сообщений.
75
+ Команда `upload` автоматически получает подпись (`POST /uploads`) и загружает файл на S3. Возвращает `key` — используйте его в `--files` при создании сообщений. Расширение `name` в файле сообщения должно совпадать с расширением имени в `key`.
76
76
 
77
77
  ## Пагинация
78
78
 
@@ -122,7 +122,7 @@ pachca users list --columns id,email,role
122
122
  pachca users list --columns email --no-header
123
123
 
124
124
  # Пайп — цвет отключается автоматически
125
- pachca users list -o json | jq '.[].name'
125
+ pachca users list -o json | jq -r '.[] | "\(.first_name) \(.last_name)"'
126
126
 
127
127
  # Текст из файла или pipe
128
128
  pachca messages create --entity-id 123 < message.txt
@@ -155,7 +155,7 @@ Escape hatch для нестандартных запросов без сген
155
155
 
156
156
  ```
157
157
  pachca api GET /messages --query chat_id=123
158
- 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]=Привет'
159
159
  pachca api POST /messages --input payload.json
160
160
  pachca api GET /profile -o yaml --dry-run
161
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';
@@ -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
@@ -57,7 +57,8 @@ function maskToken(token) {
57
57
  export function formatDryRun(opts, isJson) {
58
58
  const url = buildUrl(opts.path, opts.query);
59
59
  const headers = {
60
- 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)}` }),
61
62
  ...(opts.body ? { 'Content-Type': 'application/json' } : {}),
62
63
  ...opts.headers,
63
64
  };
@@ -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',
@@ -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);
@@ -10,7 +10,7 @@ export default class AuthLogin extends BaseCommand {
10
10
  static examples = [
11
11
  '<%= config.bin %> auth login',
12
12
  '<%= config.bin %> auth login --profile personal',
13
- '<%= config.bin %> auth login --profile ci --token $PACHCA_TOKEN',
13
+ 'pbpaste | <%= config.bin %> auth login --profile bot-notify --token -',
14
14
  ];
15
15
  static flags = {
16
16
  ...BaseCommand.baseFlags,
@@ -42,7 +42,7 @@ export default class AuthLogin extends BaseCommand {
42
42
  // the person guesses to press Ctrl-D. Say what was expected instead.
43
43
  if (flags.token === '-' && process.stdin.isTTY) {
44
44
  outputError({
45
- error: 'Option --token - reads the token from stdin. Pipe it in: echo "$TOKEN" | pachca auth login --token -',
45
+ error: 'Option --token - reads the token from stdin. Pipe it in, for example from the clipboard: pbpaste | pachca auth login --token -',
46
46
  type: 'PACHCA_USAGE_ERROR',
47
47
  code: null,
48
48
  }, this.getOutputFormat());
@@ -104,6 +104,10 @@ export default class AuthLogin extends BaseCommand {
104
104
  * The address and the code are printed separately and the ready-made link with
105
105
  * the code baked in is never shown — a link like that works as phishing when it
106
106
  * is forwarded to someone else's device.
107
+ *
108
+ * They are printed even under `--quiet`: the browser opens either way, and a
109
+ * confirmation page with no code to type leaves the person stuck. `--quiet`
110
+ * drops only the waiting line and the final report.
107
111
  */
108
112
  async deviceLogin(profileName, useBrowser, quiet) {
109
113
  let grant;
@@ -113,14 +117,13 @@ export default class AuthLogin extends BaseCommand {
113
117
  catch (error) {
114
118
  this.failOAuth(error, 'Не удалось начать вход');
115
119
  }
116
- if (!quiet) {
117
- const copied = copyToClipboard(grant.user_code);
118
- process.stderr.write(`\n Откройте ${grant.verification_uri}\n`);
119
- process.stderr.write(` и введите код: ${grant.user_code}\n`);
120
- if (copied)
121
- process.stderr.write(` (код скопирован в буфер обмена)\n`);
120
+ const copied = copyToClipboard(grant.user_code);
121
+ process.stderr.write(`\n Откройте ${grant.verification_uri}\n`);
122
+ process.stderr.write(` и введите код: ${grant.user_code}\n`);
123
+ if (copied)
124
+ process.stderr.write(` (код скопирован в буфер обмена)\n`);
125
+ if (!quiet)
122
126
  process.stderr.write(`\n Ожидаю подтверждения…\n`);
123
- }
124
127
  if (useBrowser)
125
128
  openBrowser(grant.verification_uri);
126
129
  try {
@@ -16,19 +16,19 @@ export default class BotsCreate extends BaseCommand {
16
16
  description: "Имя бота (макс. 255 символов)",
17
17
  }),
18
18
  'nickname': Flags.string({
19
- description: "Никнейм бота. Должен заканчиваться на `_bot`. (макс. 255 символов)",
19
+ description: "Никнейм бота: латинские буквы, цифры и `_`, в конце `_bot`, свободный в пространстве. Ник не по формату отвечает `400` на поле `bot.nickname` с кодом `invalid`, а занятый ник — с кодом `taken`. Без поля ник собирается из имени в латинской транслитерации с `_bot` в конце, а если такой ник занят, с `_2_bot`, `_3_bot` и далее. (макс. 255 символов)",
20
20
  }),
21
21
  'outgoing-url': Flags.string({
22
- description: "URL исходящего вебхука",
22
+ description: "URL исходящего вебхука (макс. 255 символов)",
23
23
  }),
24
24
  'events': Flags.string({
25
- description: "События, на которые подписан бот",
25
+ description: "События, на которые подписан бот. Без поля бот подписан на `message_new`.",
26
26
  }),
27
27
  'trigger-on': Flags.string({
28
28
  description: "Условие срабатывания исходящего вебхука",
29
29
  }),
30
30
  'commands': Flags.string({
31
- description: "Команды бота (триггер-слова), на которые он реагирует при trigger_on = commands. Суммарная длина команд, объединённых через запятую, не должна превышать 255 символов.",
31
+ description: "Команды бота (триггер-слова), на которые он реагирует при trigger_on = commands. Событие о сообщении приходит боту, когда первое слово текста совпадает с одной из команд целиком, с учётом регистра. Команду можно задать и без слэша в начале. Суммарная длина команд вместе с разделителями `, ` (запятая и пробел) не должна превышать 255 символов.",
32
32
  }),
33
33
  'scopes': Flags.string({
34
34
  description: "Скоупы (права доступа) токена бота. Если не указано, бот получает набор по умолчанию. Боту доступны не все скоупы: часть из них разрешена только пользовательским ролям, и попытка назначить такой скоуп возвращает `400`. Служебные значения `bot` и `all` назначать нельзя.",
@@ -40,14 +40,14 @@ export default class BotsCreate extends BaseCommand {
40
40
  description: "Шаблонизатор для обработки шаблона входящего вебхука. Без этого поля бот получает `liquid`, а пустой бот (`empty: true`) — `mustache`.",
41
41
  }),
42
42
  'challenge-key': Flags.string({
43
- description: "Название поля проверки для верификации входящего вебхука",
43
+ description: "Название поля проверки для верификации входящего вебхука (макс. 255 символов)",
44
44
  }),
45
45
  'link-preview-enabled': Flags.boolean({
46
46
  description: "Показывать превью ссылок в сообщениях входящего вебхука",
47
47
  allowNo: true,
48
48
  }),
49
49
  'ignore-self-messages': Flags.boolean({
50
- description: "Не присылать боту события о его собственных сообщениях и реакциях",
50
+ description: "Не присылать боту события о сообщениях, которые он отправил сам, и о реакциях на эти сообщения, кто бы их ни поставил или снял. Нажатия кнопок приходят всегда.",
51
51
  allowNo: true,
52
52
  }),
53
53
  'events-history-enabled': Flags.boolean({
@@ -71,7 +71,7 @@ export default class BotsCreate extends BaseCommand {
71
71
  description: "Домены, ссылки на которые бот разворачивает, не больше 5. Работают вместе с событием `message_link_shared`.",
72
72
  }),
73
73
  'empty': Flags.boolean({
74
- description: "Создать пустого бота: без токена и без входящего вебхука. Без этого поля бот создаётся с входящим вебхуком в своём формате, а его токен приходит в `access_token`.",
74
+ description: "Создать пустого бота: без входящего вебхука и, если не переданы скоупы, без токена. Без этого поля бот создаётся с входящим вебхуком в своём формате, а его токен приходит в `access_token`.",
75
75
  allowNo: true,
76
76
  }),
77
77
  'oauth-client': Flags.string({
@@ -117,6 +117,12 @@ export default class BotsCreate extends BaseCommand {
117
117
  if (flags['nickname'] && String(flags['nickname']).length > 255) {
118
118
  validationErrors.push({ message: `--nickname: максимум 255 символов (передано: ${String(flags['nickname']).length})`, flag: 'nickname' });
119
119
  }
120
+ if (flags['outgoing-url'] && String(flags['outgoing-url']).length > 255) {
121
+ validationErrors.push({ message: `--outgoing-url: максимум 255 символов (передано: ${String(flags['outgoing-url']).length})`, flag: 'outgoing-url' });
122
+ }
123
+ if (flags['challenge-key'] && String(flags['challenge-key']).length > 255) {
124
+ validationErrors.push({ message: `--challenge-key: максимум 255 символов (передано: ${String(flags['challenge-key']).length})`, flag: 'challenge-key' });
125
+ }
120
126
  if (validationErrors.length > 0) {
121
127
  this.validationError(validationErrors);
122
128
  }
@@ -34,6 +34,7 @@ export default class BotsListCompany 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
  query: flags['query'],
@@ -49,7 +50,7 @@ export default class BotsListCompany 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 BotsListCompany 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
  }
@@ -30,6 +30,7 @@ export default class BotsListEvents extends BaseCommand {
30
30
  let nextCursor = undefined;
31
31
  let pages = 0;
32
32
  const seenCursors = new Set();
33
+ let total;
33
34
  while (pages < 500) {
34
35
  const query = {
35
36
  limit: flags.limit,
@@ -44,7 +45,7 @@ export default class BotsListEvents extends BaseCommand {
44
45
  const paginate = meta?.paginate;
45
46
  nextCursor = paginate?.next_page;
46
47
  pages++;
47
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
48
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
48
49
  const hasNext = paginate?.has_next;
49
50
  if (typeof hasNext === 'boolean') {
50
51
  if (!hasNext)
@@ -54,7 +55,8 @@ export default class BotsListEvents extends BaseCommand {
54
55
  break;
55
56
  }
56
57
  if (process.stderr.isTTY) {
57
- const total = paginate?.total;
58
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
59
+ total ??= meta?.total;
58
60
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
59
61
  process.stderr.write(`\r Загружено: ${progress}...`);
60
62
  }
@@ -35,6 +35,7 @@ export default class BotsListTokens 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 BotsListTokens 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 BotsListTokens 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
  }
@@ -11,7 +11,7 @@ export default class BotsList extends BaseCommand {
11
11
  static flags = {
12
12
  ...BaseCommand.baseFlags,
13
13
  'query': Flags.string({
14
- description: "Поисковая фраза для фильтрации ботов по имени",
14
+ description: "Поисковая фраза для фильтрации ботов по имени или нику",
15
15
  }),
16
16
  limit: Flags.integer({
17
17
  description: 'Количество результатов на страницу',
@@ -33,6 +33,7 @@ export default class BotsList extends BaseCommand {
33
33
  let nextCursor = undefined;
34
34
  let pages = 0;
35
35
  const seenCursors = new Set();
36
+ let total;
36
37
  while (pages < 500) {
37
38
  const query = {
38
39
  query: flags['query'],
@@ -48,7 +49,7 @@ export default class BotsList 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 BotsList 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
  }
@@ -13,7 +13,7 @@ export default class BotsUpdateWebhook extends BaseCommand {
13
13
  static flags = {
14
14
  ...BaseCommand.baseFlags,
15
15
  'outgoing-url': Flags.string({
16
- description: "URL исходящего вебхука. Пустая строка отключает вебхук.",
16
+ description: "URL исходящего вебхука. Пустая строка отключает вебхук. (макс. 255 символов)",
17
17
  }),
18
18
  };
19
19
  async run() {
@@ -45,6 +45,13 @@ export default class BotsUpdateWebhook extends BaseCommand {
45
45
  this.validationError(missingRequired.map((f) => ({ message: `Обязательный флаг --${f.flag} не передан`, flag: f.flag })), { hint: "Обязательные: --outgoing-url <string>. pachca introspect bots update-webhook" });
46
46
  }
47
47
  }
48
+ const validationErrors = [];
49
+ if (flags['outgoing-url'] && String(flags['outgoing-url']).length > 255) {
50
+ validationErrors.push({ message: `--outgoing-url: максимум 255 символов (передано: ${String(flags['outgoing-url']).length})`, flag: 'outgoing-url' });
51
+ }
52
+ if (validationErrors.length > 0) {
53
+ this.validationError(validationErrors);
54
+ }
48
55
  const body = { webhook: {
49
56
  outgoing_url: flags['outgoing-url'],
50
57
  } };