@iola_adm/iola-cli 0.2.13 → 0.2.14

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
@@ -188,11 +188,12 @@ iola cloud backup
188
188
 
189
189
  Инструкция: [Облачные диски](https://github.com/adm-iola/iola-cli/wiki/Облачные-диски).
190
190
 
191
- Yandex Connector объединяет пользовательские сервисы Яндекса в категории:
191
+ Yandex Connector при подключении запрашивает максимальный набор OAuth-прав для пользовательских сервисов Яндекса. Какие функции CLI реально использует, выбирается отдельно:
192
192
 
193
193
  ```bash
194
194
  iola yandex services
195
195
  iola yandex setup
196
+ iola yandex menu
196
197
  iola yandex status
197
198
  ```
198
199
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
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
@@ -348,6 +348,7 @@ const DEFAULT_AI_CONFIG = {
348
348
  },
349
349
  },
350
350
  yandex: {
351
+ authorizedServices: [],
351
352
  enabledServices: [],
352
353
  categories: {},
353
354
  oauth: {
@@ -440,6 +441,7 @@ const SLASH_COMMANDS = [
440
441
  { command: "/tools", description: "tools и toolsets" },
441
442
  { command: "/files status", description: "локальные файловые операции" },
442
443
  { command: "/cloud status", description: "облачные диски" },
444
+ { command: "/yandex", description: "выбор сервисов Yandex Connector" },
443
445
  { command: "/archive doctor", description: "архиватор" },
444
446
  { command: "/changes list", description: "подготовленные изменения" },
445
447
  { command: "/index status", description: "индекс документов" },
@@ -707,7 +709,7 @@ Usage:
707
709
  iola tools list|toolsets|enable|disable|profile
708
710
  iola files status|mode|approvals|tree|read|search|write|patch
709
711
  iola cloud setup|status|ls|find|upload|download|share|save|backup
710
- iola yandex setup|status|services|enable|disable|oauth-url|token
712
+ iola yandex setup|menu|status|services|enable|disable|oauth-url|token
711
713
  iola archive doctor|list|test|extract|create|index
712
714
  iola changes list|show|apply|discard
713
715
  iola import file|folder
@@ -1446,6 +1448,7 @@ async function handleAgentLine(line, state) {
1446
1448
  skills: ["skills", args],
1447
1449
  files: ["files", args],
1448
1450
  archive: ["archive", args],
1451
+ yandex: ["yandex", args.length ? args : ["menu"]],
1449
1452
  changes: ["changes", args],
1450
1453
  index: ["index", args],
1451
1454
  reports: ["reports", args],
@@ -2019,6 +2022,7 @@ async function doctor(args = []) {
2019
2022
  openrouterKey: process.env.OPENROUTER_API_KEY ? "env" : secrets.openrouter?.apiKey ? "local" : "missing",
2020
2023
  yandexGeocoderKey: (process.env.YANDEX_GEOCODER_API_KEY || process.env.YANDEX_MAPS_API_KEY) ? "env" : secrets.yandexGeocoder?.apiKey ? "local" : "missing",
2021
2024
  yandexConnector: (process.env.YANDEX_OAUTH_TOKEN || secrets.yandex?.oauthToken || secrets.cloud?.["yandex-disk"]?.token) ? "local/env" : "missing",
2025
+ yandexAuthorized: config.yandex?.authorizedServices?.join(", ") || "-",
2022
2026
  yandexServices: config.yandex?.enabledServices?.join(", ") || (secrets.cloud?.["yandex-disk"]?.token ? "disk (legacy cloud token)" : "-"),
2023
2027
  ollama: diagnostics.ollama.installed ? diagnostics.ollama.version : "not-installed",
2024
2028
  },
@@ -3200,9 +3204,14 @@ async function handleCloud(args) {
3200
3204
  }
3201
3205
 
3202
3206
  async function handleYandex(args) {
3203
- const [action = "status", target, ...rest] = args;
3207
+ const [action = process.stdin.isTTY ? "menu" : "status", target, ...rest] = args;
3204
3208
  const options = parseOptions(rest);
3205
3209
 
3210
+ if (action === "menu" || action === "choose" || action === "select") {
3211
+ await chooseYandexServicesMenu();
3212
+ return;
3213
+ }
3214
+
3206
3215
  if (action === "services" || action === "list") {
3207
3216
  printYandexServices();
3208
3217
  return;
@@ -3250,6 +3259,7 @@ async function handleYandex(args) {
3250
3259
 
3251
3260
  throw new Error(`Команды yandex:
3252
3261
  iola yandex setup
3262
+ iola yandex menu
3253
3263
  iola yandex status|doctor
3254
3264
  iola yandex services
3255
3265
  iola yandex enable disk mail calendar
@@ -3284,17 +3294,10 @@ function printYandexServices(options = {}) {
3284
3294
  async function setupYandexConnector(args = []) {
3285
3295
  const options = parseOptions(args);
3286
3296
  const config = await loadConfig();
3287
- let services = normalizeYandexServiceList(options._);
3288
-
3289
- if (process.stdin.isTTY && services.length === 0) {
3290
- console.log("Yandex Connector: выберите функции Яндекса.");
3291
- printYandexServices();
3292
- const answer = await askText("Сервисы через запятую [identity,disk]: ");
3293
- services = normalizeYandexServiceList(answer.trim() ? answer.split(/[,\s]+/) : ["identity", "disk"]);
3294
- }
3295
-
3296
- if (services.length === 0) services = ["identity", "disk"];
3297
- await saveYandexEnabledServices(services);
3297
+ const authorizedServices = getYandexOAuthCapableServiceIds();
3298
+ const enabledServices = config.yandex?.enabledServices?.length ? config.yandex.enabledServices : ["identity", "disk"];
3299
+ await saveYandexAuthorizedServices(authorizedServices);
3300
+ await saveYandexEnabledServices(enabledServices);
3298
3301
 
3299
3302
  const clientId = options["client-id"] || config.yandex?.oauth?.clientId || (process.stdin.isTTY ? (await askText("Yandex OAuth Client ID [Enter - пропустить]: ")).trim() : "");
3300
3303
  if (clientId) {
@@ -3307,9 +3310,11 @@ async function setupYandexConnector(args = []) {
3307
3310
  }
3308
3311
 
3309
3312
  console.log("Yandex Connector настроен.");
3310
- console.log(`Включены сервисы: ${services.join(", ")}`);
3313
+ console.log(`Запрошены максимальные OAuth-права: ${authorizedServices.join(", ")}`);
3314
+ console.log(`Активные функции CLI: ${normalizeYandexServiceList(enabledServices).join(", ")}`);
3315
+ console.log("Выбрать активные функции можно командой /yandex или iola yandex menu.");
3311
3316
  if (clientId) {
3312
- const url = buildYandexOAuthUrl({ clientId, services });
3317
+ const url = buildYandexOAuthUrl({ clientId, services: authorizedServices });
3313
3318
  console.log("Откройте ссылку авторизации, получите OAuth-токен и сохраните его командой: iola yandex token set");
3314
3319
  console.log(url);
3315
3320
  if (options.open) await openUrl(url);
@@ -3318,6 +3323,46 @@ async function setupYandexConnector(args = []) {
3318
3323
  }
3319
3324
  }
3320
3325
 
3326
+ async function chooseYandexServicesMenu() {
3327
+ if (!process.stdin.isTTY) {
3328
+ await printYandexConnectorStatus();
3329
+ return;
3330
+ }
3331
+ const config = await loadConfig();
3332
+ const serviceIds = Object.keys(YANDEX_CONNECTOR_SERVICES);
3333
+ const enabled = new Set(config.yandex?.enabledServices?.length ? config.yandex.enabledServices : ["identity", "disk"]);
3334
+ console.log("Yandex Connector: выберите сервисы.");
3335
+ serviceIds.forEach((id, index) => {
3336
+ const service = YANDEX_CONNECTOR_SERVICES[id];
3337
+ const marker = enabled.has(id) ? "✓" : " ";
3338
+ console.log(`${index + 1}. [${marker}] ${service.title} (${id}, ${service.status}) - ${service.hint}`);
3339
+ });
3340
+ console.log("0. Отмена");
3341
+ const defaults = serviceIds.map((id, index) => enabled.has(id) ? String(index + 1) : "").filter(Boolean);
3342
+ const answer = (await askText(`Номера через запятую [${defaults.join(",") || "1,2"}]: `)).trim();
3343
+ if (answer === "0") {
3344
+ console.log("Выбор сервисов отменен.");
3345
+ return;
3346
+ }
3347
+ const selectedNumbers = answer ? answer.split(/[,\s]+/).filter(Boolean) : (defaults.length ? defaults : ["1", "2"]);
3348
+ const selected = selectedNumbers.map((item) => {
3349
+ const index = Number(item) - 1;
3350
+ if (!Number.isInteger(index) || index < 0 || index >= serviceIds.length) {
3351
+ throw new Error(`Неизвестный номер сервиса: ${item}`);
3352
+ }
3353
+ return serviceIds[index];
3354
+ });
3355
+ await saveYandexEnabledServices(selected);
3356
+ console.log(`Включены сервисы: ${normalizeYandexServiceList(selected).join(", ")}`);
3357
+ const nextConfig = await loadConfig();
3358
+ if (nextConfig.yandex?.oauth?.clientId) {
3359
+ console.log("OAuth-ссылка с максимальными правами коннектора:");
3360
+ console.log(await buildYandexOAuthUrlFromConfig([]));
3361
+ } else {
3362
+ console.log("Для авторизации создайте OAuth Client ID и выполните: iola yandex oauth-url --client-id CLIENT_ID");
3363
+ }
3364
+ }
3365
+
3321
3366
  async function updateYandexEnabledServices(rawServices, enabled) {
3322
3367
  const config = await loadConfig();
3323
3368
  const current = new Set(config.yandex?.enabledServices || []);
@@ -3347,12 +3392,23 @@ async function saveYandexEnabledServices(services) {
3347
3392
  });
3348
3393
  }
3349
3394
 
3395
+ async function saveYandexAuthorizedServices(services) {
3396
+ const config = await loadConfig();
3397
+ const normalized = normalizeYandexServiceList(services.length ? services : getYandexOAuthCapableServiceIds());
3398
+ await saveConfig({
3399
+ yandex: {
3400
+ ...(config.yandex || {}),
3401
+ authorizedServices: normalized,
3402
+ },
3403
+ });
3404
+ }
3405
+
3350
3406
  async function buildYandexOAuthUrlFromConfig(rawArgs = []) {
3351
3407
  const options = parseOptions(rawArgs);
3352
3408
  const config = await loadConfig();
3353
3409
  const clientId = options["client-id"] || config.yandex?.oauth?.clientId;
3354
3410
  if (!clientId) throw new Error("Yandex OAuth Client ID не задан. Пример: iola yandex oauth-url disk --client-id CLIENT_ID");
3355
- const services = normalizeYandexServiceList(options._.length ? options._ : (config.yandex?.enabledServices || ["identity", "disk"]));
3411
+ const services = normalizeYandexServiceList(options._.length ? options._ : (config.yandex?.authorizedServices?.length ? config.yandex.authorizedServices : getYandexOAuthCapableServiceIds()));
3356
3412
  return buildYandexOAuthUrl({ clientId, services });
3357
3413
  }
3358
3414
 
@@ -3377,6 +3433,12 @@ function getYandexScopesForServices(services) {
3377
3433
  return [...scopes].join(" ");
3378
3434
  }
3379
3435
 
3436
+ function getYandexOAuthCapableServiceIds() {
3437
+ return Object.entries(YANDEX_CONNECTOR_SERVICES)
3438
+ .filter(([, service]) => service.scope)
3439
+ .map(([id]) => id);
3440
+ }
3441
+
3380
3442
  async function setYandexConnectorToken(args = []) {
3381
3443
  const options = parseOptions(args);
3382
3444
  const token = options.token || (process.stdin.isTTY ? (await askText("Yandex OAuth token: ")).trim() : "");
@@ -3406,6 +3468,7 @@ async function deleteYandexConnectorToken() {
3406
3468
  async function printYandexConnectorStatus(options = {}) {
3407
3469
  const [config, secrets] = await Promise.all([loadConfig(), loadSecrets()]);
3408
3470
  const enabled = config.yandex?.enabledServices || [];
3471
+ const authorized = config.yandex?.authorizedServices?.length ? config.yandex.authorizedServices : [];
3409
3472
  const legacyDiskToken = Boolean(secrets.cloud?.["yandex-disk"]?.token && !secrets.yandex?.oauthToken);
3410
3473
  const token = process.env.YANDEX_OAUTH_TOKEN || secrets.yandex?.oauthToken || secrets.cloud?.["yandex-disk"]?.token || "";
3411
3474
  const rows = Object.entries(YANDEX_CONNECTOR_SERVICES).map(([id, service]) => ({
@@ -3413,7 +3476,8 @@ async function printYandexConnectorStatus(options = {}) {
3413
3476
  enabled: enabled.includes(id) ? "yes" : (legacyDiskToken && id === "disk" ? "legacy" : "no"),
3414
3477
  category: service.category,
3415
3478
  status: service.status,
3416
- token: service.scope && (enabled.includes(id) || (legacyDiskToken && id === "disk")) ? (token ? "local/env" : "missing") : "-",
3479
+ token: service.scope && (authorized.includes(id) || enabled.includes(id) || (legacyDiskToken && id === "disk")) ? (token ? "local/env" : "missing") : "-",
3480
+ authorized: authorized.includes(id) ? "yes" : "-",
3417
3481
  title: service.title,
3418
3482
  }));
3419
3483
  printTable(rows, [
@@ -3421,6 +3485,7 @@ async function printYandexConnectorStatus(options = {}) {
3421
3485
  ["enabled", "Вкл"],
3422
3486
  ["category", "Категория"],
3423
3487
  ["status", "Статус"],
3488
+ ["authorized", "Права"],
3424
3489
  ["token", "Токен"],
3425
3490
  ["title", "Сервис"],
3426
3491
  ]);
@@ -13219,6 +13284,9 @@ function validateConfig(config) {
13219
13284
  for (const service of config.yandex?.enabledServices || []) {
13220
13285
  if (!YANDEX_CONNECTOR_SERVICES[service]) errors.push(`yandex.enabledServices содержит неизвестный сервис: ${service}`);
13221
13286
  }
13287
+ for (const service of config.yandex?.authorizedServices || []) {
13288
+ if (!YANDEX_CONNECTOR_SERVICES[service]) errors.push(`yandex.authorizedServices содержит неизвестный сервис: ${service}`);
13289
+ }
13222
13290
  return errors;
13223
13291
  }
13224
13292
 
@@ -57,10 +57,15 @@ assertIncludes(cliSource, "isOpenAiTextGenerationModel", "OpenAI model selection
57
57
  assertIncludes(cliSource, "dedupeDatedOpenAiModels", "OpenAI model selection should hide dated duplicates when aliases exist");
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
+ assertIncludes(cliSource, "chooseYandexServicesMenu", "Yandex Connector should have a service selection menu");
61
+ assertIncludes(cliSource, "Запрошены максимальные OAuth-права", "Yandex setup should request maximum connector permissions");
62
+ assertIncludes(cliSource, "Выбрать активные функции можно командой /yandex", "Yandex setup should direct service selection to /yandex");
63
+ assertNotIncludes(cliSource, "Сервисы через запятую [identity,disk]", "Yandex setup should not ask for services during connector setup");
60
64
 
61
65
  const commands = await runCli(["commands"]);
62
66
  assertIncludes(commands, "iola browser status|install|open|text|html|screenshot|pdf|click|type|eval", "commands");
63
67
  assertIncludes(commands, "iola mcp list|status|install|remove|serve [--stdio]", "commands");
68
+ assertIncludes(commands, "iola yandex setup|menu|status|services|enable|disable|oauth-url|token", "commands");
64
69
  assertIncludes(commands, "iola delete", "commands");
65
70
  assertNotIncludes(commands, "iola uninstall", "commands");
66
71
  assertNotIncludes(commands, "Госуслуг", "commands");
@@ -2,7 +2,7 @@
2
2
 
3
3
  `Yandex Connector` - единая точка подключения пользовательских сервисов Яндекса в `iola-cli`.
4
4
 
5
- Цель: пользователь один раз настраивает вход через Яндекс, а CLI хранит токен локально и включает только выбранные категории функций.
5
+ Цель: пользователь один раз настраивает вход через Яндекс с максимальным набором OAuth-прав, а CLI хранит токен локально. Какие функции CLI реально использует, пользователь выбирает отдельно через `/yandex`.
6
6
 
7
7
  Секреты сохраняются только на компьютере пользователя в `~/.iola/secrets.json`. Они не отправляются на сервер IOLA и не попадают в `iola cloud backup`.
8
8
 
@@ -11,6 +11,7 @@
11
11
  ```bash
12
12
  iola yandex services
13
13
  iola yandex setup
14
+ iola yandex menu
14
15
  iola yandex status
15
16
  iola yandex doctor
16
17
  iola yandex enable disk mail calendar
@@ -57,13 +58,13 @@ Backlog после первого контура:
57
58
  - `cloud_api:disk.read`;
58
59
  - `cloud_api:disk.write`;
59
60
  - `cloud_api:disk.info`.
60
- 3. Запустите:
61
+ 3. Запустите подключение. Оно не спрашивает список сервисов, а готовит OAuth-ссылку с максимальным набором прав коннектора:
61
62
 
62
63
  ```bash
63
- iola yandex setup disk --client-id CLIENT_ID
64
+ iola yandex setup --client-id CLIENT_ID
64
65
  ```
65
66
 
66
- 4. Откройте ссылку авторизации, которую выведет CLI.
67
+ 4. Откройте ссылку авторизации, которую выведет CLI. В ней будут запрошены максимальные права для поддерживаемых пользовательских сервисов Яндекса.
67
68
  5. Скопируйте OAuth-токен.
68
69
  6. Сохраните токен:
69
70
 
@@ -71,7 +72,19 @@ iola yandex setup disk --client-id CLIENT_ID
71
72
  iola yandex token set
72
73
  ```
73
74
 
74
- 7. Проверьте:
75
+ 7. Выберите, какие функции CLI реально использует:
76
+
77
+ ```bash
78
+ iola yandex menu
79
+ ```
80
+
81
+ В интерактивном CLI это же меню открывается slash-командой:
82
+
83
+ ```text
84
+ /yandex
85
+ ```
86
+
87
+ 8. Проверьте:
75
88
 
76
89
  ```bash
77
90
  iola yandex doctor
@@ -64,6 +64,7 @@ Yandex Connector:
64
64
  ```bash
65
65
  iola yandex services
66
66
  iola yandex setup
67
+ iola yandex menu
67
68
  iola yandex status
68
69
  iola yandex doctor
69
70
  iola yandex enable disk mail calendar
@@ -114,6 +114,8 @@ iola index folder ./docs
114
114
  - Yandex ID;
115
115
  - Яндекс Диск.
116
116
 
117
+ Мастер не спрашивает список сервисов. Он подключает коннектор с максимальным набором OAuth-прав, а активные функции CLI выбираются позже командой `/yandex` в интерактивном CLI или `iola yandex menu` в терминале.
118
+
117
119
  В коннектор также заложены категории для проверки: Почта, Календарь, Контакты, Wiki, Tracker, Forms, Документы/360. Такси, Маркет и Доставка записаны в backlog только как подготовка ссылки, маршрута или списка без заказа и оплаты.
118
120
 
119
121
  Инструкция: [Yandex Connector](Yandex-Connector).