@iola_adm/iola-cli 0.2.10 → 0.2.12

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,6 +188,18 @@ iola cloud backup
188
188
 
189
189
  Инструкция: [Облачные диски](https://github.com/adm-iola/iola-cli/wiki/Облачные-диски).
190
190
 
191
+ Yandex Connector объединяет пользовательские сервисы Яндекса в категории:
192
+
193
+ ```bash
194
+ iola yandex services
195
+ iola yandex setup
196
+ iola yandex status
197
+ ```
198
+
199
+ Первый контур: Yandex ID и Яндекс Диск. Почта, календарь, контакты, Wiki, Tracker, Forms и документы 360 заложены как категории для проверки. Такси, Маркет и Доставка записаны в backlog только как сценарии подготовки ссылки/маршрута/списка без заказа и оплаты.
200
+
201
+ Инструкция: [Yandex Connector](https://github.com/adm-iola/iola-cli/wiki/Yandex-Connector).
202
+
191
203
  Зарубежные API-ключи:
192
204
 
193
205
  - OpenAI Platform: регистрация `https://platform.openai.com/`, ключи `https://platform.openai.com/api-keys`;
@@ -215,6 +227,7 @@ iola version --check
215
227
  - [Мастер настройки](https://github.com/adm-iola/iola-cli/wiki/Мастер-настройки)
216
228
  - [AI-профили](https://github.com/adm-iola/iola-cli/wiki/AI-профили)
217
229
  - [Yandex Geocoder API key](https://github.com/adm-iola/iola-cli/wiki/Yandex-Geocoder-API-key)
230
+ - [Yandex Connector](https://github.com/adm-iola/iola-cli/wiki/Yandex-Connector)
218
231
  - [Облачные диски](https://github.com/adm-iola/iola-cli/wiki/Облачные-диски)
219
232
  - [Скиллы для жителей](https://github.com/adm-iola/iola-cli/wiki/Скиллы-для-жителей)
220
233
  - [Локальный инструментальный агент](https://github.com/adm-iola/iola-cli/wiki/Локальный-инструментальный-агент)
@@ -236,6 +249,7 @@ iola version --check
236
249
  - поиск и выгрузка открытых данных;
237
250
  - локальная SQLite-БД, история, сессии и FTS-поиск;
238
251
  - AI-профили для IOLA local, Ollama, YandexGPT, GigaChat, OpenAI, OpenRouter и Codex CLI;
252
+ - Yandex Connector: единая точка подключения пользовательских сервисов Яндекса с локальным хранением OAuth-токена;
239
253
  - локальный tool-agent для модели IOLA с tools `search_data`, `search_entities`, `resolve_entity_field`, `get_card`, `export_report`, `file_read`, `browser_open`;
240
254
  - ленивые skills, toolsets, permissions, memory, hooks и готовые agents;
241
255
  - личные облачные диски: Яндекс Диск и Облако Mail.ru для сохранения отчетов, backup и документов;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
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
@@ -40,6 +40,115 @@ const LOCAL_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "local.json");
40
40
  const BROWSER_RUNTIME_DIR = path.join(CONFIG_DIR, "browser-runtime");
41
41
  const BROWSER_RUNTIME_PACKAGE = path.join(BROWSER_RUNTIME_DIR, "node_modules", "playwright", "package.json");
42
42
  const CLOUD_DEFAULT_REMOTE_DIR = "/IOLA";
43
+ const YANDEX_OAUTH_AUTHORIZE_URL = "https://oauth.yandex.ru/authorize";
44
+ const YANDEX_OAUTH_REDIRECT_URL = "https://oauth.yandex.ru/verification_code";
45
+ const YANDEX_CONNECTOR_SERVICES = {
46
+ identity: {
47
+ title: "Yandex ID",
48
+ category: "identity",
49
+ scope: "login:info login:email",
50
+ status: "ready",
51
+ hint: "профиль, логин и email пользователя",
52
+ },
53
+ disk: {
54
+ title: "Яндекс Диск",
55
+ category: "cloud-storage",
56
+ scope: "cloud_api:disk.read cloud_api:disk.write cloud_api:disk.info",
57
+ status: "ready",
58
+ hint: "файлы, папка /IOLA, загрузка, скачивание, публичные ссылки",
59
+ },
60
+ mail: {
61
+ title: "Яндекс Почта",
62
+ category: "mail",
63
+ scope: "mail:imap_full mail:smtp",
64
+ status: "research",
65
+ hint: "чтение/поиск писем и отправка только после подтверждения",
66
+ },
67
+ calendar: {
68
+ title: "Яндекс Календарь",
69
+ category: "calendar",
70
+ scope: "calendar:all",
71
+ status: "research",
72
+ hint: "события и напоминания, протокол требует отдельной проверки",
73
+ },
74
+ contacts: {
75
+ title: "Яндекс Контакты",
76
+ category: "contacts",
77
+ scope: "carddav",
78
+ status: "research",
79
+ hint: "контакты через CardDAV/360, требует проверки",
80
+ },
81
+ wiki: {
82
+ title: "Yandex Wiki",
83
+ category: "workspace",
84
+ scope: "wiki:read wiki:write",
85
+ status: "research",
86
+ hint: "страницы wiki, больше полезно организациям",
87
+ },
88
+ tracker: {
89
+ title: "Yandex Tracker",
90
+ category: "workspace",
91
+ scope: "tracker:read tracker:write",
92
+ status: "research",
93
+ hint: "задачи и обращения, больше полезно организациям",
94
+ },
95
+ forms: {
96
+ title: "Yandex Forms",
97
+ category: "forms",
98
+ scope: "forms:read forms:write",
99
+ status: "research",
100
+ hint: "формы и опросы, API надо подтвердить",
101
+ },
102
+ docs: {
103
+ title: "Яндекс Документы / 360",
104
+ category: "documents",
105
+ scope: "cloud_api:disk.read cloud_api:disk.write",
106
+ status: "research",
107
+ hint: "обычно работает через файлы на Диске",
108
+ },
109
+ telemost: {
110
+ title: "Яндекс Телемост",
111
+ category: "meetings",
112
+ scope: "",
113
+ status: "research",
114
+ hint: "создание встреч через публичный API надо подтвердить",
115
+ },
116
+ cloud: {
117
+ title: "Yandex Cloud",
118
+ category: "cloud-platform",
119
+ scope: "",
120
+ status: "separate",
121
+ hint: "YandexGPT, Geocoder, SpeechKit, Vision, IAM и folder ID",
122
+ },
123
+ maps: {
124
+ title: "Яндекс Карты",
125
+ category: "maps",
126
+ scope: "",
127
+ status: "separate",
128
+ hint: "геокодер, маршруты и ссылки на карты через отдельный API key",
129
+ },
130
+ taxi: {
131
+ title: "Яндекс Go / Такси",
132
+ category: "mobility",
133
+ scope: "",
134
+ status: "backlog",
135
+ hint: "только подготовка маршрута/deep link, без заказа и оплаты",
136
+ },
137
+ market: {
138
+ title: "Яндекс Маркет",
139
+ category: "shopping",
140
+ scope: "",
141
+ status: "backlog",
142
+ hint: "только поиск и список покупок, без корзины и оплаты",
143
+ },
144
+ delivery: {
145
+ title: "Яндекс Доставка",
146
+ category: "delivery",
147
+ scope: "",
148
+ status: "backlog",
149
+ hint: "только подготовка заявки/ссылки, без оформления и оплаты",
150
+ },
151
+ };
43
152
  const INDEXABLE_EXTENSIONS = /\.(md|txt|csv|json|html|docx|xlsx|pptx|pdf)$/i;
44
153
  const LOCAL_TOOLS = ["search_data", "search_entities", "resolve_entity_field", "get_card", "export_report", "file_read", "browser_open", "get_current_date"];
45
154
  const LEGACY_LOCAL_TOOLS = ["search_local", "export_data", "run_report", "save_view"];
@@ -238,6 +347,14 @@ const DEFAULT_AI_CONFIG = {
238
347
  "mailru-cloud": { root: CLOUD_DEFAULT_REMOTE_DIR },
239
348
  },
240
349
  },
350
+ yandex: {
351
+ enabledServices: [],
352
+ categories: {},
353
+ oauth: {
354
+ clientId: "",
355
+ redirectUrl: YANDEX_OAUTH_REDIRECT_URL,
356
+ },
357
+ },
241
358
  daemon: {
242
359
  host: "127.0.0.1",
243
360
  port: DAEMON_PORT,
@@ -399,6 +516,7 @@ const COMMANDS = new Map([
399
516
  ["tools", handleTools],
400
517
  ["files", handleFiles],
401
518
  ["cloud", handleCloud],
519
+ ["yandex", handleYandex],
402
520
  ["archive", handleArchive],
403
521
  ["changes", handleChanges],
404
522
  ["import", handleImport],
@@ -548,6 +666,7 @@ async function showHelp() {
548
666
  iola ai setup настройка AI-профиля
549
667
  iola browser status браузерный runtime
550
668
  iola cloud status облачные диски
669
+ iola yandex status Yandex Connector
551
670
  iola mcp status MCP-подключение
552
671
  iola doctor диагностика
553
672
  iola wiki документация
@@ -588,6 +707,7 @@ Usage:
588
707
  iola tools list|toolsets|enable|disable|profile
589
708
  iola files status|mode|approvals|tree|read|search|write|patch
590
709
  iola cloud setup|status|ls|find|upload|download|share|save|backup
710
+ iola yandex setup|status|services|enable|disable|oauth-url|token
591
711
  iola archive doctor|list|test|extract|create|index
592
712
  iola changes list|show|apply|discard
593
713
  iola import file|folder
@@ -965,9 +1085,9 @@ async function startAgentRawInput() {
965
1085
  const shouldExit = await handleAgentLine(line, state);
966
1086
  stopActivity();
967
1087
  flushPendingAgentOutput(state);
968
- await refreshAgentAiStatus(state);
969
- if (!shouldExit) restoreRawInput();
970
1088
  if (shouldExit) break;
1089
+ await refreshAgentAiStatus(state);
1090
+ restoreRawInput();
971
1091
  } catch (error) {
972
1092
  stopActivity();
973
1093
  restoreRawInput();
@@ -1898,6 +2018,8 @@ async function doctor(args = []) {
1898
2018
  openaiKey: process.env.OPENAI_API_KEY ? "env" : secrets.openai?.apiKey ? "local" : "missing",
1899
2019
  openrouterKey: process.env.OPENROUTER_API_KEY ? "env" : secrets.openrouter?.apiKey ? "local" : "missing",
1900
2020
  yandexGeocoderKey: (process.env.YANDEX_GEOCODER_API_KEY || process.env.YANDEX_MAPS_API_KEY) ? "env" : secrets.yandexGeocoder?.apiKey ? "local" : "missing",
2021
+ yandexConnector: (process.env.YANDEX_OAUTH_TOKEN || secrets.yandex?.oauthToken || secrets.cloud?.["yandex-disk"]?.token) ? "local/env" : "missing",
2022
+ yandexServices: config.yandex?.enabledServices?.join(", ") || (secrets.cloud?.["yandex-disk"]?.token ? "disk (legacy cloud token)" : "-"),
1901
2023
  ollama: diagnostics.ollama.installed ? diagnostics.ollama.version : "not-installed",
1902
2024
  },
1903
2025
  skills: {
@@ -2330,14 +2452,42 @@ async function handleUninstall(args = []) {
2330
2452
 
2331
2453
  console.log("Локальные данные iola-cli удалены.");
2332
2454
  console.log(`Удаляю npm-пакет ${npmPackage}...`);
2333
- await runCommand(getNpmCommand(), ["remove", "-g", npmPackage], { inherit: true });
2334
- console.log("npm-пакет iola-cli удален.");
2455
+ const packageRemoval = await removeGlobalNpmPackage(npmPackage).catch((error) => ({
2456
+ status: "failed",
2457
+ error: error instanceof Error ? error.message : String(error),
2458
+ }));
2459
+ if (packageRemoval.status === "scheduled") {
2460
+ console.log("Удаление npm-пакета запланировано после выхода из CLI.");
2461
+ } else if (packageRemoval.status === "removed") {
2462
+ console.log("npm-пакет iola-cli удален.");
2463
+ } else {
2464
+ console.log(`Не удалось удалить npm-пакет автоматически: ${packageRemoval.error}`);
2465
+ console.log("Локальные данные уже удалены, CLI сейчас выйдет.");
2466
+ console.log("После выхода выполните вручную:");
2467
+ console.log(` npm remove -g ${npmPackage}`);
2468
+ }
2335
2469
  console.log("Codex CLI не тронут.");
2336
2470
  console.log("Для повторной установки:");
2337
2471
  console.log(" npm install -g @iola_adm/iola-cli@latest");
2338
2472
  return { deleted: true };
2339
2473
  }
2340
2474
 
2475
+ async function removeGlobalNpmPackage(npmPackage) {
2476
+ if (process.platform === "win32") {
2477
+ const command = quoteWindowsCommand(getNpmCommand(), ["remove", "-g", npmPackage]);
2478
+ const script = `ping 127.0.0.1 -n 3 > nul & ${command}`;
2479
+ const child = spawn(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", script], {
2480
+ detached: true,
2481
+ stdio: "ignore",
2482
+ windowsHide: true,
2483
+ });
2484
+ child.unref();
2485
+ return { status: "scheduled" };
2486
+ }
2487
+ await runCommand(getNpmCommand(), ["remove", "-g", npmPackage], { inherit: true });
2488
+ return { status: "removed" };
2489
+ }
2490
+
2341
2491
  async function handleDb(args) {
2342
2492
  const [action = "status"] = args;
2343
2493
  const options = parseOptions(args);
@@ -3022,6 +3172,296 @@ async function handleCloud(args) {
3022
3172
  iola cloud backup`);
3023
3173
  }
3024
3174
 
3175
+ async function handleYandex(args) {
3176
+ const [action = "status", target, ...rest] = args;
3177
+ const options = parseOptions(rest);
3178
+
3179
+ if (action === "services" || action === "list") {
3180
+ printYandexServices();
3181
+ return;
3182
+ }
3183
+
3184
+ if (action === "status" || action === "doctor") {
3185
+ await printYandexConnectorStatus({ check: action === "doctor" || options.check });
3186
+ return;
3187
+ }
3188
+
3189
+ if (action === "setup") {
3190
+ await setupYandexConnector([target, ...rest].filter(Boolean));
3191
+ return;
3192
+ }
3193
+
3194
+ if (action === "enable" || action === "disable") {
3195
+ const services = [target, ...rest].filter((item) => item && !String(item).startsWith("--"));
3196
+ if (services.length === 0) throw new Error("Укажите сервисы. Пример: iola yandex enable disk mail calendar");
3197
+ await updateYandexEnabledServices(services, action === "enable");
3198
+ return;
3199
+ }
3200
+
3201
+ if (action === "oauth-url" || action === "url") {
3202
+ const url = await buildYandexOAuthUrlFromConfig([target, ...rest].filter(Boolean));
3203
+ console.log(url);
3204
+ if (options.open) await openUrl(url);
3205
+ return;
3206
+ }
3207
+
3208
+ if (action === "token") {
3209
+ if (target === "set") {
3210
+ await setYandexConnectorToken(rest);
3211
+ return;
3212
+ }
3213
+ if (target === "delete") {
3214
+ await deleteYandexConnectorToken();
3215
+ return;
3216
+ }
3217
+ }
3218
+
3219
+ if (action === "backlog") {
3220
+ printYandexServices({ status: "backlog" });
3221
+ return;
3222
+ }
3223
+
3224
+ throw new Error(`Команды yandex:
3225
+ iola yandex setup
3226
+ iola yandex status|doctor
3227
+ iola yandex services
3228
+ iola yandex enable disk mail calendar
3229
+ iola yandex disable mail
3230
+ iola yandex oauth-url [disk mail calendar] [--client-id ID] [--open]
3231
+ iola yandex token set
3232
+ iola yandex token delete
3233
+ iola yandex backlog`);
3234
+ }
3235
+
3236
+ function printYandexServices(options = {}) {
3237
+ const rows = Object.entries(YANDEX_CONNECTOR_SERVICES)
3238
+ .filter(([, service]) => !options.status || service.status === options.status)
3239
+ .map(([id, service]) => ({
3240
+ id,
3241
+ title: service.title,
3242
+ category: service.category,
3243
+ status: service.status,
3244
+ scope: service.scope || "-",
3245
+ hint: service.hint,
3246
+ }));
3247
+ printTable(rows, [
3248
+ ["id", "ID"],
3249
+ ["title", "Сервис"],
3250
+ ["category", "Категория"],
3251
+ ["status", "Статус"],
3252
+ ["scope", "Scope"],
3253
+ ["hint", "Суть"],
3254
+ ]);
3255
+ }
3256
+
3257
+ async function setupYandexConnector(args = []) {
3258
+ const options = parseOptions(args);
3259
+ 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);
3271
+
3272
+ const clientId = options["client-id"] || config.yandex?.oauth?.clientId || (process.stdin.isTTY ? (await askText("Yandex OAuth Client ID [Enter - пропустить]: ")).trim() : "");
3273
+ if (clientId) {
3274
+ await saveConfig({
3275
+ yandex: {
3276
+ ...(config.yandex || {}),
3277
+ oauth: { ...(config.yandex?.oauth || {}), clientId, redirectUrl: YANDEX_OAUTH_REDIRECT_URL },
3278
+ },
3279
+ });
3280
+ }
3281
+
3282
+ console.log("Yandex Connector настроен.");
3283
+ console.log(`Включены сервисы: ${services.join(", ")}`);
3284
+ if (clientId) {
3285
+ const url = buildYandexOAuthUrl({ clientId, services });
3286
+ console.log("Откройте ссылку авторизации, получите OAuth-токен и сохраните его командой: iola yandex token set");
3287
+ console.log(url);
3288
+ if (options.open) await openUrl(url);
3289
+ } else {
3290
+ console.log("Client ID не задан. Создайте OAuth-приложение Яндекса и запустите: iola yandex oauth-url --client-id CLIENT_ID");
3291
+ }
3292
+ }
3293
+
3294
+ async function updateYandexEnabledServices(rawServices, enabled) {
3295
+ const config = await loadConfig();
3296
+ const current = new Set(config.yandex?.enabledServices || []);
3297
+ for (const service of normalizeYandexServiceList(rawServices)) {
3298
+ if (enabled) current.add(service);
3299
+ else current.delete(service);
3300
+ }
3301
+ await saveYandexEnabledServices([...current]);
3302
+ console.log(`Yandex services: ${[...current].join(", ") || "-"}`);
3303
+ }
3304
+
3305
+ async function saveYandexEnabledServices(services) {
3306
+ const config = await loadConfig();
3307
+ const normalized = normalizeYandexServiceList(services);
3308
+ if (normalized.length > 0 && !normalized.includes("identity")) normalized.unshift("identity");
3309
+ const categories = {};
3310
+ for (const id of normalized) {
3311
+ const meta = YANDEX_CONNECTOR_SERVICES[id];
3312
+ if (meta) categories[meta.category] = [...new Set([...(categories[meta.category] || []), id])];
3313
+ }
3314
+ await saveConfig({
3315
+ yandex: {
3316
+ ...(config.yandex || {}),
3317
+ enabledServices: normalized,
3318
+ categories,
3319
+ },
3320
+ });
3321
+ }
3322
+
3323
+ async function buildYandexOAuthUrlFromConfig(rawArgs = []) {
3324
+ const options = parseOptions(rawArgs);
3325
+ const config = await loadConfig();
3326
+ const clientId = options["client-id"] || config.yandex?.oauth?.clientId;
3327
+ 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"]));
3329
+ return buildYandexOAuthUrl({ clientId, services });
3330
+ }
3331
+
3332
+ function buildYandexOAuthUrl({ clientId, services }) {
3333
+ const scopes = getYandexScopesForServices(services);
3334
+ const url = new URL(YANDEX_OAUTH_AUTHORIZE_URL);
3335
+ url.searchParams.set("response_type", "token");
3336
+ url.searchParams.set("client_id", clientId);
3337
+ url.searchParams.set("redirect_uri", YANDEX_OAUTH_REDIRECT_URL);
3338
+ if (scopes) url.searchParams.set("scope", scopes);
3339
+ return url.toString();
3340
+ }
3341
+
3342
+ function getYandexScopesForServices(services) {
3343
+ const scopes = new Set();
3344
+ const normalized = normalizeYandexServiceList(services);
3345
+ if (normalized.length > 0 && !normalized.includes("identity")) normalized.unshift("identity");
3346
+ for (const id of normalized) {
3347
+ const raw = YANDEX_CONNECTOR_SERVICES[id]?.scope || "";
3348
+ for (const scope of raw.split(/\s+/).filter(Boolean)) scopes.add(scope);
3349
+ }
3350
+ return [...scopes].join(" ");
3351
+ }
3352
+
3353
+ async function setYandexConnectorToken(args = []) {
3354
+ const options = parseOptions(args);
3355
+ const token = options.token || (process.stdin.isTTY ? (await askText("Yandex OAuth token: ")).trim() : "");
3356
+ if (!token) throw new Error("OAuth token обязателен.");
3357
+ const secrets = await loadSecrets();
3358
+ secrets.yandex = secrets.yandex || {};
3359
+ secrets.yandex.oauthToken = token;
3360
+ secrets.yandex.updatedAt = new Date().toISOString();
3361
+ secrets.cloud = secrets.cloud || {};
3362
+ secrets.cloud["yandex-disk"] = { token };
3363
+ await saveSecrets(secrets);
3364
+ const config = await loadConfig();
3365
+ await saveConfig({ cloud: { ...(config.cloud || {}), activeProvider: "yandex-disk" } });
3366
+ console.log(`Yandex OAuth token сохранен локально: ${SECRETS_FILE}`);
3367
+ console.log("Токен также подключен к cloud provider yandex-disk.");
3368
+ }
3369
+
3370
+ async function deleteYandexConnectorToken() {
3371
+ const secrets = await loadSecrets();
3372
+ delete secrets.yandex;
3373
+ if (secrets.cloud?.["yandex-disk"]) delete secrets.cloud["yandex-disk"];
3374
+ if (secrets.cloud && Object.keys(secrets.cloud).length === 0) delete secrets.cloud;
3375
+ await saveSecrets(secrets);
3376
+ console.log("Yandex Connector token удален. Токен Яндекс Диска в cloud тоже удален.");
3377
+ }
3378
+
3379
+ async function printYandexConnectorStatus(options = {}) {
3380
+ const [config, secrets] = await Promise.all([loadConfig(), loadSecrets()]);
3381
+ const enabled = config.yandex?.enabledServices || [];
3382
+ const legacyDiskToken = Boolean(secrets.cloud?.["yandex-disk"]?.token && !secrets.yandex?.oauthToken);
3383
+ const token = process.env.YANDEX_OAUTH_TOKEN || secrets.yandex?.oauthToken || secrets.cloud?.["yandex-disk"]?.token || "";
3384
+ const rows = Object.entries(YANDEX_CONNECTOR_SERVICES).map(([id, service]) => ({
3385
+ id,
3386
+ enabled: enabled.includes(id) ? "yes" : (legacyDiskToken && id === "disk" ? "legacy" : "no"),
3387
+ category: service.category,
3388
+ status: service.status,
3389
+ token: service.scope && (enabled.includes(id) || (legacyDiskToken && id === "disk")) ? (token ? "local/env" : "missing") : "-",
3390
+ title: service.title,
3391
+ }));
3392
+ printTable(rows, [
3393
+ ["id", "ID"],
3394
+ ["enabled", "Вкл"],
3395
+ ["category", "Категория"],
3396
+ ["status", "Статус"],
3397
+ ["token", "Токен"],
3398
+ ["title", "Сервис"],
3399
+ ]);
3400
+ if (options.check && token) {
3401
+ const profile = await yandexUserInfo(token).catch((error) => ({ error: error instanceof Error ? error.message : String(error) }));
3402
+ console.log("");
3403
+ if (profile.error) console.log(`Yandex ID check: ${profile.error}`);
3404
+ else printKeyValue({
3405
+ login: profile.login || "-",
3406
+ displayName: profile.display_name || profile.real_name || "-",
3407
+ defaultEmail: profile.default_email || "-",
3408
+ });
3409
+ }
3410
+ }
3411
+
3412
+ async function yandexUserInfo(token) {
3413
+ const response = await fetch("https://login.yandex.ru/info?format=json", {
3414
+ headers: { Authorization: `OAuth ${token}` },
3415
+ });
3416
+ if (!response.ok) {
3417
+ const text = await response.text().catch(() => "");
3418
+ throw new Error(`Yandex ID недоступен: ${response.status} ${text.slice(0, 200)}`);
3419
+ }
3420
+ return response.json();
3421
+ }
3422
+
3423
+ function normalizeYandexServiceList(values) {
3424
+ const aliases = {
3425
+ id: "identity",
3426
+ login: "identity",
3427
+ "профиль": "identity",
3428
+ "диск": "disk",
3429
+ "яндекс-диск": "disk",
3430
+ yandexdisk: "disk",
3431
+ "yandex-disk": "disk",
3432
+ "почта": "mail",
3433
+ "календарь": "calendar",
3434
+ "контакты": "contacts",
3435
+ "вики": "wiki",
3436
+ "трекер": "tracker",
3437
+ "формы": "forms",
3438
+ "документы": "docs",
3439
+ "телемост": "telemost",
3440
+ "облако": "cloud",
3441
+ "карты": "maps",
3442
+ "такси": "taxi",
3443
+ "маркет": "market",
3444
+ "доставка": "delivery",
3445
+ all: "all",
3446
+ "все": "all",
3447
+ };
3448
+ const result = [];
3449
+ for (const raw of values.flatMap((item) => String(item || "").split(","))) {
3450
+ const normalized = raw.trim().toLocaleLowerCase("ru-RU");
3451
+ if (!normalized) continue;
3452
+ const id = aliases[normalized] || normalized;
3453
+ if (id === "all") {
3454
+ result.push(...Object.keys(YANDEX_CONNECTOR_SERVICES));
3455
+ continue;
3456
+ }
3457
+ if (!YANDEX_CONNECTOR_SERVICES[id]) {
3458
+ throw new Error(`Неизвестный сервис Яндекса: ${raw}. Список: iola yandex services`);
3459
+ }
3460
+ result.push(id);
3461
+ }
3462
+ return [...new Set(result)];
3463
+ }
3464
+
3025
3465
  async function setupCloudProvider(providerValue, options = {}) {
3026
3466
  const provider = normalizeCloudProvider(providerValue);
3027
3467
  if (!process.stdin.isTTY) throw new Error("Для настройки облака запустите команду в интерактивном терминале.");
@@ -10582,6 +11022,9 @@ async function onboard(args = []) {
10582
11022
  else console.log("Настройка облачного диска пропущена.");
10583
11023
  }
10584
11024
  }
11025
+ if (components.includes("yandex")) {
11026
+ await setupYandexConnector([]);
11027
+ }
10585
11028
  if (components.includes("codex")) {
10586
11029
  await installCodexIfMissing();
10587
11030
  await aiSetup(["codex"]);
@@ -10632,6 +11075,7 @@ async function chooseOnboardComponents(status = null) {
10632
11075
  13: "ollama",
10633
11076
  14: "yandex-geocoder",
10634
11077
  15: "cloud",
11078
+ 16: "yandex",
10635
11079
  };
10636
11080
  return [...selected].map((item) => map[item] || item).filter(Boolean);
10637
11081
  } finally {
@@ -10644,7 +11088,7 @@ function isOnboardExitAnswer(answer) {
10644
11088
  }
10645
11089
 
10646
11090
  async function getOnboardComponentStatus() {
10647
- const [config, readiness, browser, archive, codexVersion, ollamaVersion, yandexGeocoderKey, cloudSecrets] = await Promise.all([
11091
+ const [config, readiness, browser, archive, codexVersion, ollamaVersion, yandexGeocoderKey, secrets] = await Promise.all([
10648
11092
  loadConfig(),
10649
11093
  getAiReadiness(),
10650
11094
  getBrowserStatus(),
@@ -10652,8 +11096,9 @@ async function getOnboardComponentStatus() {
10652
11096
  getCommandVersion("codex", ["--version"]),
10653
11097
  getOllamaVersion(),
10654
11098
  getYandexGeocoderKey(),
10655
- loadSecrets().then((secrets) => secrets.cloud || {}),
11099
+ loadSecrets(),
10656
11100
  ]);
11101
+ const cloudSecrets = secrets.cloud || {};
10657
11102
  const workspaceReady = existsSync(PROJECT_CONTEXT_FILE) || existsSync(PROJECT_CONTEXT_DIR_FILE) || existsSync(PROJECT_IOLA_DIR);
10658
11103
  const policyReady = (config.toolsets?.enabled || []).includes("analyst");
10659
11104
  return {
@@ -10672,6 +11117,7 @@ async function getOnboardComponentStatus() {
10672
11117
  browser: browser.installed === "yes",
10673
11118
  "yandex-geocoder": Boolean(yandexGeocoderKey),
10674
11119
  cloud: Object.keys(cloudSecrets).length > 0,
11120
+ yandex: Boolean(secrets.yandex?.oauthToken || config.yandex?.enabledServices?.length),
10675
11121
  };
10676
11122
  }
10677
11123
 
@@ -10692,6 +11138,7 @@ function onboardComponentRows(status) {
10692
11138
  ["13", "ollama", "Ollama", "опциональный локальный runtime"],
10693
11139
  ["14", "yandex-geocoder", "Yandex Geocoder API", "ключ геокодера сохранен или есть в env"],
10694
11140
  ["15", "cloud", "Облачный диск", "Яндекс Диск или Облако Mail.ru"],
11141
+ ["16", "yandex", "Yandex Connector", "единый вход и категории сервисов Яндекса"],
10695
11142
  ];
10696
11143
  return rows.map(([number, key, title, hint]) => ({ number, key, title, hint, status: status[key] ? "готово" : "не настроено" }));
10697
11144
  }
@@ -10706,7 +11153,7 @@ function defaultOnboardSelection(status) {
10706
11153
  }
10707
11154
 
10708
11155
  function defaultOnboardComponents(status) {
10709
- const map = { 1: "workspace", 2: "policy", 3: "iola", 4: "yandexgpt", 5: "gigachat", 6: "openai", 7: "openrouter", 8: "codex", 9: "codex-mcp", 10: "archive", 11: "index", 12: "browser", 13: "ollama", 14: "yandex-geocoder", 15: "cloud" };
11156
+ const map = { 1: "workspace", 2: "policy", 3: "iola", 4: "yandexgpt", 5: "gigachat", 6: "openai", 7: "openrouter", 8: "codex", 9: "codex-mcp", 10: "archive", 11: "index", 12: "browser", 13: "ollama", 14: "yandex-geocoder", 15: "cloud", 16: "yandex" };
10710
11157
  return defaultOnboardSelection(status).map((item) => map[item]).filter(Boolean);
10711
11158
  }
10712
11159
 
@@ -10715,12 +11162,12 @@ function parseOptions(args) {
10715
11162
 
10716
11163
  for (let index = 0; index < args.length; index += 1) {
10717
11164
  const arg = args[index];
10718
- 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") {
11165
+ 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") {
10719
11166
  result[arg.slice(2)] = true;
10720
11167
  } else if (arg === "--check" || arg === "--upgrade-node") {
10721
11168
  result.check = true;
10722
11169
  result[arg.slice(2)] = true;
10723
- } 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") {
11170
+ } 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") {
10724
11171
  result[arg.slice(2)] = args[index + 1];
10725
11172
  index += 1;
10726
11173
  } else {
@@ -12618,6 +13065,18 @@ function mergeConfig(base, override) {
12618
13065
  ...(override.cloud?.providers || {}),
12619
13066
  },
12620
13067
  },
13068
+ yandex: {
13069
+ ...base.yandex,
13070
+ ...(override.yandex || {}),
13071
+ oauth: {
13072
+ ...(base.yandex?.oauth || {}),
13073
+ ...(override.yandex?.oauth || {}),
13074
+ },
13075
+ categories: {
13076
+ ...(base.yandex?.categories || {}),
13077
+ ...(override.yandex?.categories || {}),
13078
+ },
13079
+ },
12621
13080
  memory: {
12622
13081
  ...base.memory,
12623
13082
  ...(override.memory || {}),
@@ -12730,6 +13189,9 @@ function validateConfig(config) {
12730
13189
  if (config.cloud?.activeProvider && !["yandex-disk", "mailru-cloud"].includes(config.cloud.activeProvider)) {
12731
13190
  errors.push(`cloud.activeProvider неизвестен: ${config.cloud.activeProvider}`);
12732
13191
  }
13192
+ for (const service of config.yandex?.enabledServices || []) {
13193
+ if (!YANDEX_CONNECTOR_SERVICES[service]) errors.push(`yandex.enabledServices содержит неизвестный сервис: ${service}`);
13194
+ }
12733
13195
  return errors;
12734
13196
  }
12735
13197
 
@@ -12744,6 +13206,7 @@ function configSchema() {
12744
13206
  toolsets: { available: Object.keys(TOOLSETS) },
12745
13207
  files: { modes: ["locked", "read-only", "workspace-write", "full-access"], approvals: ["never", "on-write", "on-danger", "always"] },
12746
13208
  cloud: { providers: ["yandex-disk", "mailru-cloud"], root: CLOUD_DEFAULT_REMOTE_DIR },
13209
+ yandex: { services: Object.keys(YANDEX_CONNECTOR_SERVICES), statuses: ["ready", "research", "separate", "backlog"] },
12747
13210
  skills: { enabled: "array of skill names" },
12748
13211
  daemon: { host: "127.0.0.1", port: DAEMON_PORT },
12749
13212
  },
package/wiki/Home.md CHANGED
@@ -12,6 +12,7 @@
12
12
  - работа с локальной моделью Ollama;
13
13
  - работа с YandexGPT, GigaChat, OpenAI, OpenRouter и Codex CLI;
14
14
  - geo-сценарии для жителей через Yandex Geocoder API;
15
+ - Yandex Connector для пользовательских сервисов Яндекса;
15
16
  - личные облачные диски: Яндекс Диск и Облако Mail.ru;
16
17
  - подключение публичного MCP-сервера.
17
18
 
@@ -31,6 +32,7 @@ iola ask "найди школу 29"
31
32
  - [Мастер настройки](Мастер-настройки)
32
33
  - [AI-профили](AI-профили)
33
34
  - [Yandex Geocoder API key](Yandex-Geocoder-API-key)
35
+ - [Yandex Connector](Yandex-Connector)
34
36
  - [Облачные диски](Облачные-диски)
35
37
  - [Скиллы для жителей](Скиллы-для-жителей)
36
38
  - [Локальный инструментальный агент](Локальный-инструментальный-агент)
@@ -0,0 +1,89 @@
1
+ # Yandex Connector
2
+
3
+ `Yandex Connector` - единая точка подключения пользовательских сервисов Яндекса в `iola-cli`.
4
+
5
+ Цель: пользователь один раз настраивает вход через Яндекс, а CLI хранит токен локально и включает только выбранные категории функций.
6
+
7
+ Секреты сохраняются только на компьютере пользователя в `~/.iola/secrets.json`. Они не отправляются на сервер IOLA и не попадают в `iola cloud backup`.
8
+
9
+ ## Команды
10
+
11
+ ```bash
12
+ iola yandex services
13
+ iola yandex setup
14
+ iola yandex status
15
+ iola yandex doctor
16
+ iola yandex enable disk mail calendar
17
+ iola yandex disable mail
18
+ iola yandex oauth-url disk --client-id CLIENT_ID
19
+ iola yandex token set
20
+ iola yandex token delete
21
+ ```
22
+
23
+ ## Категории
24
+
25
+ Готово к первому контуру:
26
+
27
+ - `identity` - Yandex ID, профиль пользователя, логин и email;
28
+ - `disk` - Яндекс Диск, папка `/IOLA`, файлы, папки, загрузка, скачивание и публичные ссылки.
29
+
30
+ Исследуется:
31
+
32
+ - `mail` - Яндекс Почта, чтение и поиск писем, отправка только после явного подтверждения;
33
+ - `calendar` - Яндекс Календарь;
34
+ - `contacts` - Яндекс Контакты;
35
+ - `wiki` - Yandex Wiki;
36
+ - `tracker` - Yandex Tracker;
37
+ - `forms` - Yandex Forms;
38
+ - `docs` - Яндекс Документы / 360.
39
+
40
+ Отдельные ключи, не обычный OAuth бытового Яндекса:
41
+
42
+ - `cloud` - Yandex Cloud, YandexGPT, SpeechKit, Vision, IAM и folder ID;
43
+ - `maps` - Yandex Geocoder API и карты.
44
+
45
+ Backlog после первого контура:
46
+
47
+ - `taxi` - Яндекс Go / Такси: только подготовить маршрут и открыть приложение, без заказа и оплаты;
48
+ - `market` - Яндекс Маркет: поиск и список покупок, без корзины и оплаты;
49
+ - `delivery` - Яндекс Доставка: подготовка заявки/ссылки, без оформления и оплаты.
50
+
51
+ ## Как подключить
52
+
53
+ 1. Создайте OAuth-приложение Яндекса по инструкции на странице [Облачные диски](Облачные-диски).
54
+ 2. Включите нужные scope. Для первого контура нужны:
55
+ - `login:info`;
56
+ - `login:email`;
57
+ - `cloud_api:disk.read`;
58
+ - `cloud_api:disk.write`;
59
+ - `cloud_api:disk.info`.
60
+ 3. Запустите:
61
+
62
+ ```bash
63
+ iola yandex setup disk --client-id CLIENT_ID
64
+ ```
65
+
66
+ 4. Откройте ссылку авторизации, которую выведет CLI.
67
+ 5. Скопируйте OAuth-токен.
68
+ 6. Сохраните токен:
69
+
70
+ ```bash
71
+ iola yandex token set
72
+ ```
73
+
74
+ 7. Проверьте:
75
+
76
+ ```bash
77
+ iola yandex doctor
78
+ iola cloud doctor
79
+ ```
80
+
81
+ Если включен `disk`, токен автоматически подключается и к старому облачному провайдеру `yandex-disk`, поэтому команды `iola cloud ...` продолжают работать.
82
+
83
+ ## Важно
84
+
85
+ Yandex Connector не является универсальным ключом ко всему Яндексу.
86
+
87
+ Обычные пользовательские сервисы работают через OAuth и scope. Yandex Cloud, YandexGPT и Geocoder требуют отдельные ключи, folder ID или настройки в Yandex Cloud.
88
+
89
+ CLI не должен автоматически оформлять покупки, вызывать такси, подтверждать доставку или выполнять платежи. Для таких сценариев допустима только подготовка ссылки, маршрута или списка, а финальное действие делает пользователь в приложении Яндекса.
@@ -59,6 +59,20 @@ iola cloud save --text "Текст заметки" --path /IOLA/notes/note.txt
59
59
  iola cloud backup
60
60
  ```
61
61
 
62
+ Yandex Connector:
63
+
64
+ ```bash
65
+ iola yandex services
66
+ iola yandex setup
67
+ iola yandex status
68
+ iola yandex doctor
69
+ iola yandex enable disk mail calendar
70
+ iola yandex disable mail
71
+ iola yandex oauth-url disk --client-id CLIENT_ID
72
+ iola yandex token set
73
+ iola yandex token delete
74
+ ```
75
+
62
76
  Локальная БД:
63
77
 
64
78
  ```bash
@@ -22,9 +22,9 @@ iola master
22
22
 
23
23
  Включает профиль разрешений для аналитической работы: открытые данные, отчеты, безопасные локальные операции.
24
24
 
25
- ### 3. Ollama + локальная модель
25
+ ### 3. IOLA локальная модель
26
26
 
27
- Проверяет Ollama и локальные модели. Если Ollama не установлена, мастер может установить ее и предложить модель под возможности ПК.
27
+ Проверяет штатную локальную модель IOLA и готовит ее к работе через доступный runtime.
28
28
 
29
29
  Если модель уже скачана и доступна, пункт показывается как `готово`.
30
30
 
@@ -105,6 +105,19 @@ iola index folder ./docs
105
105
 
106
106
  Инструкция: [Облачные диски](Облачные-диски).
107
107
 
108
+ ### 16. Yandex Connector
109
+
110
+ Настраивает единый коннектор пользовательских сервисов Яндекса.
111
+
112
+ Первый контур:
113
+
114
+ - Yandex ID;
115
+ - Яндекс Диск.
116
+
117
+ В коннектор также заложены категории для проверки: Почта, Календарь, Контакты, Wiki, Tracker, Forms, Документы/360. Такси, Маркет и Доставка записаны в backlog только как подготовка ссылки, маршрута или списка без заказа и оплаты.
118
+
119
+ Инструкция: [Yandex Connector](Yandex-Connector).
120
+
108
121
  ## Повторный запуск
109
122
 
110
123
  Если компонент уже настроен, его можно не выбирать. Если нужно переустановить или обновить компонент, выберите его номер вручную.
@@ -31,6 +31,10 @@ iola cloud backup
31
31
 
32
32
  Яндекс Диск подключается через OAuth-токен пользователя.
33
33
 
34
+ Новый рекомендуемый путь - через [Yandex Connector](Yandex-Connector). Он сохраняет общий OAuth-токен Яндекса и автоматически подключает его к провайдеру `yandex-disk`.
35
+
36
+ Старый прямой способ `iola cloud setup yandex-disk` остается доступным.
37
+
34
38
  Официальная документация:
35
39
 
36
40
  - REST API Диска: `https://yandex.ru/dev/disk/rest?lang=ru`
@@ -146,3 +146,36 @@ iola geo services "Йошкар-Ола, улица Петрова, 15"
146
146
  ### cloud-organize
147
147
 
148
148
  Помочь разобрать личные документы: найти старые отчеты, крупные файлы, дубликаты и предложить структуру папок.
149
+
150
+ ## Yandex Connector backlog
151
+
152
+ Эти сценарии зафиксированы для следующего этапа после базового Yandex Connector. Они не должны оформлять заказ, списывать деньги или нажимать финальную кнопку вместо пользователя.
153
+
154
+ ### taxi-prepare-ride
155
+
156
+ Подготовить поездку в Яндекс Go:
157
+
158
+ - уточнить точку отправления и назначения;
159
+ - геокодировать адреса;
160
+ - сформировать ссылку/deep link в Яндекс Go;
161
+ - открыть приложение или страницу;
162
+ - пользователь сам проверяет цену и нажимает заказ.
163
+
164
+ ### market-prepare-cart
165
+
166
+ Подготовить список покупок для Яндекс Маркета:
167
+
168
+ - разобрать запрос пользователя;
169
+ - найти подходящие товары или поисковые ссылки;
170
+ - собрать список вариантов;
171
+ - сохранить список в CLI или на Диск;
172
+ - пользователь сам добавляет товары в корзину и оплачивает.
173
+
174
+ ### delivery-prepare-order
175
+
176
+ Подготовить доставку:
177
+
178
+ - уточнить адрес отправления и получения;
179
+ - сформировать ссылку или маршрут;
180
+ - открыть подходящий сервис Яндекса;
181
+ - пользователь сам подтверждает заявку и оплату.