@iola_adm/iola-cli 0.2.8 → 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.8",
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 сейчас поддерживаются только для Яндекс Диска.");
@@ -7873,6 +7894,20 @@ async function aiAsk(args, context = {}) {
7873
7894
  const historyEnabled = !options.bare && !options["no-history"] && isFeatureEnabled("sqlite-history");
7874
7895
  const sessionId = historyEnabled && isFeatureEnabled("sessions") ? ensureSessionForAsk(options, providerConfig, question) : null;
7875
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
+ }
7876
7911
  const geoAnswer = await buildGeoDirectAnswer(question);
7877
7912
  if (geoAnswer) {
7878
7913
  if (historyEnabled) {
@@ -7973,6 +8008,119 @@ async function buildDirectDataAnswer(question, dataContext) {
7973
8008
  ].join("\n");
7974
8009
  }
7975
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
+
7976
8124
  function detectDirectDataFields(normalizedQuestion) {
7977
8125
  const fields = [];
7978
8126
  if (/(директ|руководител|заведующ|кто возглавляет)/iu.test(normalizedQuestion)) fields.push("head");
@@ -8256,6 +8404,11 @@ async function localToolAsk(question, providerConfig, options) {
8256
8404
  if (!options.quiet) console.log(casualAnswer);
8257
8405
  return casualAnswer;
8258
8406
  }
8407
+ const cloudAnswer = await buildCloudDirectAnswer(question);
8408
+ if (cloudAnswer) {
8409
+ if (!options.quiet) console.log(cloudAnswer);
8410
+ return cloudAnswer;
8411
+ }
8259
8412
  const geoAnswer = await buildGeoDirectAnswer(question);
8260
8413
  if (geoAnswer) {
8261
8414
  if (!options.quiet) console.log(geoAnswer);
@@ -10809,8 +10962,10 @@ function selectSkillsForPrompt(config, question = "", options = {}) {
10809
10962
  if (enabled.has("local-model")) selected.add("local-model");
10810
10963
  if (enabled.has("open-data") && shouldUseDataContext(question, options)) selected.add("open-data");
10811
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");
10812
10967
  if (enabled.has("reports") && /(отчет|отчёт|выгруз|csv|xlsx|качество|провер)/iu.test(normalized)) selected.add("reports");
10813
- 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");
10814
10969
  if (enabled.has("browser-agent") && /(браузер|сайт|страниц|url|https?:\/\/)/iu.test(normalized)) selected.add("browser-agent");
10815
10970
  return selected;
10816
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
@@ -45,7 +46,7 @@ iola cloud backup
45
46
  5. Нажмите `Перейти к созданию`.
46
47
  6. На шаге `Создание приложения` заполните:
47
48
  - `Название вашего сервиса`: например `iola-cli`;
48
- - `Иконка сервиса`: загрузите любую простую PNG-иконку до 1 МБ;
49
+ - `Иконка сервиса`: загрузите PNG-иконку до 1 МБ. Готовая иконка iola-cli доступна по ссылке: `https://raw.githubusercontent.com/adm-iola/iola-cli/main/docs/assets/iola-oauth-icon.png`;
49
50
  - `Почта для связи`: оставьте свою почту или укажите актуальную.
50
51
  7. Нажмите `Продолжить`.
51
52
  8. На шаге `Платформы приложений` выберите `Веб-сервисы`.
@@ -101,6 +102,16 @@ iola cloud doctor
101
102
 
102
103
  Обычно такой токен выдается на длительный срок. Если доступ перестал работать, получите новый токен через ту же ссылку авторизации и повторите `iola cloud setup yandex-disk`.
103
104
 
105
+ После подключения можно работать с облаком обычными фразами в CLI:
106
+
107
+ ```text
108
+ создай у меня на яндекс диске папку фото
109
+ покажи что лежит на яндекс диске
110
+ найди на яндекс диске справку
111
+ ```
112
+
113
+ Такие запросы обрабатывает skill `personal-docs`. Они не требуют включать локальный режим `iola files mode`, потому что это не работа с файлами на ПК.
114
+
104
115
  ## Облако Mail.ru
105
116
 
106
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
  Создать публичную ссылку на файл. В первом контуре поддерживается для Яндекс Диска.