@iola_adm/iola-cli 0.2.6 → 0.2.8
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 +16 -0
- package/package.json +1 -1
- package/skills/personal-docs/SKILL.md +27 -0
- package/src/cli.js +515 -3
- package/wiki/Home.md +2 -0
- package/wiki//320/232/320/276/320/274/320/260/320/275/320/264/321/213.md +17 -0
- package/wiki//320/234/320/260/321/201/321/202/320/265/321/200-/320/275/320/260/321/201/321/202/321/200/320/276/320/271/320/272/320/270.md +11 -0
- package/wiki//320/236/320/261/320/273/320/260/321/207/320/275/321/213/320/265-/320/264/320/270/321/201/320/272/320/270.md +134 -0
- package/wiki//320/241/320/272/320/270/320/273/320/273/321/213-/320/264/320/273/321/217-/320/266/320/270/321/202/320/265/320/273/320/265/320/271.md +38 -0
package/README.md
CHANGED
|
@@ -173,6 +173,20 @@ iola geo services "Йошкар-Ола, улица Петрова, 15"
|
|
|
173
173
|
Инструкция по получению ключа: [Yandex Geocoder API key](https://github.com/adm-iola/iola-cli/wiki/Yandex-Geocoder-API-key).
|
|
174
174
|
Список сценариев: [Скиллы для жителей](https://github.com/adm-iola/iola-cli/wiki/Скиллы-для-жителей).
|
|
175
175
|
|
|
176
|
+
Облачные диски для личных документов:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
iola cloud setup yandex-disk
|
|
180
|
+
iola cloud setup mailru-cloud
|
|
181
|
+
iola cloud status
|
|
182
|
+
iola cloud find "справка" --path /IOLA
|
|
183
|
+
iola cloud upload report.md /IOLA/reports/report.md
|
|
184
|
+
iola cloud share /IOLA/reports/report.md
|
|
185
|
+
iola cloud backup
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Инструкция: [Облачные диски](https://github.com/adm-iola/iola-cli/wiki/Облачные-диски).
|
|
189
|
+
|
|
176
190
|
Зарубежные API-ключи:
|
|
177
191
|
|
|
178
192
|
- OpenAI Platform: регистрация `https://platform.openai.com/`, ключи `https://platform.openai.com/api-keys`;
|
|
@@ -200,6 +214,7 @@ iola version --check
|
|
|
200
214
|
- [Мастер настройки](https://github.com/adm-iola/iola-cli/wiki/Мастер-настройки)
|
|
201
215
|
- [AI-профили](https://github.com/adm-iola/iola-cli/wiki/AI-профили)
|
|
202
216
|
- [Yandex Geocoder API key](https://github.com/adm-iola/iola-cli/wiki/Yandex-Geocoder-API-key)
|
|
217
|
+
- [Облачные диски](https://github.com/adm-iola/iola-cli/wiki/Облачные-диски)
|
|
203
218
|
- [Скиллы для жителей](https://github.com/adm-iola/iola-cli/wiki/Скиллы-для-жителей)
|
|
204
219
|
- [Локальный инструментальный агент](https://github.com/adm-iola/iola-cli/wiki/Локальный-инструментальный-агент)
|
|
205
220
|
- [Skills и toolsets](https://github.com/adm-iola/iola-cli/wiki/Skills-и-toolsets)
|
|
@@ -222,6 +237,7 @@ iola version --check
|
|
|
222
237
|
- AI-профили для IOLA local, Ollama, YandexGPT, GigaChat, OpenAI, OpenRouter и Codex CLI;
|
|
223
238
|
- локальный tool-agent для модели IOLA с tools `search_data`, `search_entities`, `resolve_entity_field`, `get_card`, `export_report`, `file_read`, `browser_open`;
|
|
224
239
|
- ленивые skills, toolsets, permissions, memory, hooks и готовые agents;
|
|
240
|
+
- личные облачные диски: Яндекс Диск и Облако Mail.ru для сохранения отчетов, backup и документов;
|
|
225
241
|
- subagents, skill bundles, layered settings, usage/budget accounting и trajectory export;
|
|
226
242
|
- локальный MCP-сервер по stdio/http для подключения iola-cli к другим AI-клиентам;
|
|
227
243
|
- ответы по открытым данным берутся из публичного MCP `https://apiiola.yasg.ru/mcp`;
|
package/package.json
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: personal-docs
|
|
3
|
+
description: Личные документы пользователя в облаке: поиск, сохранение, ссылки, пакеты документов и резервные копии.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Используй этот skill, когда пользователь явно говорит про облако, Яндекс Диск, Облако Mail.ru, ссылку на файл, сохранить в облако, загрузить на диск, поделиться документом или личный архив документов.
|
|
7
|
+
|
|
8
|
+
Не включай cloud-сценарий только по словам "файл", "папка" или "документ". Для обычной работы с файлами на ПК используй `local-files`.
|
|
9
|
+
|
|
10
|
+
Если непонятно, где искать документ, спроси: "Где искать: на компьютере или в облаке?"
|
|
11
|
+
|
|
12
|
+
Основные сценарии:
|
|
13
|
+
|
|
14
|
+
- `cloud-find-document` - найти документ в подключенном облаке.
|
|
15
|
+
- `cloud-save-result` - сохранить ответ, отчет, карточку или список в облако.
|
|
16
|
+
- `cloud-share-link` - создать публичную ссылку на файл в Яндекс Диске.
|
|
17
|
+
- `cloud-document-pack` - собрать папку с материалами для обращения.
|
|
18
|
+
- `cloud-inbox` - проверить входящую папку `/IOLA/inbox`.
|
|
19
|
+
- `cloud-backup-cli` - сохранить резервную копию CLI без секретов.
|
|
20
|
+
- `cloud-restore-cli` - восстановить данные CLI из резервной копии.
|
|
21
|
+
- `cloud-organize` - помочь разобрать личные документы и дубликаты.
|
|
22
|
+
|
|
23
|
+
Безопасность:
|
|
24
|
+
|
|
25
|
+
- Не выгружай API-ключи, OAuth-токены и пароли в облако.
|
|
26
|
+
- Перед удалением, перемещением или публикацией файла требуй подтверждение.
|
|
27
|
+
- Если провайдер не поддерживает публичные ссылки через CLI, честно скажи об ограничении.
|
package/src/cli.js
CHANGED
|
@@ -39,6 +39,7 @@ const PROJECT_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "config.json");
|
|
|
39
39
|
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
|
+
const CLOUD_DEFAULT_REMOTE_DIR = "/IOLA";
|
|
42
43
|
const INDEXABLE_EXTENSIONS = /\.(md|txt|csv|json|html|docx|xlsx|pptx|pdf)$/i;
|
|
43
44
|
const LOCAL_TOOLS = ["search_data", "search_entities", "resolve_entity_field", "get_card", "export_report", "file_read", "browser_open", "get_current_date"];
|
|
44
45
|
const LEGACY_LOCAL_TOOLS = ["search_local", "export_data", "run_report", "save_view"];
|
|
@@ -228,7 +229,14 @@ const DEFAULT_AI_CONFIG = {
|
|
|
228
229
|
suggestions: true,
|
|
229
230
|
},
|
|
230
231
|
skills: {
|
|
231
|
-
enabled: ["education", "open-data", "geo", "reports", "local-model", "local-files", "browser-agent"],
|
|
232
|
+
enabled: ["education", "open-data", "geo", "personal-docs", "reports", "local-model", "local-files", "browser-agent"],
|
|
233
|
+
},
|
|
234
|
+
cloud: {
|
|
235
|
+
activeProvider: "",
|
|
236
|
+
providers: {
|
|
237
|
+
"yandex-disk": { root: CLOUD_DEFAULT_REMOTE_DIR },
|
|
238
|
+
"mailru-cloud": { root: CLOUD_DEFAULT_REMOTE_DIR },
|
|
239
|
+
},
|
|
232
240
|
},
|
|
233
241
|
daemon: {
|
|
234
242
|
host: "127.0.0.1",
|
|
@@ -314,6 +322,7 @@ const SLASH_COMMANDS = [
|
|
|
314
322
|
{ command: "/permissions", description: "разрешения" },
|
|
315
323
|
{ command: "/tools", description: "tools и toolsets" },
|
|
316
324
|
{ command: "/files status", description: "локальные файловые операции" },
|
|
325
|
+
{ command: "/cloud status", description: "облачные диски" },
|
|
317
326
|
{ command: "/archive doctor", description: "архиватор" },
|
|
318
327
|
{ command: "/changes list", description: "подготовленные изменения" },
|
|
319
328
|
{ command: "/index status", description: "индекс документов" },
|
|
@@ -389,6 +398,7 @@ const COMMANDS = new Map([
|
|
|
389
398
|
["skills", handleSkills],
|
|
390
399
|
["tools", handleTools],
|
|
391
400
|
["files", handleFiles],
|
|
401
|
+
["cloud", handleCloud],
|
|
392
402
|
["archive", handleArchive],
|
|
393
403
|
["changes", handleChanges],
|
|
394
404
|
["import", handleImport],
|
|
@@ -537,6 +547,7 @@ async function showHelp() {
|
|
|
537
547
|
iola agent интерактивный режим
|
|
538
548
|
iola ai setup настройка AI-профиля
|
|
539
549
|
iola browser status браузерный runtime
|
|
550
|
+
iola cloud status облачные диски
|
|
540
551
|
iola mcp status MCP-подключение
|
|
541
552
|
iola doctor диагностика
|
|
542
553
|
iola wiki документация
|
|
@@ -576,6 +587,7 @@ Usage:
|
|
|
576
587
|
iola skills list|show|paths|enable|disable|bundles|bundle|doctor
|
|
577
588
|
iola tools list|toolsets|enable|disable|profile
|
|
578
589
|
iola files status|mode|approvals|tree|read|search|write|patch
|
|
590
|
+
iola cloud setup|status|ls|find|upload|download|share|save|backup
|
|
579
591
|
iola archive doctor|list|test|extract|create|index
|
|
580
592
|
iola changes list|show|apply|discard
|
|
581
593
|
iola import file|folder
|
|
@@ -2879,6 +2891,479 @@ async function handleFiles(args) {
|
|
|
2879
2891
|
throw new Error("Команды files: status, mode MODE, approvals POLICY, tree [PATH], read FILE, search TEXT, write FILE --text TEXT, patch FILE --search OLD --replace NEW.");
|
|
2880
2892
|
}
|
|
2881
2893
|
|
|
2894
|
+
async function handleCloud(args) {
|
|
2895
|
+
const [action = "status", target, maybeRemote, ...rest] = args;
|
|
2896
|
+
const options = parseOptions(rest);
|
|
2897
|
+
|
|
2898
|
+
if (action === "setup") {
|
|
2899
|
+
await setupCloudProvider(target, options);
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
if (action === "use") {
|
|
2904
|
+
const provider = normalizeCloudProvider(target);
|
|
2905
|
+
await requireCloudCredentials(provider);
|
|
2906
|
+
const config = await loadConfig();
|
|
2907
|
+
await saveConfig({ cloud: { ...(config.cloud || {}), activeProvider: provider } });
|
|
2908
|
+
console.log(`Активный облачный диск: ${provider}`);
|
|
2909
|
+
return;
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
if (action === "status") {
|
|
2913
|
+
await printCloudStatus({ check: Boolean(options.check) });
|
|
2914
|
+
return;
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
if (action === "doctor") {
|
|
2918
|
+
await printCloudStatus({ check: true });
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
if (action === "delete") {
|
|
2923
|
+
const provider = normalizeCloudProvider(target);
|
|
2924
|
+
const secrets = await loadSecrets();
|
|
2925
|
+
delete secrets.cloud?.[provider];
|
|
2926
|
+
if (secrets.cloud && Object.keys(secrets.cloud).length === 0) delete secrets.cloud;
|
|
2927
|
+
await saveSecrets(secrets);
|
|
2928
|
+
const config = await loadConfig();
|
|
2929
|
+
if (config.cloud?.activeProvider === provider) {
|
|
2930
|
+
await saveConfig({ cloud: { ...(config.cloud || {}), activeProvider: "" } });
|
|
2931
|
+
}
|
|
2932
|
+
console.log(`Локальные секреты облака удалены: ${provider}`);
|
|
2933
|
+
return;
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
if (action === "ls" || action === "list") {
|
|
2937
|
+
const provider = await getCloudProvider(options.provider);
|
|
2938
|
+
const rows = await cloudList(provider, target || cloudRootForProvider(provider));
|
|
2939
|
+
printTable(rows, [["type", "Тип"], ["name", "Имя"], ["path", "Путь"], ["size", "Размер"]]);
|
|
2940
|
+
return;
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
if (action === "find" || action === "search") {
|
|
2944
|
+
if (!target) throw new Error('Пример: iola cloud find "справка" --path /IOLA');
|
|
2945
|
+
const provider = await getCloudProvider(options.provider);
|
|
2946
|
+
const rows = await cloudFind(provider, target, { path: options.path || cloudRootForProvider(provider), limit: Number(options.limit || 50) });
|
|
2947
|
+
printTable(rows, [["type", "Тип"], ["name", "Имя"], ["path", "Путь"], ["size", "Размер"]]);
|
|
2948
|
+
return;
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
if (action === "upload") {
|
|
2952
|
+
if (!target) throw new Error('Пример: iola cloud upload report.md /IOLA/reports/report.md');
|
|
2953
|
+
const provider = await getCloudProvider(options.provider);
|
|
2954
|
+
const remotePath = maybeRemote || `${cloudRootForProvider(provider)}/${path.basename(target)}`;
|
|
2955
|
+
const result = await cloudUpload(provider, target, remotePath, { overwrite: options.overwrite !== false });
|
|
2956
|
+
printKeyValue(result);
|
|
2957
|
+
return;
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
if (action === "download") {
|
|
2961
|
+
if (!target) throw new Error('Пример: iola cloud download /IOLA/report.md ./report.md');
|
|
2962
|
+
const provider = await getCloudProvider(options.provider);
|
|
2963
|
+
const outputPath = maybeRemote || path.basename(target);
|
|
2964
|
+
const result = await cloudDownload(provider, target, outputPath);
|
|
2965
|
+
printKeyValue(result);
|
|
2966
|
+
return;
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
if (action === "share") {
|
|
2970
|
+
if (!target) throw new Error('Пример: iola cloud share /IOLA/report.md');
|
|
2971
|
+
const provider = await getCloudProvider(options.provider);
|
|
2972
|
+
const result = await cloudShare(provider, target);
|
|
2973
|
+
printKeyValue(result);
|
|
2974
|
+
return;
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
if (action === "save") {
|
|
2978
|
+
const provider = await getCloudProvider(options.provider);
|
|
2979
|
+
const text = options.text ?? [target, maybeRemote, ...rest].filter(Boolean).join(" ");
|
|
2980
|
+
if (!text) throw new Error('Пример: iola cloud save --text "Текст" --path /IOLA/notes/note.txt');
|
|
2981
|
+
const remotePath = options.path || `${cloudRootForProvider(provider)}/notes/iola-${timestampForFile()}.txt`;
|
|
2982
|
+
const tempPath = path.join(CONFIG_DIR, `cloud-save-${Date.now()}.txt`);
|
|
2983
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
2984
|
+
await writeFile(tempPath, text, "utf8");
|
|
2985
|
+
try {
|
|
2986
|
+
const result = await cloudUpload(provider, tempPath, remotePath, { overwrite: true });
|
|
2987
|
+
printKeyValue(result);
|
|
2988
|
+
} finally {
|
|
2989
|
+
await rm(tempPath, { force: true }).catch(() => {});
|
|
2990
|
+
}
|
|
2991
|
+
return;
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
if (action === "backup") {
|
|
2995
|
+
const provider = await getCloudProvider(options.provider);
|
|
2996
|
+
const result = await cloudBackup(provider);
|
|
2997
|
+
printKeyValue(result);
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
throw new Error(`Команды cloud:
|
|
3002
|
+
iola cloud setup yandex-disk
|
|
3003
|
+
iola cloud setup mailru-cloud
|
|
3004
|
+
iola cloud status|doctor
|
|
3005
|
+
iola cloud use yandex-disk
|
|
3006
|
+
iola cloud ls /IOLA
|
|
3007
|
+
iola cloud find "справка" --path /IOLA
|
|
3008
|
+
iola cloud upload local.txt /IOLA/local.txt
|
|
3009
|
+
iola cloud download /IOLA/local.txt ./local.txt
|
|
3010
|
+
iola cloud share /IOLA/local.txt
|
|
3011
|
+
iola cloud save --text "Текст" --path /IOLA/notes/note.txt
|
|
3012
|
+
iola cloud backup`);
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
async function setupCloudProvider(providerValue, options = {}) {
|
|
3016
|
+
const provider = normalizeCloudProvider(providerValue);
|
|
3017
|
+
if (!process.stdin.isTTY) throw new Error("Для настройки облака запустите команду в интерактивном терминале.");
|
|
3018
|
+
const secrets = await loadSecrets();
|
|
3019
|
+
secrets.cloud = secrets.cloud || {};
|
|
3020
|
+
|
|
3021
|
+
if (provider === "yandex-disk") {
|
|
3022
|
+
console.log("Яндекс Диск использует OAuth-токен пользователя с доступом к Диску.");
|
|
3023
|
+
console.log("Инструкция: https://github.com/adm-iola/iola-cli/wiki/Облачные-диски");
|
|
3024
|
+
const token = (await askText("Введите OAuth-токен Яндекс Диска: ")).trim();
|
|
3025
|
+
if (!token) throw new Error("Токен пустой, сохранение отменено.");
|
|
3026
|
+
secrets.cloud[provider] = { token };
|
|
3027
|
+
} else if (provider === "mailru-cloud") {
|
|
3028
|
+
console.log("Облако Mail.ru подключается через WebDAV и пароль внешнего приложения.");
|
|
3029
|
+
console.log("Инструкция: https://github.com/adm-iola/iola-cli/wiki/Облачные-диски");
|
|
3030
|
+
const username = (await askText("Введите email Mail.ru: ")).trim();
|
|
3031
|
+
const password = (await askText("Введите пароль внешнего приложения/WebDAV: ")).trim();
|
|
3032
|
+
if (!username || !password) throw new Error("Email или пароль пустой, сохранение отменено.");
|
|
3033
|
+
secrets.cloud[provider] = { username, password, baseUrl: options.baseUrl || "https://webdav.cloud.mail.ru" };
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
await saveSecrets(secrets);
|
|
3037
|
+
const config = await loadConfig();
|
|
3038
|
+
await saveConfig({ cloud: { ...(config.cloud || {}), activeProvider: provider } });
|
|
3039
|
+
console.log(`Облачный диск сохранен и выбран: ${provider}`);
|
|
3040
|
+
await printCloudStatus({ check: true });
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
async function printCloudStatus(options = {}) {
|
|
3044
|
+
const config = await loadConfig();
|
|
3045
|
+
const secrets = await loadSecrets();
|
|
3046
|
+
const rows = ["yandex-disk", "mailru-cloud"].map((provider) => ({
|
|
3047
|
+
provider,
|
|
3048
|
+
active: config.cloud?.activeProvider === provider ? "yes" : "no",
|
|
3049
|
+
configured: secrets.cloud?.[provider] ? "yes" : "no",
|
|
3050
|
+
root: cloudRootForProvider(provider, config),
|
|
3051
|
+
}));
|
|
3052
|
+
printTable(rows, [["provider", "Провайдер"], ["active", "Активен"], ["configured", "Настроен"], ["root", "Папка"]]);
|
|
3053
|
+
if (options.check) {
|
|
3054
|
+
for (const row of rows.filter((item) => item.configured === "yes")) {
|
|
3055
|
+
try {
|
|
3056
|
+
const listed = await cloudList(row.provider, cloudRootForProvider(row.provider), { allowMissingRoot: true });
|
|
3057
|
+
console.log(`${row.provider}: ok (${listed.length} объектов в корневой папке IOLA или папка доступна)`);
|
|
3058
|
+
} catch (error) {
|
|
3059
|
+
console.log(`${row.provider}: error - ${error instanceof Error ? error.message : String(error)}`);
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
function normalizeCloudProvider(value) {
|
|
3066
|
+
const text = String(value || "").toLocaleLowerCase("ru-RU").trim();
|
|
3067
|
+
if (!text || text === "yandex" || text === "yandex-disk" || text === "яндекс" || text === "яндекс-диск") return "yandex-disk";
|
|
3068
|
+
if (text === "mailru" || text === "mailru-cloud" || text === "mail" || text === "mail.ru" || text === "облако-mail") return "mailru-cloud";
|
|
3069
|
+
throw new Error("Поддерживаются облака: yandex-disk, mailru-cloud.");
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
async function getCloudProvider(value) {
|
|
3073
|
+
if (value) return normalizeCloudProvider(value);
|
|
3074
|
+
const config = await loadConfig();
|
|
3075
|
+
const provider = normalizeCloudProvider(config.cloud?.activeProvider || "yandex-disk");
|
|
3076
|
+
await requireCloudCredentials(provider);
|
|
3077
|
+
return provider;
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
async function requireCloudCredentials(provider) {
|
|
3081
|
+
const secrets = await loadSecrets();
|
|
3082
|
+
const credentials = secrets.cloud?.[provider];
|
|
3083
|
+
if (!credentials) throw new Error(`Облачный диск не настроен: ${provider}. Запустите: iola cloud setup ${provider}`);
|
|
3084
|
+
return credentials;
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
function cloudRootForProvider(provider, config = null) {
|
|
3088
|
+
const loaded = config || readConfigLayerSync(CONFIG_FILE) || {};
|
|
3089
|
+
return loaded.cloud?.providers?.[provider]?.root || DEFAULT_AI_CONFIG.cloud.providers[provider]?.root || CLOUD_DEFAULT_REMOTE_DIR;
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
async function cloudList(provider, remotePath, options = {}) {
|
|
3093
|
+
if (provider === "yandex-disk") return yandexDiskList(remotePath, options);
|
|
3094
|
+
if (provider === "mailru-cloud") return mailruCloudList(remotePath, options);
|
|
3095
|
+
throw new Error(`Провайдер не поддерживается: ${provider}`);
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
async function cloudUpload(provider, localPath, remotePath, options = {}) {
|
|
3099
|
+
if (provider === "yandex-disk") return yandexDiskUpload(localPath, remotePath, options);
|
|
3100
|
+
if (provider === "mailru-cloud") return mailruCloudUpload(localPath, remotePath, options);
|
|
3101
|
+
throw new Error(`Провайдер не поддерживается: ${provider}`);
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
async function cloudDownload(provider, remotePath, outputPath) {
|
|
3105
|
+
if (provider === "yandex-disk") return yandexDiskDownload(remotePath, outputPath);
|
|
3106
|
+
if (provider === "mailru-cloud") return mailruCloudDownload(remotePath, outputPath);
|
|
3107
|
+
throw new Error(`Провайдер не поддерживается: ${provider}`);
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
async function cloudFind(provider, query, options = {}) {
|
|
3111
|
+
if (provider === "yandex-disk") return yandexDiskFind(query, options);
|
|
3112
|
+
if (provider === "mailru-cloud") {
|
|
3113
|
+
const rows = await mailruCloudList(options.path || CLOUD_DEFAULT_REMOTE_DIR);
|
|
3114
|
+
return rows.filter((row) => normalizeGeoText(`${row.name} ${row.path}`).includes(normalizeGeoText(query))).slice(0, Number(options.limit || 50));
|
|
3115
|
+
}
|
|
3116
|
+
throw new Error(`Провайдер не поддерживается: ${provider}`);
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
async function cloudShare(provider, remotePath) {
|
|
3120
|
+
if (provider === "yandex-disk") return yandexDiskShare(remotePath);
|
|
3121
|
+
throw new Error("Публичные ссылки через CLI сейчас поддерживаются только для Яндекс Диска.");
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
async function yandexDiskRequest(method, apiPath, options = {}) {
|
|
3125
|
+
const credentials = await requireCloudCredentials("yandex-disk");
|
|
3126
|
+
const url = new URL(`https://cloud-api.yandex.net/v1/disk${apiPath}`);
|
|
3127
|
+
for (const [key, value] of Object.entries(options.query || {})) {
|
|
3128
|
+
if (value !== undefined && value !== "") url.searchParams.set(key, String(value));
|
|
3129
|
+
}
|
|
3130
|
+
const response = await fetch(url, {
|
|
3131
|
+
method,
|
|
3132
|
+
headers: {
|
|
3133
|
+
Authorization: `OAuth ${credentials.token}`,
|
|
3134
|
+
...(options.headers || {}),
|
|
3135
|
+
},
|
|
3136
|
+
body: options.body,
|
|
3137
|
+
signal: AbortSignal.timeout(Number(options.timeout || 30000)),
|
|
3138
|
+
});
|
|
3139
|
+
if (!response.ok) {
|
|
3140
|
+
const text = await response.text();
|
|
3141
|
+
throw new Error(`Yandex Disk request failed: ${response.status} ${response.statusText}\n${sanitizeSecretFromText(text, credentials.token)}`);
|
|
3142
|
+
}
|
|
3143
|
+
if (response.status === 204) return {};
|
|
3144
|
+
const text = await response.text();
|
|
3145
|
+
if (!text.trim()) return {};
|
|
3146
|
+
return JSON.parse(text);
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
async function yandexDiskList(remotePath, options = {}) {
|
|
3150
|
+
await ensureYandexDiskDir(remotePath, { allowExisting: true, allowMissingRoot: Boolean(options.allowMissingRoot) });
|
|
3151
|
+
const payload = await yandexDiskRequest("GET", "/resources", { query: { path: normalizeYandexDiskPath(remotePath), limit: 100 } });
|
|
3152
|
+
const items = payload._embedded?.items || [];
|
|
3153
|
+
return items.map((item) => ({
|
|
3154
|
+
type: item.type === "dir" ? "dir" : "file",
|
|
3155
|
+
name: item.name || path.basename(item.path || ""),
|
|
3156
|
+
path: denormalizeYandexDiskPath(item.path || ""),
|
|
3157
|
+
size: item.size || "-",
|
|
3158
|
+
}));
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
async function yandexDiskFind(query, options = {}) {
|
|
3162
|
+
const needle = normalizeGeoText(query);
|
|
3163
|
+
const rows = await yandexDiskListRecursive(options.path || CLOUD_DEFAULT_REMOTE_DIR, { depth: Number(options.depth || 4), limit: Math.max(100, Number(options.limit || 50) * 5) });
|
|
3164
|
+
return rows
|
|
3165
|
+
.filter((item) => normalizeGeoText(`${item.name} ${item.path}`).includes(needle))
|
|
3166
|
+
.slice(0, Number(options.limit || 50));
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
async function yandexDiskListRecursive(remotePath, options = {}) {
|
|
3170
|
+
const depth = Number(options.depth || 4);
|
|
3171
|
+
const limit = Number(options.limit || 200);
|
|
3172
|
+
const rows = [];
|
|
3173
|
+
await yandexDiskWalk(remotePath, rows, depth, limit);
|
|
3174
|
+
return rows;
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
async function yandexDiskWalk(remotePath, rows, depth, limit) {
|
|
3178
|
+
if (rows.length >= limit || depth < 0) return;
|
|
3179
|
+
const items = await yandexDiskList(remotePath).catch(() => []);
|
|
3180
|
+
for (const item of items) {
|
|
3181
|
+
if (rows.length >= limit) break;
|
|
3182
|
+
rows.push(item);
|
|
3183
|
+
if (item.type === "dir") await yandexDiskWalk(item.path, rows, depth - 1, limit);
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
async function yandexDiskUpload(localPath, remotePath, options = {}) {
|
|
3188
|
+
const resolved = path.resolve(localPath);
|
|
3189
|
+
const info = await stat(resolved);
|
|
3190
|
+
if (!info.isFile()) throw new Error(`Это не файл: ${localPath}`);
|
|
3191
|
+
await ensureYandexDiskDir(path.dirname(remotePath), { allowExisting: true });
|
|
3192
|
+
const upload = await yandexDiskRequest("GET", "/resources/upload", { query: { path: normalizeYandexDiskPath(remotePath), overwrite: options.overwrite !== false } });
|
|
3193
|
+
const bytes = await readFile(resolved);
|
|
3194
|
+
const response = await fetch(upload.href, { method: upload.method || "PUT", body: bytes, signal: AbortSignal.timeout(120000) });
|
|
3195
|
+
if (!response.ok) throw new Error(`Yandex Disk upload failed: ${response.status} ${response.statusText}`);
|
|
3196
|
+
return { provider: "yandex-disk", local: resolved, remote: remotePath, size: info.size };
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
async function yandexDiskDownload(remotePath, outputPath) {
|
|
3200
|
+
const download = await yandexDiskRequest("GET", "/resources/download", { query: { path: normalizeYandexDiskPath(remotePath) } });
|
|
3201
|
+
const response = await fetch(download.href, { signal: AbortSignal.timeout(120000) });
|
|
3202
|
+
if (!response.ok) throw new Error(`Yandex Disk download failed: ${response.status} ${response.statusText}`);
|
|
3203
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
3204
|
+
const resolved = path.resolve(outputPath);
|
|
3205
|
+
await mkdir(path.dirname(resolved), { recursive: true });
|
|
3206
|
+
await writeFile(resolved, buffer);
|
|
3207
|
+
return { provider: "yandex-disk", remote: remotePath, local: resolved, size: buffer.length };
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
async function yandexDiskShare(remotePath) {
|
|
3211
|
+
await yandexDiskRequest("PUT", "/resources/publish", { query: { path: normalizeYandexDiskPath(remotePath) } });
|
|
3212
|
+
const payload = await yandexDiskRequest("GET", "/resources", { query: { path: normalizeYandexDiskPath(remotePath), fields: "name,path,public_url" } });
|
|
3213
|
+
return { provider: "yandex-disk", remote: remotePath, publicUrl: payload.public_url || "-" };
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
async function ensureYandexDiskDir(remotePath, options = {}) {
|
|
3217
|
+
const normalized = normalizeYandexDiskPath(remotePath || CLOUD_DEFAULT_REMOTE_DIR);
|
|
3218
|
+
const plain = denormalizeYandexDiskPath(normalized);
|
|
3219
|
+
const parts = plain.split("/").filter(Boolean);
|
|
3220
|
+
let current = "";
|
|
3221
|
+
for (const part of parts) {
|
|
3222
|
+
current += `/${part}`;
|
|
3223
|
+
try {
|
|
3224
|
+
await yandexDiskRequest("PUT", "/resources", { query: { path: current } });
|
|
3225
|
+
} catch (error) {
|
|
3226
|
+
const message = String(error?.message || "");
|
|
3227
|
+
if (/409|DiskPathPointsToExistentDirectoryError|уже существует/iu.test(message)) continue;
|
|
3228
|
+
if (options.allowMissingRoot && /404/iu.test(message)) continue;
|
|
3229
|
+
throw error;
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
function normalizeYandexDiskPath(remotePath) {
|
|
3235
|
+
const text = String(remotePath || CLOUD_DEFAULT_REMOTE_DIR).trim().replace(/\\/g, "/");
|
|
3236
|
+
if (text.startsWith("disk:") || text.startsWith("app:")) return text;
|
|
3237
|
+
return text.startsWith("/") ? text : `/${text}`;
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
function denormalizeYandexDiskPath(remotePath) {
|
|
3241
|
+
return String(remotePath || "").replace(/^disk:/u, "").replace(/^app:/u, "") || "/";
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
async function mailruCloudRequest(method, remotePath, options = {}) {
|
|
3245
|
+
const credentials = await requireCloudCredentials("mailru-cloud");
|
|
3246
|
+
const baseUrl = String(credentials.baseUrl || "https://webdav.cloud.mail.ru").replace(/\/+$/u, "");
|
|
3247
|
+
const url = `${baseUrl}${encodeWebDavPath(remotePath)}`;
|
|
3248
|
+
const response = await fetch(url, {
|
|
3249
|
+
method,
|
|
3250
|
+
headers: {
|
|
3251
|
+
Authorization: `Basic ${Buffer.from(`${credentials.username}:${credentials.password}`).toString("base64")}`,
|
|
3252
|
+
...(options.headers || {}),
|
|
3253
|
+
},
|
|
3254
|
+
body: options.body,
|
|
3255
|
+
signal: AbortSignal.timeout(Number(options.timeout || 30000)),
|
|
3256
|
+
});
|
|
3257
|
+
if (!response.ok && !(method === "MKCOL" && response.status === 405)) {
|
|
3258
|
+
const text = await response.text().catch(() => "");
|
|
3259
|
+
throw new Error(`Mail.ru Cloud WebDAV request failed: ${response.status} ${response.statusText}\n${sanitizeSecretFromText(text, credentials.password)}`);
|
|
3260
|
+
}
|
|
3261
|
+
return response;
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
async function mailruCloudList(remotePath, options = {}) {
|
|
3265
|
+
await ensureMailruCloudDir(remotePath, { allowMissingRoot: Boolean(options.allowMissingRoot) });
|
|
3266
|
+
const response = await mailruCloudRequest("PROPFIND", remotePath, { headers: { Depth: "1" } });
|
|
3267
|
+
const text = await response.text();
|
|
3268
|
+
return parseWebDavList(text, remotePath);
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
async function mailruCloudUpload(localPath, remotePath) {
|
|
3272
|
+
const resolved = path.resolve(localPath);
|
|
3273
|
+
const info = await stat(resolved);
|
|
3274
|
+
if (!info.isFile()) throw new Error(`Это не файл: ${localPath}`);
|
|
3275
|
+
await ensureMailruCloudDir(path.dirname(remotePath));
|
|
3276
|
+
const bytes = await readFile(resolved);
|
|
3277
|
+
await mailruCloudRequest("PUT", remotePath, { body: bytes, timeout: 120000 });
|
|
3278
|
+
return { provider: "mailru-cloud", local: resolved, remote: remotePath, size: info.size };
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
async function mailruCloudDownload(remotePath, outputPath) {
|
|
3282
|
+
const response = await mailruCloudRequest("GET", remotePath, { timeout: 120000 });
|
|
3283
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
3284
|
+
const resolved = path.resolve(outputPath);
|
|
3285
|
+
await mkdir(path.dirname(resolved), { recursive: true });
|
|
3286
|
+
await writeFile(resolved, buffer);
|
|
3287
|
+
return { provider: "mailru-cloud", remote: remotePath, local: resolved, size: buffer.length };
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
async function ensureMailruCloudDir(remotePath, options = {}) {
|
|
3291
|
+
const parts = String(remotePath || CLOUD_DEFAULT_REMOTE_DIR).replace(/\\/g, "/").split("/").filter(Boolean);
|
|
3292
|
+
let current = "";
|
|
3293
|
+
for (const part of parts) {
|
|
3294
|
+
current += `/${part}`;
|
|
3295
|
+
try {
|
|
3296
|
+
await mailruCloudRequest("MKCOL", current);
|
|
3297
|
+
} catch (error) {
|
|
3298
|
+
const message = String(error?.message || "");
|
|
3299
|
+
if (/405|409/iu.test(message)) continue;
|
|
3300
|
+
if (options.allowMissingRoot && /404/iu.test(message)) continue;
|
|
3301
|
+
throw error;
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
function encodeWebDavPath(remotePath) {
|
|
3307
|
+
const normalized = String(remotePath || "/").replace(/\\/g, "/");
|
|
3308
|
+
const segments = normalized.split("/").filter(Boolean).map(encodeURIComponent);
|
|
3309
|
+
return `/${segments.join("/")}${normalized.endsWith("/") ? "/" : ""}`;
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
function parseWebDavList(xml, basePath) {
|
|
3313
|
+
const responses = String(xml || "").split(/<[^:>]*:?response[^>]*>/iu).slice(1);
|
|
3314
|
+
const rows = [];
|
|
3315
|
+
for (const response of responses) {
|
|
3316
|
+
const href = decodeXml(stripXmlTags(response.match(/<[^:>]*:?href[^>]*>([\s\S]*?)<\/[^:>]*:?href>/iu)?.[1] || ""));
|
|
3317
|
+
const name = decodeXml(stripXmlTags(response.match(/<[^:>]*:?displayname[^>]*>([\s\S]*?)<\/[^:>]*:?displayname>/iu)?.[1] || "")) || path.basename(href);
|
|
3318
|
+
const size = decodeXml(stripXmlTags(response.match(/<[^:>]*:?getcontentlength[^>]*>([\s\S]*?)<\/[^:>]*:?getcontentlength>/iu)?.[1] || ""));
|
|
3319
|
+
const isDir = /<[^:>]*:?collection\s*\/?>/iu.test(response);
|
|
3320
|
+
const normalizedPath = decodeURIComponent(href || "");
|
|
3321
|
+
if (normalizeGeoText(normalizedPath).replace(/\s+/g, "") === normalizeGeoText(basePath).replace(/\s+/g, "")) continue;
|
|
3322
|
+
rows.push({ type: isDir ? "dir" : "file", name, path: normalizedPath, size: size || "-" });
|
|
3323
|
+
}
|
|
3324
|
+
return rows;
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
function stripXmlTags(value) {
|
|
3328
|
+
return String(value || "").replace(/<[^>]+>/g, "").trim();
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
function decodeXml(value) {
|
|
3332
|
+
return String(value || "")
|
|
3333
|
+
.replace(/&/g, "&")
|
|
3334
|
+
.replace(/</g, "<")
|
|
3335
|
+
.replace(/>/g, ">")
|
|
3336
|
+
.replace(/"/g, "\"")
|
|
3337
|
+
.replace(/'/g, "'");
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
async function cloudBackup(provider) {
|
|
3341
|
+
const config = await loadConfig();
|
|
3342
|
+
const payload = {
|
|
3343
|
+
createdAt: new Date().toISOString(),
|
|
3344
|
+
version: getPackageVersion(),
|
|
3345
|
+
note: "Секреты, API-ключи и токены не включены в резервную копию.",
|
|
3346
|
+
config: {
|
|
3347
|
+
api: config.api,
|
|
3348
|
+
ai: { activeProfile: config.ai?.activeProfile, provider: config.ai?.provider, model: config.ai?.model, profiles: config.ai?.profiles },
|
|
3349
|
+
files: config.files,
|
|
3350
|
+
skills: config.skills,
|
|
3351
|
+
cloud: config.cloud,
|
|
3352
|
+
},
|
|
3353
|
+
};
|
|
3354
|
+
const tempPath = path.join(CONFIG_DIR, `iola-backup-${timestampForFile()}.json`);
|
|
3355
|
+
await writeFile(tempPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
3356
|
+
try {
|
|
3357
|
+
return cloudUpload(provider, tempPath, `${cloudRootForProvider(provider)}/backup/${path.basename(tempPath)}`, { overwrite: true });
|
|
3358
|
+
} finally {
|
|
3359
|
+
await rm(tempPath, { force: true }).catch(() => {});
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
|
|
3363
|
+
function timestampForFile() {
|
|
3364
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
3365
|
+
}
|
|
3366
|
+
|
|
2882
3367
|
async function handleArchive(args) {
|
|
2883
3368
|
const [action = "doctor", target, ...rest] = args;
|
|
2884
3369
|
const options = parseOptions(rest);
|
|
@@ -9928,6 +10413,14 @@ async function onboard(args = []) {
|
|
|
9928
10413
|
await setYandexGeocoderKey();
|
|
9929
10414
|
}
|
|
9930
10415
|
}
|
|
10416
|
+
if (components.includes("cloud")) {
|
|
10417
|
+
if (process.stdin.isTTY) {
|
|
10418
|
+
const providerAnswer = (await askText("Облачный диск: 1 - Яндекс Диск, 2 - Облако Mail.ru, 0 - пропустить: ")).trim();
|
|
10419
|
+
if (providerAnswer === "1") await setupCloudProvider("yandex-disk");
|
|
10420
|
+
else if (providerAnswer === "2") await setupCloudProvider("mailru-cloud");
|
|
10421
|
+
else console.log("Настройка облачного диска пропущена.");
|
|
10422
|
+
}
|
|
10423
|
+
}
|
|
9931
10424
|
if (components.includes("codex")) {
|
|
9932
10425
|
await installCodexIfMissing();
|
|
9933
10426
|
await aiSetup(["codex"]);
|
|
@@ -9977,6 +10470,7 @@ async function chooseOnboardComponents(status = null) {
|
|
|
9977
10470
|
12: "browser",
|
|
9978
10471
|
13: "ollama",
|
|
9979
10472
|
14: "yandex-geocoder",
|
|
10473
|
+
15: "cloud",
|
|
9980
10474
|
};
|
|
9981
10475
|
return [...selected].map((item) => map[item] || item).filter(Boolean);
|
|
9982
10476
|
} finally {
|
|
@@ -9989,7 +10483,7 @@ function isOnboardExitAnswer(answer) {
|
|
|
9989
10483
|
}
|
|
9990
10484
|
|
|
9991
10485
|
async function getOnboardComponentStatus() {
|
|
9992
|
-
const [config, readiness, browser, archive, codexVersion, ollamaVersion, yandexGeocoderKey] = await Promise.all([
|
|
10486
|
+
const [config, readiness, browser, archive, codexVersion, ollamaVersion, yandexGeocoderKey, cloudSecrets] = await Promise.all([
|
|
9993
10487
|
loadConfig(),
|
|
9994
10488
|
getAiReadiness(),
|
|
9995
10489
|
getBrowserStatus(),
|
|
@@ -9997,6 +10491,7 @@ async function getOnboardComponentStatus() {
|
|
|
9997
10491
|
getCommandVersion("codex", ["--version"]),
|
|
9998
10492
|
getOllamaVersion(),
|
|
9999
10493
|
getYandexGeocoderKey(),
|
|
10494
|
+
loadSecrets().then((secrets) => secrets.cloud || {}),
|
|
10000
10495
|
]);
|
|
10001
10496
|
const workspaceReady = existsSync(PROJECT_CONTEXT_FILE) || existsSync(PROJECT_CONTEXT_DIR_FILE) || existsSync(PROJECT_IOLA_DIR);
|
|
10002
10497
|
const policyReady = (config.toolsets?.enabled || []).includes("analyst");
|
|
@@ -10015,6 +10510,7 @@ async function getOnboardComponentStatus() {
|
|
|
10015
10510
|
index: false,
|
|
10016
10511
|
browser: browser.installed === "yes",
|
|
10017
10512
|
"yandex-geocoder": Boolean(yandexGeocoderKey),
|
|
10513
|
+
cloud: Object.keys(cloudSecrets).length > 0,
|
|
10018
10514
|
};
|
|
10019
10515
|
}
|
|
10020
10516
|
|
|
@@ -10034,6 +10530,7 @@ function onboardComponentRows(status) {
|
|
|
10034
10530
|
["12", "browser", "Browser runtime", "Playwright/Chromium установлен"],
|
|
10035
10531
|
["13", "ollama", "Ollama", "опциональный локальный runtime"],
|
|
10036
10532
|
["14", "yandex-geocoder", "Yandex Geocoder API", "ключ геокодера сохранен или есть в env"],
|
|
10533
|
+
["15", "cloud", "Облачный диск", "Яндекс Диск или Облако Mail.ru"],
|
|
10037
10534
|
];
|
|
10038
10535
|
return rows.map(([number, key, title, hint]) => ({ number, key, title, hint, status: status[key] ? "готово" : "не настроено" }));
|
|
10039
10536
|
}
|
|
@@ -10048,7 +10545,7 @@ function defaultOnboardSelection(status) {
|
|
|
10048
10545
|
}
|
|
10049
10546
|
|
|
10050
10547
|
function defaultOnboardComponents(status) {
|
|
10051
|
-
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" };
|
|
10548
|
+
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" };
|
|
10052
10549
|
return defaultOnboardSelection(status).map((item) => map[item]).filter(Boolean);
|
|
10053
10550
|
}
|
|
10054
10551
|
|
|
@@ -11947,6 +12444,14 @@ function mergeConfig(base, override) {
|
|
|
11947
12444
|
...base.files,
|
|
11948
12445
|
...(override.files || {}),
|
|
11949
12446
|
},
|
|
12447
|
+
cloud: {
|
|
12448
|
+
...base.cloud,
|
|
12449
|
+
...(override.cloud || {}),
|
|
12450
|
+
providers: {
|
|
12451
|
+
...(base.cloud?.providers || {}),
|
|
12452
|
+
...(override.cloud?.providers || {}),
|
|
12453
|
+
},
|
|
12454
|
+
},
|
|
11950
12455
|
memory: {
|
|
11951
12456
|
...base.memory,
|
|
11952
12457
|
...(override.memory || {}),
|
|
@@ -12006,6 +12511,9 @@ function sanitizeConfig(config) {
|
|
|
12006
12511
|
if (Array.isArray(next.skills?.enabled) && next.skills.enabled.includes("open-data") && !next.skills.enabled.includes("education")) {
|
|
12007
12512
|
next.skills.enabled = ["education", ...next.skills.enabled];
|
|
12008
12513
|
}
|
|
12514
|
+
if (Array.isArray(next.skills?.enabled) && next.skills.enabled.includes("local-files") && !next.skills.enabled.includes("personal-docs")) {
|
|
12515
|
+
next.skills.enabled = [...next.skills.enabled, "personal-docs"];
|
|
12516
|
+
}
|
|
12009
12517
|
const localProfile = next.ai?.profiles?.local;
|
|
12010
12518
|
if (localProfile?.provider === "iola") {
|
|
12011
12519
|
if (!localProfile.runtime || localProfile.model === "iola-router-1b") {
|
|
@@ -12050,6 +12558,9 @@ function validateConfig(config) {
|
|
|
12050
12558
|
for (const toolset of config.toolsets?.enabled || []) {
|
|
12051
12559
|
if (!TOOLSETS[toolset]) errors.push(`toolsets.enabled содержит неизвестный toolset: ${toolset}`);
|
|
12052
12560
|
}
|
|
12561
|
+
if (config.cloud?.activeProvider && !["yandex-disk", "mailru-cloud"].includes(config.cloud.activeProvider)) {
|
|
12562
|
+
errors.push(`cloud.activeProvider неизвестен: ${config.cloud.activeProvider}`);
|
|
12563
|
+
}
|
|
12053
12564
|
return errors;
|
|
12054
12565
|
}
|
|
12055
12566
|
|
|
@@ -12063,6 +12574,7 @@ function configSchema() {
|
|
|
12063
12574
|
permissions: { localTools: ALL_LOCAL_TOOLS, runtime: ["readFiles", "writeFiles", "editFiles", "deleteFiles", "sync", "externalApi", "externalAi", "codex"] },
|
|
12064
12575
|
toolsets: { available: Object.keys(TOOLSETS) },
|
|
12065
12576
|
files: { modes: ["locked", "read-only", "workspace-write", "full-access"], approvals: ["never", "on-write", "on-danger", "always"] },
|
|
12577
|
+
cloud: { providers: ["yandex-disk", "mailru-cloud"], root: CLOUD_DEFAULT_REMOTE_DIR },
|
|
12066
12578
|
skills: { enabled: "array of skill names" },
|
|
12067
12579
|
daemon: { host: "127.0.0.1", port: DAEMON_PORT },
|
|
12068
12580
|
},
|
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
|
+
- личные облачные диски: Яндекс Диск и Облако Mail.ru;
|
|
15
16
|
- подключение публичного MCP-сервера.
|
|
16
17
|
|
|
17
18
|
Быстрый старт:
|
|
@@ -30,6 +31,7 @@ iola ask "найди школу 29"
|
|
|
30
31
|
- [Мастер настройки](Мастер-настройки)
|
|
31
32
|
- [AI-профили](AI-профили)
|
|
32
33
|
- [Yandex Geocoder API key](Yandex-Geocoder-API-key)
|
|
34
|
+
- [Облачные диски](Облачные-диски)
|
|
33
35
|
- [Скиллы для жителей](Скиллы-для-жителей)
|
|
34
36
|
- [Локальный инструментальный агент](Локальный-инструментальный-агент)
|
|
35
37
|
- [Skills и toolsets](Skills-и-toolsets)
|
|
@@ -41,6 +41,23 @@ iola geo route-context "школа 7"
|
|
|
41
41
|
iola geo services "Йошкар-Ола, улица Петрова, 15"
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
Облачные диски:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
iola cloud setup yandex-disk
|
|
48
|
+
iola cloud setup mailru-cloud
|
|
49
|
+
iola cloud status
|
|
50
|
+
iola cloud doctor
|
|
51
|
+
iola cloud use yandex-disk
|
|
52
|
+
iola cloud ls /IOLA
|
|
53
|
+
iola cloud find "справка" --path /IOLA
|
|
54
|
+
iola cloud upload report.md /IOLA/reports/report.md
|
|
55
|
+
iola cloud download /IOLA/reports/report.md ./report.md
|
|
56
|
+
iola cloud share /IOLA/reports/report.md
|
|
57
|
+
iola cloud save --text "Текст заметки" --path /IOLA/notes/note.txt
|
|
58
|
+
iola cloud backup
|
|
59
|
+
```
|
|
60
|
+
|
|
44
61
|
Локальная БД:
|
|
45
62
|
|
|
46
63
|
```bash
|
|
@@ -94,6 +94,17 @@ iola index folder ./docs
|
|
|
94
94
|
|
|
95
95
|
Инструкция по получению ключа: [Yandex Geocoder API key](Yandex-Geocoder-API-key).
|
|
96
96
|
|
|
97
|
+
### 15. Облачный диск
|
|
98
|
+
|
|
99
|
+
Подключает личный облачный диск пользователя:
|
|
100
|
+
|
|
101
|
+
- Яндекс Диск;
|
|
102
|
+
- Облако Mail.ru.
|
|
103
|
+
|
|
104
|
+
Секреты сохраняются локально в `~/.iola/secrets.json`. Резервные копии CLI не включают OAuth-токены, пароли и API-ключи.
|
|
105
|
+
|
|
106
|
+
Инструкция: [Облачные диски](Облачные-диски).
|
|
107
|
+
|
|
97
108
|
## Повторный запуск
|
|
98
109
|
|
|
99
110
|
Если компонент уже настроен, его можно не выбирать. Если нужно переустановить или обновить компонент, выберите его номер вручную.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Облачные диски
|
|
2
|
+
|
|
3
|
+
`iola-cli` поддерживает личные облачные диски для бытовых сценариев: сохранить отчет, найти документ, сделать резервную копию, создать ссылку и собрать пакет документов.
|
|
4
|
+
|
|
5
|
+
Сейчас поддерживаются:
|
|
6
|
+
|
|
7
|
+
- Яндекс Диск;
|
|
8
|
+
- Облако Mail.ru через WebDAV.
|
|
9
|
+
|
|
10
|
+
Корпоративные хранилища, S3, Nextcloud и ownCloud в пользовательский контур не входят.
|
|
11
|
+
|
|
12
|
+
## Команды
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
iola cloud setup yandex-disk
|
|
16
|
+
iola cloud setup mailru-cloud
|
|
17
|
+
iola cloud status
|
|
18
|
+
iola cloud doctor
|
|
19
|
+
iola cloud use yandex-disk
|
|
20
|
+
iola cloud ls /IOLA
|
|
21
|
+
iola cloud find "справка" --path /IOLA
|
|
22
|
+
iola cloud upload report.md /IOLA/reports/report.md
|
|
23
|
+
iola cloud download /IOLA/reports/report.md ./report.md
|
|
24
|
+
iola cloud share /IOLA/reports/report.md
|
|
25
|
+
iola cloud save --text "Текст заметки" --path /IOLA/notes/note.txt
|
|
26
|
+
iola cloud backup
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Яндекс Диск
|
|
30
|
+
|
|
31
|
+
Яндекс Диск подключается через OAuth-токен пользователя.
|
|
32
|
+
|
|
33
|
+
Официальная документация:
|
|
34
|
+
|
|
35
|
+
- REST API Диска: `https://yandex.ru/dev/disk/rest?lang=ru`
|
|
36
|
+
- получение OAuth-токена: `https://yandex.ru/dev/id/doc/ru/access`
|
|
37
|
+
- получение токена вручную для проверки: `https://yandex.ru/dev/id/doc/ru/tokens/debug-token`
|
|
38
|
+
|
|
39
|
+
### Получение OAuth-токена Яндекс Диска
|
|
40
|
+
|
|
41
|
+
1. Откройте страницу Яндекс OAuth: `https://oauth.yandex.ru/`.
|
|
42
|
+
2. Если Яндекс просит пройти верификацию через Госуслуги, пройдите ее. Без этого создание OAuth-приложения может быть недоступно.
|
|
43
|
+
3. Нажмите `Создать`.
|
|
44
|
+
4. В окне `Какое приложение хотите создать?` выберите `Для авторизации пользователей`.
|
|
45
|
+
5. Нажмите `Перейти к созданию`.
|
|
46
|
+
6. На шаге `Создание приложения` заполните:
|
|
47
|
+
- `Название вашего сервиса`: например `iola-cli`;
|
|
48
|
+
- `Иконка сервиса`: загрузите любую простую PNG-иконку до 1 МБ;
|
|
49
|
+
- `Почта для связи`: оставьте свою почту или укажите актуальную.
|
|
50
|
+
7. Нажмите `Продолжить`.
|
|
51
|
+
8. На шаге `Платформы приложений` выберите `Веб-сервисы`.
|
|
52
|
+
9. В поле `Redirect URI` укажите:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
https://oauth.yandex.ru/verification_code
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
10. Нажмите кнопку добавления рядом с `Redirect URI`, если интерфейс показывает такую кнопку.
|
|
59
|
+
11. В поле `Suggest Hostname`, если оно обязательно, укажите сайт проекта или любой понятный URL сервиса, например:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
https://github.com
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
12. Нажмите `Продолжить`.
|
|
66
|
+
13. На шаге `Права доступа к данным пользователей` не выбирайте лишние основные права: телефон, почта, дата рождения, профиль не нужны.
|
|
67
|
+
14. В блоке `Дополнительные` через поле `Название доступа` добавьте три права:
|
|
68
|
+
- `Чтение всего Диска` / `cloud_api:disk.read`;
|
|
69
|
+
- `Запись в любом месте на Диске` / `cloud_api:disk.write`;
|
|
70
|
+
- `Доступ к информации о Диске` / `cloud_api:disk.info`.
|
|
71
|
+
15. Нажмите `Продолжить`.
|
|
72
|
+
16. На финальном экране проверьте, что приложение запрашивает только три права Диска.
|
|
73
|
+
17. Нажмите `Всё верно, создать приложение`.
|
|
74
|
+
18. На странице созданного приложения скопируйте `ClientID`.
|
|
75
|
+
19. Откройте ссылку, заменив `CLIENT_ID` на ваш `ClientID`:
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
https://oauth.yandex.ru/authorize?response_type=token&client_id=CLIENT_ID
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
20. На экране авторизации нажмите `Войти как ...`.
|
|
82
|
+
21. Если Яндекс показывает предупреждение `Сервис ещё не верифицирован`, это нормально для личного приложения. Продолжайте только если приложение создавали вы сами и доверяете ему.
|
|
83
|
+
22. Яндекс перенаправит на страницу `verification_code`, где будет показан OAuth-токен. В адресной строке он также находится после `access_token=`.
|
|
84
|
+
23. Скопируйте OAuth-токен.
|
|
85
|
+
24. В CLI выполните:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
iola cloud setup yandex-disk
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
25. Вставьте OAuth-токен.
|
|
92
|
+
26. Проверьте подключение:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
iola cloud doctor
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Секрет сохраняется локально в `~/.iola/secrets.json`.
|
|
99
|
+
|
|
100
|
+
После успешной проверки CLI создаст или использует папку `/IOLA` на Яндекс Диске.
|
|
101
|
+
|
|
102
|
+
Обычно такой токен выдается на длительный срок. Если доступ перестал работать, получите новый токен через ту же ссылку авторизации и повторите `iola cloud setup yandex-disk`.
|
|
103
|
+
|
|
104
|
+
## Облако Mail.ru
|
|
105
|
+
|
|
106
|
+
Облако Mail.ru подключается через WebDAV.
|
|
107
|
+
|
|
108
|
+
Официальная инструкция Mail.ru по WebDAV: `https://help.mail.ru/cloud/desktop/webdav/`
|
|
109
|
+
|
|
110
|
+
Что сделать:
|
|
111
|
+
|
|
112
|
+
1. Войдите в аккаунт Mail.ru.
|
|
113
|
+
2. Создайте пароль для внешнего приложения. Для WebDAV обычный пароль от почты использовать не нужно.
|
|
114
|
+
3. В CLI выполните:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
iola cloud setup mailru-cloud
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
4. Введите email и пароль внешнего приложения/WebDAV.
|
|
121
|
+
5. Проверьте подключение:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
iola cloud doctor
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Публичные ссылки через CLI сейчас поддерживаются только для Яндекс Диска. Для Mail.ru доступен список файлов, загрузка и скачивание через WebDAV.
|
|
128
|
+
|
|
129
|
+
## Безопасность
|
|
130
|
+
|
|
131
|
+
- OAuth-токены, пароли и API-ключи не попадают в резервные копии.
|
|
132
|
+
- `iola cloud backup` сохраняет конфигурацию без секретов.
|
|
133
|
+
- Удаление и массовое перемещение файлов в первом контуре не реализованы намеренно.
|
|
134
|
+
- Для локальных файлов продолжает действовать отдельный режим `iola files mode`.
|
|
@@ -99,3 +99,41 @@ iola geo services "Йошкар-Ола, улица Петрова, 15"
|
|
|
99
99
|
- Расстояние считается по прямой между координатами. Это не автомобильный и не пешеходный маршрут.
|
|
100
100
|
- CLI не должен подставлять похожий объект из другого населенного пункта. Если слой не содержит нужный объект, ответ должен прямо говорить, что данных недостаточно.
|
|
101
101
|
- При первом поиске рядом CLI геокодирует объекты слоя. Повторные запросы в той же сессии работают быстрее за счет памяти процесса.
|
|
102
|
+
|
|
103
|
+
## Personal docs skills
|
|
104
|
+
|
|
105
|
+
Эти skills работают с личными документами пользователя в подключенном облаке. Они включаются только при явном облачном намерении: `Яндекс Диск`, `Облако Mail.ru`, `облако`, `сохрани на диск`, `загрузи`, `дай ссылку`, `поделиться`.
|
|
106
|
+
|
|
107
|
+
Если пользователь говорит только `файл`, `папка` или `документ`, CLI использует `local-files`, а не облако.
|
|
108
|
+
|
|
109
|
+
### cloud-find-document
|
|
110
|
+
|
|
111
|
+
Найти документ в подключенном облаке: справку, квитанцию, школьный документ, файл обращения.
|
|
112
|
+
|
|
113
|
+
### cloud-save-result
|
|
114
|
+
|
|
115
|
+
Сохранить ответ CLI, отчет, карточку учреждения или список ближайших объектов в `/IOLA`.
|
|
116
|
+
|
|
117
|
+
### cloud-share-link
|
|
118
|
+
|
|
119
|
+
Создать публичную ссылку на файл. В первом контуре поддерживается для Яндекс Диска.
|
|
120
|
+
|
|
121
|
+
### cloud-document-pack
|
|
122
|
+
|
|
123
|
+
Собрать папку с материалами для обращения: текст, найденные данные, ссылки, вложенные документы и скриншоты.
|
|
124
|
+
|
|
125
|
+
### cloud-inbox
|
|
126
|
+
|
|
127
|
+
Проверить `/IOLA/inbox`, найти новые документы и предложить их разобрать или проиндексировать.
|
|
128
|
+
|
|
129
|
+
### cloud-backup-cli
|
|
130
|
+
|
|
131
|
+
Сохранить резервную копию настроек CLI без секретов, OAuth-токенов и API-ключей.
|
|
132
|
+
|
|
133
|
+
### cloud-restore-cli
|
|
134
|
+
|
|
135
|
+
Восстановить CLI из резервной копии, когда пользователь переустановил программу или перешел на другой компьютер.
|
|
136
|
+
|
|
137
|
+
### cloud-organize
|
|
138
|
+
|
|
139
|
+
Помочь разобрать личные документы: найти старые отчеты, крупные файлы, дубликаты и предложить структуру папок.
|