@pachca/cli 2026.5.1 → 2026.5.2

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.5.2 (17 мая 2026)
4
+
5
+ - **Добавлено** (users create): Параметр `--chat-ids` — добавить сотрудника в указанные чаты сразу при создании. Параметр `--role` теперь принимает значение `guest`: для роли `guest` нужно передать `--chat-ids` ровно с одним активным чатом, иначе API вернёт `400`
6
+ - **Добавлено** (api): Список эндпоинтов и справка по API прямо в CLI: `pachca api ls` (список всех эндпоинтов), `pachca api <МЕТОД> <путь> --describe` / `--spec` / `--docs` (параметры, тело, схема, пример). Данные берутся из той же OpenAPI-спецификации, из которой собран CLI, — всегда совпадают с актуальным API. Агенту не нужно искать эндпоинт в документации
7
+ - **Добавлено** (api): Флаг `--data` — тело запроса инлайн JSON-строкой (`pachca api POST /messages --data '{...}'`)
8
+ - **Добавлено**: Флаг `--plain` — плоский TSV-вывод для скриптов: без заголовка, колонка id первой, без цвета и обрезки
9
+ - **Добавлено**: Переменная окружения `PACHCA_HOME` — переопределение каталога конфига (для изоляции в CI, тестах и у агентов)
10
+
3
11
  ## 2026.5.1 (6 мая 2026)
4
12
 
5
13
  - **Изменено**: Поддержка обновлённого формата курсорной пагинации в list-командах
package/README.md CHANGED
@@ -223,17 +223,20 @@ pachca guide # список всех сценариев
223
223
 
224
224
  | Флаг | Короткий | Описание |
225
225
  |------|----------|----------|
226
- | `--output <format>` | `-o` | Формат вывода: table, json, yaml, csv |
227
- | `--columns <list>` | `-c` | Колонки для table-вывода |
228
- | `--no-header` | | Скрыть заголовок таблицы |
229
- | `--profile <name>` | `-p` | Профиль для этой команды |
230
- | `--token <value>` | | Bearer-токен для этого вызова |
231
- | `--quiet` | `-q` | Подавить вывод кроме ошибок |
232
- | `--verbose` | `-v` | Показывать HTTP-детали |
233
- | `--no-input` | | Отключить промпты |
234
- | `--dry-run` | | Показать запрос без отправки |
235
- | `--timeout <seconds>` | | Таймаут запроса |
236
- | `--no-retry` | | Отключить авто-retry |
226
+ | `--output <value>` | `-o` | Формат вывода: table, json, yaml, csv |
227
+ | `--columns <value>` | `-c` | Колонки для table-вывода (через запятую) |
228
+ | `--no-header` | | Скрыть заголовок таблицы |
229
+ | `--no-truncate` | | Не обрезать длинные значения |
230
+ | `--profile <value>` | `-p` | Профиль для этой команды |
231
+ | `--token <value>` | | Токен для этого вызова (без сохранения) |
232
+ | `--quiet` | `-q` | Подавить вывод (только exit code и ошибки) |
233
+ | `--no-color` | | Отключить цвета |
234
+ | `--verbose` | `-v` | Показывать HTTP-запросы и ответы |
235
+ | `--no-input` | | Отключить интерактивные промпты |
236
+ | `--dry-run` | | Показать запрос без отправки |
237
+ | `--timeout <value>` | | Таймаут запроса в секундах (по умолчанию 30) |
238
+ | `--no-retry` | | Отключить авто-retry при 429/503 |
239
+ | `--plain` | | Плоский вывод: TSV без заголовка, ID первым, без цвета (для скриптов) |
237
240
 
238
241
  <!-- AUTO:FLAGS:END -->
239
242
 
@@ -366,9 +369,9 @@ pachca changelog
366
369
  ## Shell completion
367
370
 
368
371
  ```
369
- pachca autocomplete zsh # zsh completion
370
- pachca autocomplete bash # bash completion
371
- pachca autocomplete fish # fish completion
372
+ pachca autocomplete zsh # zsh completion
373
+ pachca autocomplete bash # bash completion
374
+ pachca autocomplete powershell # PowerShell completion
372
375
  ```
373
376
 
374
377
  ## Переменные окружения
