@pachca/cli 2026.7.2 → 2026.8.1

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.
@@ -1,6 +1,22 @@
1
+ import { Flags } from '@oclif/core';
1
2
  import { BaseCommand } from '../../base-command.js';
2
- import { getActiveProfile, getProfile } from '../../profiles.js';
3
+ import { getActiveProfile, getAuthMethod, getProfile, getSecretStorage, updateProfileScopes, } from '../../profiles.js';
4
+ import { request } from '../../client.js';
3
5
  import { outputError } from '../../output.js';
6
+ /** Human-readable time left, or null when the token has no expiry at all. */
7
+ function describeExpiry(profile) {
8
+ if (getAuthMethod(profile) === 'token')
9
+ return null;
10
+ if (!profile.expires_at)
11
+ return null;
12
+ const msLeft = Date.parse(profile.expires_at) - Date.now();
13
+ if (Number.isNaN(msLeft))
14
+ return null;
15
+ if (msLeft <= 0)
16
+ return 'истёк, обновится при следующей команде';
17
+ const minutes = Math.round(msLeft / 60_000);
18
+ return minutes < 60 ? `осталось ${minutes} мин` : `осталось ${Math.round(minutes / 60)} ч`;
19
+ }
4
20
  export default class AuthStatus extends BaseCommand {
5
21
  static description = 'Статус текущего профиля';
6
22
  static examples = [
@@ -9,6 +25,10 @@ export default class AuthStatus extends BaseCommand {
9
25
  ];
10
26
  static flags = {
11
27
  ...BaseCommand.baseFlags,
28
+ remote: Flags.boolean({
29
+ description: 'Спросить права у сервера, а не показывать сохранённые при входе',
30
+ default: false,
31
+ }),
12
32
  };
13
33
  async run() {
14
34
  const { flags } = await this.parse(AuthStatus);
@@ -24,17 +44,84 @@ export default class AuthStatus extends BaseCommand {
24
44
  outputError({ error: `Profile "${profileName}" not found`, type: 'PACHCA_USAGE_ERROR', code: null }, format);
25
45
  this.exit(2);
26
46
  }
47
+ const authMethod = getAuthMethod(profile);
48
+ // The stored scope list is frozen at login. A role change since then makes it
49
+ // stale, and this is the command people are sent to when a call is refused —
50
+ // so offer to ask the server what the token actually carries.
51
+ let scopes = profile.scopes;
52
+ let drifted = false;
53
+ // Whether the answer really came from the server. `--remote` alone is not
54
+ // enough: the request can fail, and labelling the cached list as server truth
55
+ // is worse than not asking at all.
56
+ let fromServer = false;
57
+ if (flags.remote && profile.token) {
58
+ const remote = await this.fetchRemoteScopes(profile.token);
59
+ if (remote) {
60
+ fromServer = true;
61
+ drifted = JSON.stringify([...(scopes ?? [])].sort()) !== JSON.stringify([...remote].sort());
62
+ scopes = remote;
63
+ if (drifted)
64
+ updateProfileScopes(profileName, remote);
65
+ }
66
+ }
27
67
  if (format === 'json') {
28
68
  this.output({
29
69
  profile: profileName,
30
70
  type: profile.type,
31
71
  user: profile.user,
32
72
  email: profile.email || null,
73
+ auth: authMethod,
74
+ expires_at: profile.expires_at || null,
75
+ scopes: scopes || null,
76
+ scopes_source: fromServer ? 'server' : 'profile',
77
+ storage: getSecretStorage(profile),
78
+ // A keyring-backed profile with no token means the store did not answer.
79
+ // Surfaced here because this is the command people run to debug access.
80
+ secret_readable: !!profile.token,
33
81
  });
82
+ return;
83
+ }
84
+ const emailStr = profile.email ? ` (${profile.email})` : '';
85
+ process.stderr.write(` Подключён как: ${profile.user}${emailStr} [${profile.type}, profile: ${profileName}]\n`);
86
+ const expiry = describeExpiry(profile);
87
+ if (authMethod === 'oauth') {
88
+ process.stderr.write(` Вход: через браузер${expiry ? `, ${expiry}` : ''}\n`);
89
+ }
90
+ else {
91
+ process.stderr.write(` Вход: готовым токеном, без срока\n`);
92
+ }
93
+ if (getSecretStorage(profile) === 'keyring') {
94
+ process.stderr.write(profile.token
95
+ ? ` Хранение: хранилище ключей ОС\n`
96
+ : ` Хранение: хранилище ключей ОС — не читается, войдите заново\n`);
34
97
  }
35
98
  else {
36
- const emailStr = profile.email ? ` (${profile.email})` : '';
37
- process.stderr.write(` Подключён как: ${profile.user}${emailStr} [${profile.type}, profile: ${profileName}]\n`);
99
+ process.stderr.write(` Хранение: файл конфигурации\n`);
100
+ }
101
+ if (scopes?.length) {
102
+ const source = fromServer ? ' по данным сервера' : '';
103
+ process.stderr.write(` Права${source} (${scopes.length}): ${scopes.join(', ')}\n`);
104
+ }
105
+ else if (fromServer) {
106
+ // An empty set is an answer, not a missing one — and a token that carries
107
+ // no rights is exactly the failure worth naming out loud.
108
+ process.stderr.write(` Права по данным сервера: ни одного\n`);
109
+ }
110
+ if (drifted) {
111
+ process.stderr.write(` Список прав в профиле расходился с сервером и был обновлён\n`);
112
+ }
113
+ }
114
+ /** Ask the server what the token really carries. Null when it cannot be asked. */
115
+ async fetchRemoteScopes(token) {
116
+ try {
117
+ const response = await request({ method: 'GET', path: '/oauth/token/info', token }, { quiet: true });
118
+ const info = response.data.data;
119
+ return info?.scopes ?? null;
120
+ }
121
+ catch {
122
+ // Offline or a dead token: the local view is still worth printing.
123
+ process.stderr.write(` Спросить права у сервера не удалось — показаны сохранённые при входе\n`);
124
+ return null;
38
125
  }
39
126
  }
40
127
  }
@@ -1,6 +1,6 @@
1
1
  import { Args } from '@oclif/core';
2
2
  import { BaseCommand } from '../../base-command.js';
3
- import { listProfiles, setActiveProfile } from '../../profiles.js';
3
+ import { listProfiles, setActiveProfile, getProfile } from '../../profiles.js';
4
4
  import { outputError } from '../../output.js';
5
5
  export default class AuthSwitch extends BaseCommand {
6
6
  static description = 'Переключение активного профиля';
@@ -26,11 +26,25 @@ export default class AuthSwitch extends BaseCommand {
26
26
  this.exit(2);
27
27
  }
28
28
  setActiveProfile(args.profile);
29
+ // Say right here if the profile cannot work, rather than letting the next
30
+ // unrelated command fail: switching is where the user can still act on it.
31
+ const profile = getProfile(args.profile);
32
+ const secretMissing = !profile?.token;
33
+ const expired = !!profile?.expires_at && new Date(profile.expires_at) <= new Date();
29
34
  if (format === 'json') {
30
- this.output({ active_profile: args.profile });
35
+ this.output({
36
+ active_profile: args.profile,
37
+ secret_readable: !secretMissing,
38
+ expired,
39
+ });
40
+ return;
31
41
  }
32
- else {
33
- this.success(`Активный профиль: ${args.profile}`);
42
+ this.success(`Активный профиль: ${args.profile}`);
43
+ if (secretMissing) {
44
+ process.stderr.write(` Секрет профиля не читается — войдите заново: pachca auth login --profile ${args.profile}\n`);
45
+ }
46
+ else if (expired) {
47
+ process.stderr.write(` Срок действия токена истёк — он будет обновлён при следующей команде\n`);
34
48
  }
35
49
  }
36
50
  }
@@ -30,6 +30,11 @@ export default class ChatsList extends BaseCommand {
30
30
  options: ["is_member", "public"],
31
31
  default: "is_member",
32
32
  }),
