@iola_adm/iola-cli 0.2.7 → 0.2.9

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
@@ -179,6 +179,7 @@ iola geo services "Йошкар-Ола, улица Петрова, 15"
179
179
  iola cloud setup yandex-disk
180
180
  iola cloud setup mailru-cloud
181
181
  iola cloud status
182
+ iola cloud mkdir /IOLA/Фото
182
183
  iola cloud find "справка" --path /IOLA
183
184
  iola cloud upload report.md /IOLA/reports/report.md
184
185
  iola cloud share /IOLA/reports/report.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "CLI и AI-агент городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
@@ -12,6 +12,7 @@ description: Личные документы пользователя в обл
12
12
  Основные сценарии:
13
13
 
14
14
  - `cloud-find-document` - найти документ в подключенном облаке.
15
+ - `cloud-create-folder` - создать папку на подключенном облачном диске.
15
16
  - `cloud-save-result` - сохранить ответ, отчет, карточку или список в облако.
16
17
  - `cloud-share-link` - создать публичную ссылку на файл в Яндекс Диске.
17
18
  - `cloud-document-pack` - собрать папку с материалами для обращения.
package/src/cli.js CHANGED
@@ -2948,6 +2948,14 @@ async function handleCloud(args) {
2948
2948
  return;
2949
2949
  }
2950
2950
 
