@iola_adm/iola-cli 0.2.15 → 0.2.17

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/README.md CHANGED
@@ -197,7 +197,9 @@ iola yandex menu
197
197
  iola yandex status
198
198
  ```
199
199
 
200
- Первый контур: Yandex ID, Яндекс Диск и Яндекс Почта. Календарь, контакты, Wiki, Tracker, Forms и документы 360 заложены как категории для проверки и могут потребовать отдельное OAuth-приложение Яндекса. Такси, Маркет и Доставка записаны в backlog только как сценарии подготовки ссылки/маршрута/списка без заказа и оплаты.
200
+ Yandex Connector использует две встроенные OAuth-группы: `IOLA CLI A` для Yandex ID, Диска, Почты и документов через Диск; `IOLA CLI B` для Календаря, Контактов и Телемоста через календарь. Такси, Маркет и Доставка записаны в backlog только как сценарии подготовки ссылки/маршрута/списка без заказа и оплаты.
201
+
202
+ В `/yandex` функции выбираются номерами через запятую, как в мастере настройки. Там же есть пункт `Удалить подключение-коннектор`, который чистит локальные токены и настройки Yandex Connector. OAuth-права сами по себе не создают функциональность: под каждый сервис нужны отдельные команды и тулы.
201
203
 
202
204
  Инструкция: [Yandex Connector](https://github.com/adm-iola/iola-cli/wiki/Yandex-Connector).
203
205
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "description": "CLI и AI-агент городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
package/src/cli.js CHANGED
@@ -42,8 +42,8 @@ const BROWSER_RUNTIME_PACKAGE = path.join(BROWSER_RUNTIME_DIR, "node_modules", "
42
42
  const CLOUD_DEFAULT_REMOTE_DIR = "/IOLA";
43
43
  const YANDEX_OAUTH_AUTHORIZE_URL = "https://oauth.yandex.ru/authorize";
44
44
  const YANDEX_OAUTH_REDIRECT_URL = "https://oauth.yandex.ru/verification_code";
45
- const YANDEX_CONNECTOR_CLIENT_ID = process.env.IOLA_YANDEX_OAUTH_CLIENT_ID || process.env.YANDEX_OAUTH_CLIENT_ID || "915b0b6ef0474f3a9b3edd70515c5d60";
46
- const YANDEX_CONNECTOR_WORKSPACE_CLIENT_ID = process.env.IOLA_YANDEX_WORKSPACE_OAUTH_CLIENT_ID || "";
45
+ const YANDEX_CONNECTOR_CLIENT_ID = process.env.IOLA_YANDEX_OAUTH_CLIENT_ID || process.env.YANDEX_OAUTH_CLIENT_ID || "9b7c9bcf81e8491f9bd36ba44fa76128";
46
+ const YANDEX_CONNECTOR_ORGANIZER_CLIENT_ID = process.env.IOLA_YANDEX_ORGANIZER_OAUTH_CLIENT_ID || "4ab53d8557e64ac98534ed60295cb138";
47
47
  const YANDEX_CONNECTOR_REDIRECT_HOST = "127.0.0.1";
48
48
  const YANDEX_CONNECTOR_REDIRECT_PORT = Number(process.env.IOLA_YANDEX_OAUTH_PORT || 18791);
49
49
  const YANDEX_CONNECTOR_REDIRECT_PATH = "/yandex/oauth/callback";
@@ -79,30 +79,9 @@ const YANDEX_CONNECTOR_SERVICES = {
79
79
  contacts: {
80
80
  title: "Яндекс Контакты",
81
81
  category: "contacts",
82
- scope: "carddav",
82
+ scope: "addressbook:all",
83
83
  status: "research",
84
- hint: "контакты через CardDAV/360, требует проверки",
85
- },
86
- wiki: {
87
- title: "Yandex Wiki",
88
- category: "workspace",
89
- scope: "wiki:read wiki:write",
90
- status: "research",
91
- hint: "страницы wiki, больше полезно организациям",
92
- },
93
- tracker: {
94
- title: "Yandex Tracker",
95
- category: "workspace",
96
- scope: "tracker:read tracker:write",
97
- status: "research",
98
- hint: "задачи и обращения, больше полезно организациям",
99
- },
100
- forms: {
101
- title: "Yandex Forms",
102
- category: "forms",
103
- scope: "forms:read forms:write",
104
- status: "research",
105
- hint: "формы и опросы, API надо подтвердить",
84
+ hint: "адресная книга и контакты, требует проверки API",
106
85
  },
107
86
  docs: {
108
87
  title: "Яндекс Документы / 360",
@@ -114,9 +93,9 @@ const YANDEX_CONNECTOR_SERVICES = {
114
93
  telemost: {
115
94
  title: "Яндекс Телемост",
116
95
  category: "meetings",
117
- scope: "",
96
+ scope: "calendar:all",
118
97
  status: "research",
119
- hint: "создание встреч через публичный API надо подтвердить",
98
+ hint: "встречи через календарное событие, если поддерживается",
120
99
  },
121
100
  cloud: {
122
101
  title: "Yandex Cloud",
@@ -157,15 +136,15 @@ const YANDEX_CONNECTOR_SERVICES = {
157
136
  const YANDEX_CONNECTOR_OAUTH_APPS = [
158
137
  {
159
138
  id: "core",
160
- title: "IOLA Yandex Core",
139
+ title: "IOLA CLI A",
161
140
  clientId: YANDEX_CONNECTOR_CLIENT_ID,
162
- services: ["identity", "disk", "mail"],
141
+ services: ["identity", "disk", "mail", "docs"],
163
142
  },
164
143
  {
165
- id: "workspace",
166
- title: "IOLA Yandex Workspace",
167
- clientId: YANDEX_CONNECTOR_WORKSPACE_CLIENT_ID,
168
- services: ["contacts", "wiki", "tracker", "forms", "docs"],
144
+ id: "organizer",
145
+ title: "IOLA CLI B",
146
+ clientId: YANDEX_CONNECTOR_ORGANIZER_CLIENT_ID,
147
+ services: ["calendar", "contacts", "telemost"],
169
148
  },
170
149
  ];
171
150
  const INDEXABLE_EXTENSIONS = /\.(md|txt|csv|json|html|docx|xlsx|pptx|pdf)$/i;
@@ -2040,7 +2019,7 @@ async function doctor(args = []) {
2040
2019
  openaiKey: process.env.OPENAI_API_KEY ? "env" : secrets.openai?.apiKey ? "local" : "missing",
2041
2020
  openrouterKey: process.env.OPENROUTER_API_KEY ? "env" : secrets.openrouter?.apiKey ? "local" : "missing",
2042
2021
  yandexGeocoderKey: (process.env.YANDEX_GEOCODER_API_KEY || process.env.YANDEX_MAPS_API_KEY) ? "env" : secrets.yandexGeocoder?.apiKey ? "local" : "missing",
2043
- yandexConnector: (process.env.YANDEX_OAUTH_TOKEN || secrets.yandex?.oauthToken || secrets.cloud?.["yandex-disk"]?.token) ? "local/env" : "missing",
2022
+ yandexConnector: (process.env.YANDEX_OAUTH_TOKEN || secrets.yandex?.oauthToken || Object.keys(secrets.yandex?.oauthApps || {}).length || secrets.cloud?.["yandex-disk"]?.token) ? "local/env" : "missing",
2044
2023
  yandexAuthorized: config.yandex?.authorizedServices?.join(", ") || "-",
2045
2024
  yandexServices: config.yandex?.enabledServices?.join(", ") || (secrets.cloud?.["yandex-disk"]?.token ? "disk (legacy cloud token)" : "-"),
2046
2025
  ollama: diagnostics.ollama.installed ? diagnostics.ollama.version : "not-installed",
@@ -3285,7 +3264,7 @@ async function handleYandex(args) {
3285
3264
  iola yandex disable mail
3286
3265
  iola yandex oauth-url [disk mail calendar] [--client-id ID] [--open]
3287
3266
  iola yandex token set
3288
- iola yandex token delete
3267
+ iola yandex token delete удалить локальные токены и настройки коннектора
3289
3268
  iola yandex backlog`);
