ktalk-cli 1.0.0__tar.gz → 2.1.0__tar.gz

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 (113) hide show
  1. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/PKG-INFO +55 -7
  2. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/README.md +54 -6
  3. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/pyproject.toml +1 -1
  4. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/cli.py +1 -1
  5. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/cli_content.py +78 -5
  6. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/formatters.py +34 -6
  7. ktalk_cli-2.1.0/src/ktalk_cli/transcript_identity.py +69 -0
  8. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_cli_content.py +8 -2
  9. ktalk_cli-2.1.0/tests/test_fr41_last_synced.py +243 -0
  10. ktalk_cli-2.1.0/tests/test_issue3_null_id_fallback.py +177 -0
  11. ktalk_cli-2.1.0/tests/test_nfr17_identity_verification.py +739 -0
  12. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/.gitignore +0 -0
  13. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/LICENSE +0 -0
  14. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/CLAUDE.md +0 -0
  15. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/__init__.py +0 -0
  16. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/auth.py +0 -0
  17. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/calendar_reader.py +0 -0
  18. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/cli_contacts.py +0 -0
  19. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/cli_meeting.py +0 -0
  20. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/cli_meeting_args.py +0 -0
  21. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/cli_meeting_confirm.py +0 -0
  22. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/cli_meetings_read.py +0 -0
  23. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/cli_sanction.py +0 -0
  24. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/cli_store.py +0 -0
  25. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/cli_sync.py +0 -0
  26. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/cli_token.py +0 -0
  27. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/client.py +0 -0
  28. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/config.py +0 -0
  29. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/confirmation.py +0 -0
  30. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/contacts.py +0 -0
  31. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/contour_diagnostics.py +0 -0
  32. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/download.py +0 -0
  33. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/endpoints.py +0 -0
  34. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/enrichment.py +0 -0
  35. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/host_config.py +0 -0
  36. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/meeting_body.py +0 -0
  37. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/meeting_cancel.py +0 -0
  38. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/meeting_scheduling.py +0 -0
  39. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/pagination.py +0 -0
  40. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/reconciliation.py +0 -0
  41. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/registry.py +0 -0
  42. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/rooms.py +0 -0
  43. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/store.py +0 -0
  44. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/store_migration.py +0 -0
  45. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/token_file.py +0 -0
  46. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/write_journal.py +0 -0
  47. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/src/ktalk_cli/write_sanction.py +0 -0
  48. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/conftest.py +0 -0
  49. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/access-info-expired.json +0 -0
  50. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/access-info-full.json +0 -0
  51. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/archive-8col.md +0 -0
  52. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/archive-dup-id.md +0 -0
  53. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/archive-escaped-pipe-7col.md +0 -0
  54. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/archive-page1.json +0 -0
  55. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/archive-page2-empty.json +0 -0
  56. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/archive-pipe-in-name.md +0 -0
  57. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/calendar-item-session.json +0 -0
  58. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/chat-messages-session.json +0 -0
  59. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/conference-history-session-oversized.json +0 -0
  60. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/conference-history-session.json +0 -0
  61. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/error-body-problem-json.json +0 -0
  62. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/error-body-validation.json +0 -0
  63. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/recording-detail-session-full-participants.json +0 -0
  64. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/recording-detail-session-oversized-partial.json +0 -0
  65. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/recording-detail-with-qualities.json +0 -0
  66. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/recording-list-item-session.json +0 -0
  67. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/recordings-list-apikey-page1.json +0 -0
  68. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/recordings-list-apikey-page2.json +0 -0
  69. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/registry-archive-2026-04.md +0 -0
  70. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/registry.md +0 -0
  71. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/fixtures/room-detail-session.json +0 -0
  72. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_adr022_ktalk_cli_rename.py +0 -0
  73. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_archive.py +0 -0
  74. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_auth_modes.py +0 -0
  75. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_calendar.py +0 -0
  76. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_chat.py +0 -0
  77. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_cli.py +0 -0
  78. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_cli_config_show.py +0 -0
  79. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_cli_export_store_isolation.py +0 -0
  80. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_cli_host_config_wiring.py +0 -0
  81. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_cli_meeting.py +0 -0
  82. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_cli_meeting_sanctioned.py +0 -0
  83. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_cli_meetings_surface.py +0 -0
  84. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_cli_migrate_to_central_store.py +0 -0
  85. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_client.py +0 -0
  86. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_concurrency.py +0 -0
  87. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_config.py +0 -0
  88. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_confirmation.py +0 -0
  89. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_contour_diagnostics.py +0 -0
  90. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_diagnostics.py +0 -0
  91. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_download.py +0 -0
  92. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_enrichment.py +0 -0
  93. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_formatters.py +0 -0
  94. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_fr19_auth_status.py +0 -0
  95. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_fr21_no_vault_layout.py +0 -0
  96. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_fr39_calendar_inclusive_end.py +0 -0
  97. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_fr40_timezone_format.py +0 -0
  98. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_host_config.py +0 -0
  99. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_meeting_body.py +0 -0
  100. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_meeting_cancel.py +0 -0
  101. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_meeting_scheduling.py +0 -0
  102. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_migration.py +0 -0
  103. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_pagination.py +0 -0
  104. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_reconciliation.py +0 -0
  105. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_registry.py +0 -0
  106. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_rooms.py +0 -0
  107. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_search_contacts.py +0 -0
  108. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_secret_masking.py +0 -0
  109. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_store.py +0 -0
  110. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_store_migration.py +0 -0
  111. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_token_file.py +0 -0
  112. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_write_journal.py +0 -0
  113. {ktalk_cli-1.0.0 → ktalk_cli-2.1.0}/tests/test_write_sanction.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ktalk-cli