2951
+ if (action === "mkdir" || action === "create-folder") {
2952
+ const provider = await getCloudProvider(options.provider);
2953
+ const remotePath = target || `${cloudRootForProvider(provider)}/Новая папка`;
2954
+ const result = await cloudCreateFolder(provider, remotePath);
2955
+ printKeyValue(result);
2956
+ return;
2957
+ }
2958
+
2951
2959
  if (action === "upload") {
2952
2960
  if (!target) throw new Error('Пример: iola cloud upload report.md /IOLA/reports/report.md');
2953
2961
  const provider = await getCloudProvider(options.provider);
@@ -3004,6 +3012,7 @@ async function handleCloud(args) {
3004
3012
  iola cloud status|doctor
3005
3013
  iola cloud use yandex-disk
3006
3014
  iola cloud ls /IOLA
3015
+ iola cloud mkdir /IOLA/Фото
3007
3016
  iola cloud find "справка" --path /IOLA
3008
3017
  iola cloud upload local.txt /IOLA/local.txt
3009
3018
  iola cloud download /IOLA/local.txt ./local.txt
@@ -3116,6 +3125,18 @@ async function cloudFind(provider, query, options = {}) {
3116
3125
  throw new Error(`Провайдер не поддерживается: ${provider}`);
3117
3126
  }
3118
3127
 
3128
+ async function cloudCreateFolder(provider, remotePath) {
3129
+ if (provider === "yandex-disk") {
3130
+ await ensureYandexDiskDir(remotePath, { allowExisting: true });
3131
+ return { provider, path: normalizeYandexDiskPath(remotePath), status: "created-or-exists" };
3132
+ }
3133
+ if (provider === "mailru-cloud") {
3134
+ await ensureMailruCloudDir(remotePath);
3135
+ return { provider, path: remotePath, status: "created-or-exists" };
3136
+ }
3137
+ throw new Error(`Провайдер не поддерживается: ${provider}`);
3138
+ }
3139
+
3119
3140
  async function cloudShare(provider, remotePath) {
3120
3141
  if (provider === "yandex-disk") return yandexDiskShare(remotePath);
3121
3142
  throw new Error("Публичные ссылки через CLI сейчас поддерживаются только для Яндекс Диска.");
@@ -3159,19 +3180,31 @@ async function yandexDiskList(remotePath, options = {}) {
3159
3180
  }
3160
3181
 
3161
3182
  async function yandexDiskFind(query, options = {}) {
3162
- const payload = await yandexDiskRequest("GET", "/resources/files", { query: { limit: Math.max(100, Number(options.limit || 50) * 5), fields: "items.name,items.path,items.size,items.type" } });
3163
3183
  const needle = normalizeGeoText(query);
3164
- return (payload.items || [])
3165
- .map((item) => ({
3166
- type: item.type === "dir" ? "dir" : "file",
3167
- name: item.name || path.basename(item.path || ""),
3168
- path: denormalizeYandexDiskPath(item.path || ""),
3169
- size: item.size || "-",
3170
- }))
3184
+ const rows = await yandexDiskListRecursive(options.path || CLOUD_DEFAULT_REMOTE_DIR, { depth: Number(options.depth || 4), limit: Math.max(100, Number(options.limit || 50) * 5) });
3185
+ return rows
3171
3186
  .filter((item) => normalizeGeoText(`${item.name} ${item.path}`).includes(needle))
3172
3187
  .slice(0, Number(options.limit || 50));
3173
3188
  }
3174
3189
 
3190
+ async function yandexDiskListRecursive(remotePath, options = {}) {
3191
+ const depth = Number(options.depth || 4);
3192
+ const limit = Number(options.limit || 200);
3193
+ const rows = [];
3194
+ await yandexDiskWalk(remotePath, rows, depth, limit);
3195
+ return rows;
3196
+ }
3197
+
3198
+ async function yandexDiskWalk(remotePath, rows, depth, limit) {
3199
+ if (rows.length >= limit || depth < 0) return;
3200
+ const items = await yandexDiskList(remotePath).catch(() => []);
3201
+ for (const item of items) {
3202
+ if (rows.length >= limit) break;
3203
+ rows.push(item);
3204
+ if (item.type === "dir") await yandexDiskWalk(item.path, rows, depth - 1, limit);
3205
+ }
3206
+ }
3207
+
3175
3208
  async function yandexDiskUpload(localPath, remotePath, options = {}) {
3176
3209
  const resolved = path.resolve(localPath);
3177
3210
  const info = await stat(resolved);
@@ -7861,6 +7894,20 @@ async function aiAsk(args, context = {}) {
7861
7894
  const historyEnabled = !options.bare && !options["no-history"] && isFeatureEnabled("sqlite-history");
7862
7895
  const sessionId = historyEnabled && isFeatureEnabled("sessions") ? ensureSessionForAsk(options, providerConfig, question) : null;
7863
7896
  const history = context.history || (sessionId ? getSessionAiHistory(sessionId) : []);
7897
+ const cloudAnswer = await buildCloudDirectAnswer(question);
7898
+ if (cloudAnswer) {
7899
+ if (historyEnabled) {
7900
+ recordAskHistory({ question, answer: cloudAnswer, providerConfig, dataContext, error: "", sessionId });
7901
+ appendSessionExchange(sessionId, question, cloudAnswer, dataContext, "");
7902
+ }
7903
+ emitEvent(options, "answer", { length: cloudAnswer.length, sessionId, direct: true, cloud: true });
7904
+ if (options.output) {
7905
+ await assertPermission("writeFiles");
7906
+ await writeFile(options.output, cloudAnswer, "utf8");
7907
+ }
7908
+ if (!options.quiet) console.log(cloudAnswer);
7909
+ return cloudAnswer;
7910
+ }
7864
7911
  const geoAnswer = await buildGeoDirectAnswer(question);
7865
7912
  if (geoAnswer) {
7866
7913
  if (historyEnabled) {
@@ -7961,6 +8008,119 @@ async function buildDirectDataAnswer(question, dataContext) {
7961
8008
  ].join("\n");
7962
8009
  }
7963
8010
 
8011
+ async function buildCloudDirectAnswer(question) {
8012
+ if (!isCloudQuestion(question)) return "";
8013
+ const normalized = String(question || "").toLocaleLowerCase("ru-RU");
8014
+ try {
8015
+ const provider = await getCloudProvider();
8016
+ if (/(созда|сдела|добав).{0,30}(папк|директор)/iu.test(normalized) || /(папк|директор).{0,30}(созда|сдела|добав)/iu.test(normalized)) {
8017
+ const folderName = extractCloudFolderName(question) || "Новая папка";
8018
+ const remotePath = normalizeCloudUserPath(folderName, provider);
8019
+ const result = await cloudCreateFolder(provider, remotePath);
8020
+ return `Папка создана или уже была на облачном диске: ${result.path}`;
8021
+ }
8022
+
8023
+ if (/(покажи|список|что.+лежит|файлы|папки)/iu.test(normalized)) {
8024
+ const remotePath = extractCloudPath(question) || cloudRootForProvider(provider);
8025
+ const rows = await cloudList(provider, normalizeCloudUserPath(remotePath, provider));
8026
+ if (rows.length === 0) return `В папке ${normalizeCloudUserPath(remotePath, provider)} нет данных.`;
8027
+ return [
8028
+ `Облачный диск ${provider}, папка ${normalizeCloudUserPath(remotePath, provider)}:`,
8029
+ ...rows.slice(0, 20).map((row, index) => `${index + 1}. ${row.type === "dir" ? "папка" : "файл"} ${row.name} — ${row.path}`),
8030
+ ].join("\n");
8031
+ }
8032
+
8033
+ if (/(найди|поиск|где лежит)/iu.test(normalized)) {
8034
+ const query = cleanupCloudQuery(question);
8035
+ const rows = await cloudFind(provider, query, { path: cloudRootForProvider(provider), limit: 10 });
8036
+ if (rows.length === 0) return `На облачном диске не нашел: ${query}`;
8037
+ return [
8038
+ `Нашел на облачном диске ${provider}:`,
8039
+ ...rows.map((row, index) => `${index + 1}. ${row.name} — ${row.path}`),
8040
+ ].join("\n");
8041
+ }
8042
+
8043
+ if (/(ссылк|поделись|опубликуй)/iu.test(normalized)) {
8044
+ const remotePath = extractCloudPath(question);
8045
+ if (!remotePath) return "Укажите путь к файлу на облачном диске, например: /IOLA/reports/report.md";
8046
+ const result = await cloudShare(provider, normalizeCloudUserPath(remotePath, provider));
8047
+ return `Публичная ссылка: ${result.publicUrl}`;
8048
+ }
8049
+
8050
+ if (/(сохрани|запиши).{0,40}(на яндекс диске|в облак|на диск)/iu.test(normalized)) {
8051
+ const text = cleanupCloudSaveText(question);
8052
+ if (!text) return "Что сохранить на облачный диск?";
8053
+ const remotePath = `${cloudRootForProvider(provider)}/notes/iola-${timestampForFile()}.txt`;
8054
+ const tempPath = path.join(CONFIG_DIR, `cloud-save-${Date.now()}.txt`);
8055
+ await mkdir(CONFIG_DIR, { recursive: true });
8056
+ await writeFile(tempPath, text, "utf8");
8057
+ try {
8058
+ await cloudUpload(provider, tempPath, remotePath, { overwrite: true });
8059
+ } finally {
8060
+ await rm(tempPath, { force: true }).catch(() => {});
8061
+ }
8062
+ return `Сохранил текст на облачный диск: ${remotePath}`;
8063
+ }
8064
+ } catch (error) {
8065
+ return `Не смог выполнить облачный запрос: ${error instanceof Error ? error.message : String(error)}`;
8066
+ }
8067
+ return "";
8068
+ }
8069
+
8070
+ function isCloudQuestion(question) {
8071
+ return /(яндекс.?диск|yandex.?disk|облак|облачн|на диск|с диска|в диск|cloud|mail\.?ru|публичн.*ссылк|поделиться.*файл)/iu.test(String(question || ""));
8072
+ }
8073
+
8074
+ function normalizeCloudUserPath(value, provider = "yandex-disk") {
8075
+ const root = cloudRootForProvider(provider);
8076
+ let text = String(value || "").trim().replace(/\\/g, "/");
8077
+ text = text.replace(/^["'«»]+|["'«»]+$/gu, "").trim();
8078
+ if (!text) return root;
8079
+ if (text.startsWith("/")) return text;
8080
+ if (normalizeGeoText(text).startsWith(normalizeGeoText(root).replace(/^\//u, ""))) return `/${text.replace(/^\/+/u, "")}`;
8081
+ return `${root.replace(/\/+$/u, "")}/${text.replace(/^\/+/u, "")}`;
8082
+ }
8083
+
8084
+ function extractCloudFolderName(question) {
8085
+ const text = String(question || "").trim();
8086
+ const match = text.match(/(?:папк[ауи]?|директор(?:ию|ия|ии)?)\s+["'«]?([^"'».,!?]+)["'»]?/iu)
8087
+ || text.match(/(?:названи(?:ем|е)|имя)\s+["'«]?([^"'».,!?]+)["'»]?/iu);
8088
+ if (!match?.[1]) return "";
8089
+ return cleanupCloudObjectName(match[1]);
8090
+ }
8091
+
8092
+ function extractCloudPath(question) {
8093
+ const text = String(question || "").trim();
8094
+ const pathMatch = text.match(/(?:^|\s)(\/IOLA\/[^\s]+|\/[^\s]+)/iu);
8095
+ if (pathMatch?.[1]) return pathMatch[1];
8096
+ const quoted = text.match(/["«]([^"»]+)["»]/u);
8097
+ if (quoted?.[1]) return quoted[1];
8098
+ const afterFolder = text.match(/(?:папк[аеуы]?|файл[ае]?)\s+([^,.!?]+)/iu);
8099
+ return afterFolder?.[1] ? cleanupCloudObjectName(afterFolder[1]) : "";
8100
+ }
8101
+
8102
+ function cleanupCloudObjectName(value) {
8103
+ return String(value || "")
8104
+ .replace(/\b(?:на|в|у меня|яндекс.?диск(?:е)?|диск(?:е)?|облак(?:е|о)?|создай|сделай|добавь|покажи)\b/giu, " ")
8105
+ .replace(/\s+/g, " ")
8106
+ .trim();
8107
+ }
8108
+
8109
+ function cleanupCloudQuery(question) {
8110
+ return String(question || "")
8111
+ .replace(/\b(?:найди|поиск|где лежит|на|в|яндекс.?диск(?:е)?|облак(?:е|о)?|диск(?:е)?|файл|документ)\b/giu, " ")
8112
+ .replace(/[?.!]+$/u, "")
8113
+ .replace(/\s+/g, " ")
8114
+ .trim();
8115
+ }
8116
+
8117
+ function cleanupCloudSaveText(question) {
8118
+ return String(question || "")
8119
+ .replace(/^.*?(?:сохрани|запиши)\s+/iu, "")
8120
+ .replace(/\s+(?:на яндекс диске|в облак[ео]|на диск).*$/iu, "")
8121
+ .trim();
8122
+ }
8123
+
7964
8124
  function detectDirectDataFields(normalizedQuestion) {
7965
8125
  const fields = [];
7966
8126
  if (/(директ|руководител|заведующ|кто возглавляет)/iu.test(normalizedQuestion)) fields.push("head");
@@ -8244,6 +8404,11 @@ async function localToolAsk(question, providerConfig, options) {
8244
8404
  if (!options.quiet) console.log(casualAnswer);
8245
8405
  return casualAnswer;
8246
8406
  }
8407
+ const cloudAnswer = await buildCloudDirectAnswer(question);
8408
+ if (cloudAnswer) {
8409
+ if (!options.quiet) console.log(cloudAnswer);
8410
+ return cloudAnswer;
8411
+ }
8247
8412
  const geoAnswer = await buildGeoDirectAnswer(question);
8248
8413
  if (geoAnswer) {
8249
8414
  if (!options.quiet) console.log(geoAnswer);
@@ -10797,8 +10962,10 @@ function selectSkillsForPrompt(config, question = "", options = {}) {
10797
10962
  if (enabled.has("local-model")) selected.add("local-model");
10798
10963
  if (enabled.has("open-data") && shouldUseDataContext(question, options)) selected.add("open-data");
10799
10964
  if (enabled.has("geo") && isGeoQuestion(normalized)) selected.add("geo");
10965
+ const cloudQuestion = isCloudQuestion(normalized);
10966
+ if (enabled.has("personal-docs") && cloudQuestion) selected.add("personal-docs");
10800
10967
  if (enabled.has("reports") && /(отчет|отчёт|выгруз|csv|xlsx|качество|провер)/iu.test(normalized)) selected.add("reports");
10801
- if (enabled.has("local-files") && (options.files || /(файл|папк|readme|документ|архив)/iu.test(normalized))) selected.add("local-files");
10968
+ if (enabled.has("local-files") && !cloudQuestion && (options.files || /(файл|папк|readme|документ|архив)/iu.test(normalized))) selected.add("local-files");
10802
10969
  if (enabled.has("browser-agent") && /(браузер|сайт|страниц|url|https?:\/\/)/iu.test(normalized)) selected.add("browser-agent");
10803
10970
  return selected;
10804
10971
  }
@@ -50,6 +50,7 @@ iola cloud status
50
50
  iola cloud doctor
51
51
  iola cloud use yandex-disk
52
52
  iola cloud ls /IOLA
53
+ iola cloud mkdir /IOLA/Фото
53
54
  iola cloud find "справка" --path /IOLA
54
55
  iola cloud upload report.md /IOLA/reports/report.md
55
56
  iola cloud download /IOLA/reports/report.md ./report.md
@@ -18,6 +18,7 @@ iola cloud status
18
18
  iola cloud doctor
19
19
  iola cloud use yandex-disk
20
20
  iola cloud ls /IOLA
21
+ iola cloud mkdir /IOLA/Фото
21
22
  iola cloud find "справка" --path /IOLA
22
23
  iola cloud upload report.md /IOLA/reports/report.md
23
24
  iola cloud download /IOLA/reports/report.md ./report.md
@@ -36,20 +37,60 @@ iola cloud backup
36
37
  - получение OAuth-токена: `https://yandex.ru/dev/id/doc/ru/access`
37
38
  - получение токена вручную для проверки: `https://yandex.ru/dev/id/doc/ru/tokens/debug-token`
38
39
 
39
- Что сделать:
40
+ ### Получение OAuth-токена Яндекс Диска
40
41
 
41
42
  1. Откройте страницу Яндекс OAuth: `https://oauth.yandex.ru/`.
42
- 2. Создайте приложение для личного использования.
43
- 3. В правах приложения выберите доступ к Яндекс Диску.
44
- 4. Получите OAuth-токен.
45
- 5. В CLI выполните:
43
+ 2. Если Яндекс просит пройти верификацию через Госуслуги, пройдите ее. Без этого создание OAuth-приложения может быть недоступно.
44
+ 3. Нажмите `Создать`.
45
+ 4. В окне `Какое приложение хотите создать?` выберите `Для авторизации пользователей`.
46
+ 5. Нажмите `Перейти к созданию`.
47
+ 6. На шаге `Создание приложения` заполните:
48
+ - `Название вашего сервиса`: например `iola-cli`;
49
+ - `Иконка сервиса`: загрузите PNG-иконку до 1 МБ. Готовая иконка iola-cli доступна по ссылке: `https://raw.githubusercontent.com/adm-iola/iola-cli/main/docs/assets/iola-oauth-icon.png`;
50
+ - `Почта для связи`: оставьте свою почту или укажите актуальную.
51
+ 7. Нажмите `Продолжить`.
52
+ 8. На шаге `Платформы приложений` выберите `Веб-сервисы`.
53
+ 9. В поле `Redirect URI` укажите:
54
+
55
+ ```text
56
+ https://oauth.yandex.ru/verification_code
57
+ ```
58
+
59
+ 10. Нажмите кнопку добавления рядом с `Redirect URI`, если интерфейс показывает такую кнопку.
60
+ 11. В поле `Suggest Hostname`, если оно обязательно, укажите сайт проекта или любой понятный URL сервиса, например:
61
+
62
+ ```text
63
+ https://github.com
64
+ ```
65
+
66
+ 12. Нажмите `Продолжить`.
67
+ 13. На шаге `Права доступа к данным пользователей` не выбирайте лишние основные права: телефон, почта, дата рождения, профиль не нужны.
68
+ 14. В блоке `Дополнительные` через поле `Название доступа` добавьте три права:
69
+ - `Чтение всего Диска` / `cloud_api:disk.read`;
70
+ - `Запись в любом месте на Диске` / `cloud_api:disk.write`;
71
+ - `Доступ к информации о Диске` / `cloud_api:disk.info`.
72
+ 15. Нажмите `Продолжить`.
73
+ 16. На финальном экране проверьте, что приложение запрашивает только три права Диска.
74
+ 17. Нажмите `Всё верно, создать приложение`.
75
+ 18. На странице созданного приложения скопируйте `ClientID`.
76
+ 19. Откройте ссылку, заменив `CLIENT_ID` на ваш `ClientID`:
77
+
78
+ ```text
79
+ https://oauth.yandex.ru/authorize?response_type=token&client_id=CLIENT_ID
80
+ ```
81
+
82
+ 20. На экране авторизации нажмите `Войти как ...`.
83
+ 21. Если Яндекс показывает предупреждение `Сервис ещё не верифицирован`, это нормально для личного приложения. Продолжайте только если приложение создавали вы сами и доверяете ему.
84
+ 22. Яндекс перенаправит на страницу `verification_code`, где будет показан OAuth-токен. В адресной строке он также находится после `access_token=`.
85
+ 23. Скопируйте OAuth-токен.
86
+ 24. В CLI выполните:
46
87
 
47
88
  ```bash
48
89
  iola cloud setup yandex-disk
49
90
  ```
50
91
 
51
- 6. Вставьте OAuth-токен.
52
- 7. Проверьте подключение:
92
+ 25. Вставьте OAuth-токен.
93
+ 26. Проверьте подключение:
53
94
 
54
95
  ```bash
55
96
  iola cloud doctor
@@ -57,6 +98,20 @@ iola cloud doctor
57
98
 
58
99
  Секрет сохраняется локально в `~/.iola/secrets.json`.
59
100
 
101
+ После успешной проверки CLI создаст или использует папку `/IOLA` на Яндекс Диске.
102
+
103
+ Обычно такой токен выдается на длительный срок. Если доступ перестал работать, получите новый токен через ту же ссылку авторизации и повторите `iola cloud setup yandex-disk`.
104
+
105
+ После подключения можно работать с облаком обычными фразами в CLI:
106
+
107
+ ```text
108
+ создай у меня на яндекс диске папку фото
109
+ покажи что лежит на яндекс диске
110
+ найди на яндекс диске справку
111
+ ```
112
+
113
+ Такие запросы обрабатывает skill `personal-docs`. Они не требуют включать локальный режим `iola files mode`, потому что это не работа с файлами на ПК.
114
+
60
115
  ## Облако Mail.ru
61
116
 
62
117
  Облако Mail.ru подключается через WebDAV.
@@ -114,6 +114,15 @@ iola geo services "Йошкар-Ола, улица Петрова, 15"
114
114
 
115
115
  Сохранить ответ CLI, отчет, карточку учреждения или список ближайших объектов в `/IOLA`.
116
116
 
117
+ ### cloud-create-folder
118
+
119
+ Создать папку на подключенном облачном диске.
120
+
121
+ Пример:
122
+
123
+ - `создай у меня на яндекс диске папку фото`;
124
+ - `создай папку документы в облаке`.
125
+
117
126
  ### cloud-share-link
118
127
 
119
128
  Создать публичную ссылку на файл. В первом контуре поддерживается для Яндекс Диска.