@iola_adm/iola-cli 0.2.12 → 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.12",
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
  },
@@ -2398,6 +2402,7 @@ async function handleUninstall(args = []) {
2398
2402
  description: "локальная папка .iola текущего проекта",
2399
2403
  });
2400
2404
  }
2405
+ targets.push(...getIolaTempCleanupTargets());
2401
2406
  const npmPackage = "@iola_adm/iola-cli";
2402
2407
 
2403
2408
  const safeTargets = targets.map((target) => ({
@@ -2405,10 +2410,12 @@ async function handleUninstall(args = []) {
2405
2410
  path: path.resolve(target.path),
2406
2411
  }));
2407
2412
  const home = path.resolve(os.homedir());
2413
+ const temp = path.resolve(os.tmpdir());
2408
2414
  for (const target of safeTargets) {
2409
2415
  const isUserConfig = target.path === path.resolve(CONFIG_DIR) && target.path.startsWith(home);
2410
2416
  const isProjectConfig = target.path === path.resolve(PROJECT_IOLA_DIR) && target.path.startsWith(path.resolve(process.cwd()));
2411
- if (!isUserConfig && !isProjectConfig) {
2417
+ const isTemp = target.path.startsWith(temp + path.sep) && path.basename(target.path).startsWith("iola-");
2418
+ if (!isUserConfig && !isProjectConfig && !isTemp) {
2412
2419
  throw new Error(`Небезопасный путь удаления: ${target.path}`);
2413
2420
  }
2414
2421
  }
@@ -2472,10 +2479,34 @@ async function handleUninstall(args = []) {
2472
2479
  return { deleted: true };
2473
2480
  }
2474
2481
 
2482
+ function getIolaTempCleanupTargets() {
2483
+ const tempDir = os.tmpdir();
2484
+ const names = [
2485
+ "iola-cli-test",
2486
+ "iola-model-check.txt",
2487
+ ];
2488
+ let dynamicNames = [];
2489
+ try {
2490
+ dynamicNames = readdirSync(tempDir)
2491
+ .filter((name) => /^iola-(archive|codex|browser)-/u.test(name))
2492
+ .slice(0, 100);
2493
+ } catch {
2494
+ dynamicNames = [];
2495
+ }
2496
+ return [...new Set([...names, ...dynamicNames])].map((name) => ({
2497
+ label: "temp",
2498
+ path: path.join(tempDir, name),
2499
+ description: "временные файлы iola-cli",
2500
+ }));
2501
+ }
2502
+
2475
2503
  async function removeGlobalNpmPackage(npmPackage) {
2476
2504
  if (process.platform === "win32") {
2477
2505
  const command = quoteWindowsCommand(getNpmCommand(), ["remove", "-g", npmPackage]);
2478
- const script = `ping 127.0.0.1 -n 3 > nul & ${command}`;
2506
+ const cleanupConfig = quoteWindowsCommand("rmdir", ["/s", "/q", CONFIG_DIR]);
2507
+ const cleanupTempTest = quoteWindowsCommand("rmdir", ["/s", "/q", path.join(os.tmpdir(), "iola-cli-test")]);
2508
+ const cleanupTempModel = quoteWindowsCommand("del", ["/f", "/q", path.join(os.tmpdir(), "iola-model-check.txt")]);
2509
+ const script = `ping 127.0.0.1 -n 3 > nul & ${cleanupConfig} 2> nul & ${cleanupTempTest} 2> nul & ${cleanupTempModel} 2> nul & ${command}`;
2479
2510
  const child = spawn(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", script], {
2480
2511
  detached: true,
2481
2512
  stdio: "ignore",
@@ -3173,9 +3204,14 @@ async function handleCloud(args) {
3173
3204
  }
3174
3205
 
3175
3206
  async function handleYandex(args) {
3176
- const [action = "status", target, ...rest] = args;
3207
+ const [action = process.stdin.isTTY ? "menu" : "status", target, ...rest] = args;
3177
3208
  const options = parseOptions(rest);
3178
3209
 
3210
+ if (action === "menu" || action === "choose" || action === "select") {
3211
+ await chooseYandexServicesMenu();
3212
+ return;
3213
+ }
3214
+
3179
3215
  if (action === "services" || action === "list") {
3180
3216
  printYandexServices();
3181
3217
  return;
@@ -3223,6 +3259,7 @@ async function handleYandex(args) {
3223
3259
 
3224
3260
  throw new Error(`Команды yandex:
3225
3261
  iola yandex setup
3262
+ iola yandex menu
3226
3263
  iola yandex status|doctor
3227
3264
  iola yandex services
3228
3265
  iola yandex enable disk mail calendar
@@ -3257,17 +3294,10 @@ function printYandexServices(options = {}) {
3257
3294
  async function setupYandexConnector(args = []) {
3258
3295
  const options = parseOptions(args);
3259
3296
  const config = await loadConfig();
3260
- let services = normalizeYandexServiceList(options._);
3261
-
3262
- if (process.stdin.isTTY && services.length === 0) {
3263
- console.log("Yandex Connector: выберите функции Яндекса.");
3264
- printYandexServices();
3265
- const answer = await askText("Сервисы через запятую [identity,disk]: ");
3266
- services = normalizeYandexServiceList(answer.trim() ? answer.split(/[,\s]+/) : ["identity", "disk"]);
3267
- }
3268
-
3269
- if (services.length === 0) services = ["identity", "disk"];
3270
- 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);
3271
3301
 
3272
3302
  const clientId = options["client-id"] || config.yandex?.oauth?.clientId || (process.stdin.isTTY ? (await askText("Yandex OAuth Client ID [Enter - пропустить]: ")).trim() : "");
3273
3303
  if (clientId) {
@@ -3280,9 +3310,11 @@ async function setupYandexConnector(args = []) {
3280
3310
  }
3281
3311
 
3282
3312
  console.log("Yandex Connector настроен.");
3283
- console.log(`Включены сервисы: ${services.join(", ")}`);
3313
+ console.log(`Запрошены максимальные OAuth-права: ${authorizedServices.join(", ")}`);
3314
+ console.log(`Активные функции CLI: ${normalizeYandexServiceList(enabledServices).join(", ")}`);
3315
+ console.log("Выбрать активные функции можно командой /yandex или iola yandex menu.");
3284
3316
  if (clientId) {
3285
- const url = buildYandexOAuthUrl({ clientId, services });
3317
+ const url = buildYandexOAuthUrl({ clientId, services: authorizedServices });
3286
3318
  console.log("Откройте ссылку авторизации, получите OAuth-токен и сохраните его командой: iola yandex token set");
3287
3319
  console.log(url);
3288
3320
  if (options.open) await openUrl(url);
@@ -3291,6 +3323,46 @@ async function setupYandexConnector(args = []) {
3291
3323
  }
3292
3324
  }
3293
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
+
3294
3366
  async function updateYandexEnabledServices(rawServices, enabled) {
3295
3367
  const config = await loadConfig();
3296
3368
  const current = new Set(config.yandex?.enabledServices || []);
@@ -3320,12 +3392,23 @@ async function saveYandexEnabledServices(services) {
3320
3392
  });
3321
3393
  }
3322
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
+
3323
3406
  async function buildYandexOAuthUrlFromConfig(rawArgs = []) {
3324
3407
  const options = parseOptions(rawArgs);
3325
3408
  const config = await loadConfig();
3326
3409
  const clientId = options["client-id"] || config.yandex?.oauth?.clientId;
3327
3410
  if (!clientId) throw new Error("Yandex OAuth Client ID не задан. Пример: iola yandex oauth-url disk --client-id CLIENT_ID");
3328
- 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()));
3329
3412
  return buildYandexOAuthUrl({ clientId, services });
3330
3413
  }
3331
3414
 
@@ -3350,6 +3433,12 @@ function getYandexScopesForServices(services) {
3350
3433
  return [...scopes].join(" ");
3351
3434
  }
3352
3435
 
3436
+ function getYandexOAuthCapableServiceIds() {
3437
+ return Object.entries(YANDEX_CONNECTOR_SERVICES)
3438
+ .filter(([, service]) => service.scope)
3439
+ .map(([id]) => id);
3440
+ }
3441
+
3353
3442
  async function setYandexConnectorToken(args = []) {
3354
3443
  const options = parseOptions(args);
3355
3444
  const token = options.token || (process.stdin.isTTY ? (await askText("Yandex OAuth token: ")).trim() : "");
@@ -3379,6 +3468,7 @@ async function deleteYandexConnectorToken() {
3379
3468
  async function printYandexConnectorStatus(options = {}) {
3380
3469
  const [config, secrets] = await Promise.all([loadConfig(), loadSecrets()]);
3381
3470
  const enabled = config.yandex?.enabledServices || [];
3471
+ const authorized = config.yandex?.authorizedServices?.length ? config.yandex.authorizedServices : [];
3382
3472
  const legacyDiskToken = Boolean(secrets.cloud?.["yandex-disk"]?.token && !secrets.yandex?.oauthToken);
3383
3473
  const token = process.env.YANDEX_OAUTH_TOKEN || secrets.yandex?.oauthToken || secrets.cloud?.["yandex-disk"]?.token || "";
3384
3474
  const rows = Object.entries(YANDEX_CONNECTOR_SERVICES).map(([id, service]) => ({
@@ -3386,7 +3476,8 @@ async function printYandexConnectorStatus(options = {}) {
3386
3476
  enabled: enabled.includes(id) ? "yes" : (legacyDiskToken && id === "disk" ? "legacy" : "no"),
3387
3477
  category: service.category,
3388
3478
  status: service.status,
3389
- 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" : "-",
3390
3481
  title: service.title,
3391
3482
  }));
3392
3483
  printTable(rows, [
@@ -3394,6 +3485,7 @@ async function printYandexConnectorStatus(options = {}) {
3394
3485
  ["enabled", "Вкл"],
3395
3486
  ["category", "Категория"],
3396
3487
  ["status", "Статус"],
3488
+ ["authorized", "Права"],
3397
3489
  ["token", "Токен"],
3398
3490
  ["title", "Сервис"],
3399
3491
  ]);
@@ -13192,6 +13284,9 @@ function validateConfig(config) {
13192
13284
  for (const service of config.yandex?.enabledServices || []) {
13193
13285
  if (!YANDEX_CONNECTOR_SERVICES[service]) errors.push(`yandex.enabledServices содержит неизвестный сервис: ${service}`);
13194
13286
  }
13287
+ for (const service of config.yandex?.authorizedServices || []) {
13288
+ if (!YANDEX_CONNECTOR_SERVICES[service]) errors.push(`yandex.authorizedServices содержит неизвестный сервис: ${service}`);
13289
+ }
13195
13290
  return errors;
13196
13291
  }
13197
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).