3
- Version: 1.0.0
3
+ Version: 2.1.0
4
4
  Summary: CLI for accessing Kontur Talk (KTalk) recordings, transcripts and summaries
5
5
  Project-URL: Homepage, https://github.com/mdemyanov/ktalk-cli
6
6
  Project-URL: Repository, https://github.com/mdemyanov/ktalk-cli
@@ -26,15 +26,18 @@ Description-Content-Type: text/markdown
26
26
  [![PyPI](https://img.shields.io/pypi/v/ktalk-cli)](https://pypi.org/project/ktalk-cli/)
27
27
  [![Python](https://img.shields.io/pypi/pyversions/ktalk-cli)](https://pypi.org/project/ktalk-cli/)
28
28
 
29
- CLI `ktalk` для доступа к записям [Контур.Толк](https://ktalk.ru) (KTalk) — чтение
30
- записей, транскриптов и саммари, работа с расписанием, операционный реестр
31
- обработки записей на SQLite.
29
+ CLI `ktalk` — интерфейс командной строки для тех, кто работает с записями видеовстреч
30
+ [Контур.Толк](https://ktalk.ru) (KTalk) программно: читает записи, транскрипты и саммари,
31
+ управляет расписанием, ведёт локальный реестр обработки записей на SQLite. Годится и как
32
+ самостоятельный инструмент, и как предусловие плагина Claude Code `ktalk` — подробнее в
33
+ разделе «Пакет и плагин Claude Code» ниже.
32
34
 
33
35
  > Раньше пакет назывался `ktalk-mcp` и, помимо CLI, поднимал MCP-сервер для
34
36
  > Claude Code (инструменты вида `ktalk_list_recordings`). Этот слой снят
35
37
  > целиком — MCP в пакете больше нет, единственная точка входа — команда
36
- > `ktalk`. Нужна интеграция с Claude Code — отдельный плагин `ktalk` вызывает
37
- > эту же CLI напрямую, без MCP-сервера.
38
+ > `ktalk`. Пришли по старой ссылке или ищете `ktalk-mcp` — это тот же проект
39
+ > под новым именем, старый пакет дальше не развивается (о конфликте имени
40
+ > команды при апгрейде — ниже, в «Установке»).
38
41
 
39
42
  Умеет:
40
43
  - Список записей конференций и детали одной записи.
@@ -73,6 +76,18 @@ uv tool uninstall ktalk-mcp
73
76
  uv tool install ktalk-cli
74
77
  ```
75
78
 
79
+ **Проверка версии** — после установки или обновления:
80
+
81
+ ```bash
82
+ ktalk --version # печатает, например: ktalk-cli 2.1.0
83
+ ```
84
+
85
+ **Обновление** до последней версии — та же команда `install`, только `upgrade`:
86
+
87
+ ```bash
88
+ uv tool upgrade ktalk-cli
89
+ ```
90
+
76
91
  ## Авторизация
77
92
 
78
93
  CLI поддерживает два способа авторизации: session token (кука браузера) и
@@ -222,11 +237,20 @@ ktalk auth-status
222
237
  Все команды поддерживают `--json` (валидный JSON в stdout; ошибки — в stderr с
223
238
  ненулевым кодом возврата).
224
239
 
240
+ ### Коды возврата
241
+
242
+ | Код | Значение |
243
+ |---|---|
244
+ | `0` | Успех. |
245
+ | `1` | Отказ вызова — сеть, сервер, конфигурация. |
246
+ | `2` | Usage error — неверные аргументы CLI (`argparse`). |
247
+ | `3` | Только `ktalk get-transcript`. Данные получены и напечатаны полностью, но независимая сверка идентичности не сошлась (`identity_check.result == "mismatch"`) — состав участников транскрипта разошёлся с составом записи. Это не сбой команды: код 3 отличает «данные есть, но сверка не сошлась» от `0` (сошлось или не проверялось) и от `1`/`2` (данных нет вовсе). Подробности — в самом теле ответа, поле `identity_check` (ADR-024 §Д1). |
248
+
225
249
  | Команда | Назначение |
226
250
  |---|---|
227
251
  | `ktalk list-recordings [--query Q] [--start-from ISO] [--start-to ISO] [--top N] [--order O] [--page-token T]` | Список записей. `--top` 1–1000 (по умолчанию 30); `--order`: `byTimeNewFirst` (умолчание), `byTimeOldFirst`, `byTitle`, `bySizeBigFirst`, `bySizeSmallFirst`. |
228
252
  | `ktalk get-recording <recording_key>` | Детали записи — автор, дата, длительность, участники (список ограничен 6, полный состав — `get-participants`). |
229
- | `ktalk get-transcript <recording_key> [--chunk N] [--chunk-size N]` | Транскрипт по спикерам с таймкодами. Длинный транскрипт режется на чанки по границам реплик: `--chunk 0` (умолчание) — целиком или первый чанк; `--chunk-size` — макс. символов в чанке (умолчание 30000, ~7500 токенов). |
253
+ | `ktalk get-transcript <recording_key> [--chunk N] [--chunk-size N]` | Транскрипт по спикерам с таймкодами. Длинный транскрипт режется на чанки по границам реплик: `--chunk 0` (умолчание) — целиком или первый чанк; `--chunk-size` — макс. символов в чанке (умолчание 30000, ~7500 токенов). Независимая сверка идентичности включена по умолчанию (`--no-verify-identity` отключает); `--chunk` вне диапазона сверку по сети не запускает вовсе, `identity_check.result == "not_checked"`/`reason: "chunk_out_of_range"`. |
230
254
  | `ktalk get-summary <recording_key>` | Полное саммари (краткое резюме + протокол). |
231
255
  | `ktalk get-summary-type <recording_key> --type shortSummary\|protocol` | Саммари одного типа. |
232
256
  | `ktalk get-participants <recording_key>` | Полный состав участников, включая анонимных — обходит лимит в 6, который отдают `get-recording`/`list-recordings`. |
@@ -386,6 +410,30 @@ uv run ruff check .
386
410
  KTALK_SESSION_TOKEN=... KTALK_BASE_URL=... uv run ktalk auth-status
387
411
  ```
388
412
 
413
+ ## Пакет и плагин Claude Code
414
+
415
+ `ktalk-cli` работает и сам по себе, и как предусловие плагина Claude Code `ktalk`. Плагин не
416
+ обращается к KTalk напрямую и не поднимает MCP-сервер — он вызывает эту же команду `ktalk`
417
+ как единственную точку входа в контур.
418
+
419
+ Плагин пинует точную версию пакета (не нижний порог: «ровно эта версия», не «эта или новее») в
420
+ собственном файле совместимости. Если что-то в интеграции с плагином ведёт себя не так, как
421
+ описано в его документации, — первым делом сверьте версию:
422
+
423
+ ```bash
424
+ ktalk --version # см. «Проверка версии» в разделе «Установка»
425
+ ```
426
+
427
+ Версия не совпадает с той, что требует плагин, — обновите пакет тем же способом, что при
428
+ установке (`uv tool upgrade ktalk-cli`, см. «Установка»); не совпадает в другую сторону
429
+ (пакет новее, чем ожидает плагин) — не откатывайте его самостоятельно, сверьтесь с тем, кто
430
+ настраивал плагин.
431
+
432
+ ## Проблемы и вопросы
433
+
434
+ Нашли баг, некорректное поведение или неточность в документации — заведите issue в этом
435
+ репозитории: https://github.com/mdemyanov/ktalk-cli/issues.
436
+
389
437
  ## Лицензия
390
438
 
391
439
  MIT
@@ -3,15 +3,18 @@
3
3
  [![PyPI](https://img.shields.io/pypi/v/ktalk-cli)](https://pypi.org/project/ktalk-cli/)
4
4
  [![Python](https://img.shields.io/pypi/pyversions/ktalk-cli)](https://pypi.org/project/ktalk-cli/)
5
5
 
6
- CLI `ktalk` для доступа к записям [Контур.Толк](https://ktalk.ru) (KTalk) — чтение
7
- записей, транскриптов и саммари, работа с расписанием, операционный реестр
8
- обработки записей на SQLite.
6
+ CLI `ktalk` — интерфейс командной строки для тех, кто работает с записями видеовстреч
7
+ [Контур.Толк](https://ktalk.ru) (KTalk) программно: читает записи, транскрипты и саммари,
8
+ управляет расписанием, ведёт локальный реестр обработки записей на SQLite. Годится и как
9
+ самостоятельный инструмент, и как предусловие плагина Claude Code `ktalk` — подробнее в
10
+ разделе «Пакет и плагин Claude Code» ниже.
9
11
 
10
12
  > Раньше пакет назывался `ktalk-mcp` и, помимо CLI, поднимал MCP-сервер для
11
13
  > Claude Code (инструменты вида `ktalk_list_recordings`). Этот слой снят
12
14
  > целиком — MCP в пакете больше нет, единственная точка входа — команда
13
- > `ktalk`. Нужна интеграция с Claude Code — отдельный плагин `ktalk` вызывает
14
- > эту же CLI напрямую, без MCP-сервера.
15
+ > `ktalk`. Пришли по старой ссылке или ищете `ktalk-mcp` — это тот же проект
16
+ > под новым именем, старый пакет дальше не развивается (о конфликте имени
17
+ > команды при апгрейде — ниже, в «Установке»).
15
18
 
16
19
  Умеет:
17
20
  - Список записей конференций и детали одной записи.
@@ -50,6 +53,18 @@ uv tool uninstall ktalk-mcp
50
53
  uv tool install ktalk-cli
51
54
  ```
52
55
 
56
+ **Проверка версии** — после установки или обновления:
57
+
58
+ ```bash
59
+ ktalk --version # печатает, например: ktalk-cli 2.1.0
60
+ ```
61
+
62
+ **Обновление** до последней версии — та же команда `install`, только `upgrade`:
63
+
64
+ ```bash
65
+ uv tool upgrade ktalk-cli
66
+ ```
67
+
53
68
  ## Авторизация
54
69
 
55
70
  CLI поддерживает два способа авторизации: session token (кука браузера) и
@@ -199,11 +214,20 @@ ktalk auth-status
199
214
  Все команды поддерживают `--json` (валидный JSON в stdout; ошибки — в stderr с
200
215
  ненулевым кодом возврата).
201
216
 
217
+ ### Коды возврата
218
+
219
+ | Код | Значение |
220
+ |---|---|
221
+ | `0` | Успех. |
222
+ | `1` | Отказ вызова — сеть, сервер, конфигурация. |
223
+ | `2` | Usage error — неверные аргументы CLI (`argparse`). |
224
+ | `3` | Только `ktalk get-transcript`. Данные получены и напечатаны полностью, но независимая сверка идентичности не сошлась (`identity_check.result == "mismatch"`) — состав участников транскрипта разошёлся с составом записи. Это не сбой команды: код 3 отличает «данные есть, но сверка не сошлась» от `0` (сошлось или не проверялось) и от `1`/`2` (данных нет вовсе). Подробности — в самом теле ответа, поле `identity_check` (ADR-024 §Д1). |
225
+
202
226
  | Команда | Назначение |
203
227
  |---|---|
204
228
  | `ktalk list-recordings [--query Q] [--start-from ISO] [--start-to ISO] [--top N] [--order O] [--page-token T]` | Список записей. `--top` 1–1000 (по умолчанию 30); `--order`: `byTimeNewFirst` (умолчание), `byTimeOldFirst`, `byTitle`, `bySizeBigFirst`, `bySizeSmallFirst`. |
205
229
  | `ktalk get-recording <recording_key>` | Детали записи — автор, дата, длительность, участники (список ограничен 6, полный состав — `get-participants`). |
206
- | `ktalk get-transcript <recording_key> [--chunk N] [--chunk-size N]` | Транскрипт по спикерам с таймкодами. Длинный транскрипт режется на чанки по границам реплик: `--chunk 0` (умолчание) — целиком или первый чанк; `--chunk-size` — макс. символов в чанке (умолчание 30000, ~7500 токенов). |
230
+ | `ktalk get-transcript <recording_key> [--chunk N] [--chunk-size N]` | Транскрипт по спикерам с таймкодами. Длинный транскрипт режется на чанки по границам реплик: `--chunk 0` (умолчание) — целиком или первый чанк; `--chunk-size` — макс. символов в чанке (умолчание 30000, ~7500 токенов). Независимая сверка идентичности включена по умолчанию (`--no-verify-identity` отключает); `--chunk` вне диапазона сверку по сети не запускает вовсе, `identity_check.result == "not_checked"`/`reason: "chunk_out_of_range"`. |
207
231
  | `ktalk get-summary <recording_key>` | Полное саммари (краткое резюме + протокол). |
208
232
  | `ktalk get-summary-type <recording_key> --type shortSummary\|protocol` | Саммари одного типа. |
209
233
  | `ktalk get-participants <recording_key>` | Полный состав участников, включая анонимных — обходит лимит в 6, который отдают `get-recording`/`list-recordings`. |
@@ -363,6 +387,30 @@ uv run ruff check .
363
387
  KTALK_SESSION_TOKEN=... KTALK_BASE_URL=... uv run ktalk auth-status
364
388
  ```
365
389
 
390
+ ## Пакет и плагин Claude Code
391
+
392
+ `ktalk-cli` работает и сам по себе, и как предусловие плагина Claude Code `ktalk`. Плагин не
393
+ обращается к KTalk напрямую и не поднимает MCP-сервер — он вызывает эту же команду `ktalk`
394
+ как единственную точку входа в контур.
395
+
396
+ Плагин пинует точную версию пакета (не нижний порог: «ровно эта версия», не «эта или новее») в
397
+ собственном файле совместимости. Если что-то в интеграции с плагином ведёт себя не так, как
398
+ описано в его документации, — первым делом сверьте версию:
399
+
400
+ ```bash
401
+ ktalk --version # см. «Проверка версии» в разделе «Установка»
402
+ ```
403
+
404
+ Версия не совпадает с той, что требует плагин, — обновите пакет тем же способом, что при
405
+ установке (`uv tool upgrade ktalk-cli`, см. «Установка»); не совпадает в другую сторону
406
+ (пакет новее, чем ожидает плагин) — не откатывайте его самостоятельно, сверьтесь с тем, кто
407
+ настраивал плагин.
408
+
409
+ ## Проблемы и вопросы
410
+
411
+ Нашли баг, некорректное поведение или неточность в документации — заведите issue в этом
412
+ репозитории: https://github.com/mdemyanov/ktalk-cli/issues.
413
+
366
414
  ## Лицензия
367
415
 
368
416
  MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ktalk-cli"
3
- version = "1.0.0"
3
+ version = "2.1.0"
4
4
  description = "CLI for accessing Kontur Talk (KTalk) recordings, transcripts and summaries"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -212,7 +212,7 @@ def _cmd_dashboard(reg: Registry, args) -> int:
212
212
  new = [r for r in recs if r["status"] == "new"]
213
213
  stats = {s: sum(1 for r in recs if r["status"] == s) for s in _STATUSES}
214
214
  if args.json:
215
- _print_json({"new": new, "stats": stats})
215
+ _print_json({"new": new, "stats": stats, "last_synced": reg.get_meta("last_synced")})
216
216
  return 0
217
217
  print("# Дашборд KTalk\n")
218
218
  print("## Новые записи")
@@ -11,6 +11,7 @@ from __future__ import annotations
11
11
 
12
12
  import argparse
13
13
  import asyncio
14
+ import json
14
15
  import sys
15
16
  from collections.abc import Awaitable, Callable
16
17
 
@@ -26,7 +27,9 @@ from ktalk_cli.formatters import (
26
27
  format_summary_by_type,
27
28
  render_tool_output,
28
29
  render_transcript_output,
30
+ resolve_chunk_range,
29
31
  )
32
+ from ktalk_cli.transcript_identity import check_identity
30
33
 
31
34
 
32
35
  def register_subparsers(sub) -> None:
@@ -48,6 +51,12 @@ def register_subparsers(sub) -> None:
48
51
  p_tr.add_argument("--chunk", type=int, default=0)
49
52
  p_tr.add_argument("--chunk-size", type=int, default=30000)
50
53
  p_tr.add_argument("--json", action="store_true")
54
+ p_tr.add_argument(
55
+ "--no-verify-identity",
56
+ action="store_true",
57
+ help="Отключить сверку идентичности спикеров с участниками записи (NFR-17, "
58
+ "по умолчанию включена)",
59
+ )
51
60
 
52
61
  p_sum = sub.add_parser("get-summary", help="Полное саммари записи (сеть)")
53
62
  p_sum.add_argument("recording_key")
@@ -111,18 +120,82 @@ def cmd_get_recording(_reg, args: argparse.Namespace) -> int:
111
120
  return _run(_get_recording(args), format_recording, json_flag=args.json)
112
121
 
113
122
 
123
+ async def _verify_transcript_identity(client: KTalkClient, recording_key: str, transcript: dict) -> dict:
124
+ """Оркестрация NFR-17 (ADR-023 §1, ред. 2a2f6e3): независимый вызов
125
+ `get_recording`; отказ вызова -> `not_checked` (маскирование той же функцией,
126
+ что и основная ошибка, NFR-5), не исключение наружу — основной результат
127
+ транскрипта возвращается в любом случае."""
128
+ try:
129
+ recording = await client.get_recording(recording_key)
130
+ except Exception as exc: # noqa: BLE001 - not_checked, не отказ команды
131
+ return {"result": "not_checked", "reason": redact_secrets(str(exc))}
132
+ return check_identity(transcript, recording)
133
+
134
+
135
+ def _render_transcript_with_identity(
136
+ output_text: str, identity_check: dict | None, *, json_flag: bool, in_range: bool = True
137
+ ) -> str:
138
+ """Сборка вывода `get-transcript` (companion-спека «Оркестрация», шаг 5).
139
+ `identity_check is None` -> сверка отключена (`--no-verify-identity`), выводим
140
+ как раньше. `in_range` — валидность `--chunk` уже известна вызывающей стороне
141
+ (`resolve_chunk_range`, ADR-024 §Д3) — на пути вне диапазона JSON-конверт
142
+ собирается явно (`{"error": …}`), без `try/except JSONDecodeError`."""
143
+ if identity_check is None:
144
+ return output_text
145
+
146
+ if not json_flag:
147
+ line = f"[identity-check] {identity_check['result']}"
148
+ if "reason" in identity_check:
149
+ line += f" ({identity_check['reason']})"
150
+ return f"{output_text}\n{line}"
151
+
152
+ if not in_range:
153
+ return json.dumps(
154
+ {"error": output_text, "identity_check": identity_check},
155
+ ensure_ascii=False,
156
+ indent=2,
157
+ )
158
+
159
+ parsed = json.loads(output_text)
160
+ return json.dumps(
161
+ {"transcript": parsed, "identity_check": identity_check}, ensure_ascii=False, indent=2
162
+ )
163
+
164
+
114
165
  def cmd_get_transcript(_reg, args: argparse.Namespace) -> int:
115
- async def _fetch() -> dict:
166
+ fmt = "raw" if args.json else "markdown"
167
+
168
+ async def _fetch() -> tuple[dict, dict | None, bool]:
116
169
  async with KTalkClient.from_settings(Settings()) as client:
117
- return await client.get_transcript(args.recording_key)
170
+ data = await client.get_transcript(args.recording_key)
171
+ in_range, _total_chunks = resolve_chunk_range(data, fmt, args.chunk, args.chunk_size)
172
+ identity_check = None
173
+ if not args.no_verify_identity:
174
+ if in_range:
175
+ identity_check = await _verify_transcript_identity(
176
+ client, args.recording_key, data
177
+ )
178
+ else:
179
+ # ADR-024 §Д3: чанк заведомо вне диапазона — сверка не
180
+ # оплачивается сетевым вызовом, `not_checked` формируется
181
+ # локально, переиспользуя словарь исходов ADR-023.
182
+ identity_check = {"result": "not_checked", "reason": "chunk_out_of_range"}
183
+ return data, identity_check, in_range
118
184
 
119
185
  try:
120
- data = asyncio.run(_fetch())
186
+ data, identity_check, in_range = asyncio.run(_fetch())
121
187
  except Exception as exc: # noqa: BLE001 - surface as CLI error, NFR-5 маскирует
122
188
  print(f"Ошибка: {redact_secrets(str(exc))}", file=sys.stderr)
123
189
  return 1
124
- fmt = "raw" if args.json else "markdown"
125
- print(render_transcript_output(data, fmt, args.chunk, args.chunk_size))
190
+ output_text = render_transcript_output(data, fmt, args.chunk, args.chunk_size)
191
+ print(
192
+ _render_transcript_with_identity(
193
+ output_text, identity_check, json_flag=args.json, in_range=in_range
194
+ )
195
+ )
196
+ if identity_check is not None and identity_check.get("result") == "mismatch":
197
+ # ADR-024 §Д1: отказ становится громким — код 3, отдельный от 0/1/2.
198
+ return 3
126
199
  return 0
127
200
 
128
201
 
@@ -123,7 +123,7 @@ def format_recording(data: dict) -> str:
123
123
  title = data.get("title", "Без названия")
124
124
  lines = [f"# {title}", ""]
125
125
 
126
- lines.append(f"- **Ключ:** {data.get('id', data.get('key', 'N/A'))}")
126
+ lines.append(f"- **Ключ:** {data.get('id') or data.get('key') or 'N/A'}")
127
127
  lines.append(f"- **Дата:** {_format_datetime(data.get('createdDate'))}")
128
128
 
129
129
  created_by = data.get("createdBy")
@@ -162,7 +162,7 @@ def format_recordings_list(data: dict) -> str:
162
162
  ]
163
163
 
164
164
  for rec in entities:
165
- rec_id = rec.get("id", rec.get("key", "N/A"))
165
+ rec_id = rec.get("id") or rec.get("key") or "N/A"
166
166
  title = rec.get("title", "Без названия")
167
167
  date = _format_datetime(rec.get("createdDate"))
168
168
  created_by = rec.get("createdBy") or {}
@@ -318,6 +318,37 @@ def chunk_transcript_raw(data: dict, chunk_size: int) -> list[str]:
318
318
  return chunks
319
319
 
320
320
 
321
+ def _chunk_transcript(data: dict, fmt: str, chunk_size: int, full_text: str) -> list[str]:
322
+ """Общая точка чанкинга для `render_transcript_output`/`resolve_chunk_range` —
323
+ не дублирует ветвление raw/markdown (ADR-024 companion-спека §3)."""
324
+ if fmt == "raw":
325
+ return chunk_transcript_raw(data, chunk_size)
326
+ return chunk_transcript_markdown(full_text, chunk_size)
327
+
328
+
329
+ def resolve_chunk_range(data: dict, fmt: str, chunk: int, chunk_size: int) -> tuple[bool, int]:
330
+ """Определяет, лежит ли `chunk` в валидном диапазоне для `data`, БЕЗ сети
331
+ (ADR-024 §Д3, issue #9) — переиспользует чанкинг `render_transcript_output`,
332
+ не дублирует его. Отрицательный `chunk` — тоже вне диапазона (не Python-
333
+ отрицательная индексация).
334
+
335
+ Возвращает `(in_range, total_chunks)`.
336
+ """
337
+ if fmt == "raw":
338
+ full_text = format_raw(data)
339
+ else:
340
+ full_text = format_transcript(data)
341
+
342
+ if chunk == 0 and len(full_text) <= chunk_size:
343
+ return True, 1
344
+
345
+ chunks = _chunk_transcript(data, fmt, chunk_size, full_text)
346
+ total_chunks = len(chunks)
347
+ chunk_index = 0 if chunk == 0 else chunk - 1
348
+
349
+ return 0 <= chunk_index < total_chunks, total_chunks
350
+
351
+
321
352
  def render_transcript_output(data: dict, fmt: str, chunk: int, chunk_size: int) -> str:
322
353
  """Общий слой чтения транскрипта с чанкингом — единственная точка правды для
323
354
  `ktalk_get_transcript` (MCP) и `ktalk get-transcript` (CLI, DEV-002 волны 3):
@@ -332,10 +363,7 @@ def render_transcript_output(data: dict, fmt: str, chunk: int, chunk_size: int)
332
363
  if chunk == 0 and total_characters <= chunk_size:
333
364
  return full_text
334
365
 
335
- if fmt == "raw":
336
- chunks = chunk_transcript_raw(data, chunk_size)
337
- else:
338
- chunks = chunk_transcript_markdown(full_text, chunk_size)
366
+ chunks = _chunk_transcript(data, fmt, chunk_size, full_text)
339
367
 
340
368
  total_chunks = len(chunks)
341
369
  chunk_index = 0 if chunk == 0 else chunk - 1
@@ -0,0 +1,69 @@
1
+ """Сверка идентичности транскрипта (NFR-17, issue #5, ADR-023 §1 + companion-спека
2
+ «Компоненты — NFR-17»): сверяет состав спикеров транскрипта с составом участников
3
+ записи того же `recording_id` через независимый вызов `get_recording` — обнаруживает
4
+ подмену транскрипта под конкуренцией (RES-006), не связанную с валидностью самого
5
+ запроса.
6
+
7
+ Чистые функции без сети и побочных эффектов — сеть (`client.get_recording`) и исход
8
+ `not_checked` (отказ вызова) живут в оркестрации `cmd_get_transcript`
9
+ (`cli_content.py`), не здесь.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+
15
+ def _identity_key(user_ref: dict | None) -> str | None:
16
+ """Идентификатор пользователя: `userInfo.key` или `.login` (именованный),
17
+ `anonymousId` или `anonymousName` (аноним), `None` если ссылка пуста — та же
18
+ приоритетность, что `_format_user_name(with_id=True)` в `formatters.py`, но
19
+ возвращает сырой токен для сравнения, не строку для вывода."""
20
+ if not user_ref:
21
+ return None
22
+ if user_ref.get("isAnonymous"):
23
+ return user_ref.get("anonymousId") or user_ref.get("anonymousName")
24
+ user_info = user_ref.get("userInfo")
25
+ if not user_info:
26
+ return None
27
+ return user_info.get("key") or user_info.get("login")
28
+
29
+
30
+ def speaker_identities(transcript: dict) -> set[str]:
31
+ """Состав спикеров транскрипта: обход `tracks[]`, `speaker`, при отсутствии —
32
+ `diarizedSpeaker`; треки без обоих не подмешивают `None` в результат."""
33
+ identities: set[str] = set()
34
+ for track in transcript.get("tracks") or []:
35
+ speaker_ref = track.get("speaker") or track.get("diarizedSpeaker")
36
+ key = _identity_key(speaker_ref)
37
+ if key is not None:
38
+ identities.add(key)
39
+ return identities
40
+
41
+
42
+ def participant_identities(recording: dict) -> set[str]:
43
+ """Состав участников записи: обход `recording["participants"]` (`TalkUserRef[]`)."""
44
+ identities: set[str] = set()
45
+ for participant in recording.get("participants") or []:
46
+ key = _identity_key(participant)
47
+ if key is not None:
48
+ identities.add(key)
49
+ return identities
50
+
51
+
52
+ def check_identity(transcript: dict, recording: dict) -> dict:
53
+ """Четыре исхода (`not_checked` не формируется здесь — исход уровня оркестрации,
54
+ когда сам вызов `get_recording` падает):
55
+ - `inconclusive` — любое множество пусто, сравнивать не с чем;
56
+ - `match` — пересечение непусто;
57
+ - `mismatch` — оба множества непусты и не пересекаются (сигнатура RES-006)."""
58
+ transcript_speakers = speaker_identities(transcript)
59
+ recording_participants = participant_identities(recording)
60
+
61
+ if not transcript_speakers or not recording_participants:
62
+ return {"result": "inconclusive"}
63
+
64
+ result = "match" if transcript_speakers & recording_participants else "mismatch"
65
+ return {
66
+ "result": result,
67
+ "transcript_speakers": sorted(transcript_speakers),
68
+ "recording_participants": sorted(recording_participants),
69
+ }
@@ -113,11 +113,15 @@ def test_get_recording_error_goes_to_stderr_with_nonzero_exit(httpx_mock: HTTPXM
113
113
 
114
114
  @pytest.mark.httpx_mock(assert_all_responses_were_requested=False)
115
115
  def test_get_transcript_json_default_returns_raw_json(httpx_mock: HTTPXMock, capsys):
116
+ """`--no-verify-identity`: предмет теста — форма raw-JSON транскрипта, не NFR-17
117
+ (ADR-023 §1 сделал сверку идентичности умолчанием — без флага здесь потребовался
118
+ бы и мок `get_recording`; отдельное покрытие умолчания-включено —
119
+ `tests/test_nfr17_identity_verification.py`)."""
116
120
  from ktalk_cli.cli import main
117
121
 
118
122
  httpx_mock.add_response(json={"status": "done", "tracks": []})
119
123
 
120
- rc = main(["get-transcript", "REC-1", "--json"])
124
+ rc = main(["get-transcript", "REC-1", "--json", "--no-verify-identity"])
121
125
 
122
126
  assert rc == 0
123
127
  out = json.loads(capsys.readouterr().out)
@@ -126,11 +130,13 @@ def test_get_transcript_json_default_returns_raw_json(httpx_mock: HTTPXMock, cap
126
130
 
127
131
  @pytest.mark.httpx_mock(assert_all_responses_were_requested=False)
128
132
  def test_get_transcript_markdown_default(httpx_mock: HTTPXMock, capsys):
133
+ """`--no-verify-identity` — см. комментарий у `test_get_transcript_json_default_
134
+ returns_raw_json` выше: предмет здесь — markdown-рендер, не NFR-17."""
129
135
  from ktalk_cli.cli import main
130
136
 
131
137
  httpx_mock.add_response(json={"status": "done", "tracks": []})
132
138
 
133
- rc = main(["get-transcript", "REC-1"])
139
+ rc = main(["get-transcript", "REC-1", "--no-verify-identity"])
134
140
 
135
141
  assert rc == 0
136
142
  assert "Транскрипт" in capsys.readouterr().out