3290
3269
  }
3291
3270
 
@@ -3343,11 +3322,12 @@ async function setupYandexConnector(args = []) {
3343
3322
  }
3344
3323
  await printYandexConnectorStatus({ check: true });
3345
3324
  } else {
3346
- const app = oauthApps[0] || { clientId, services: authorizedServices };
3347
- const url = buildYandexOAuthUrl({ clientId: app.clientId, services: app.services, redirectUrl });
3348
- console.log("Откройте ссылку авторизации, получите OAuth-токен и сохраните его командой: iola yandex token set");
3349
- console.log(url);
3350
- if (options.open) await openUrl(url);
3325
+ console.log("Откройте ссылки авторизации, получите OAuth-токены и сохраните их командой: iola yandex token set --app APP_ID");
3326
+ for (const app of oauthApps.length ? oauthApps : [{ id: "custom", title: "Yandex Connector", clientId, services: authorizedServices }]) {
3327
+ const url = buildYandexOAuthUrl({ clientId: app.clientId, services: app.services, redirectUrl });
3328
+ console.log(`${app.id}: ${url}`);
3329
+ if (options.open) await openUrl(url);
3330
+ }
3351
3331
  }
3352
3332
  } else {
3353
3333
  console.log("Yandex Connector не может открыть браузер: в этой сборке не задан public OAuth client_id приложения IOLA.");
@@ -3362,14 +3342,20 @@ async function chooseYandexServicesMenu() {
3362
3342
  return;
3363
3343
  }
3364
3344
  const config = await loadConfig();
3365
- const serviceIds = Object.keys(YANDEX_CONNECTOR_SERVICES);
3345
+ const serviceIds = getYandexConnectorMenuServiceIds();
3346
+ const deleteNumber = serviceIds.length + 1;
3366
3347
  const enabled = new Set(config.yandex?.enabledServices?.length ? config.yandex.enabledServices : ["identity", "disk"]);
3367
- console.log("Yandex Connector: выберите сервисы.");
3348
+ const authState = await getYandexServiceAuthState();
3349
+ console.log("Функции Яндекса.");
3350
+ console.log("Выберите номера функций через запятую:");
3368
3351
  serviceIds.forEach((id, index) => {
3369
3352
  const service = YANDEX_CONNECTOR_SERVICES[id];
3370
3353
  const marker = enabled.has(id) ? "✓" : " ";
3371
- console.log(`${index + 1}. [${marker}] ${service.title} (${id}, ${service.status}) - ${service.hint}`);
3354
+ const auth = authState.byService[id];
3355
+ const authLabel = auth?.hasToken ? "подключено" : (auth?.authorized ? "нужен вход" : "нет прав");
3356
+ console.log(`${index + 1}. [${marker}] ${service.title} - ${service.hint} (${service.status}, ${authLabel})`);
3372
3357
  });
3358
+ console.log(`${deleteNumber}. Удалить подключение-коннектор`);
3373
3359
  console.log("0. Отмена");
3374
3360
  const defaults = serviceIds.map((id, index) => enabled.has(id) ? String(index + 1) : "").filter(Boolean);
3375
3361
  const answer = (await askText(`Номера через запятую [${defaults.join(",") || "1,2"}]: `)).trim();
@@ -3378,6 +3364,16 @@ async function chooseYandexServicesMenu() {
3378
3364
  return;
3379
3365
  }
3380
3366
  const selectedNumbers = answer ? answer.split(/[,\s]+/).filter(Boolean) : (defaults.length ? defaults : ["1", "2"]);
3367
+ if (selectedNumbers.includes(String(deleteNumber))) {
3368
+ if (selectedNumbers.length > 1) throw new Error("Удаление коннектора выбирается отдельно, без других пунктов.");
3369
+ const ok = await askYesNo("Удалить локальные токены и настройки Yandex Connector? [y/N] ", false);
3370
+ if (!ok) {
3371
+ console.log("Удаление отменено.");
3372
+ return;
3373
+ }
3374
+ await deleteYandexConnectorToken();
3375
+ return;
3376
+ }
3381
3377
  const selected = selectedNumbers.map((item) => {
3382
3378
  const index = Number(item) - 1;
3383
3379
  if (!Number.isInteger(index) || index < 0 || index >= serviceIds.length) {
@@ -3387,13 +3383,16 @@ async function chooseYandexServicesMenu() {
3387
3383
  });
3388
3384
  await saveYandexEnabledServices(selected);
3389
3385
  console.log(`Включены сервисы: ${normalizeYandexServiceList(selected).join(", ")}`);
3390
- const nextConfig = await loadConfig();
3391
- if (nextConfig.yandex?.oauth?.clientId) {
3392
- console.log("OAuth-ссылка с максимальными правами коннектора:");
3393
- console.log(await buildYandexOAuthUrlFromConfig([]));
3394
- } else {
3395
- console.log("Для авторизации создайте OAuth Client ID и выполните: iola yandex oauth-url --client-id CLIENT_ID");
3396
- }
3386
+ const missingAuth = selected.filter((id) => !authState.byService[id]?.authorized);
3387
+ const missingToken = selected.filter((id) => authState.byService[id]?.authorized && !authState.byService[id]?.hasToken);
3388
+ if (missingAuth.length) console.log(`Нет OAuth-прав в текущей сборке: ${missingAuth.join(", ")}. Для них нужно отдельное OAuth-приложение Яндекса.`);
3389
+ if (missingToken.length) console.log(`Нужно пройти вход Яндекса для: ${missingToken.join(", ")}. Запустите iola yandex setup.`);
3390
+ }
3391
+
3392
+ function getYandexConnectorMenuServiceIds() {
3393
+ return Object.entries(YANDEX_CONNECTOR_SERVICES)
3394
+ .filter(([, service]) => service.status === "ready" || service.status === "research")
3395
+ .map(([id]) => id);
3397
3396
  }
3398
3397
 
3399
3398
  async function updateYandexEnabledServices(rawServices, enabled) {
@@ -3439,9 +3438,15 @@ async function saveYandexAuthorizedServices(services) {
3439
3438
  async function buildYandexOAuthUrlFromConfig(rawArgs = []) {
3440
3439
  const options = parseOptions(rawArgs);
3441
3440
  const config = await loadConfig();
3442
- const clientId = options["client-id"] || config.yandex?.oauth?.clientId || YANDEX_CONNECTOR_CLIENT_ID;
3441
+ const apps = getConfiguredYandexOAuthApps();
3442
+ const capableServices = getYandexOAuthCapableServiceIds();
3443
+ const requestedServices = normalizeYandexServiceList(options._.length ? options._ : capableServices);
3444
+ const app = options.app
3445
+ ? apps.find((item) => item.id === options.app)
3446
+ : apps.find((item) => requestedServices.some((service) => item.services.includes(service))) || apps[0];
3447
+ const clientId = options["client-id"] || app?.clientId || config.yandex?.oauth?.clientId || YANDEX_CONNECTOR_CLIENT_ID;
3443
3448
  if (!clientId) throw new Error("Yandex OAuth Client ID не задан. Пример: iola yandex oauth-url disk --client-id CLIENT_ID");
3444
- const services = normalizeYandexServiceList(options._.length ? options._ : (config.yandex?.authorizedServices?.length ? config.yandex.authorizedServices : getYandexOAuthCapableServiceIds()));
3449
+ const services = requestedServices.filter((id) => capableServices.includes(id) && (!app || app.services.includes(id)));
3445
3450
  return buildYandexOAuthUrl({ clientId, services, redirectUrl: options["redirect-url"] || config.yandex?.oauth?.redirectUrl || YANDEX_OAUTH_REDIRECT_URL });
3446
3451
  }
3447
3452
 
@@ -3554,7 +3559,6 @@ function waitForYandexOAuthToken({ clientId, services, redirectUrl }) {
3554
3559
  function getYandexScopesForServices(services) {
3555
3560
  const scopes = new Set();
3556
3561
  const normalized = normalizeYandexServiceList(services);
3557
- if (normalized.length > 0 && !normalized.includes("identity")) normalized.unshift("identity");
3558
3562
  for (const id of normalized) {
3559
3563
  const raw = YANDEX_CONNECTOR_SERVICES[id]?.scope || "";
3560
3564
  for (const scope of raw.split(/\s+/).filter(Boolean)) scopes.add(scope);
@@ -3611,22 +3615,33 @@ async function deleteYandexConnectorToken() {
3611
3615
  if (secrets.cloud?.["yandex-disk"]) delete secrets.cloud["yandex-disk"];
3612
3616
  if (secrets.cloud && Object.keys(secrets.cloud).length === 0) delete secrets.cloud;
3613
3617
  await saveSecrets(secrets);
3614
- console.log("Yandex Connector token удален. Токен Яндекс Диска в cloud тоже удален.");
3618
+ await deleteLocalYandexConnectorConfig();
3619
+ console.log("Yandex Connector удален локально. Токены и настройки приложений очищены.");
3620
+ }
3621
+
3622
+ async function deleteLocalYandexConnectorConfig() {
3623
+ const local = await readConfigLayer(CONFIG_FILE);
3624
+ if (!local) return;
3625
+ delete local.yandex;
3626
+ if (local.cloud?.activeProvider === "yandex-disk") local.cloud.activeProvider = "";
3627
+ await mkdir(CONFIG_DIR, { recursive: true });
3628
+ if (existsSync(CONFIG_FILE)) await copyFile(CONFIG_FILE, LAST_GOOD_CONFIG_FILE).catch(() => {});
3629
+ await writeFile(CONFIG_FILE, `${JSON.stringify(local, null, 2)}\n`, "utf8");
3615
3630
  }
3616
3631
 
3617
3632
  async function printYandexConnectorStatus(options = {}) {
3618
3633
  const [config, secrets] = await Promise.all([loadConfig(), loadSecrets()]);
3619
3634
  const enabled = config.yandex?.enabledServices || [];
3620
- const authorized = config.yandex?.authorizedServices?.length ? config.yandex.authorizedServices : [];
3621
3635
  const legacyDiskToken = Boolean(secrets.cloud?.["yandex-disk"]?.token && !secrets.yandex?.oauthToken);
3636
+ const authState = await getYandexServiceAuthState({ config, secrets });
3622
3637
  const token = process.env.YANDEX_OAUTH_TOKEN || secrets.yandex?.oauthToken || secrets.cloud?.["yandex-disk"]?.token || "";
3623
3638
  const rows = Object.entries(YANDEX_CONNECTOR_SERVICES).map(([id, service]) => ({
3624
3639
  id,
3625
3640
  enabled: enabled.includes(id) ? "yes" : (legacyDiskToken && id === "disk" ? "legacy" : "no"),
3626
3641
  category: service.category,
3627
3642
  status: service.status,
3628
- token: service.scope && (authorized.includes(id) || enabled.includes(id) || (legacyDiskToken && id === "disk")) ? (token ? "local/env" : "missing") : "-",
3629
- authorized: authorized.includes(id) ? "yes" : "-",
3643
+ token: service.scope && authState.byService[id]?.authorized ? (authState.byService[id]?.hasToken ? "local/env" : "missing") : "-",
3644
+ authorized: authState.byService[id]?.authorized ? "yes" : "-",
3630
3645
  title: service.title,
3631
3646
  }));
3632
3647
  printTable(rows, [
@@ -3650,6 +3665,35 @@ async function printYandexConnectorStatus(options = {}) {
3650
3665
  }
3651
3666
  }
3652
3667
 
3668
+ async function getYandexServiceAuthState({ config = null, secrets = null } = {}) {
3669
+ const loadedConfig = config || await loadConfig();
3670
+ const loadedSecrets = secrets || await loadSecrets();
3671
+ const apps = getConfiguredYandexOAuthApps();
3672
+ const appTokens = loadedSecrets.yandex?.oauthApps || {};
3673
+ const envToken = process.env.YANDEX_OAUTH_TOKEN || "";
3674
+ const byService = {};
3675
+ for (const id of Object.keys(YANDEX_CONNECTOR_SERVICES)) {
3676
+ byService[id] = { authorized: false, hasToken: false, apps: [] };
3677
+ }
3678
+ for (const app of apps) {
3679
+ const hasToken = Boolean(envToken || appTokens[app.id]?.token || (app.id === "core" && loadedSecrets.yandex?.oauthToken));
3680
+ for (const id of normalizeYandexServiceList(app.services)) {
3681
+ byService[id] = byService[id] || { authorized: false, hasToken: false, apps: [] };
3682
+ byService[id].authorized = true;
3683
+ byService[id].hasToken = byService[id].hasToken || hasToken;
3684
+ byService[id].apps.push(app.id);
3685
+ }
3686
+ }
3687
+ if (loadedSecrets.cloud?.["yandex-disk"]?.token) {
3688
+ byService.disk.authorized = true;
3689
+ byService.disk.hasToken = true;
3690
+ byService.disk.apps.push("legacy-cloud");
3691
+ }
3692
+ const authorizedServices = Object.entries(byService).filter(([, state]) => state.authorized).map(([id]) => id);
3693
+ const enabledServices = loadedConfig.yandex?.enabledServices || [];
3694
+ return { byService, authorizedServices, enabledServices };
3695
+ }
3696
+
3653
3697
  async function yandexUserInfo(token) {
3654
3698
  const response = await fetch("https://login.yandex.ru/info?format=json", {
3655
3699
  headers: { Authorization: `OAuth ${token}` },
@@ -11358,7 +11402,7 @@ async function getOnboardComponentStatus() {
11358
11402
  browser: browser.installed === "yes",
11359
11403
  "yandex-geocoder": Boolean(yandexGeocoderKey),
11360
11404
  cloud: Object.keys(cloudSecrets).length > 0,
11361
- yandex: Boolean(secrets.yandex?.oauthToken || config.yandex?.enabledServices?.length),
11405
+ yandex: Boolean(secrets.yandex?.oauthToken || Object.keys(secrets.yandex?.oauthApps || {}).length || config.yandex?.enabledServices?.length),
11362
11406
  };
11363
11407
  }
11364
11408
 
@@ -13383,6 +13427,12 @@ function sanitizeConfig(config) {
13383
13427
  if (Array.isArray(next.skills?.enabled) && next.skills.enabled.includes("local-files") && !next.skills.enabled.includes("personal-docs")) {
13384
13428
  next.skills.enabled = [...next.skills.enabled, "personal-docs"];
13385
13429
  }
13430
+ if (Array.isArray(next.yandex?.enabledServices)) {
13431
+ next.yandex.enabledServices = next.yandex.enabledServices.filter((service) => Boolean(YANDEX_CONNECTOR_SERVICES[service]));
13432
+ }
13433
+ if (Array.isArray(next.yandex?.authorizedServices)) {
13434
+ next.yandex.authorizedServices = next.yandex.authorizedServices.filter((service) => Boolean(YANDEX_CONNECTOR_SERVICES[service]));
13435
+ }
13386
13436
  const localProfile = next.ai?.profiles?.local;
13387
13437
  if (localProfile?.provider === "iola") {
13388
13438
  if (!localProfile.runtime || localProfile.model === "iola-router-1b") {
@@ -58,10 +58,16 @@ assertIncludes(cliSource, "dedupeDatedOpenAiModels", "OpenAI model selection sho
58
58
  assertIncludes(cliSource, "chooseLocalModel", "Local model selection should support IOLA and Ollama models");
59
59
  assertIncludes(cliSource, "Другая Ollama-модель", "Local model selection should allow manual Ollama model names");
60
60
  assertIncludes(cliSource, "chooseYandexServicesMenu", "Yandex Connector should have a service selection menu");
61
+ assertIncludes(cliSource, "Функции Яндекса.", "Yandex service selection should use a numbered menu");
62
+ assertIncludes(cliSource, "Выберите номера функций через запятую", "Yandex service selection should ask for numbers");
63
+ assertIncludes(cliSource, "Удалить подключение-коннектор", "Yandex service selection should allow connector deletion");
64
+ assertIncludes(cliSource, "getYandexServiceAuthState", "Yandex status should derive permissions from configured OAuth apps");
61
65
  assertIncludes(cliSource, "OAuth-права встроенного приложения", "Yandex setup should report packaged OAuth app permissions");
62
66
  assertIncludes(cliSource, "Выбрать активные функции можно командой /yandex", "Yandex setup should direct service selection to /yandex");
63
67
  assertIncludes(cliSource, "runYandexBrowserOAuth", "Yandex setup should support browser OAuth flow");
64
68
  assertIncludes(cliSource, "IOLA_YANDEX_OAUTH_CLIENT_ID", "Yandex setup should use a packaged/env OAuth client id");
69
+ assertIncludes(cliSource, "IOLA_YANDEX_ORGANIZER_OAUTH_CLIENT_ID", "Yandex setup should support the organizer OAuth app group");
70
+ assertIncludes(cliSource, "addressbook:all", "Yandex contacts should use the addressbook OAuth scope");
65
71
  assertIncludes(cliSource, "--app", "Yandex token command should persist tokens by OAuth app group");
66
72
  assertNotIncludes(cliSource, "Сервисы через запятую [identity,disk]", "Yandex setup should not ask for services during connector setup");
67
73
  if (!packageJson.files.includes("docs/assets/iola-oauth-icon.png")) {
@@ -21,6 +21,15 @@ iola yandex token set
21
21
  iola yandex token delete
22
22
  ```
23
23
 
24
+ ## OAuth-приложения
25
+
26
+ Для пользователя это один `Yandex Connector`, но внутри CLI проходит две авторизации Яндекса:
27
+
28
+ - `IOLA CLI A` - Yandex ID, Яндекс Диск, Яндекс Почта, Яндекс Документы через Диск;
29
+ - `IOLA CLI B` - Яндекс Календарь, Яндекс Контакты, Телемост через календарное событие.
30
+
31
+ `client_id` этих приложений публичный и встроен в CLI. Пользователь не создает OAuth-приложения вручную: он только входит в свой Яндекс-аккаунт и разрешает доступ. Токены сохраняются локально на компьютере пользователя.
32
+
24
33
  ## Категории
25
34
 
26
35
  Готово к первому контуру:
@@ -33,10 +42,8 @@ iola yandex token delete
33
42
  - `mail` - Яндекс Почта, чтение и поиск писем, отправка только после явного подтверждения;
34
43
  - `calendar` - Яндекс Календарь;
35
44
  - `contacts` - Яндекс Контакты;
36
- - `wiki` - Yandex Wiki;
37
- - `tracker` - Yandex Tracker;
38
- - `forms` - Yandex Forms;
39
- - `docs` - Яндекс Документы / 360.
45
+ - `docs` - Яндекс Документы / 360 через Диск;
46
+ - `telemost` - Яндекс Телемост через календарь, если сценарий подтвердится.
40
47
 
41
48
  Отдельные ключи, не обычный OAuth бытового Яндекса:
42
49
 
@@ -57,7 +64,7 @@ Backlog после первого контура:
57
64
  iola yandex setup
58
65
  ```
59
66
 
60
- 2. Авторизуйтесь в Яндексе и разрешите доступ. В текущем встроенном приложении `IOLA Yandex Core` запрашиваются права:
67
+ 2. Авторизуйтесь в Яндексе и разрешите доступ для `IOLA CLI A`. Запрашиваются права:
61
68
  - `login:info`;
62
69
  - `login:email`;
63
70
  - `cloud_api:disk.read`;
@@ -65,15 +72,18 @@ iola yandex setup
65
72
  - `cloud_api:disk.info`;
66
73
  - `mail:imap_full`;
67
74
  - `mail:smtp`.
68
- 3. После успешной авторизации браузер вернется на локальную страницу `iola-cli`, а CLI сам сохранит OAuth-токен.
69
- 4. Если автоматический браузерный flow недоступен, используйте fallback для разработки:
75
+ 3. Затем CLI откроет вторую авторизацию для `IOLA CLI B`. Запрашиваются права:
76
+ - `calendar:all`;
77
+ - `addressbook:all`.
78
+ 4. После каждой успешной авторизации браузер вернется на локальную страницу `iola-cli`, а CLI сам сохранит OAuth-токен нужной группы.
79
+ 5. Если автоматический браузерный flow недоступен, используйте fallback для разработки:
70
80
 
71
81
  ```bash
72
82
  iola yandex setup --client-id CLIENT_ID --print-url
73
83
  iola yandex token set
74
84
  ```
75
85
 
76
- 5. Выберите, какие функции CLI реально использует:
86
+ 6. Выберите, какие функции CLI реально использует:
77
87
 
78
88
  ```bash
79
89
  iola yandex menu
@@ -85,7 +95,10 @@ iola yandex menu
85
95
  /yandex
86
96
  ```
87
97
 
88
- 6. Проверьте:
98
+ Меню `/yandex` работает как мастер настройки: сервисы выбираются номерами через запятую, а не вводом технических названий.
99
+ В этом же меню есть отдельный пункт `Удалить подключение-коннектор`: он удаляет локальные токены и настройки Yandex Connector, чтобы можно было подключить другой Яндекс-аккаунт или отказаться от сервисов Яндекса.
100
+
101
+ 7. Проверьте:
89
102
 
90
103
  ```bash
91
104
  iola yandex doctor
@@ -94,6 +107,20 @@ iola cloud doctor
94
107
 
95
108
  Если включен `disk`, токен автоматически подключается и к старому облачному провайдеру `yandex-disk`, поэтому команды `iola cloud ...` продолжают работать.
96
109
 
110
+ ## Что значит включить сервис
111
+
112
+ OAuth-права дают CLI разрешение обращаться к сервису Яндекса. Но для практической работы под каждый сервис нужны отдельные команды и тулы:
113
+
114
+ - `identity` - проверить пользователя и email;
115
+ - `disk` - папки, загрузка, скачивание, поиск файлов, публичные ссылки;
116
+ - `mail` - список писем, поиск, чтение письма, отправка письма после явного подтверждения;
117
+ - `calendar` - список событий, создание события, напоминания;
118
+ - `contacts` - поиск контактов и карточки контактов;
119
+ - `docs` - поиск и работа с документами через Диск;
120
+ - `telemost` - подготовка встречи через календарь, если подтвердится.
121
+
122
+ Включение сервиса в `/yandex` только разрешает CLI использовать соответствующую категорию. Если прав или тула еще нет, CLI должен честно показать это, а не имитировать работу.
123
+
97
124
  ## Иконка приложения
98
125
 
99
126
  CLI поставляется с готовой иконкой OAuth-приложения. При установке она копируется в:
@@ -118,6 +145,6 @@ Yandex Connector не является универсальным ключом
118
145
 
119
146
  Для браузерного подключения в сборке CLI уже указан public OAuth `client_id` приложения IOLA. Это не секретный ключ. Пользователь не должен создавать OAuth-приложение вручную.
120
147
 
121
- Яндекс ограничивает количество разных групп сервисов в одном OAuth-приложении. Поэтому один токен не всегда может покрыть Диск, Почту, Календарь, Контакты, Wiki, Tracker и Forms сразу. CLI поддерживает группировку по нескольким OAuth-приложениям; текущий первый контур - `identity`, `disk`, `mail`.
148
+ Яндекс ограничивает количество разных групп сервисов в одном OAuth-приложении. Поэтому один токен не может покрыть Диск, Почту, Календарь и Контакты сразу. CLI использует две встроенные группы: `IOLA CLI A` и `IOLA CLI B`.
122
149
 
123
150
  CLI не должен автоматически оформлять покупки, вызывать такси, подтверждать доставку или выполнять платежи. Для таких сценариев допустима только подготовка ссылки, маршрута или списка, а финальное действие делает пользователь в приложении Яндекса.