@@ -200,9 +200,9 @@ pachca changelog
200
200
  ## Shell completion
201
201
 
202
202
  ```
203
- pachca autocomplete zsh # zsh completion
204
- pachca autocomplete bash # bash completion
205
- pachca autocomplete fish # fish completion
203
+ pachca autocomplete zsh # zsh completion
204
+ pachca autocomplete bash # bash completion
205
+ pachca autocomplete powershell # PowerShell completion
206
206
  ```
207
207
 
208
208
  ## Переменные окружения
@@ -0,0 +1,60 @@
1
+ /**
2
+ * C1 — Self-documenting `pachca api`.
3
+ *
4
+ * Pure helpers over the build-time generated `src/data/endpoints.json`
5
+ * (emitted by scripts/generate-cli.ts from the OpenAPI spec).
6
+ *
7
+ * The heavy lifting (parsing, $ref resolution, markdown, examples, curl)
8
+ * is done at build time by reused apps/docs generators; at runtime the CLI
9
+ * only reads JSON — no YAML parser in the bundle.
10
+ */
11
+ export function buildListRows(entries) {
12
+ return entries.map((e) => ({
13
+ method: e.method,
14
+ path: e.path,
15
+ summary: e.summary,
16
+ scope: e.scope ?? '',
17
+ }));
18
+ }
19
+ /**
20
+ * Detect a self-documenting mode from raw argv (before this.parse()).
21
+ * `pachca api` already requires the method positionally, so
22
+ * `pachca api POST /messages --describe` is unambiguous by construction
23
+ * (no `-X` needed, unlike Notion's `ntn api`).
24
+ */
25
+ export function detectIntrospectMode(argv) {
26
+ if (argv.includes('--describe'))
27
+ return 'describe';
28
+ if (argv.includes('--spec'))
29
+ return 'spec';
30
+ if (argv.includes('--docs'))
31
+ return 'docs';
32
+ return null;
33
+ }
34
+ /**
35
+ * Leading run of positional tokens (method, path) — stops at the first flag,
36
+ * so flag values (e.g. `-o json`) are never mistaken for the path.
37
+ * `pachca api` always takes method and path before any flag.
38
+ */
39
+ export function positionals(argv) {
40
+ const out = [];
41
+ for (const a of argv) {
42
+ if (a.startsWith('-'))
43
+ break;
44
+ out.push(a);
45
+ }
46
+ return out;
47
+ }
48
+ export function findEndpoint(entries, method, path) {
49
+ const m = method.toUpperCase();
50
+ const p = path.replace(/\/+$/, '') || '/';
51
+ return entries.find((e) => e.method === m && (e.path === path || e.path.replace(/\/+$/, '') === p));
52
+ }
53
+ /** Render the selected introspection output for a found endpoint. */
54
+ export function renderIntrospect(entry, mode) {
55
+ if (mode === 'spec')
56
+ return JSON.stringify(entry.spec, null, 2);
57
+ if (mode === 'docs')
58
+ return entry.docs;
59
+ return entry.describe;
60
+ }
@@ -63,6 +63,10 @@ export class BaseCommand extends Command {
63
63
  default: false,
64
64
  hidden: true,
65
65
  }),
66
+ plain: Flags.boolean({
67
+ description: 'Plain output: TSV, no header, ID column first, no color (for scripts)',
68
+ default: false,
69
+ }),
66
70
  };
67
71
  _parsedFlags;
68
72
  get parsedFlags() { return this._parsedFlags; }
@@ -95,6 +99,7 @@ export class BaseCommand extends Command {
95
99
  noHeader: this.parsedFlags['no-header'],
96
100
  noTruncate: this.parsedFlags['no-truncate'],
97
101
  quiet: this.parsedFlags.quiet,
102
+ plain: this.parsedFlags.plain,
98
103
  };
99
104
  }
