@iola_adm/iola-cli 0.2.14 → 0.2.15

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,7 +188,7 @@ iola cloud backup
188
188
 
189
189
  Инструкция: [Облачные диски](https://github.com/adm-iola/iola-cli/wiki/Облачные-диски).
190
190
 
191
- Yandex Connector при подключении запрашивает максимальный набор OAuth-прав для пользовательских сервисов Яндекса. Какие функции CLI реально использует, выбирается отдельно:
191
+ Yandex Connector открывает браузер, авторизует пользователя в Яндексе и сохраняет OAuth-токен локально. Какие функции CLI реально использует, выбирается отдельно:
192
192
 
193
193
  ```bash
194
194
  iola yandex services
@@ -197,7 +197,7 @@ iola yandex menu
197
197
  iola yandex status
198
198
  ```
199
199
 
200
- Первый контур: Yandex ID и Яндекс Диск. Почта, календарь, контакты, Wiki, Tracker, Forms и документы 360 заложены как категории для проверки. Такси, Маркет и Доставка записаны в backlog только как сценарии подготовки ссылки/маршрута/списка без заказа и оплаты.
200
+ Первый контур: Yandex ID, Яндекс Диск и Яндекс Почта. Календарь, контакты, Wiki, Tracker, Forms и документы 360 заложены как категории для проверки и могут потребовать отдельное OAuth-приложение Яндекса. Такси, Маркет и Доставка записаны в backlog только как сценарии подготовки ссылки/маршрута/списка без заказа и оплаты.
201
201
 
202
202
  Инструкция: [Yandex Connector](https://github.com/adm-iola/iola-cli/wiki/Yandex-Connector).
203
203
 
@@ -1,10 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
3
  import { fileURLToPath } from "node:url";
4
- import { dirname, resolve } from "node:path";
4
+ import { copyFileSync, existsSync, mkdirSync } from "node:fs";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import os from "node:os";
5
7
 
6
8
  const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
7
9
  const cliPath = resolve(rootDir, "bin", "iola.js");
10
+ const oauthIconSource = resolve(rootDir, "docs", "assets", "iola-oauth-icon.png");
11
+ const oauthIconTarget = join(os.homedir(), ".iola", "assets", "iola-oauth-icon.png");
8
12
  const node = process.execPath;
9
13
  const frames = ["|", "/", "-", "\\"];
10
14
 
@@ -21,6 +25,10 @@ const steps = [
21
25
  title: "Проверка локальной модели IOLA",
22
26
  args: [cliPath, "ai", "setup", "iola", "--yes", "--quiet", "--optional", "--preserve-active"],
23
27
  },
28
+ {
29
+ title: "Установка иконки Yandex OAuth",
30
+ local: installOauthIcon,
31
+ },
24
32
  ];
25
33
 
26
34
  const canAnimate = process.stdout.isTTY && process.env.CI !== "true";
@@ -52,9 +60,11 @@ async function runStep(step, current, total) {
52
60
  }
53
61
  render();
54
62
  const timer = setInterval(render, 120);
55
- const result = await run(node, ["--no-warnings", ...step.args], (chunk) => {
56
- lastOutput = chunk.trim() || lastOutput;
57
- });
63
+ const result = step.local
64
+ ? await runLocalStep(step.local)
65
+ : await run(node, ["--no-warnings", ...step.args], (chunk) => {
66
+ lastOutput = chunk.trim() || lastOutput;
67
+ });
58
68
  clearInterval(timer);
59
69
 
60
70
  if (result.code !== 0) {
@@ -71,6 +81,21 @@ async function runStep(step, current, total) {
71
81
  }
72
82
  }
73
83
 
84
+ async function runLocalStep(fn) {
85
+ try {
86
+ await fn();
87
+ return { code: 0 };
88
+ } catch (error) {
89
+ return { code: 1, error };
90
+ }
91
+ }
92
+
93
+ function installOauthIcon() {
94
+ if (!existsSync(oauthIconSource)) return;
95
+ mkdirSync(dirname(oauthIconTarget), { recursive: true });
96
+ copyFileSync(oauthIconSource, oauthIconTarget);
97
+ }
98
+
74
99
  function run(command, args, onOutput) {
75
100
  return new Promise((resolvePromise) => {
76
101
  const child = spawn(command, args, {
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
4
4
  "description": "CLI и AI-агент городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
@@ -27,6 +27,7 @@
27
27
  "skills",
28
28
  "wiki",
29
29
  "docs/assets/readme-header.png",
30
+ "docs/assets/iola-oauth-icon.png",
30
31
  "README.md",
31
32
  "LICENSE"
32
33
  ],
package/src/cli.js CHANGED
@@ -42,6 +42,11 @@ 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 || "";
47
+ const YANDEX_CONNECTOR_REDIRECT_HOST = "127.0.0.1";
48
+ const YANDEX_CONNECTOR_REDIRECT_PORT = Number(process.env.IOLA_YANDEX_OAUTH_PORT || 18791);
49
+ const YANDEX_CONNECTOR_REDIRECT_PATH = "/yandex/oauth/callback";
45
50
  const YANDEX_CONNECTOR_SERVICES = {
46
51
  identity: {
47
52
  title: "Yandex ID",
@@ -149,6 +154,20 @@ const YANDEX_CONNECTOR_SERVICES = {
149
154
  hint: "только подготовка заявки/ссылки, без оформления и оплаты",
150
155
  },
151
156
  };
157
+ const YANDEX_CONNECTOR_OAUTH_APPS = [
158
+ {
159
+ id: "core",
160
+ title: "IOLA Yandex Core",
161
+ clientId: YANDEX_CONNECTOR_CLIENT_ID,
162
+ services: ["identity", "disk", "mail"],
163
+ },
164
+ {
165
+ id: "workspace",
166
+ title: "IOLA Yandex Workspace",
167
+ clientId: YANDEX_CONNECTOR_WORKSPACE_CLIENT_ID,
168
+ services: ["contacts", "wiki", "tracker", "forms", "docs"],
169
+ },
170
+ ];
152
171
  const INDEXABLE_EXTENSIONS = /\.(md|txt|csv|json|html|docx|xlsx|pptx|pdf)$/i;
153
172
  const LOCAL_TOOLS = ["search_data", "search_entities", "resolve_entity_field", "get_card", "export_report", "file_read", "browser_open", "get_current_date"];
154
173
  const LEGACY_LOCAL_TOOLS = ["search_local", "export_data", "run_report", "save_view"];
@@ -3294,32 +3313,46 @@ function printYandexServices(options = {}) {
3294
3313
  async function setupYandexConnector(args = []) {
3295
3314
  const options = parseOptions(args);
3296
3315
  const config = await loadConfig();
3316
+ const oauthApps = getConfiguredYandexOAuthApps();
3297
3317
  const authorizedServices = getYandexOAuthCapableServiceIds();
3298
3318
  const enabledServices = config.yandex?.enabledServices?.length ? config.yandex.enabledServices : ["identity", "disk"];
3299
3319
  await saveYandexAuthorizedServices(authorizedServices);
3300
3320
  await saveYandexEnabledServices(enabledServices);
3301
3321
 
3302
- const clientId = options["client-id"] || config.yandex?.oauth?.clientId || (process.stdin.isTTY ? (await askText("Yandex OAuth Client ID [Enter - пропустить]: ")).trim() : "");
3322
+ const clientId = options["client-id"] || config.yandex?.oauth?.clientId || oauthApps[0]?.clientId || "";
3323
+ const redirectUrl = options["redirect-url"] || getYandexConnectorRedirectUrl();
3303
3324
  if (clientId) {
3304
3325
  await saveConfig({
3305
3326
  yandex: {
3306
3327
  ...(config.yandex || {}),
3307
- oauth: { ...(config.yandex?.oauth || {}), clientId, redirectUrl: YANDEX_OAUTH_REDIRECT_URL },
3328
+ oauth: { ...(config.yandex?.oauth || {}), clientId, redirectUrl },
3308
3329
  },
3309
3330
  });
3310
3331
  }
3311
3332
 
3312
3333
  console.log("Yandex Connector настроен.");
3313
- console.log(`Запрошены максимальные OAuth-права: ${authorizedServices.join(", ")}`);
3334
+ console.log(`OAuth-права встроенного приложения: ${authorizedServices.join(", ")}`);
3314
3335
  console.log(`Активные функции CLI: ${normalizeYandexServiceList(enabledServices).join(", ")}`);
3315
3336
  console.log("Выбрать активные функции можно командой /yandex или iola yandex menu.");
3316
- if (clientId) {
3317
- const url = buildYandexOAuthUrl({ clientId, services: authorizedServices });
3318
- console.log("Откройте ссылку авторизации, получите OAuth-токен и сохраните его командой: iola yandex token set");
3319
- console.log(url);
3320
- if (options.open) await openUrl(url);
3337
+ if (clientId || oauthApps.length) {
3338
+ if (process.stdin.isTTY && !options["print-url"]) {
3339
+ console.log("Открываю браузер для входа в Яндекс. После авторизации токен сохранится автоматически.");
3340
+ for (const app of oauthApps.length ? oauthApps : [{ id: "custom", title: "Yandex Connector", clientId, services: authorizedServices }]) {
3341
+ console.log(`Авторизация: ${app.title}`);
3342
+ await runYandexBrowserOAuth({ appId: app.id, clientId: app.clientId, services: app.services, redirectUrl });
3343
+ }
3344
+ await printYandexConnectorStatus({ check: true });
3345
+ } 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);
3351
+ }
3321
3352
  } else {
3322
- console.log("Client ID не задан. Создайте OAuth-приложение Яндекса и запустите: iola yandex oauth-url --client-id CLIENT_ID");
3353
+ console.log("Yandex Connector не может открыть браузер: в этой сборке не задан public OAuth client_id приложения IOLA.");
3354
+ console.log("Нужно один раз зарегистрировать OAuth-приложение IOLA и задать IOLA_YANDEX_OAUTH_CLIENT_ID при сборке/запуске CLI.");
3355
+ console.log("Ручной fallback для разработки: iola yandex setup --client-id CLIENT_ID");
3323
3356
  }
3324
3357
  }
3325
3358
 
@@ -3406,22 +3439,118 @@ async function saveYandexAuthorizedServices(services) {
3406
3439
  async function buildYandexOAuthUrlFromConfig(rawArgs = []) {
3407
3440
  const options = parseOptions(rawArgs);
3408
3441
  const config = await loadConfig();
3409
- const clientId = options["client-id"] || config.yandex?.oauth?.clientId;
3442
+ const clientId = options["client-id"] || config.yandex?.oauth?.clientId || YANDEX_CONNECTOR_CLIENT_ID;
3410
3443
  if (!clientId) throw new Error("Yandex OAuth Client ID не задан. Пример: iola yandex oauth-url disk --client-id CLIENT_ID");
3411
3444
  const services = normalizeYandexServiceList(options._.length ? options._ : (config.yandex?.authorizedServices?.length ? config.yandex.authorizedServices : getYandexOAuthCapableServiceIds()));
3412
- return buildYandexOAuthUrl({ clientId, services });
3445
+ return buildYandexOAuthUrl({ clientId, services, redirectUrl: options["redirect-url"] || config.yandex?.oauth?.redirectUrl || YANDEX_OAUTH_REDIRECT_URL });
3413
3446
  }
3414
3447
 
3415
- function buildYandexOAuthUrl({ clientId, services }) {
3448
+ function buildYandexOAuthUrl({ clientId, services, redirectUrl = YANDEX_OAUTH_REDIRECT_URL }) {
3416
3449
  const scopes = getYandexScopesForServices(services);
3417
3450
  const url = new URL(YANDEX_OAUTH_AUTHORIZE_URL);
3418
3451
  url.searchParams.set("response_type", "token");
3419
3452
  url.searchParams.set("client_id", clientId);
3420
- url.searchParams.set("redirect_uri", YANDEX_OAUTH_REDIRECT_URL);
3453
+ url.searchParams.set("redirect_uri", redirectUrl);
3421
3454
  if (scopes) url.searchParams.set("scope", scopes);
3422
3455
  return url.toString();
3423
3456
  }
3424
3457
 
3458
+ function getYandexConnectorRedirectUrl() {
3459
+ return `http://${YANDEX_CONNECTOR_REDIRECT_HOST}:${YANDEX_CONNECTOR_REDIRECT_PORT}${YANDEX_CONNECTOR_REDIRECT_PATH}`;
3460
+ }
3461
+
3462
+ async function runYandexBrowserOAuth({ appId = "core", clientId, services, redirectUrl }) {
3463
+ const token = await waitForYandexOAuthToken({ clientId, services, redirectUrl });
3464
+ await setYandexConnectorToken(["--token", token, "--app", appId]);
3465
+ console.log("Yandex Connector подключен.");
3466
+ }
3467
+
3468
+ function waitForYandexOAuthToken({ clientId, services, redirectUrl }) {
3469
+ return new Promise((resolvePromise, reject) => {
3470
+ let settled = false;
3471
+ const timeoutMs = 180000;
3472
+ const server = createServer(async (req, res) => {
3473
+ try {
3474
+ const url = new URL(req.url || "/", redirectUrl);
3475
+ if (url.pathname === YANDEX_CONNECTOR_REDIRECT_PATH && req.method === "GET") {
3476
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
3477
+ res.end(`<!doctype html>
3478
+ <html lang="ru"><head><meta charset="utf-8"><title>IOLA Yandex Connector</title></head>
3479
+ <body>
3480
+ <p>Передаю токен в iola-cli...</p>
3481
+ <script>
3482
+ (async () => {
3483
+ const params = new URLSearchParams(location.hash.replace(/^#/, ""));
3484
+ const token = params.get("access_token");
3485
+ const error = params.get("error") || "";
3486
+ await fetch("/yandex/oauth/token", {
3487
+ method: "POST",
3488
+ headers: { "content-type": "application/json" },
3489
+ body: JSON.stringify({ token, error })
3490
+ });
3491
+ document.body.innerHTML = token
3492
+ ? "<p>Yandex Connector подключен. Можно закрыть вкладку и вернуться в терминал.</p>"
3493
+ : "<p>Не удалось получить токен. Вернитесь в терминал.</p>";
3494
+ })();
3495
+ </script>
3496
+ </body></html>`);
3497
+ return;
3498
+ }
3499
+ if (url.pathname === "/yandex/oauth/token" && req.method === "POST") {
3500
+ const chunks = [];
3501
+ for await (const chunk of req) chunks.push(chunk);
3502
+ const payload = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
3503
+ if (payload.error) throw new Error(`Yandex OAuth error: ${payload.error}`);
3504
+ if (!payload.token) throw new Error("Yandex OAuth token не получен.");
3505
+ if (!settled) {
3506
+ settled = true;
3507
+ clearTimeout(timer);
3508
+ resolvePromise(String(payload.token));
3509
+ }
3510
+ res.writeHead(200, { "content-type": "application/json" });
3511
+ res.end(JSON.stringify({ ok: true }));
3512
+ server.close();
3513
+ return;
3514
+ }
3515
+ res.writeHead(404);
3516
+ res.end("not found");
3517
+ } catch (error) {
3518
+ if (!settled) {
3519
+ settled = true;
3520
+ clearTimeout(timer);
3521
+ reject(error);
3522
+ }
3523
+ res.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
3524
+ res.end(error instanceof Error ? error.message : String(error));
3525
+ server.close();
3526
+ }
3527
+ });
3528
+ const timer = setTimeout(() => {
3529
+ if (!settled) {
3530
+ settled = true;
3531
+ server.close();
3532
+ reject(new Error("Время ожидания авторизации Яндекса истекло."));
3533
+ }
3534
+ }, timeoutMs);
3535
+ server.on("error", (error) => {
3536
+ clearTimeout(timer);
3537
+ if (!settled) {
3538
+ settled = true;
3539
+ reject(error);
3540
+ }
3541
+ });
3542
+ server.listen(YANDEX_CONNECTOR_REDIRECT_PORT, YANDEX_CONNECTOR_REDIRECT_HOST, async () => {
3543
+ const authUrl = buildYandexOAuthUrl({ clientId, services, redirectUrl });
3544
+ console.log(`Если браузер не открылся, откройте ссылку вручную: ${authUrl}`);
3545
+ try {
3546
+ await openUrl(authUrl);
3547
+ } catch (error) {
3548
+ console.log(`Не удалось открыть браузер автоматически: ${error instanceof Error ? error.message : String(error)}`);
3549
+ }
3550
+ });
3551
+ });
3552
+ }
3553
+
3425
3554
  function getYandexScopesForServices(services) {
3426
3555
  const scopes = new Set();
3427
3556
  const normalized = normalizeYandexServiceList(services);
@@ -3434,26 +3563,46 @@ function getYandexScopesForServices(services) {
3434
3563
  }
3435
3564
 
3436
3565
  function getYandexOAuthCapableServiceIds() {
3437
- return Object.entries(YANDEX_CONNECTOR_SERVICES)
3438
- .filter(([, service]) => service.scope)
3439
- .map(([id]) => id);
3566
+ return [...new Set(getConfiguredYandexOAuthApps().flatMap((app) => app.services))];
3567
+ }
3568
+
3569
+ function getConfiguredYandexOAuthApps() {
3570
+ return YANDEX_CONNECTOR_OAUTH_APPS
3571
+ .filter((app) => app.clientId)
3572
+ .map((app) => ({ ...app, services: normalizeYandexServiceList(app.services) }));
3573
+ }
3574
+
3575
+ function getYandexOAuthAppById(appId) {
3576
+ return getConfiguredYandexOAuthApps().find((app) => app.id === appId)
3577
+ || YANDEX_CONNECTOR_OAUTH_APPS.find((app) => app.id === appId)
3578
+ || null;
3440
3579
  }
3441
3580
 
3442
3581
  async function setYandexConnectorToken(args = []) {
3443
3582
  const options = parseOptions(args);
3583
+ const appId = options.app || "core";
3444
3584
  const token = options.token || (process.stdin.isTTY ? (await askText("Yandex OAuth token: ")).trim() : "");
3445
3585
  if (!token) throw new Error("OAuth token обязателен.");
3586
+ const app = getYandexOAuthAppById(appId);
3587
+ const appServices = normalizeYandexServiceList(app?.services || []);
3588
+ const hasDiskAccess = appServices.includes("disk") || appId === "core";
3446
3589
  const secrets = await loadSecrets();
3447
3590
  secrets.yandex = secrets.yandex || {};
3448
- secrets.yandex.oauthToken = token;
3591
+ secrets.yandex.oauthApps = secrets.yandex.oauthApps || {};
3592
+ secrets.yandex.oauthApps[appId] = { token, updatedAt: new Date().toISOString() };
3593
+ if (appId === "core") secrets.yandex.oauthToken = token;
3449
3594
  secrets.yandex.updatedAt = new Date().toISOString();
3450
- secrets.cloud = secrets.cloud || {};
3451
- secrets.cloud["yandex-disk"] = { token };
3595
+ if (hasDiskAccess) {
3596
+ secrets.cloud = secrets.cloud || {};
3597
+ secrets.cloud["yandex-disk"] = { token };
3598
+ }
3452
3599
  await saveSecrets(secrets);
3453
- const config = await loadConfig();
3454
- await saveConfig({ cloud: { ...(config.cloud || {}), activeProvider: "yandex-disk" } });
3600
+ if (hasDiskAccess) {
3601
+ const config = await loadConfig();
3602
+ await saveConfig({ cloud: { ...(config.cloud || {}), activeProvider: "yandex-disk" } });
3603
+ }
3455
3604
  console.log(`Yandex OAuth token сохранен локально: ${SECRETS_FILE}`);
3456
- console.log("Токен также подключен к cloud provider yandex-disk.");
3605
+ if (hasDiskAccess) console.log("Токен также подключен к cloud provider yandex-disk.");
3457
3606
  }
3458
3607
 
3459
3608
  async function deleteYandexConnectorToken() {
@@ -11254,12 +11403,12 @@ function parseOptions(args) {
11254
11403
 
11255
11404
  for (let index = 0; index < args.length; index += 1) {
11256
11405
  const arg = args[index];
11257
- if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || arg === "--system" || arg === "--headed" || arg === "--headless" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--full" || arg === "--unread" || arg === "--once" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--optional" || arg === "--project" || arg === "--dry-run" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--append" || arg === "--preserve-active" || arg === "--open") {
11406
+ if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || arg === "--system" || arg === "--headed" || arg === "--headless" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--full" || arg === "--unread" || arg === "--once" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--optional" || arg === "--project" || arg === "--dry-run" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--append" || arg === "--preserve-active" || arg === "--open" || arg === "--print-url") {
11258
11407
  result[arg.slice(2)] = true;
11259
11408
  } else if (arg === "--check" || arg === "--upgrade-node") {
11260
11409
  result.check = true;
11261
11410
  result[arg.slice(2)] = true;
11262
- } else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || arg === "--base-url" || arg === "--repo" || arg === "--model-dir" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--selector" || arg === "--url" || arg === "--timeout" || arg === "--wait" || arg === "--viewport" || arg === "--press" || arg === "--script" || arg === "--auth-url" || arg === "--token-url" || arg === "--userinfo-url" || arg === "--client-id" || arg === "--client-secret" || arg === "--redirect-host" || arg === "--redirect-port" || arg === "--redirect-path" || arg === "--debug-file" || arg === "--from" || arg === "--to" || arg === "--radius" || arg === "--address" || arg === "--token") {
11411
+ } else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || arg === "--base-url" || arg === "--repo" || arg === "--model-dir" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--selector" || arg === "--url" || arg === "--timeout" || arg === "--wait" || arg === "--viewport" || arg === "--press" || arg === "--script" || arg === "--auth-url" || arg === "--token-url" || arg === "--userinfo-url" || arg === "--client-id" || arg === "--client-secret" || arg === "--redirect-url" || arg === "--redirect-host" || arg === "--redirect-port" || arg === "--redirect-path" || arg === "--debug-file" || arg === "--from" || arg === "--to" || arg === "--radius" || arg === "--address" || arg === "--token" || arg === "--app") {
11263
11412
  result[arg.slice(2)] = args[index + 1];
11264
11413
  index += 1;
11265
11414
  } else {
@@ -58,9 +58,15 @@ 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, "Запрошены максимальные OAuth-права", "Yandex setup should request maximum connector permissions");
61
+ assertIncludes(cliSource, "OAuth-права встроенного приложения", "Yandex setup should report packaged OAuth app permissions");
62
62
  assertIncludes(cliSource, "Выбрать активные функции можно командой /yandex", "Yandex setup should direct service selection to /yandex");
63
+ assertIncludes(cliSource, "runYandexBrowserOAuth", "Yandex setup should support browser OAuth flow");
64
+ assertIncludes(cliSource, "IOLA_YANDEX_OAUTH_CLIENT_ID", "Yandex setup should use a packaged/env OAuth client id");
65
+ assertIncludes(cliSource, "--app", "Yandex token command should persist tokens by OAuth app group");
63
66
  assertNotIncludes(cliSource, "Сервисы через запятую [identity,disk]", "Yandex setup should not ask for services during connector setup");
67
+ if (!packageJson.files.includes("docs/assets/iola-oauth-icon.png")) {
68
+ throw new Error("package files should include the Yandex OAuth icon");
69
+ }
64
70
 
65
71
  const commands = await runCli(["commands"]);
66
72
  assertIncludes(commands, "iola browser status|install|open|text|html|screenshot|pdf|click|type|eval", "commands");
@@ -2,7 +2,7 @@
2
2
 
3
3
  `Yandex Connector` - единая точка подключения пользовательских сервисов Яндекса в `iola-cli`.
4
4
 
5
- Цель: пользователь один раз настраивает вход через Яндекс с максимальным набором OAuth-прав, а CLI хранит токен локально. Какие функции CLI реально использует, пользователь выбирает отдельно через `/yandex`.
5
+ Цель: пользователь подключает сервисы Яндекса через обычный вход в браузере, а CLI хранит OAuth-токены локально. Какие функции CLI реально использует, пользователь выбирает отдельно через `/yandex`.
6
6
 
7
7
  Секреты сохраняются только на компьютере пользователя в `~/.iola/secrets.json`. Они не отправляются на сервер IOLA и не попадают в `iola cloud backup`.
8
8
 
@@ -51,28 +51,29 @@ Backlog после первого контура:
51
51
 
52
52
  ## Как подключить
53
53
 
54
- 1. Создайте OAuth-приложение Яндекса по инструкции на странице [Облачные диски](Облачные-диски).
55
- 2. Включите нужные scope. Для первого контура нужны:
56
- - `login:info`;
57
- - `login:email`;
58
- - `cloud_api:disk.read`;
59
- - `cloud_api:disk.write`;
60
- - `cloud_api:disk.info`.
61
- 3. Запустите подключение. Оно не спрашивает список сервисов, а готовит OAuth-ссылку с максимальным набором прав коннектора:
54
+ 1. Запустите подключение. Оно не спрашивает список сервисов, а открывает браузер для входа в Яндекс:
62
55
 
63
56
  ```bash
64
- iola yandex setup --client-id CLIENT_ID
57
+ iola yandex setup
65
58
  ```
66
59
 
67
- 4. Откройте ссылку авторизации, которую выведет CLI. В ней будут запрошены максимальные права для поддерживаемых пользовательских сервисов Яндекса.
68
- 5. Скопируйте OAuth-токен.
69
- 6. Сохраните токен:
60
+ 2. Авторизуйтесь в Яндексе и разрешите доступ. В текущем встроенном приложении `IOLA Yandex Core` запрашиваются права:
61
+ - `login:info`;
62
+ - `login:email`;
63
+ - `cloud_api:disk.read`;
64
+ - `cloud_api:disk.write`;
65
+ - `cloud_api:disk.info`;
66
+ - `mail:imap_full`;
67
+ - `mail:smtp`.
68
+ 3. После успешной авторизации браузер вернется на локальную страницу `iola-cli`, а CLI сам сохранит OAuth-токен.
69
+ 4. Если автоматический браузерный flow недоступен, используйте fallback для разработки:
70
70
 
71
71
  ```bash
72
+ iola yandex setup --client-id CLIENT_ID --print-url
72
73
  iola yandex token set
73
74
  ```
74
75
 
75
- 7. Выберите, какие функции CLI реально использует:
76
+ 5. Выберите, какие функции CLI реально использует:
76
77
 
77
78
  ```bash
78
79
  iola yandex menu
@@ -84,7 +85,7 @@ iola yandex menu
84
85
  /yandex
85
86
  ```
86
87
 
87
- 8. Проверьте:
88
+ 6. Проверьте:
88
89
 
89
90
  ```bash
90
91
  iola yandex doctor
@@ -93,10 +94,30 @@ iola cloud doctor
93
94
 
94
95
  Если включен `disk`, токен автоматически подключается и к старому облачному провайдеру `yandex-disk`, поэтому команды `iola cloud ...` продолжают работать.
95
96
 
97
+ ## Иконка приложения
98
+
99
+ CLI поставляется с готовой иконкой OAuth-приложения. При установке она копируется в:
100
+
101
+ ```text
102
+ ~/.iola/assets/iola-oauth-icon.png
103
+ ```
104
+
105
+ В репозитории файл лежит здесь:
106
+
107
+ ```text
108
+ docs/assets/iola-oauth-icon.png
109
+ ```
110
+
111
+ Если создается новое OAuth-приложение Яндекса вручную, эту иконку можно указать в поле "Иконка сервиса".
112
+
96
113
  ## Важно
97
114
 
98
115
  Yandex Connector не является универсальным ключом ко всему Яндексу.
99
116
 
100
117
  Обычные пользовательские сервисы работают через OAuth и scope. Yandex Cloud, YandexGPT и Geocoder требуют отдельные ключи, folder ID или настройки в Yandex Cloud.
101
118
 
119
+ Для браузерного подключения в сборке CLI уже указан public OAuth `client_id` приложения IOLA. Это не секретный ключ. Пользователь не должен создавать OAuth-приложение вручную.
120
+
121
+ Яндекс ограничивает количество разных групп сервисов в одном OAuth-приложении. Поэтому один токен не всегда может покрыть Диск, Почту, Календарь, Контакты, Wiki, Tracker и Forms сразу. CLI поддерживает группировку по нескольким OAuth-приложениям; текущий первый контур - `identity`, `disk`, `mail`.
122
+
102
123
  CLI не должен автоматически оформлять покупки, вызывать такси, подтверждать доставку или выполнять платежи. Для таких сценариев допустима только подготовка ссылки, маршрута или списка, а финальное действие делает пользователь в приложении Яндекса.