@iola_adm/iola-cli 0.1.66 → 0.1.68

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +65 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.66",
3
+ "version": "0.1.68",
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
@@ -4077,18 +4077,14 @@ async function listAiModels(provider) {
4077
4077
  }
4078
4078
 
4079
4079
  const payload = await response.json();
4080
- return (payload.models || []).map((model) => ({
4080
+ const models = (payload.models || []).map((model) => ({
4081
4081
  id: model.name,
4082
4082
  provider: "ollama",
4083
4083
  note: model.modified_at ? `updated ${model.modified_at}` : "local",
4084
4084
  }));
4085
+ return models.length > 0 ? models : getRecommendedOllamaModels("not installed");
4085
4086
  } catch {
4086
- return [
4087
- { id: "llama3.2:1b", provider: "ollama", note: "recommended low RAM" },
4088
- { id: "llama3.2:3b", provider: "ollama", note: "recommended standard" },
4089
- { id: "qwen3:4b", provider: "ollama", note: "recommended balanced" },
4090
- { id: "qwen3:8b", provider: "ollama", note: "recommended good GPU" },
4091
- ];
4087
+ return getRecommendedOllamaModels("recommended");
4092
4088
  }
4093
4089
  }
4094
4090
 
@@ -4139,6 +4135,15 @@ async function listAiModels(provider) {
4139
4135
  ];
4140
4136
  }
4141
4137
 