100
105
  /**
@@ -2,23 +2,30 @@ import * as fs from 'node:fs';
2
2
  import { Args, Flags } from '@oclif/core';
3
3
  import { BaseCommand } from '../base-command.js';
4
4
  import { outputError } from '../output.js';
5
+ import { buildListRows, detectIntrospectMode, positionals, findEndpoint, renderIntrospect, } from '../api-introspect.js';
5
6
  export default class Api extends BaseCommand {
6
- static description = 'Произвольный запрос к API';
7
+ static description = 'Произвольный запрос к API. Список и справка: `api ls` (список эндпоинтов), `api <МЕТОД> <путь> --describe` / `--spec` / `--docs`';
7
8
  static examples = [
8
9
  '<%= config.bin %> api GET /messages --query chat_id=123',
9
10
  '<%= config.bin %> api POST /messages -F message[chat_id]=12345 -f message[content]="Привет"',
10
11
  '<%= config.bin %> api POST /messages --input payload.json',
12
+ `<%= config.bin %> api POST /messages --data '{"message":{"entity_id":123,"content":"Привет"}}'`,
11
13
  '<%= config.bin %> api GET /profile -o yaml',
14
+ '<%= config.bin %> api ls',
15
+ '<%= config.bin %> api ls --json',
16
+ '<%= config.bin %> api POST /messages --describe',
17
+ '<%= config.bin %> api GET /messages --spec',
18
+ '<%= config.bin %> api POST /messages --docs',
12
19
  ];
13
20
  static strict = false;
14
21
  static args = {
15
22
  method: Args.string({
16
- description: 'HTTP method (GET, POST, PUT, DELETE)',
23
+ description: 'HTTP-метод (GET, POST, PUT, DELETE, PATCH)',
17
24
  required: true,
18
25
  options: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
19
26
  }),
20
27
  path: Args.string({
21
- description: 'API path (e.g., /messages)',
28
+ description: 'Путь API (например, /messages)',
22
29
  required: true,
23
30
  }),
24
31
  };
@@ -26,23 +33,40 @@ export default class Api extends BaseCommand {
26
33
  ...BaseCommand.baseFlags,
27
34
  'raw-field': Flags.string({
28
35
  char: 'f',
29
- description: 'String field (key=value)',
36
+ description: 'Строковое поле (key=value)',
30
37
  multiple: true,
31
38
  }),
32
39
  field: Flags.string({
33
40
  char: 'F',
34
- description: 'Typed field (numbers/bools auto-converted, @file reads file)',
41
+ description: 'Типизированное поле (числа/boolean автоматически, @file читает файл)',
35
42
  multiple: true,
36
43
  }),
37
44
  input: Flags.string({
38
- description: 'JSON file to send as body (- for stdin)',
45
+ description: 'JSON-файл с телом запроса (- для stdin)',
46
+ }),
47
+ data: Flags.string({
48
+ description: 'Тело запроса инлайн JSON-строкой',
39
49
  }),
40
50
  query: Flags.string({
41
- description: 'Query parameter (key=value)',
51
+ description: 'Query-параметр (key=value)',
42
52
  multiple: true,
43
53
  }),
44
54
  };
45
55
  async run() {
56
+ // C1 — self-documenting `api`. Intercept the raw argv BEFORE this.parse(Api):
57
+ // the `method` arg is required+enum, so `api ls` / `--describe` would fail
58
+ // oclif validation before run(). This branch is strictly additive — those
59
+ // argv shapes errored before; the normal request path below is untouched.
60
+ const raw = this.argv;
61
+ if (raw[0] === 'ls') {
62
+ await this.runApiList(raw.includes('--json'));
63
+ return;
64
+ }
65
+ const mode = detectIntrospectMode(raw);
66
+ if (mode) {
67
+ await this.runApiIntrospect(raw, mode);
68
+ return;
69
+ }
46
70
  const { args, flags } = await this.parse(Api);
47
71
  this.parsedFlags = flags;
48
72
  const format = this.getOutputFormat();
@@ -52,6 +76,11 @@ export default class Api extends BaseCommand {
52
76
  outputError({ error: '-f/-F and --input are mutually exclusive', type: 'PACHCA_USAGE_ERROR', code: null }, format);
53
77
  this.exit(2);
54
78
  }
79
+ // C6 — --data (inline JSON) shares the body path; only one body source allowed.
80
+ if (flags.data && (hasFields || flags.input)) {
81
+ outputError({ error: '--data is mutually exclusive with -f/-F and --input', type: 'PACHCA_USAGE_ERROR', code: null }, format);
82
+ this.exit(2);
83
+ }
55
84
  // Build query params
56
85
  const query = {};
57
86
  if (flags.query) {
@@ -88,6 +117,16 @@ export default class Api extends BaseCommand {
88
117
  this.exit(2);
89
118
  }
90
119
  }
120
+ else if (flags.data) {
121
+ // Same string → JSON → body path as --input, just inline (thin alias).
122
+ try {
123
+ body = JSON.parse(flags.data);
124
+ }
125
+ catch {
126
+ outputError({ error: 'Invalid JSON in --data', type: 'PACHCA_USAGE_ERROR', code: null }, format);
127
+ this.exit(2);
128
+ }
129
+ }
91
130
  else if (hasFields) {
92
131
  const merged = {};
93
132
  // Process -f (raw string fields)
@@ -121,6 +160,49 @@ export default class Api extends BaseCommand {
121
160
  const { outputData } = await import('../output.js');
122
161
  outputData(data, { ...this.getOutputOptions(), format: outFormat });
123
162
  }
163
+ /** Load the build-time endpoint index (mirrors guide.ts workflows.json loading). */
164
+ async loadEndpoints() {
165
+ try {
166
+ const data = await import('../data/endpoints.json', { with: { type: 'json' } });
167
+ return (data.default || data);
168
+ }
169
+ catch {
170
+ return [];
171
+ }
172
+ }
173
+ /** `pachca api ls [--json]` — list every API endpoint. */
174
+ async runApiList(asJson) {
175
+ const entries = await this.loadEndpoints();
176
+ const rows = buildListRows(entries);
177
+ const { outputData } = await import('../output.js');
178
+ outputData(rows, { format: asJson ? 'json' : 'table', quiet: false });
179
+ }
180
+ /** `pachca api <METHOD> <path> --spec|--docs|--describe`. */
181
+ async runApiIntrospect(raw, mode) {
182
+ const errFormat = process.stdout.isTTY ? 'table' : 'json';
183
+ const pos = positionals(raw);
184
+ const method = pos[0];
185
+ const path = pos[1];
186
+ if (!method || !path) {
187
+ outputError({
188
+ error: `Укажите метод и путь: pachca api <МЕТОД> <путь> --${mode}. Список: pachca api ls`,
189
+ type: 'PACHCA_USAGE_ERROR',
190
+ code: null,
191
+ }, errFormat);
192
+ this.exit(2);
193
+ }
194
+ const entries = await this.loadEndpoints();
195
+ const entry = findEndpoint(entries, method, path);
196
+ if (!entry) {
197
+ outputError({
198
+ error: `Эндпоинт не найден: ${method.toUpperCase()} ${path}. Список: pachca api ls`,
199
+ type: 'PACHCA_NOT_FOUND_ERROR',
200
+ code: null,
201
+ }, errFormat);
202
+ this.exit(2);
203
+ }
204
+ process.stdout.write(renderIntrospect(entry, mode) + '\n');
205
+ }
124
206
  }