33
+ 'archived': Flags.boolean({
34
+ description: "Добавляет к выдаче архивные чаты. По умолчанию возвращаются только активные. Работает при любом значении `availability`.",
35
+ default: false,
36
+ allowNo: true,
37
+ }),
33
38
  'last-message-at-after': Flags.string({
34
39
  description: "Фильтрация по времени создания последнего сообщения. Будут возвращены те чаты, время последнего созданного сообщения в которых не раньше чем указанное (в формате YYYY-MM-DDThh:mm:ss.sssZ).",
35
40
  }),
@@ -65,6 +70,7 @@ export default class ChatsList extends BaseCommand {
65
70
  sort: flags['sort'],
66
71
  order: flags['order'],
67
72
  availability: flags['availability'],
73
+ archived: flags['archived'],
68
74
  'last_message_at_after': flags['last-message-at-after'],
69
75
  'last_message_at_before': flags['last-message-at-before'],
70
76
  personal: flags['personal'],
@@ -117,6 +123,7 @@ export default class ChatsList extends BaseCommand {
117
123
  sort: flags['sort'],
118
124
  order: flags['order'],
119
125
  availability: flags['availability'],
126
+ archived: flags['archived'],
120
127
  'last_message_at_after': flags['last-message-at-after'],
121
128
  'last_message_at_before': flags['last-message-at-before'],
122
129
  personal: flags['personal'],
@@ -2,7 +2,9 @@ import * as fs from 'node:fs';
2
2
  import * as process from 'node:process';
3
3
  import semver from 'semver';
4
4
  import { BaseCommand } from '../base-command.js';
5
- import { getActiveProfile, getProfile, getConfigFilePath, getConfigFilePermissions, } from '../profiles.js';
5
+ import { getActiveProfile, getProfile, getConfigFilePath, getConfigFilePermissions, getAuthMethod, getSecretStorage, isExpiring, isRefreshable, listProfiles, } from '../profiles.js';
6
+ import { isSecretStoreAvailable } from '../secret-store.js';
7
+ import { getBaseUrl } from '../client.js';
6
8
  export default class Doctor extends BaseCommand {
7
9
  static description = 'Диагностика окружения: Node.js, сеть, токен, конфигурация';
8
10
  static examples = [
@@ -30,30 +32,39 @@ export default class Doctor extends BaseCommand {
30
32
  checks.push({ name: 'node', status: 'error', version: nodeVersion, required: '>=20' });
31
33
  this.printCheck('error', `Node.js v${nodeVersion} — требуется >=20`, format);
32
34
  }
33
- // 2. Network
35
+ // 2. Network. Check the host the CLI actually talks to: on-premise installs
36
+ // point elsewhere, and probing the cloud there reports a healthy network
37
+ // while the server the user works against is down.
38
+ const baseUrl = getBaseUrl();
39
+ const host = new URL(baseUrl).host;
34
40
  try {
35
41
  const start = Date.now();
36
42
  const controller = new AbortController();
37
43
  const timer = setTimeout(() => controller.abort(), 5000);
38
- await fetch('https://api.pachca.com/api/shared/v1/profile', {
44
+ await fetch(`${baseUrl}/profile`, {
39
45
  method: 'HEAD',
40
46
  signal: controller.signal,
41
47
  });
42
48
  clearTimeout(timer);
43
49
  const latency = Date.now() - start;
44
- checks.push({ name: 'network', status: 'ok', host: 'api.pachca.com', latency_ms: latency });
45
- this.printCheck('ok', `Сеть api.pachca.com доступен (${latency}ms)`, format);
50
+ checks.push({ name: 'network', status: 'ok', host, latency_ms: latency });
51
+ this.printCheck('ok', `Сеть ${host} доступен (${latency}ms)`, format);
46
52
  networkOk = true;
47
53
  }
48
54
  catch {
49
- checks.push({ name: 'network', status: 'error', host: 'api.pachca.com' });
50
- this.printCheck('error', `Сеть api.pachca.com недоступен (таймаут 5s)`, format);
55
+ checks.push({ name: 'network', status: 'error', host });
56
+ this.printCheck('error', `Сеть ${host} недоступен (таймаут 5s)`, format);
51
57
  }
52
- // 3. Config file
58
+ // 3. Config file. File permissions only matter for profiles that keep the
59
+ // secret there — for a keyring profile the file holds no token at all, and
60
+ // reporting its mode as a security state would be misleading.
53
61
  const configPath = getConfigFilePath();
62
+ const activeName = getActiveProfile();
63
+ const activeProfile = activeName ? listProfiles()[activeName] : undefined;
64
+ const storage = activeProfile ? getSecretStorage(activeProfile) : 'file';
54
65
  if (fs.existsSync(configPath)) {
55
66
  const permissions = getConfigFilePermissions();
56
- if (permissions && Number.parseInt(permissions, 8) > 0o600) {
67
+ if (storage === 'file' && permissions && Number.parseInt(permissions, 8) > 0o600) {
57
68
  checks.push({ name: 'config', status: 'warning', path: configPath, permissions, expected: '600' });
58
69
  this.printCheck('warning', `Конфиг ${configPath} (права: ${permissions} — рекомендуется chmod 600)`, format);
59
70
  }
@@ -66,13 +77,48 @@ export default class Doctor extends BaseCommand {
66
77
  checks.push({ name: 'config', status: 'error', path: configPath });
67
78
  this.printCheck('error', `Конфиг файл не найден (запустите pachca auth login)`, format);
68
79
  }
80
+ // 3a. Secret store. A locked keyring is the most likely failure of the new
81
+ // login, and without this check it surfaces below as "token is invalid".
82
+ let secretReadable = true;
83
+ if (activeProfile && storage === 'keyring') {
84
+ const storeUp = isSecretStoreAvailable();
85
+ secretReadable = storeUp && !!getProfile(activeName)?.token;
86
+ if (secretReadable) {
87
+ checks.push({ name: 'secret_store', status: 'ok', storage });
88
+ this.printCheck('ok', `Хранилище хранилище ключей ОС, секрет читается`, format);
89
+ }
90
+ else {
91
+ checks.push({ name: 'secret_store', status: 'error', storage, available: storeUp });
92
+ this.printCheck('error', storeUp
93
+ ? `Хранилище секрет профиля не найден — войдите заново: pachca auth login`
94
+ : `Хранилище хранилище ключей ОС недоступно — разблокируйте связку ключей`, format);
95
+ }
96
+ }
97
+ else if (activeProfile) {
98
+ checks.push({ name: 'secret_store', status: 'ok', storage: 'file' });
99
+ this.printCheck('ok', `Хранилище файл конфигурации`, format);
100
+ }
69
101
  // 4. Profile
70
102
  const profileName = getActiveProfile();
71
103
  if (profileName) {
72
104
  const profile = getProfile(profileName);
73
105
  if (profile) {
74
- checks.push({ name: 'profile', status: 'ok', profile: profileName, type: profile.type, user: profile.user });
75
- this.printCheck('ok', `Профиль ${profileName} (${profile.type}: ${profile.user})`, format);
106
+ const method = getAuthMethod(profile);
107
+ const expiry = method === 'oauth' && profile.expires_at
108
+ ? `, истекает ${new Date(profile.expires_at).toLocaleString('ru-RU')}`
109
+ : method === 'token'
110
+ ? ', бессрочный'
111
+ : '';
112
+ checks.push({
113
+ name: 'profile',
114
+ status: 'ok',
115
+ profile: profileName,
116
+ type: profile.type,
117
+ user: profile.user,
118
+ auth: method,
119
+ storage,
120
+ });
121
+ this.printCheck('ok', `Профиль ${profileName} (${profile.type}: ${profile.user}) — ${method === 'oauth' ? 'вход через браузер' : 'готовый токен'}${expiry}`, format);
76
122
  profileOk = true;
77
123
  }
78
124
  else {
@@ -84,16 +130,22 @@ export default class Doctor extends BaseCommand {
84
130
  checks.push({ name: 'profile', status: 'error' });
85
131
  this.printCheck('error', `Профиль не настроен (запустите pachca auth login)`, format);
86
132
  }
87
- // 5. Token (skip if no network or no profile)
88
- if (!networkOk || !profileOk) {
89
- const reason = !networkOk ? 'нет сети' : 'нет профиля';
133
+ // 5. Token (skip if no network, no profile, or the secret is unreadable —
134
+ // asking the server with an empty Bearer would come back 401 and get
135
+ // reported as an invalid token, sending the user to log in for nothing)
136
+ if (!networkOk || !profileOk || !secretReadable) {
137
+ const reason = !networkOk ? 'нет сети' : !profileOk ? 'нет профиля' : 'секрет недоступен';
90
138
  checks.push({ name: 'token', status: 'skipped', reason });
91
139
  this.printCheck('skipped', `Токен пропущено (${reason})`, format);
92
140
  }
93
141
  else {
94
142
  const profile = getProfile(profileName);
143
+ // An expired token that still has its refresh token is not a broken login:
144
+ // the next command renews it on the init hook. Reporting it as invalid sent
145
+ // people to log in again for nothing.
146
+ const willRenew = isRefreshable(profile) && isExpiring(profile);
95
147
  try {
96
- const response = await fetch('https://api.pachca.com/api/shared/v1/oauth/token/info', {
148
+ const response = await fetch(`${baseUrl}/oauth/token/info`, {
97
149
  headers: { Authorization: `Bearer ${profile.token}` },
98
150
  });
99
151
  if (response.ok) {
@@ -102,6 +154,10 @@ export default class Doctor extends BaseCommand {
102
154
  checks.push({ name: 'token', status: 'ok', scopes_count: scopeCount });
103
155
  this.printCheck('ok', `Токен действителен (${scopeCount} скоупов)`, format);
104
156
  }
157
+ else if (willRenew) {
158
+ checks.push({ name: 'token', status: 'ok', renewable: true, http_status: response.status });
159
+ this.printCheck('ok', `Токен срок истёк — обновится при следующей команде`, format);
160
+ }
105
161
  else {
106
162
  checks.push({ name: 'token', status: 'error', http_status: response.status });
107
163
  this.printCheck('error', `Токен недействителен (${response.status}) — обновите: pachca auth login`, format);
@@ -48,6 +48,7 @@ export default class Introspect extends BaseCommand {
48
48
  }
49
49
  // D3 single source: baseFlagNames derived from BaseCommand.baseFlags
50
50
  const baseFlagNames = new Set(Object.keys(BaseCommand.baseFlags));
51
- this.output(normalizeCommand(commandId, cmdMeta, baseFlagNames));
51
+ const baseFlagDescriptions = new Map(Object.entries(BaseCommand.baseFlags).map(([name, def]) => [name, def.description ?? '']));
52
+ this.output(normalizeCommand(commandId, cmdMeta, baseFlagNames, baseFlagDescriptions));
52
53
  }
53
54
  }
@@ -18,7 +18,7 @@ export default class MessagesCreate extends BaseCommand {
18
18
  static flags = {
19
19
  ...BaseCommand.baseFlags,
20
20
  'entity-type': Flags.string({
21
- description: "Тип сущности",
21
+ description: "Тип сущности. Регистр значения не учитывается. Значение вне списка возвращает `400` с кодом `invalid`.",
22
22
  }),
23
23
  'entity-id': Flags.integer({
24
24
  description: "Идентификатор сущности (pachca chats list | pachca users list)",
@@ -5,7 +5,8 @@ import * as clack from '@clack/prompts';
5
5
  export default class MessagesList extends BaseCommand {
6
6
  static description = "Список сообщений чата";
7
7
  static examples = [
8
- "Получить историю сообщений чата — Получи сообщения чата с пагинацией:\n $ pachca messages list"
8
+ "Получить историю сообщений чата — Получи сообщения чата с пагинацией:\n $ pachca messages list",
9
+ "Получить список активных тредов за период — Для каждого треда возьми `chat_id` и подтяни сообщения:\n $ pachca messages list"
9
10
  ];
10
11
  static scope = "messages:read";
11
12
  static apiMethod = "GET";
@@ -24,7 +24,7 @@ export default class SecurityList extends BaseCommand {
24
24
  }),
25
25
  'event-key': Flags.string({
26
26
  description: "Фильтр по конкретному типу события",
27
- options: ["user_login", "user_logout", "user_2fa_fail", "user_2fa_success", "user_created", "user_deleted", "user_role_changed", "user_updated", "tag_created", "tag_deleted", "user_added_to_tag", "user_removed_from_tag", "chat_created", "chat_renamed", "chat_permission_changed", "user_chat_join", "user_chat_leave", "tag_added_to_chat", "tag_removed_from_chat", "message_updated", "message_deleted", "message_created", "reaction_created", "reaction_deleted", "thread_created", "access_token_created", "access_token_updated", "access_token_destroy", "kms_encrypt", "kms_decrypt", "audit_events_accessed", "dlp_violation_detected", "search_users_api", "search_chats_api", "search_messages_api", "bot_scopes_updated", "bot_webhook_settings_updated", "bot_token_recreated", "bot_deleted", "video_call_started", "video_call_finished", "video_call_recording_ready"],
27
+ options: ["user_login", "user_logout", "user_2fa_fail", "user_2fa_success", "user_created", "user_deleted", "user_role_changed", "user_updated", "tag_created", "tag_deleted", "user_added_to_tag", "user_removed_from_tag", "chat_created", "chat_renamed", "chat_permission_changed", "user_chat_join", "user_chat_leave", "tag_added_to_chat", "tag_removed_from_chat", "message_updated", "message_deleted", "message_created", "reaction_created", "reaction_deleted", "thread_created", "access_token_created", "access_token_updated", "access_token_destroy", "kms_encrypt", "kms_decrypt", "audit_events_accessed", "dlp_violation_detected", "search_users_api", "search_chats_api", "search_messages_api", "bot_scopes_updated", "bot_webhook_settings_updated", "bot_token_recreated", "bot_deleted", "bot_oauth_client_updated", "oauth_authorization_granted", "oauth_authorization_revoked", "video_call_started", "video_call_finished", "video_call_recording_ready"],
28
28
  }),
29
29
  'actor-id': Flags.string({
30
30
  description: "Идентификатор пользователя, выполнившего действие",
@@ -69,7 +69,7 @@ export default class Upgrade extends BaseCommand {
69
69
  this.output({ upgraded: false, current: currentVersion, error: 'Cannot determine latest version' });
70
70
  }
71
71
  else {
72
- process.stderr.write('✗ Не удалось определить последнюю версию\n');
72
+ process.stderr.write('✗ Не удалось определить последнюю версию.\n');
73
73
  }
74
74
  this.exit(1);
75
75
  }
@@ -1,4 +1,119 @@
1
1
  [
2
+ {
3
+ "version": "2026.8.1",
4
+ "date": "6 августа 2026",
5
+ "changes": [
6
+ {
7
+ "type": "+",
8
+ "command": "auth login",
9
+ "description": "Вход через браузер — без ручной вставки токена. CLI показывает адрес и код, открывает браузер и ждёт подтверждения. Токен такого входа живёт час и продлевается сам перед каждой командой; при параллельных процессах обновляет только один, остальные ждут результат. Флаг `--no-browser` печатает адрес и код, не открывая браузер. `--token` и `PACHCA_TOKEN` работают как раньше — это путь для CI и агентов"
10
+ },
11
+ {
12
+ "type": "+",
13
+ "command": "auth login",
14
+ "description": "Токен хранится в хранилище ключей ОС — Keychain на macOS, Secret Service на Linux, Диспетчер учётных данных на Windows; в файле конфигурации остаются только имя, почта и права. Где хранилища нет (headless-машина, контейнер), CLI сохраняет токен в файл и сообщает об этом при входе. Отказаться от хранилища: флаг `--insecure-storage` или переменная `PACHCA_SECRET_STORE=file`"
15
+ },
16
+ {
17
+ "type": "+",
18
+ "command": "auth status",
19
+ "description": "Показывает способ входа, срок жизни токена, выданные права и где хранится токен — с этого стоит начинать разбор внезапного `403`"
20
+ },
21
+ {
22
+ "type": "+",
23
+ "command": "auth list",
24
+ "description": "Колонки `auth` и `expires_at` — видно, какой профиль вошёл через браузер, а какой держит готовый токен"
25
+ },
26
+ {
27
+ "type": "~",
28
+ "command": "auth logout",
29
+ "description": "Токен входа через браузер гасится на сервере — доступ закрывается в момент выхода, а не по истечении срока. Если отозвать не удалось, CLI об этом говорит и не выдаёт удаление профиля за закрытый доступ. Готовый токен продолжает работать: отозвать его можно только в интерфейсе"
30
+ },
31
+ {
32
+ "type": "~",
33
+ "description": "Отказ по правам разбирается отдельно от прочих `403`: CLI называет недостающее право и объясняет причину. В JSON-выводе — тип `PACHCA_SCOPE_ERROR` и поле `scope`"
34
+ },
35
+ {
36
+ "type": "~",
37
+ "description": "В подсказке «токен не найден» появилась переменная окружения `PACHCA_TOKEN` — раньше предлагался только флаг `--token`, хотя переменная работает и без входа"
38
+ },
39
+ {
40
+ "type": "+",
41
+ "command": "auth status",
42
+ "description": "Флаг `--remote` — спросить права у сервера вместо сохранённых при входе. Набор прав фиксируется в момент входа, и после смены роли локальный список устаревает; при расхождении CLI говорит об этом и обновляет сохранённый"
43
+ },
44
+ {
45
+ "type": "+",
46
+ "command": "auth login",
47
+ "description": "`--token -` читает токен из потока ввода: `echo $TOKEN | pachca auth login --token -` не оставляет секрет в истории оболочки и в списке процессов. Передача значением работает как раньше"
48
+ },
49
+ {
50
+ "type": "+",
51
+ "command": "auth switch",
52
+ "description": "Сразу после переключения сообщает, если секрет профиля не читается или срок токена истёк — раньше ошибка всплывала на следующей команде"
53
+ },
54
+ {
55
+ "type": "~",
56
+ "command": "doctor",
57
+ "description": "Проверяет хранилище ключей ОС отдельным пунктом и не выдаёт заблокированную связку за недействительный токен. Показывает способ входа и срок жизни токена. Права файла конфигурации проверяются только когда токен действительно лежит в файле"
58
+ },
59
+ {
60
+ "type": "-",
61
+ "command": "doctor",
62
+ "description": "Проверяет тот адрес API, с которым работает CLI, а не всегда `api.pachca.com` — при изменённом адресе сервера команда рапортовала о доступности чужого хоста"
63
+ },
64
+ {
65
+ "type": "-",
66
+ "command": "doctor",
67
+ "description": "Истёкший, но обновляемый токен больше не выдаётся за недействительный: команда говорит, что он обновится при следующем вызове, вместо совета войти заново"
68
+ },
69
+ {
70
+ "type": "+",
71
+ "command": "chats list",
72
+ "description": "Флаг `--archived` — вернуть архивные чаты вместе с активными. Отличить их можно по полю `archived` в выдаче"
73
+ },
74
+ {
75
+ "type": "+",
76
+ "command": "security list",
77
+ "description": "Фильтр `--event-key` принимает `bot_oauth_client_updated`, `oauth_authorization_granted` и `oauth_authorization_revoked`"
78
+ },
79
+ {
80
+ "type": "+",
81
+ "command": "security list",
82
+ "description": "Фильтр `--event-key` принимает `bot_oauth_client_updated`, `oauth_authorization_granted` и `oauth_authorization_revoked`"
83
+ },
84
+ {
85
+ "type": "-",
86
+ "command": "",
87
+ "description": "Подсказка «токен не найден» вела на несуществующую страницу `/guides/authorization` — теперь на `/api/authorization`"
88
+ },
89
+ {
90
+ "type": "+",
91
+ "command": "",
92
+ "description": "`pachca --help` заканчивается ссылкой на документацию и на `pachca guide` — из терминала стало видно, куда идти за сценариями"
93
+ }
94
+ ]
95
+ },
96
+ {
97
+ "version": "2026.8.0",
98
+ "date": "1 августа 2026",
99
+ "changes": [
100
+ {
101
+ "type": "~",
102
+ "command": "api",
103
+ "description": "`api POST /messages --spec` и `--docs`: у поля `files[].name` описано требование к расширению — оно должно совпадать с расширением файла в `key`"
104
+ },
105
+ {
106
+ "type": "~",
107
+ "command": "api",
108
+ "description": "`api GET /audit_events --spec`: уточнён состав деталей событий видеозвонков, `user_updated`, `bot_deleted` и `bot_token_recreated`"
109
+ },
110
+ {
111
+ "type": "~",
112
+ "command": "messages list",
113
+ "description": "В примерах появился шаг сценария «Получить список активных тредов за период» — раньше он был спрятан в тексте описания шага"
114
+ }
115
+ ]
116
+ },
2
117
  {
3
118
  "version": "2026.7.2",
4
119
  "date": "27 июля 2026",