@iola_adm/iola-cli 0.2.10 → 0.2.11

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.11",
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
@@ -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: {
@@ -3022,6 +3144,296 @@ async function handleCloud(args) {
3022
3144
  iola cloud backup`);
3023
3145
  }
3024
3146
 
3147
+ async function handleYandex(args) {
3148
+ const [action = "status", target, ...rest] = args;
3149
+ const options = parseOptions(rest);
3150
+
3151
+ if (action === "services" || action === "list") {
3152
+ printYandexServices();
3153
+ return;
3154
+ }
3155
+
3156
+ if (action === "status" || action === "doctor") {
3157
+ await printYandexConnectorStatus({ check: action === "doctor" || options.check });
3158
+ return;
3159
+ }
3160
+
3161
+ if (action === "setup") {
3162
+ await setupYandexConnector([target, ...rest].filter(Boolean));
3163
+ return;
3164
+ }
3165
+
3166
+ if (action === "enable" || action === "disable") {
3167
+ const services = [target, ...rest].filter((item) => item && !String(item).startsWith("--"));
3168
+ if (services.length === 0) throw new Error("Укажите сервисы. Пример: iola yandex enable disk mail calendar");
3169
+ await updateYandexEnabledServices(services, action === "enable");
3170
+ return;
3171
+ }
3172
+
3173
+ if (action === "oauth-url" || action === "url") {
3174
+ const url = await buildYandexOAuthUrlFromConfig([target, ...rest].filter(Boolean));
3175
+ console.log(url);
3176
+ if (options.open) await openUrl(url);
3177
+ return;
3178
+ }
3179
+
3180
+ if (action === "token") {
3181
+ if (target === "set") {
3182
+ await setYandexConnectorToken(rest);
3183
+ return;
3184
+ }
3185
+ if (target === "delete") {
3186
+ await deleteYandexConnectorToken();
3187
+ return;
3188
+ }
3189
+ }
3190
+
3191
+ if (action === "backlog") {
3192
+ printYandexServices({ status: "backlog" });
3193
+ return;
3194
+ }
3195
+
3196
+ throw new Error(`Команды yandex:
3197
+ iola yandex setup
3198
+ iola yandex status|doctor
3199
+ iola yandex services
3200
+ iola yandex enable disk mail calendar
3201
+ iola yandex disable mail
3202
+ iola yandex oauth-url [disk mail calendar] [--client-id ID] [--open]
3203
+ iola yandex token set
3204
+ iola yandex token delete
3205
+ iola yandex backlog`);
3206
+ }
3207
+
3208
+ function printYandexServices(options = {}) {
3209
+ const rows = Object.entries(YANDEX_CONNECTOR_SERVICES)
3210
+ .filter(([, service]) => !options.status || service.status === options.status)
3211
+ .map(([id, service]) => ({
3212
+ id,
3213
+ title: service.title,
3214
+ category: service.category,
3215
+ status: service.status,
3216
+ scope: service.scope || "-",
3217
+ hint: service.hint,
3218
+ }));
3219
+ printTable(rows, [
3220
+ ["id", "ID"],
3221
+ ["title", "Сервис"],
3222
+ ["category", "Категория"],
3223
+ ["status", "Статус"],
3224
+ ["scope", "Scope"],
3225
+ ["hint", "Суть"],
3226
+ ]);
3227
+ }
3228
+
3229
+ async function setupYandexConnector(args = []) {
3230
+ const options = parseOptions(args);
3231
+ const config = await loadConfig();
3232
+ let services = normalizeYandexServiceList(options._);
3233
+
3234
+ if (process.stdin.isTTY && services.length === 0) {
3235
+ console.log("Yandex Connector: выберите функции Яндекса.");
3236
+ printYandexServices();
3237
+ const answer = await askText("Сервисы через запятую [identity,disk]: ");
3238
+ services = normalizeYandexServiceList(answer.trim() ? answer.split(/[,\s]+/) : ["identity", "disk"]);
3239
+ }
3240
+
3241
+ if (services.length === 0) services = ["identity", "disk"];
3242
+ await saveYandexEnabledServices(services);
3243
+
3244
+ const clientId = options["client-id"] || config.yandex?.oauth?.clientId || (process.stdin.isTTY ? (await askText("Yandex OAuth Client ID [Enter - пропустить]: ")).trim() : "");
3245
+ if (clientId) {
3246
+ await saveConfig({
3247
+ yandex: {
3248
+ ...(config.yandex || {}),
3249
+ oauth: { ...(config.yandex?.oauth || {}), clientId, redirectUrl: YANDEX_OAUTH_REDIRECT_URL },
3250
+ },
3251
+ });
3252
+ }
3253
+
3254
+ console.log("Yandex Connector настроен.");
3255
+ console.log(`Включены сервисы: ${services.join(", ")}`);
3256
+ if (clientId) {
3257
+ const url = buildYandexOAuthUrl({ clientId, services });
3258
+ console.log("Откройте ссылку авторизации, получите OAuth-токен и сохраните его командой: iola yandex token set");
3259
+ console.log(url);
3260
+ if (options.open) await openUrl(url);
3261
+ } else {
3262
+ console.log("Client ID не задан. Создайте OAuth-приложение Яндекса и запустите: iola yandex oauth-url --client-id CLIENT_ID");
3263
+ }
3264
+ }
3265
+
3266
+ async function updateYandexEnabledServices(rawServices, enabled) {
3267
+ const config = await loadConfig();
3268
+ const current = new Set(config.yandex?.enabledServices || []);
3269
+ for (const service of normalizeYandexServiceList(rawServices)) {
3270
+ if (enabled) current.add(service);
3271
+ else current.delete(service);
3272
+ }
3273
+ await saveYandexEnabledServices([...current]);
3274
+ console.log(`Yandex services: ${[...current].join(", ") || "-"}`);
3275
+ }
3276
+
3277
+ async function saveYandexEnabledServices(services) {
3278
+ const config = await loadConfig();
3279
+ const normalized = normalizeYandexServiceList(services);
3280
+ if (normalized.length > 0 && !normalized.includes("identity")) normalized.unshift("identity");
3281
+ const categories = {};
3282
+ for (const id of normalized) {
3283
+ const meta = YANDEX_CONNECTOR_SERVICES[id];
3284
+ if (meta) categories[meta.category] = [...new Set([...(categories[meta.category] || []), id])];
3285
+ }
3286
+ await saveConfig({
3287
+ yandex: {
3288
+ ...(config.yandex || {}),
3289
+ enabledServices: normalized,
3290
+ categories,
3291
+ },
3292
+ });
3293
+ }
3294
+
3295
+ async function buildYandexOAuthUrlFromConfig(rawArgs = []) {
3296
+ const options = parseOptions(rawArgs);
3297
+ const config = await loadConfig();
3298
+ const clientId = options["client-id"] || config.yandex?.oauth?.clientId;
3299
+ if (!clientId) throw new Error("Yandex OAuth Client ID не задан. Пример: iola yandex oauth-url disk --client-id CLIENT_ID");
3300
+ const services = normalizeYandexServiceList(options._.length ? options._ : (config.yandex?.enabledServices || ["identity", "disk"]));
3301
+ return buildYandexOAuthUrl({ clientId, services });
3302
+ }
3303
+
3304
+ function buildYandexOAuthUrl({ clientId, services }) {
3305
+ const scopes = getYandexScopesForServices(services);
3306
+ const url = new URL(YANDEX_OAUTH_AUTHORIZE_URL);
3307
+ url.searchParams.set("response_type", "token");
3308
+ url.searchParams.set("client_id", clientId);
3309
+ url.searchParams.set("redirect_uri", YANDEX_OAUTH_REDIRECT_URL);
3310
+ if (scopes) url.searchParams.set("scope", scopes);
3311
+ return url.toString();
3312
+ }
3313
+
3314
+ function getYandexScopesForServices(services) {
3315
+ const scopes = new Set();
3316
+ const normalized = normalizeYandexServiceList(services);
3317
+ if (normalized.length > 0 && !normalized.includes("identity")) normalized.unshift("identity");
3318
+ for (const id of normalized) {
3319
+ const raw = YANDEX_CONNECTOR_SERVICES[id]?.scope || "";
3320
+ for (const scope of raw.split(/\s+/).filter(Boolean)) scopes.add(scope);
3321
+ }
3322
+ return [...scopes].join(" ");
3323
+ }
3324
+
3325
+ async function setYandexConnectorToken(args = []) {
3326
+ const options = parseOptions(args);
3327
+ const token = options.token || (process.stdin.isTTY ? (await askText("Yandex OAuth token: ")).trim() : "");
3328
+ if (!token) throw new Error("OAuth token обязателен.");
3329
+ const secrets = await loadSecrets();
3330
+ secrets.yandex = secrets.yandex || {};
3331
+ secrets.yandex.oauthToken = token;
3332
+ secrets.yandex.updatedAt = new Date().toISOString();
3333
+ secrets.cloud = secrets.cloud || {};
3334
+ secrets.cloud["yandex-disk"] = { token };
3335
+ await saveSecrets(secrets);
3336
+ const config = await loadConfig();
3337
+ await saveConfig({ cloud: { ...(config.cloud || {}), activeProvider: "yandex-disk" } });
3338
+ console.log(`Yandex OAuth token сохранен локально: ${SECRETS_FILE}`);
3339
+ console.log("Токен также подключен к cloud provider yandex-disk.");
3340
+ }
3341
+
3342
+ async function deleteYandexConnectorToken() {
3343
+ const secrets = await loadSecrets();
3344
+ delete secrets.yandex;
3345
+ if (secrets.cloud?.["yandex-disk"]) delete secrets.cloud["yandex-disk"];
3346
+ if (secrets.cloud && Object.keys(secrets.cloud).length === 0) delete secrets.cloud;
3347
+ await saveSecrets(secrets);
3348
+ console.log("Yandex Connector token удален. Токен Яндекс Диска в cloud тоже удален.");
3349
+ }
3350
+
3351
+ async function printYandexConnectorStatus(options = {}) {
3352
+ const [config, secrets] = await Promise.all([loadConfig(), loadSecrets()]);
3353
+ const enabled = config.yandex?.enabledServices || [];
3354
+ const legacyDiskToken = Boolean(secrets.cloud?.["yandex-disk"]?.token && !secrets.yandex?.oauthToken);
3355
+ const token = process.env.YANDEX_OAUTH_TOKEN || secrets.yandex?.oauthToken || secrets.cloud?.["yandex-disk"]?.token || "";
3356
+ const rows = Object.entries(YANDEX_CONNECTOR_SERVICES).map(([id, service]) => ({
3357
+ id,
3358
+ enabled: enabled.includes(id) ? "yes" : (legacyDiskToken && id === "disk" ? "legacy" : "no"),
3359
+ category: service.category,
3360
+ status: service.status,
3361
+ token: service.scope && (enabled.includes(id) || (legacyDiskToken && id === "disk")) ? (token ? "local/env" : "missing") : "-",
3362
+ title: service.title,
3363
+ }));
3364
+ printTable(rows, [
3365
+ ["id", "ID"],
3366
+ ["enabled", "Вкл"],
3367
+ ["category", "Категория"],
3368
+ ["status", "Статус"],
3369
+ ["token", "Токен"],
3370
+ ["title", "Сервис"],
3371
+ ]);
3372
+ if (options.check && token) {
3373
+ const profile = await yandexUserInfo(token).catch((error) => ({ error: error instanceof Error ? error.message : String(error) }));
3374
+ console.log("");
3375
+ if (profile.error) console.log(`Yandex ID check: ${profile.error}`);
3376
+ else printKeyValue({
3377
+ login: profile.login || "-",
3378
+ displayName: profile.display_name || profile.real_name || "-",
3379
+ defaultEmail: profile.default_email || "-",
3380
+ });
3381
+ }
3382
+ }
3383
+
3384
+ async function yandexUserInfo(token) {
3385
+ const response = await fetch("https://login.yandex.ru/info?format=json", {
3386
+ headers: { Authorization: `OAuth ${token}` },
3387
+ });
3388
+ if (!response.ok) {
3389
+ const text = await response.text().catch(() => "");
3390
+ throw new Error(`Yandex ID недоступен: ${response.status} ${text.slice(0, 200)}`);
3391
+ }
3392
+ return response.json();
3393
+ }
3394
+
3395
+ function normalizeYandexServiceList(values) {
3396
+ const aliases = {
3397
+ id: "identity",
3398
+ login: "identity",
3399
+ "профиль": "identity",
3400
+ "диск": "disk",
3401
+ "яндекс-диск": "disk",
3402
+ yandexdisk: "disk",
3403
+ "yandex-disk": "disk",
3404
+ "почта": "mail",
3405
+ "календарь": "calendar",
3406
+ "контакты": "contacts",
3407
+ "вики": "wiki",
3408
+ "трекер": "tracker",
3409
+ "формы": "forms",
3410
+ "документы": "docs",
3411
+ "телемост": "telemost",
3412
+ "облако": "cloud",
3413
+ "карты": "maps",
3414
+ "такси": "taxi",
3415
+ "маркет": "market",
3416
+ "доставка": "delivery",
3417
+ all: "all",
3418
+ "все": "all",
3419
+ };
3420
+ const result = [];
3421
+ for (const raw of values.flatMap((item) => String(item || "").split(","))) {
3422
+ const normalized = raw.trim().toLocaleLowerCase("ru-RU");
3423
+ if (!normalized) continue;
3424
+ const id = aliases[normalized] || normalized;
3425
+ if (id === "all") {
3426
+ result.push(...Object.keys(YANDEX_CONNECTOR_SERVICES));
3427
+ continue;
3428
+ }
3429
+ if (!YANDEX_CONNECTOR_SERVICES[id]) {
3430
+ throw new Error(`Неизвестный сервис Яндекса: ${raw}. Список: iola yandex services`);
3431
+ }
3432
+ result.push(id);
3433
+ }
3434
+ return [...new Set(result)];
3435
+ }
3436
+
3025
3437
  async function setupCloudProvider(providerValue, options = {}) {
3026
3438
  const provider = normalizeCloudProvider(providerValue);
3027
3439
  if (!process.stdin.isTTY) throw new Error("Для настройки облака запустите команду в интерактивном терминале.");
@@ -10582,6 +10994,9 @@ async function onboard(args = []) {
10582
10994
  else console.log("Настройка облачного диска пропущена.");
10583
10995
  }
10584
10996
  }
10997
+ if (components.includes("yandex")) {
10998
+ await setupYandexConnector([]);
10999
+ }
10585
11000
  if (components.includes("codex")) {
10586
11001
  await installCodexIfMissing();
10587
11002
  await aiSetup(["codex"]);
@@ -10632,6 +11047,7 @@ async function chooseOnboardComponents(status = null) {
10632
11047
  13: "ollama",
10633
11048
  14: "yandex-geocoder",
10634
11049
  15: "cloud",
11050
+ 16: "yandex",
10635
11051
  };
10636
11052
  return [...selected].map((item) => map[item] || item).filter(Boolean);
10637
11053
  } finally {
@@ -10644,7 +11060,7 @@ function isOnboardExitAnswer(answer) {
10644
11060
  }
10645
11061
 
10646
11062
  async function getOnboardComponentStatus() {
10647
- const [config, readiness, browser, archive, codexVersion, ollamaVersion, yandexGeocoderKey, cloudSecrets] = await Promise.all([
11063
+ const [config, readiness, browser, archive, codexVersion, ollamaVersion, yandexGeocoderKey, secrets] = await Promise.all([
10648
11064
  loadConfig(),
10649
11065
  getAiReadiness(),
10650
11066
  getBrowserStatus(),
@@ -10652,8 +11068,9 @@ async function getOnboardComponentStatus() {
10652
11068
  getCommandVersion("codex", ["--version"]),
10653
11069
  getOllamaVersion(),
10654
11070
  getYandexGeocoderKey(),
10655
- loadSecrets().then((secrets) => secrets.cloud || {}),
11071
+ loadSecrets(),
10656
11072
  ]);
11073
+ const cloudSecrets = secrets.cloud || {};
10657
11074
  const workspaceReady = existsSync(PROJECT_CONTEXT_FILE) || existsSync(PROJECT_CONTEXT_DIR_FILE) || existsSync(PROJECT_IOLA_DIR);
10658
11075
  const policyReady = (config.toolsets?.enabled || []).includes("analyst");
10659
11076
  return {
@@ -10672,6 +11089,7 @@ async function getOnboardComponentStatus() {
10672
11089
  browser: browser.installed === "yes",
10673
11090
  "yandex-geocoder": Boolean(yandexGeocoderKey),
10674
11091
  cloud: Object.keys(cloudSecrets).length > 0,
11092
+ yandex: Boolean(secrets.yandex?.oauthToken || config.yandex?.enabledServices?.length),
10675
11093
  };
10676
11094
  }
10677
11095
 
@@ -10692,6 +11110,7 @@ function onboardComponentRows(status) {
10692
11110
  ["13", "ollama", "Ollama", "опциональный локальный runtime"],
10693
11111
  ["14", "yandex-geocoder", "Yandex Geocoder API", "ключ геокодера сохранен или есть в env"],
10694
11112
  ["15", "cloud", "Облачный диск", "Яндекс Диск или Облако Mail.ru"],
11113
+ ["16", "yandex", "Yandex Connector", "единый вход и категории сервисов Яндекса"],
10695
11114
  ];
10696
11115
  return rows.map(([number, key, title, hint]) => ({ number, key, title, hint, status: status[key] ? "готово" : "не настроено" }));
10697
11116
  }
@@ -10706,7 +11125,7 @@ function defaultOnboardSelection(status) {
10706
11125
  }
10707
11126
 
10708
11127
  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" };
11128
+ 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
11129
  return defaultOnboardSelection(status).map((item) => map[item]).filter(Boolean);
10711
11130
  }
10712
11131
 
@@ -10715,12 +11134,12 @@ function parseOptions(args) {
10715
11134
 
10716
11135
  for (let index = 0; index < args.length; index += 1) {
10717
11136
  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") {
11137
+ 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
11138
  result[arg.slice(2)] = true;
10720
11139
  } else if (arg === "--check" || arg === "--upgrade-node") {
10721
11140
  result.check = true;
10722
11141
  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") {
11142
+ } 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
11143
  result[arg.slice(2)] = args[index + 1];
10725
11144
  index += 1;
10726
11145
  } else {
@@ -12618,6 +13037,18 @@ function mergeConfig(base, override) {
12618
13037
  ...(override.cloud?.providers || {}),
12619
13038
  },
12620
13039
  },
13040
+ yandex: {
13041
+ ...base.yandex,
13042
+ ...(override.yandex || {}),
13043
+ oauth: {
13044
+ ...(base.yandex?.oauth || {}),
13045
+ ...(override.yandex?.oauth || {}),
13046
+ },
13047
+ categories: {
13048
+ ...(base.yandex?.categories || {}),
13049
+ ...(override.yandex?.categories || {}),
13050
+ },
13051
+ },
12621
13052
  memory: {
12622
13053
  ...base.memory,
12623
13054
  ...(override.memory || {}),
@@ -12730,6 +13161,9 @@ function validateConfig(config) {
12730
13161
  if (config.cloud?.activeProvider && !["yandex-disk", "mailru-cloud"].includes(config.cloud.activeProvider)) {
12731
13162
  errors.push(`cloud.activeProvider неизвестен: ${config.cloud.activeProvider}`);
12732
13163
  }
13164
+ for (const service of config.yandex?.enabledServices || []) {
13165
+ if (!YANDEX_CONNECTOR_SERVICES[service]) errors.push(`yandex.enabledServices содержит неизвестный сервис: ${service}`);
13166
+ }
12733
13167
  return errors;
12734
13168
  }
12735
13169
 
@@ -12744,6 +13178,7 @@ function configSchema() {
12744
13178
  toolsets: { available: Object.keys(TOOLSETS) },
12745
13179
  files: { modes: ["locked", "read-only", "workspace-write", "full-access"], approvals: ["never", "on-write", "on-danger", "always"] },
12746
13180
  cloud: { providers: ["yandex-disk", "mailru-cloud"], root: CLOUD_DEFAULT_REMOTE_DIR },
13181
+ yandex: { services: Object.keys(YANDEX_CONNECTOR_SERVICES), statuses: ["ready", "research", "separate", "backlog"] },
12747
13182
  skills: { enabled: "array of skill names" },
12748
13183
  daemon: { host: "127.0.0.1", port: DAEMON_PORT },
12749
13184
  },
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
+ - пользователь сам подтверждает заявку и оплату.