125
207
  /**
126
208
  * Parse a bracket notation key and set the value in the target object.
@@ -15,11 +15,11 @@ export default class AuthLogin extends BaseCommand {
15
15
  ...BaseCommand.baseFlags,
16
16
  profile: Flags.string({
17
17
  char: 'p',
18
- description: 'Profile name (default: "default")',
18
+ description: 'Имя профиля (по умолчанию "default")',
19
19
  default: 'default',
20
20
  }),
21
21
  token: Flags.string({
22
- description: 'Token to save (skips prompt)',
22
+ description: 'Токен для сохранения (без интерактивного ввода)',
23
23
  }),
24
24
  };
25
25
  async run() {
@@ -11,7 +11,7 @@ export default class AuthLogout extends BaseCommand {
11
11
  ];
12
12
  static args = {
13
13
  profile: Args.string({
14
- description: 'Profile name to remove',
14
+ description: 'Имя профиля для удаления',
15
15
  required: false,
16
16
  }),
17
17
  };
@@ -9,7 +9,7 @@ export default class AuthSwitch extends BaseCommand {
9
9
  ];
10
10
  static args = {
11
11
  profile: Args.string({
12
- description: 'Profile name to switch to',
12
+ description: 'Имя профиля для переключения',
13
13
  required: true,
14
14
  }),
15
15
  };
@@ -12,7 +12,7 @@ export default class Commands extends BaseCommand {
12
12
  static flags = {
13
13
  ...BaseCommand.baseFlags,
14
14
  available: Flags.boolean({
15
- description: 'Show only commands available to current token',
15
+ description: 'Показать только команды, доступные текущему токену',
16
16
  default: false,
17
17
  }),
18
18
  };
@@ -7,7 +7,7 @@ export default class ConfigGet extends BaseCommand {
7
7
  '<%= config.bin %> config get defaults.output',
8
8
  ];
9
9
  static args = {
10
- key: Args.string({ description: 'Configuration key', required: true }),
10
+ key: Args.string({ description: 'Ключ настройки', required: true }),
11
11
  };
12
12
  static flags = {
13
13
  ...BaseCommand.baseFlags,
@@ -8,8 +8,8 @@ export default class ConfigSet extends BaseCommand {
8
8
  '<%= config.bin %> config set defaults.timeout 60',
9
9
  ];
10
10
  static args = {
11
- key: Args.string({ description: 'Configuration key', required: true }),
12
- value: Args.string({ description: 'Configuration value', required: true }),
11
+ key: Args.string({ description: 'Ключ настройки', required: true }),
12
+ value: Args.string({ description: 'Значение настройки', required: true }),
13
13
  };
14
14
  static flags = {
15
15
  ...BaseCommand.baseFlags,
@@ -9,7 +9,7 @@ export default class Guide extends BaseCommand {
9
9
  ];
10
10
  static args = {
11
11
  query: Args.string({
12
- description: 'Search query',
12
+ description: 'Поисковый запрос',
13
13
  required: false,
14
14
  }),
15
15
  };
@@ -2,6 +2,7 @@ import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import { Args } from '@oclif/core';
4
4
  import { BaseCommand } from '../base-command.js';
5
+ import { listCommandEntries, normalizeCommand } from '../lib/manifest.js';
5
6
  export default class Introspect extends BaseCommand {
6
7
  static description = 'Метаданные команды в машиночитаемом формате';
7
8
  static examples = [
@@ -11,7 +12,7 @@ export default class Introspect extends BaseCommand {
11
12
  static strict = false;
12
13
  static args = {
13
14
  command: Args.string({
14
- description: 'Command name (e.g., "messages create")',
15
+ description: 'Имя команды (например, "messages create")',
15
16
  required: false,
16
17
  }),
17
18
  };
@@ -37,15 +38,7 @@ export default class Introspect extends BaseCommand {
37
38
  const commandId = commandArgs.join(':');
38
39
  if (!commandId) {
39
40
  // List all commands with metadata
40
- const entries = Object.entries(commands).map(([id, meta]) => ({
41
- command: `pachca ${id.replace(/:/g, ' ')}`,
42
- summary: meta.summary || meta.description || '',
43
- method: meta.apiMethod || null,
44
- path: meta.apiPath || null,
45
- scope: meta.scope || null,
46
- plan: meta.plan || null,
47
- }));
48
- this.output(entries);
41
+ this.output(listCommandEntries(commands));
49
42
  return;
50
43
  }
51
44
  // Single command introspection
@@ -53,39 +46,8 @@ export default class Introspect extends BaseCommand {
53
46
  if (!cmdMeta) {
54
47
  this.validationError([{ message: `Command not found: ${commandArgs.join(' ')}` }], { hint: 'pachca introspect', type: 'PACHCA_COMMAND_NOT_FOUND' });
55
48
  }
56
- const baseFlagNames = new Set(['output', 'columns', 'no-header', 'no-truncate', 'profile', 'token', 'quiet', 'no-color', 'verbose', 'no-input', 'dry-run', 'timeout', 'no-retry', 'json']);
57
- const requiredFlagNames = new Set(cmdMeta.requiredFlags || []);
58
- const flagsMeta = cmdMeta.flags;
59
- const flagEntries = flagsMeta
60
- ? Object.entries(flagsMeta)
61
- .filter(([name]) => !baseFlagNames.has(name))
62
- .map(([name, meta]) => ({
63
- name,
64
- type: meta.type || 'string',
65
- required: requiredFlagNames.has(name),
66
- description: meta.description || '',
67
- ...(meta.options ? { options: meta.options } : {}),
68
- ...(meta.maxLength ? { maxLength: meta.maxLength } : {}),
69
- }))
70
- : [];
71
- const argsMeta = cmdMeta.args;
72
- const argEntries = argsMeta
73
- ? Object.entries(argsMeta).map(([name, meta]) => ({
74
- name,
75
- required: meta.required || false,
76
- description: meta.description || '',
77
- }))
78
- : [];
79
- const result = {
80
- command: `pachca ${commandId.replace(/:/g, ' ')}`,
81
- summary: cmdMeta.summary || cmdMeta.description || '',
82
- method: cmdMeta.apiMethod || null,
83
- path: cmdMeta.apiPath || null,
84
- scope: cmdMeta.scope || null,
85
- plan: cmdMeta.plan || null,
86
- ...(argEntries.length > 0 ? { args: argEntries } : {}),
87
- flags: flagEntries,
88
- };
89
- this.output(result);
49
+ // D3 single source: baseFlagNames derived from BaseCommand.baseFlags
50
+ const baseFlagNames = new Set(Object.keys(BaseCommand.baseFlags));
51
+ this.output(normalizeCommand(commandId, cmdMeta, baseFlagNames));
90
52
  }
91
53
  }
@@ -48,6 +48,9 @@ export default class UsersCreate extends BaseCommand {
48
48
  'list-tags': Flags.string({
49
49
  description: "Массив тегов, привязываемых к сотруднику",
50
50
  }),
51
+ 'chat-ids': Flags.string({
52
+ description: "Идентификаторы чатов, в которые сотрудник будет добавлен сразу при создании. Для роли `guest` параметр обязателен и должен содержать ровно один активный чат.",
53
+ }),
51
54
  'custom-properties': Flags.string({
52
55
  description: "Задаваемые дополнительные поля",
53
56
  }),
@@ -97,6 +100,7 @@ export default class UsersCreate extends BaseCommand {
97
100
  role: flags['role'],
98
101
  suspended: flags['suspended'],
99
102
  list_tags: flags['list-tags'] ? this.parseJSON(flags['list-tags'], 'list-tags') : undefined,
103
+ chat_ids: flags['chat-ids'] ? this.parseJSON(flags['chat-ids'], 'chat-ids') : undefined,
100
104
  custom_properties: flags['custom-properties'] ? this.parseJSON(flags['custom-properties'], 'custom-properties') : undefined,
101
105
  },
102
106
  skip_email_notify: flags['skip-email-notify'],
@@ -1,4 +1,33 @@
1
1
  [
2
+ {
3
+ "version": "2026.5.2",
4
+ "date": "17 мая 2026",
5
+ "changes": [
6
+ {
7
+ "type": "+",
8
+ "command": "users create",
9
+ "description": "Параметр `--chat-ids` — добавить сотрудника в указанные чаты сразу при создании. Параметр `--role` теперь принимает значение `guest`: для роли `guest` нужно передать `--chat-ids` ровно с одним активным чатом, иначе API вернёт `400`"
10
+ },
11
+ {
12
+ "type": "+",
13
+ "command": "api",
14
+ "description": "Список эндпоинтов и справка по API прямо в CLI: `pachca api ls` (список всех эндпоинтов), `pachca api <МЕТОД> <путь> --describe` / `--spec` / `--docs` (параметры, тело, схема, пример). Данные берутся из той же OpenAPI-спецификации, из которой собран CLI, — всегда совпадают с актуальным API. Агенту не нужно искать эндпоинт в документации"
15
+ },
16
+ {
17
+ "type": "+",
18
+ "command": "api",
19
+ "description": "Флаг `--data` — тело запроса инлайн JSON-строкой (`pachca api POST /messages --data '{...}'`)"
20
+ },
21
+ {
22
+ "type": "+",
23
+ "description": "Флаг `--plain` — плоский TSV-вывод для скриптов: без заголовка, колонка id первой, без цвета и обрезки"
24
+ },
25
+ {
26
+ "type": "+",
27
+ "description": "Переменная окружения `PACHCA_HOME` — переопределение каталога конфига (для изоляции в CI, тестах и у агентов)"
28
+ }
29
+ ]
30
+ },
2
31
  {
3
32
  "version": "2026.5.1",
4
33
  "date": "6 мая 2026",