4138
+ function getRecommendedOllamaModels(notePrefix = "recommended") {
4139
+ return [
4140
+ { id: "llama3.2:1b", provider: "ollama", note: `${notePrefix} low RAM` },
4141
+ { id: "llama3.2:3b", provider: "ollama", note: `${notePrefix} standard` },
4142
+ { id: "qwen3:4b", provider: "ollama", note: `${notePrefix} balanced` },
4143
+ { id: "qwen3:8b", provider: "ollama", note: `${notePrefix} good GPU` },
4144
+ ];
4145
+ }
4146
+
4142
4147
  async function printAiProfiles() {
4143
4148
  const config = await loadConfig();
4144
4149
  const active = getActiveProfileName(config);
@@ -4455,6 +4460,10 @@ async function chooseAiModel(provider) {
4455
4460
  async function switchModelTarget(target, model) {
4456
4461
  const config = await loadConfig();
4457
4462
  const provider = target === "local" ? "ollama" : target;
4463
+ if (provider === "ollama") {
4464
+ const ready = await ensureOllamaModelAvailable(model, config);
4465
+ if (!ready) return;
4466
+ }
4458
4467
  const profileName = provider === "ollama" ? "local" : provider;
4459
4468
  const currentProfile = config.ai.profiles?.[profileName] || buildProfileFromOptions(provider, { model });
4460
4469
  const profile = {
@@ -4480,6 +4489,38 @@ async function switchModelTarget(target, model) {
4480
4489
  console.log(`Активная модель: ${profileName} (${provider}, ${model})`);
4481
4490
  }
4482
4491
 
4492
+ async function ensureOllamaModelAvailable(model, config = null) {
4493
+ if (await isOllamaModelInstalled(model, config)) return true;
4494
+
4495
+ const command = await resolveOllamaCommand();
4496
+ if (!command) {
4497
+ console.log("Ollama CLI не найден в PATH, хотя локальный API может отвечать.");
4498
+ console.log("Откройте новый PowerShell или запустите мастер: iola ai setup ollama");
4499
+ return false;
4500
+ }
4501
+
4502
+ const shouldInstall = await confirm(`Локальная модель ${model} не скачана. Скачать через "ollama pull ${model}"? [Y/n] `);
4503
+ if (!shouldInstall) {
4504
+ console.log("Переключение на локальную модель отменено.");
4505
+ return false;
4506
+ }
4507
+
4508
+ await runCommand(command, ["pull", model], { inherit: true });
4509
+ return true;
4510
+ }
4511
+
4512
+ async function isOllamaModelInstalled(model, loadedConfig = null) {
4513
+ try {
4514
+ const config = loadedConfig || await loadConfig();
4515
+ const response = await fetch(`${config.ai.profiles?.local?.baseUrl || "http://127.0.0.1:11434"}/api/tags`);
4516
+ if (!response.ok) return false;
4517
+ const payload = await response.json();
4518
+ return (payload.models || []).some((entry) => entry.name === model);
4519
+ } catch {
4520
+ return false;
4521
+ }
4522
+ }
4523
+
4483
4524
  async function askText(question) {
4484
4525
  if (!process.stdin.isTTY) return "";
4485
4526
  const rl = readline.createInterface({ input, output });
@@ -8771,7 +8812,8 @@ async function saveConfig(value) {
8771
8812
  }
8772
8813
 
8773
8814
  async function writeConfig(value) {
8774
- const errors = validateConfig(value);
8815
+ const sanitized = sanitizeConfig(value);
8816
+ const errors = validateConfig(sanitized);
8775
8817
  if (errors.length > 0) {
8776
8818
  throw new Error(`Конфигурация не сохранена: ${errors.join("; ")}`);
8777
8819
  }
@@ -8779,7 +8821,7 @@ async function writeConfig(value) {
8779
8821
  if (existsSync(CONFIG_FILE)) {
8780
8822
  await copyFile(CONFIG_FILE, LAST_GOOD_CONFIG_FILE).catch(() => {});
8781
8823
  }
8782
- await writeFile(CONFIG_FILE, `${JSON.stringify(value, null, 2)}\n`, "utf8");
8824
+ await writeFile(CONFIG_FILE, `${JSON.stringify(sanitized, null, 2)}\n`, "utf8");
8783
8825
  }
8784
8826
 
8785
8827
  async function loadConfig() {
@@ -8788,7 +8830,7 @@ async function loadConfig() {
8788
8830
  const value = await readConfigLayer(layer);
8789
8831
  if (value) config = mergeConfig(config, value);
8790
8832
  }
8791
- return config;
8833
+ return sanitizeConfig(config);
8792
8834
  }
8793
8835
 
8794
8836
  async function loadConfigLayers() {
@@ -8805,7 +8847,7 @@ async function loadConfigLayers() {
8805
8847
  continue;
8806
8848
  }
8807
8849
  const value = await readConfigLayer(layer.file);
8808
- rows.push({ ...layer, exists: Boolean(value), value, errors: value ? validateConfig(mergeConfig(DEFAULT_AI_CONFIG, value)) : [] });
8850
+ rows.push({ ...layer, exists: Boolean(value), value, errors: value ? validateConfig(sanitizeConfig(mergeConfig(DEFAULT_AI_CONFIG, value))) : [] });
8809
8851
  }
8810
8852
  rows.push({ scope: "runtime", file: "process.env", exists: true, value: { IOLA_API_BASE_URL: process.env.IOLA_API_BASE_URL || "", IOLA_MCP_BASE_URL: process.env.IOLA_MCP_BASE_URL || "" }, errors: [] });
8811
8853
  return rows;
@@ -8900,6 +8942,18 @@ function mergeConfig(base, override) {
8900
8942
  };
8901
8943
  }
8902
8944
 
8945
+ function sanitizeConfig(config) {
8946
+ const next = JSON.parse(JSON.stringify(config || {}));
8947
+ if (next.permissions?.localTools && typeof next.permissions.localTools === "object") {
8948
+ for (const tool of Object.keys(next.permissions.localTools)) {
8949
+ if (!ALL_TOOL_ALIASES.includes(tool)) {
8950
+ delete next.permissions.localTools[tool];
8951
+ }
8952
+ }
8953
+ }
8954
+ return next;
8955
+ }
8956
+
8903
8957
  function validateConfig(config) {
8904
8958
  const errors = [];
8905
8959
  if (!config || typeof config !== "object") errors.push("config must be object");