@iola_adm/iola-cli 0.1.85 → 0.1.87
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 +13 -5
- package/package.json +2 -2
- package/src/cli.js +525 -48
- package/src/iola_hf_runner.py +136 -0
- package/test/smoke-test.js +6 -0
package/README.md
CHANGED
|
@@ -116,11 +116,19 @@ iola review config
|
|
|
116
116
|
iola browser status
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
Локальная модель через
|
|
119
|
+
Локальная модель IOLA через Hugging Face:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
iola ai setup iola --yes
|
|
123
|
+
iola ask "дай телефон школы № 2"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
CLI скачивает `LMSerg/iola-1b-router-2026-05-28-merged` в `~/.iola/models/router`, проверяет свежесть модели при запуске AI-команд и обновляет ее автоматически.
|
|
127
|
+
|
|
128
|
+
Ollama остается опциональным runtime:
|
|
120
129
|
|
|
121
130
|
```bash
|
|
122
131
|
iola ai setup ollama
|
|
123
|
-
iola ask "выгрузи школы на Петрова в csv" --profile local --tools
|
|
124
132
|
```
|
|
125
133
|
|
|
126
134
|
Обновление:
|
|
@@ -154,8 +162,8 @@ iola version --check
|
|
|
154
162
|
- интеграция с публичным MCP-сервером Йошкар-Олы;
|
|
155
163
|
- поиск и выгрузка открытых данных;
|
|
156
164
|
- локальная SQLite-БД, история, сессии и FTS-поиск;
|
|
157
|
-
- AI-профили для Ollama, OpenAI, OpenRouter и Codex CLI;
|
|
158
|
-
- локальный tool-agent для
|
|
165
|
+
- AI-профили для IOLA local, Ollama, OpenAI, OpenRouter и Codex CLI;
|
|
166
|
+
- локальный tool-agent для модели IOLA с tools `search_data`, `search_entities`, `resolve_entity_field`, `get_card`, `export_report`, `file_read`, `browser_open`;
|
|
159
167
|
- ленивые skills, toolsets, permissions, memory, hooks и готовые agents;
|
|
160
168
|
- subagents, skill bundles, layered settings, usage/budget accounting и trajectory export;
|
|
161
169
|
- локальный MCP-сервер по stdio/http для подключения iola-cli к другим AI-клиентам;
|
|
@@ -169,6 +177,6 @@ iola version --check
|
|
|
169
177
|
- staged changes, импорт локальных CSV/JSON, индекс локальных документов, report packs, plugins и локальный MCP endpoint;
|
|
170
178
|
- чтение и индексирование `.docx`, `.xlsx`, `.pptx`, `.pdf`, `.md`, `.txt`, `.csv`, `.json`, `.html`;
|
|
171
179
|
- работа с архивами через 7-Zip: `.zip`, `.7z`, `.rar`, `.tar`, `.gz`, `.tgz`, `.bz2`, `.xz` и другие;
|
|
172
|
-
- расширенный `iola onboard` с установкой 7-Zip, браузерного runtime, Ollama, Codex CLI и настройкой выбранных компонентов;
|
|
180
|
+
- расширенный `iola onboard` с установкой 7-Zip, браузерного runtime, IOLA local, Ollama, Codex CLI и настройкой выбранных компонентов;
|
|
173
181
|
- cron-задачи, локальный daemon, web dashboard и RPC для автоматизаций;
|
|
174
182
|
- контекстные файлы `IOLA.md` и `.iola/context.md`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iola_adm/iola-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.87",
|
|
4
4
|
"description": "CLI и AI-агент городского округа Йошкар-Ола.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/adm-iola/iola-cli#readme",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"iola": "bin/iola.js"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
|
-
"postinstall": "node --no-warnings bin/iola.js db init --silent && node --no-warnings bin/iola.js browser install",
|
|
19
|
+
"postinstall": "node --no-warnings bin/iola.js db init --silent && node --no-warnings bin/iola.js browser install && node --no-warnings bin/iola.js ai setup iola --yes --quiet --optional",
|
|
20
20
|
"start": "node --no-warnings bin/iola.js",
|
|
21
21
|
"test": "node --no-warnings --check bin/iola.js && node --no-warnings --check src/cli.js && node --no-warnings test/smoke-test.js"
|
|
22
22
|
},
|
package/src/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import { inflateRawSync, inflateSync } from "node:zlib";
|
|
13
13
|
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
const API_BASE_URL = process.env.IOLA_API_BASE_URL || "https://apiiola.yasg.ru/api/v1";
|
|
15
16
|
const MCP_BASE_URL = process.env.IOLA_MCP_BASE_URL || "https://apiiola.yasg.ru";
|
|
16
17
|
const MIN_NODE_VERSION = "22.5.0";
|
|
@@ -20,20 +21,25 @@ const LAST_GOOD_CONFIG_FILE = path.join(CONFIG_DIR, "config.last-good.json");
|
|
|
20
21
|
const SECRETS_FILE = path.join(CONFIG_DIR, "secrets.json");
|
|
21
22
|
const DB_FILE = path.join(CONFIG_DIR, "iola.db");
|
|
22
23
|
const DB_SCHEMA_VERSION = 8;
|
|
24
|
+
const IOLA_LOCAL_MODEL = "iola-router-1b";
|
|
25
|
+
const IOLA_LOCAL_OLLAMA_MODEL = "gemma3:1b";
|
|
26
|
+
const IOLA_ROUTER_HF_REPO = process.env.IOLA_ROUTER_HF_REPO || "LMSerg/iola-1b-router-2026-05-28-merged";
|
|
27
|
+
const IOLA_MODEL_DIR = path.join(CONFIG_DIR, "models", "router");
|
|
28
|
+
const IOLA_MODEL_RUNTIME_DIR = path.join(CONFIG_DIR, "model-runtime");
|
|
29
|
+
const IOLA_MODEL_RUNNER = path.resolve(__dirname, "iola_hf_runner.py");
|
|
23
30
|
const PROJECT_IOLA_DIR = path.join(process.cwd(), ".iola");
|
|
24
31
|
const PROJECT_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "config.json");
|
|
25
32
|
const LOCAL_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "local.json");
|
|
26
33
|
const BROWSER_RUNTIME_DIR = path.join(CONFIG_DIR, "browser-runtime");
|
|
27
34
|
const BROWSER_RUNTIME_PACKAGE = path.join(BROWSER_RUNTIME_DIR, "node_modules", "playwright", "package.json");
|
|
28
35
|
const INDEXABLE_EXTENSIONS = /\.(md|txt|csv|json|html|docx|xlsx|pptx|pdf)$/i;
|
|
29
|
-
const LOCAL_TOOLS = ["search_data", "get_card", "export_report", "file_read", "browser_open"];
|
|
36
|
+
const LOCAL_TOOLS = ["search_data", "search_entities", "resolve_entity_field", "get_card", "export_report", "file_read", "browser_open"];
|
|
30
37
|
const LEGACY_LOCAL_TOOLS = ["search_local", "export_data", "run_report", "save_view"];
|
|
31
38
|
const FILE_TOOLS = ["files_tree", "files_read", "files_search", "files_write", "files_patch"];
|
|
32
39
|
const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS];
|
|
33
40
|
const ALL_TOOL_ALIASES = [...ALL_LOCAL_TOOLS, ...LEGACY_LOCAL_TOOLS];
|
|
34
41
|
const HOOK_EVENTS = ["SessionStart", "BeforeTool", "AfterTool", "PreToolUse", "PostToolUse", "OnError", "AfterSync", "BeforeExport", "SessionEnd"];
|
|
35
42
|
const DAEMON_PORT = Number(process.env.IOLA_DAEMON_PORT || 18790);
|
|
36
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
37
43
|
const BUILTIN_SKILLS_DIR = path.resolve(__dirname, "..", "skills");
|
|
38
44
|
const USER_SKILLS_DIR = path.join(CONFIG_DIR, "skills");
|
|
39
45
|
const PROJECT_CONTEXT_FILE = path.join(process.cwd(), "IOLA.md");
|
|
@@ -102,9 +108,9 @@ const SKILL_BUNDLES = {
|
|
|
102
108
|
requirements: ["files mode read-only/workspace-write", "7-Zip для архивов"],
|
|
103
109
|
},
|
|
104
110
|
"local-agent": {
|
|
105
|
-
description: "Локальная модель
|
|
111
|
+
description: "Локальная модель IOLA с проверочным reasoning и локальными tools.",
|
|
106
112
|
skills: ["local-model", "open-data"],
|
|
107
|
-
requirements: ["
|
|
113
|
+
requirements: ["Python", "локальная модель"],
|
|
108
114
|
},
|
|
109
115
|
};
|
|
110
116
|
let onboardRanThisProcess = false;
|
|
@@ -115,14 +121,14 @@ const DEFAULT_AI_CONFIG = {
|
|
|
115
121
|
},
|
|
116
122
|
ai: {
|
|
117
123
|
activeProfile: "local",
|
|
118
|
-
provider: "
|
|
119
|
-
model:
|
|
120
|
-
baseUrl: "http://127.0.0.1:11434",
|
|
124
|
+
provider: "iola",
|
|
125
|
+
model: IOLA_LOCAL_MODEL,
|
|
121
126
|
profiles: {
|
|
122
127
|
local: {
|
|
123
|
-
provider: "
|
|
124
|
-
model:
|
|
125
|
-
|
|
128
|
+
provider: "iola",
|
|
129
|
+
model: IOLA_LOCAL_MODEL,
|
|
130
|
+
repo: IOLA_ROUTER_HF_REPO,
|
|
131
|
+
modelDir: IOLA_MODEL_DIR,
|
|
126
132
|
},
|
|
127
133
|
openai: {
|
|
128
134
|
provider: "openai",
|
|
@@ -146,6 +152,8 @@ const DEFAULT_AI_CONFIG = {
|
|
|
146
152
|
permissions: {
|
|
147
153
|
localTools: {
|
|
148
154
|
search_data: true,
|
|
155
|
+
search_entities: true,
|
|
156
|
+
resolve_entity_field: true,
|
|
149
157
|
get_card: true,
|
|
150
158
|
export_report: true,
|
|
151
159
|
file_read: false,
|
|
@@ -290,6 +298,7 @@ const SLASH_COMMANDS = [
|
|
|
290
298
|
{ command: "/quality", description: "качество данных" },
|
|
291
299
|
{ command: "/views", description: "saved views" },
|
|
292
300
|
{ command: "/config get", description: "конфигурация" },
|
|
301
|
+
{ command: "/uninstall --yes", description: "удалить локальные данные iola-cli" },
|
|
293
302
|
{ command: "/layers", description: "слои данных" },
|
|
294
303
|
{ command: "/data schools --limit 10", description: "данные слоя" },
|
|
295
304
|
{ command: "/schools --limit 10", description: "школы" },
|
|
@@ -381,6 +390,8 @@ const COMMANDS = new Map([
|
|
|
381
390
|
["alias", handleAlias],
|
|
382
391
|
["run", runNaturalLanguage],
|
|
383
392
|
["config", handleConfig],
|
|
393
|
+
["uninstall", handleUninstall],
|
|
394
|
+
["purge", handleUninstall],
|
|
384
395
|
["banner", showBanner],
|
|
385
396
|
["agent", startAgent],
|
|
386
397
|
["chat", startAgent],
|
|
@@ -434,6 +445,8 @@ export async function main(argv) {
|
|
|
434
445
|
throw new Error(`Нужен Node.js ${MIN_NODE_VERSION} или новее. Сейчас: ${nodeStatus.current}. Запустите: iola init --upgrade-node`);
|
|
435
446
|
}
|
|
436
447
|
|
|
448
|
+
await maybeRefreshIolaModelForCommand(command, args);
|
|
449
|
+
|
|
437
450
|
const handler = COMMANDS.get(command);
|
|
438
451
|
|
|
439
452
|
if (!handler) {
|
|
@@ -448,6 +461,23 @@ export async function main(argv) {
|
|
|
448
461
|
await handler(runtime.debugFile ? [...args, "--debug-file", runtime.debugFile] : args);
|
|
449
462
|
}
|
|
450
463
|
|
|
464
|
+
async function maybeRefreshIolaModelForCommand(command, args = []) {
|
|
465
|
+
if (process.env.IOLA_SKIP_MODEL_CHECK === "1") return;
|
|
466
|
+
const aiRuntimeCommands = new Set(["ask", "agent", "chat"]);
|
|
467
|
+
const isAiCommand = command === "ai" && !["setup", "models", "key", "profile", "profiles", "doctor"].includes(args[0]);
|
|
468
|
+
if (!aiRuntimeCommands.has(command) && !isAiCommand) return;
|
|
469
|
+
const config = await loadConfig();
|
|
470
|
+
const profile = config.ai.profiles?.[getActiveProfileName(config)];
|
|
471
|
+
if (profile?.provider !== "iola" && config.ai.provider !== "iola") return;
|
|
472
|
+
await ensureIolaModelFresh({
|
|
473
|
+
repo: profile?.repo || IOLA_ROUTER_HF_REPO,
|
|
474
|
+
modelDir: profile?.modelDir || IOLA_MODEL_DIR,
|
|
475
|
+
quiet: true,
|
|
476
|
+
}).catch((error) => {
|
|
477
|
+
if (process.env.IOLA_DEBUG) console.error(error instanceof Error ? error.message : String(error));
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
451
481
|
async function showHelp() {
|
|
452
482
|
await showBanner();
|
|
453
483
|
console.log(`iola - CLI и AI-агент городского округа "Город Йошкар-Ола"
|
|
@@ -549,10 +579,11 @@ Usage:
|
|
|
549
579
|
iola config set api.baseUrl URL
|
|
550
580
|
iola config set api.mcpBaseUrl URL
|
|
551
581
|
iola config reset
|
|
582
|
+
iola uninstall --yes
|
|
552
583
|
iola update
|
|
553
584
|
iola ask TEXT [--profile NAME] [--model MODEL] [--tools] [--files] [--plan] [--trace] [--reasoning fast|verify|vote] [--output FILE] [--schema json|table] [--events] [--no-history] [--bare] [--quiet] [--no-color] [--fail-on-empty]
|
|
554
585
|
iola data LAYER [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
|
|
555
|
-
iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
|
|
586
|
+
iola ai ask TEXT [--provider iola|ollama|openai|openrouter] [--model MODEL]
|
|
556
587
|
iola ai context TEXT [--json]
|
|
557
588
|
iola ai key set openai
|
|
558
589
|
iola ai key set openrouter
|
|
@@ -562,9 +593,10 @@ Usage:
|
|
|
562
593
|
iola ai profile add NAME --provider PROVIDER --model MODEL
|
|
563
594
|
iola ai profile use NAME
|
|
564
595
|
iola ai profile delete NAME
|
|
565
|
-
iola ai models ollama|openai|openrouter|codex [--search TEXT]
|
|
596
|
+
iola ai models iola|ollama|openai|openrouter|codex [--search TEXT]
|
|
566
597
|
iola ai doctor [--json]
|
|
567
598
|
iola ai setup
|
|
599
|
+
iola ai setup iola [--yes]
|
|
568
600
|
iola ai setup ollama [--yes] [--model MODEL]
|
|
569
601
|
iola health [--json]
|
|
570
602
|
iola layers [--json]
|
|
@@ -682,9 +714,11 @@ async function getAiReadiness() {
|
|
|
682
714
|
hasUsableOllamaModel(),
|
|
683
715
|
hasUsableCodexAuth(),
|
|
684
716
|
]);
|
|
717
|
+
const iola = await hasUsableIolaModel();
|
|
685
718
|
const openai = Boolean(process.env.OPENAI_API_KEY || secrets.openai?.apiKey);
|
|
686
719
|
const openrouter = Boolean(process.env.OPENROUTER_API_KEY || secrets.openrouter?.apiKey);
|
|
687
720
|
const providerReady = {
|
|
721
|
+
iola,
|
|
688
722
|
ollama,
|
|
689
723
|
openai,
|
|
690
724
|
openrouter,
|
|
@@ -695,8 +729,9 @@ async function getAiReadiness() {
|
|
|
695
729
|
activeProfile: activeProfileName,
|
|
696
730
|
activeProvider: activeProfile.provider || "-",
|
|
697
731
|
activeModel: activeProfile.model || "-",
|
|
698
|
-
anyReady: Boolean(ollama || openai || openrouter || codex),
|
|
732
|
+
anyReady: Boolean(iola || ollama || openai || openrouter || codex),
|
|
699
733
|
profiles: config.ai.profiles || {},
|
|
734
|
+
iola,
|
|
700
735
|
ollama,
|
|
701
736
|
openai,
|
|
702
737
|
openrouter,
|
|
@@ -705,7 +740,7 @@ async function getAiReadiness() {
|
|
|
705
740
|
}
|
|
706
741
|
|
|
707
742
|
function getFallbackAiProfile(readiness) {
|
|
708
|
-
const priority = ["openai", "openrouter", "codex", "ollama"];
|
|
743
|
+
const priority = ["iola", "openai", "openrouter", "codex", "ollama"];
|
|
709
744
|
for (const provider of priority) {
|
|
710
745
|
if (!readiness[provider]) continue;
|
|
711
746
|
const entry = Object.entries(readiness.profiles || {}).find(([, profile]) => profile.provider === provider);
|
|
@@ -1500,6 +1535,7 @@ function buildAgentStatusLine(state) {
|
|
|
1500
1535
|
const ai = state.aiStatus;
|
|
1501
1536
|
if (!ai) return cwd;
|
|
1502
1537
|
const kind = {
|
|
1538
|
+
iola: "IOLA local",
|
|
1503
1539
|
ollama: "локальная",
|
|
1504
1540
|
openai: "API",
|
|
1505
1541
|
openrouter: "API",
|
|
@@ -1885,6 +1921,10 @@ async function upgradeNodeWithInstaller() {
|
|
|
1885
1921
|
}
|
|
1886
1922
|
|
|
1887
1923
|
async function checkConfiguredModel(config) {
|
|
1924
|
+
if (config.ai.provider === "iola") {
|
|
1925
|
+
return await hasUsableIolaModel() ? "installed" : "missing";
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1888
1928
|
if (config.ai.provider !== "ollama") {
|
|
1889
1929
|
return "external-api";
|
|
1890
1930
|
}
|
|
@@ -1934,6 +1974,7 @@ async function initCli(args = []) {
|
|
|
1934
1974
|
if (!process.stdin.isTTY || options.yes) {
|
|
1935
1975
|
console.log("");
|
|
1936
1976
|
console.log("Для настройки AI используйте:");
|
|
1977
|
+
console.log(" iola ai setup iola --yes");
|
|
1937
1978
|
console.log(" iola ai setup ollama");
|
|
1938
1979
|
console.log(" iola ai key set openai");
|
|
1939
1980
|
console.log(" iola ai setup openai --model gpt-4.1-mini");
|
|
@@ -1957,19 +1998,20 @@ async function handleAi(args) {
|
|
|
1957
1998
|
if (subcommand === "help") {
|
|
1958
1999
|
await showBanner();
|
|
1959
2000
|
console.log(`AI-команды:
|
|
1960
|
-
iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
|
|
2001
|
+
iola ai ask TEXT [--provider iola|ollama|openai|openrouter] [--model MODEL]
|
|
1961
2002
|
iola ai context TEXT [--json]
|
|
1962
2003
|
iola ai key set openai
|
|
1963
2004
|
iola ai key set openrouter
|
|
1964
2005
|
iola ai key status
|
|
1965
2006
|
iola ai key delete openai|openrouter
|
|
1966
2007
|
iola ai profiles
|
|
1967
|
-
iola ai profile add NAME --provider ollama|openai|openrouter|codex --model MODEL
|
|
2008
|
+
iola ai profile add NAME --provider iola|ollama|openai|openrouter|codex --model MODEL
|
|
1968
2009
|
iola ai profile use NAME
|
|
1969
2010
|
iola ai profile delete NAME
|
|
1970
|
-
iola ai models ollama|openai|openrouter|codex [--search TEXT]
|
|
2011
|
+
iola ai models iola|ollama|openai|openrouter|codex [--search TEXT]
|
|
1971
2012
|
iola ai doctor [--json]
|
|
1972
2013
|
iola ai setup
|
|
2014
|
+
iola ai setup iola [--yes]
|
|
1973
2015
|
iola ai setup ollama [--yes] [--model MODEL]
|
|
1974
2016
|
iola ai setup openai [--model MODEL]
|
|
1975
2017
|
iola ai setup openrouter [--model MODEL]
|
|
@@ -2078,6 +2120,79 @@ async function handleConfig(args) {
|
|
|
2078
2120
|
throw new Error("Команды config: get, set, validate, schema, reset.");
|
|
2079
2121
|
}
|
|
2080
2122
|
|
|
2123
|
+
async function handleUninstall(args = []) {
|
|
2124
|
+
const options = parseOptions(args);
|
|
2125
|
+
const targets = [
|
|
2126
|
+
{
|
|
2127
|
+
label: "user data",
|
|
2128
|
+
path: CONFIG_DIR,
|
|
2129
|
+
description: "config, secrets, SQLite-БД, модель IOLA, Python/browser runtime, cache, history",
|
|
2130
|
+
},
|
|
2131
|
+
];
|
|
2132
|
+
|
|
2133
|
+
if (options.project) {
|
|
2134
|
+
targets.push({
|
|
2135
|
+
label: "project data",
|
|
2136
|
+
path: PROJECT_IOLA_DIR,
|
|
2137
|
+
description: "локальная папка .iola текущего проекта",
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
const safeTargets = targets.map((target) => ({
|
|
2142
|
+
...target,
|
|
2143
|
+
path: path.resolve(target.path),
|
|
2144
|
+
}));
|
|
2145
|
+
const home = path.resolve(os.homedir());
|
|
2146
|
+
for (const target of safeTargets) {
|
|
2147
|
+
const isUserConfig = target.path === path.resolve(CONFIG_DIR) && target.path.startsWith(home);
|
|
2148
|
+
const isProjectConfig = target.path === path.resolve(PROJECT_IOLA_DIR) && target.path.startsWith(path.resolve(process.cwd()));
|
|
2149
|
+
if (!isUserConfig && !isProjectConfig) {
|
|
2150
|
+
throw new Error(`Небезопасный путь удаления: ${target.path}`);
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
if (options["dry-run"] || options.json) {
|
|
2155
|
+
const payload = {
|
|
2156
|
+
willDelete: safeTargets.map((target) => ({
|
|
2157
|
+
label: target.label,
|
|
2158
|
+
path: target.path,
|
|
2159
|
+
exists: existsSync(target.path),
|
|
2160
|
+
description: target.description,
|
|
2161
|
+
})),
|
|
2162
|
+
willKeep: ["Codex CLI", "Codex auth/config", "npm package files"],
|
|
2163
|
+
reinstall: "npm install -g @iola_adm/iola-cli@latest",
|
|
2164
|
+
};
|
|
2165
|
+
if (options.json) printJson(payload);
|
|
2166
|
+
else printKeyValue(Object.fromEntries(payload.willDelete.map((item) => [item.label, `${item.path} (${item.exists ? "exists" : "missing"})`])));
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
if (!options.yes) {
|
|
2171
|
+
console.log("Будет удалено:");
|
|
2172
|
+
for (const target of safeTargets) {
|
|
2173
|
+
console.log(`- ${target.path}`);
|
|
2174
|
+
console.log(` ${target.description}`);
|
|
2175
|
+
}
|
|
2176
|
+
console.log("");
|
|
2177
|
+
console.log("Codex CLI и его настройки не удаляются.");
|
|
2178
|
+
const confirmed = await confirm("Удалить локальные данные iola-cli? [y/N] ");
|
|
2179
|
+
if (!confirmed) {
|
|
2180
|
+
console.log("Удаление отменено.");
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
for (const target of safeTargets) {
|
|
2186
|
+
await rm(target.path, { recursive: true, force: true });
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
console.log("Локальные данные iola-cli удалены.");
|
|
2190
|
+
console.log("Codex CLI не тронут.");
|
|
2191
|
+
console.log("Для полной переустановки npm-пакета:");
|
|
2192
|
+
console.log(" npm uninstall -g @iola_adm/iola-cli");
|
|
2193
|
+
console.log(" npm install -g @iola_adm/iola-cli@latest");
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2081
2196
|
async function handleDb(args) {
|
|
2082
2197
|
const [action = "status"] = args;
|
|
2083
2198
|
const options = parseOptions(args);
|
|
@@ -3981,6 +4096,11 @@ async function aiSetup(args) {
|
|
|
3981
4096
|
return;
|
|
3982
4097
|
}
|
|
3983
4098
|
|
|
4099
|
+
if (provider === "iola") {
|
|
4100
|
+
await setupIolaLocal(args.slice(1));
|
|
4101
|
+
return;
|
|
4102
|
+
}
|
|
4103
|
+
|
|
3984
4104
|
if (provider === "ollama") {
|
|
3985
4105
|
await setupOllama(args.slice(1));
|
|
3986
4106
|
return;
|
|
@@ -4103,8 +4223,8 @@ async function aiModels(args) {
|
|
|
4103
4223
|
const [provider] = args;
|
|
4104
4224
|
const options = parseOptions(args.slice(1));
|
|
4105
4225
|
|
|
4106
|
-
if (!["ollama", "openai", "openrouter", "codex"].includes(provider)) {
|
|
4107
|
-
throw new Error("Провайдер обязателен: iola ai models ollama|openai|openrouter|codex");
|
|
4226
|
+
if (!["iola", "ollama", "openai", "openrouter", "codex"].includes(provider)) {
|
|
4227
|
+
throw new Error("Провайдер обязателен: iola ai models iola|ollama|openai|openrouter|codex");
|
|
4108
4228
|
}
|
|
4109
4229
|
|
|
4110
4230
|
const models = await listAiModels(provider);
|
|
@@ -4125,6 +4245,18 @@ async function aiModels(args) {
|
|
|
4125
4245
|
}
|
|
4126
4246
|
|
|
4127
4247
|
async function listAiModels(provider) {
|
|
4248
|
+
if (provider === "iola") {
|
|
4249
|
+
const state = readConfigLayerSync(getIolaModelStateFile(IOLA_MODEL_DIR)) || {};
|
|
4250
|
+
const remote = await getRemoteIolaModelRevision().catch(() => null);
|
|
4251
|
+
return [{
|
|
4252
|
+
id: IOLA_LOCAL_MODEL,
|
|
4253
|
+
provider: "iola",
|
|
4254
|
+
note: state.revision
|
|
4255
|
+
? `installed ${state.revision.slice(0, 12)}${remote?.sha && remote.sha !== state.revision ? ", update available" : ""}`
|
|
4256
|
+
: "not installed",
|
|
4257
|
+
}];
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4128
4260
|
if (provider === "ollama") {
|
|
4129
4261
|
try {
|
|
4130
4262
|
const config = await loadConfig();
|
|
@@ -4198,9 +4330,9 @@ async function listAiModels(provider) {
|
|
|
4198
4330
|
|
|
4199
4331
|
function getRecommendedOllamaModels(notePrefix = "recommended") {
|
|
4200
4332
|
return [
|
|
4333
|
+
{ id: IOLA_LOCAL_OLLAMA_MODEL, provider: "ollama", note: `${notePrefix} IOLA default low RAM` },
|
|
4201
4334
|
{ id: "qwen3:1.7b", provider: "ollama", note: `${notePrefix} recommended low RAM` },
|
|
4202
4335
|
{ id: "qwen3:4b", provider: "ollama", note: `${notePrefix} recommended balanced` },
|
|
4203
|
-
{ id: "gemma3:1b", provider: "ollama", note: `${notePrefix} Gemma low RAM` },
|
|
4204
4336
|
{ id: "gemma3:4b", provider: "ollama", note: `${notePrefix} Gemma balanced` },
|
|
4205
4337
|
{ id: "llama3.2:3b", provider: "ollama", note: `${notePrefix} legacy fallback` },
|
|
4206
4338
|
{ id: "llama3.2:1b", provider: "ollama", note: `${notePrefix} minimal fallback only` },
|
|
@@ -4249,8 +4381,8 @@ async function addAiProfile(name, args) {
|
|
|
4249
4381
|
const options = parseOptions(args);
|
|
4250
4382
|
const provider = options.provider;
|
|
4251
4383
|
|
|
4252
|
-
if (!["ollama", "openai", "openrouter", "codex"].includes(provider)) {
|
|
4253
|
-
throw new Error("Провайдер должен быть ollama, openai, openrouter или codex.");
|
|
4384
|
+
if (!["iola", "ollama", "openai", "openrouter", "codex"].includes(provider)) {
|
|
4385
|
+
throw new Error("Провайдер должен быть iola, ollama, openai, openrouter или codex.");
|
|
4254
4386
|
}
|
|
4255
4387
|
|
|
4256
4388
|
const profile = buildProfileFromOptions(provider, options);
|
|
@@ -4329,7 +4461,7 @@ async function deleteAiProfile(name) {
|
|
|
4329
4461
|
}
|
|
4330
4462
|
|
|
4331
4463
|
function buildProfileFromOptions(provider, options) {
|
|
4332
|
-
const defaults = DEFAULT_AI_CONFIG.ai.profiles[provider === "ollama" ? "local" : provider];
|
|
4464
|
+
const defaults = DEFAULT_AI_CONFIG.ai.profiles[provider === "ollama" || provider === "iola" ? "local" : provider];
|
|
4333
4465
|
const profile = {
|
|
4334
4466
|
...defaults,
|
|
4335
4467
|
provider,
|
|
@@ -4340,6 +4472,11 @@ function buildProfileFromOptions(provider, options) {
|
|
|
4340
4472
|
profile.baseUrl = options["base-url"];
|
|
4341
4473
|
}
|
|
4342
4474
|
|
|
4475
|
+
if (provider === "iola") {
|
|
4476
|
+
profile.repo = options.repo || defaults.repo || IOLA_ROUTER_HF_REPO;
|
|
4477
|
+
profile.modelDir = options["model-dir"] || defaults.modelDir || IOLA_MODEL_DIR;
|
|
4478
|
+
}
|
|
4479
|
+
|
|
4343
4480
|
if (provider === "codex") {
|
|
4344
4481
|
profile.sandbox = options.sandbox || defaults.sandbox || "read-only";
|
|
4345
4482
|
profile.approval = options.approval || defaults.approval || "never";
|
|
@@ -4363,17 +4500,18 @@ async function useAiProvider(args) {
|
|
|
4363
4500
|
|
|
4364
4501
|
const provider = providerOrProfile;
|
|
4365
4502
|
|
|
4366
|
-
if (provider !== "ollama" && provider !== "openai" && provider !== "openrouter" && provider !== "codex") {
|
|
4367
|
-
throw new Error("Провайдер должен быть ollama, openai, openrouter, codex или именем AI-профиля.");
|
|
4503
|
+
if (provider !== "iola" && provider !== "ollama" && provider !== "openai" && provider !== "openrouter" && provider !== "codex") {
|
|
4504
|
+
throw new Error("Провайдер должен быть iola, ollama, openai, openrouter, codex или именем AI-профиля.");
|
|
4368
4505
|
}
|
|
4369
4506
|
|
|
4370
4507
|
const defaultModel = {
|
|
4371
|
-
|
|
4508
|
+
iola: IOLA_LOCAL_MODEL,
|
|
4509
|
+
ollama: config.ai.provider === "ollama" ? config.ai.model : IOLA_LOCAL_OLLAMA_MODEL,
|
|
4372
4510
|
openai: config.ai.provider === "openai" ? config.ai.model : "gpt-4.1-mini",
|
|
4373
4511
|
openrouter: config.ai.provider === "openrouter" ? config.ai.model : "openai/gpt-4.1-mini",
|
|
4374
4512
|
codex: config.ai.provider === "codex" ? config.ai.model : "gpt-5.5",
|
|
4375
4513
|
}[provider];
|
|
4376
|
-
const profileName = provider === "ollama" ? "local" : provider;
|
|
4514
|
+
const profileName = provider === "ollama" || provider === "iola" ? "local" : provider;
|
|
4377
4515
|
const profile = buildProfileFromOptions(provider, { model: defaultModel });
|
|
4378
4516
|
|
|
4379
4517
|
await saveConfig({
|
|
@@ -4412,7 +4550,7 @@ async function slashModelMenu(args = []) {
|
|
|
4412
4550
|
function normalizeModelMenuTarget(value = "") {
|
|
4413
4551
|
const normalized = String(value || "").trim().toLocaleLowerCase("ru-RU");
|
|
4414
4552
|
if (!normalized) return "";
|
|
4415
|
-
if (["local", "локальная", "локально", "ollama"].includes(normalized)) return "local";
|
|
4553
|
+
if (["local", "локальная", "локально", "iola", "иола", "ollama"].includes(normalized)) return "local";
|
|
4416
4554
|
if (["api", "апи"].includes(normalized)) return "api";
|
|
4417
4555
|
if (normalized === "openai") return "openai";
|
|
4418
4556
|
if (normalized === "openrouter" || normalized === "router") return "openrouter";
|
|
@@ -4422,7 +4560,7 @@ function normalizeModelMenuTarget(value = "") {
|
|
|
4422
4560
|
|
|
4423
4561
|
async function chooseModelTarget() {
|
|
4424
4562
|
console.log("Выберите AI-подключение:");
|
|
4425
|
-
console.log(" 1. Локальная модель
|
|
4563
|
+
console.log(" 1. Локальная модель IOLA");
|
|
4426
4564
|
console.log(" 2. API (OpenAI/OpenRouter)");
|
|
4427
4565
|
console.log(" 3. Codex CLI");
|
|
4428
4566
|
console.log(" 0. Отмена");
|
|
@@ -4433,7 +4571,7 @@ async function chooseModelTarget() {
|
|
|
4433
4571
|
|
|
4434
4572
|
async function openModelTargetMenu(target) {
|
|
4435
4573
|
if (target === "local") {
|
|
4436
|
-
const model = await chooseAiModel("
|
|
4574
|
+
const model = await chooseAiModel("iola");
|
|
4437
4575
|
if (model) await switchModelTarget("local", model);
|
|
4438
4576
|
return;
|
|
4439
4577
|
}
|
|
@@ -4522,12 +4660,15 @@ async function chooseAiModel(provider) {
|
|
|
4522
4660
|
|
|
4523
4661
|
async function switchModelTarget(target, model) {
|
|
4524
4662
|
const config = await loadConfig();
|
|
4525
|
-
const provider = target === "local" ? "
|
|
4663
|
+
const provider = target === "local" ? "iola" : target;
|
|
4664
|
+
if (provider === "iola") {
|
|
4665
|
+
await ensureIolaModelFresh({ quiet: false });
|
|
4666
|
+
}
|
|
4526
4667
|
if (provider === "ollama") {
|
|
4527
4668
|
const ready = await ensureOllamaModelAvailable(model, config);
|
|
4528
4669
|
if (!ready) return;
|
|
4529
4670
|
}
|
|
4530
|
-
const profileName = provider === "ollama" ? "local" : provider;
|
|
4671
|
+
const profileName = provider === "ollama" || provider === "iola" ? "local" : provider;
|
|
4531
4672
|
const currentProfile = config.ai.profiles?.[profileName] || buildProfileFromOptions(provider, { model });
|
|
4532
4673
|
const profile = {
|
|
4533
4674
|
...currentProfile,
|
|
@@ -5911,20 +6052,22 @@ function assertKeyProvider(provider) {
|
|
|
5911
6052
|
|
|
5912
6053
|
async function chooseAiProvider() {
|
|
5913
6054
|
console.log("Выберите режим AI:");
|
|
5914
|
-
console.log("1. Локальная модель
|
|
6055
|
+
console.log("1. Локальная модель IOLA");
|
|
5915
6056
|
console.log("2. OpenAI API");
|
|
5916
6057
|
console.log("3. OpenRouter API");
|
|
5917
6058
|
console.log("4. Codex/MCP");
|
|
6059
|
+
console.log("5. Ollama");
|
|
5918
6060
|
|
|
5919
6061
|
const rl = readline.createInterface({ input, output });
|
|
5920
6062
|
try {
|
|
5921
6063
|
const answer = (await rl.question("Введите номер [1]: ")).trim() || "1";
|
|
5922
6064
|
return {
|
|
5923
|
-
1: "
|
|
6065
|
+
1: "iola",
|
|
5924
6066
|
2: "openai",
|
|
5925
6067
|
3: "openrouter",
|
|
5926
6068
|
4: "codex",
|
|
5927
|
-
|
|
6069
|
+
5: "ollama",
|
|
6070
|
+
}[answer] || "iola";
|
|
5928
6071
|
} finally {
|
|
5929
6072
|
rl.close();
|
|
5930
6073
|
}
|
|
@@ -5984,6 +6127,53 @@ async function setupOllama(args) {
|
|
|
5984
6127
|
console.log(`Готово. Локальный AI-профиль сохранен в ${CONFIG_FILE}`);
|
|
5985
6128
|
}
|
|
5986
6129
|
|
|
6130
|
+
async function setupIolaLocal(args) {
|
|
6131
|
+
const options = parseOptions(args);
|
|
6132
|
+
const repo = options.repo || IOLA_ROUTER_HF_REPO;
|
|
6133
|
+
const modelDir = options["model-dir"] || IOLA_MODEL_DIR;
|
|
6134
|
+
const profileName = options.name || "local";
|
|
6135
|
+
const optional = Boolean(options.optional);
|
|
6136
|
+
|
|
6137
|
+
if (optional && process.env.CI === "true") {
|
|
6138
|
+
return;
|
|
6139
|
+
}
|
|
6140
|
+
|
|
6141
|
+
try {
|
|
6142
|
+
await ensureIolaModelFresh({ repo, modelDir, force: true, quiet: Boolean(options.quiet) });
|
|
6143
|
+
} catch (error) {
|
|
6144
|
+
if (!optional) throw error;
|
|
6145
|
+
console.warn(`IOLA local model не установлена: ${error instanceof Error ? error.message : String(error)}`);
|
|
6146
|
+
}
|
|
6147
|
+
|
|
6148
|
+
const config = await loadConfig();
|
|
6149
|
+
await saveConfig({
|
|
6150
|
+
ai: {
|
|
6151
|
+
...config.ai,
|
|
6152
|
+
activeProfile: profileName,
|
|
6153
|
+
provider: "iola",
|
|
6154
|
+
model: IOLA_LOCAL_MODEL,
|
|
6155
|
+
profiles: {
|
|
6156
|
+
...(config.ai.profiles || {}),
|
|
6157
|
+
[profileName]: {
|
|
6158
|
+
provider: "iola",
|
|
6159
|
+
model: IOLA_LOCAL_MODEL,
|
|
6160
|
+
repo,
|
|
6161
|
+
modelDir,
|
|
6162
|
+
},
|
|
6163
|
+
},
|
|
6164
|
+
},
|
|
6165
|
+
});
|
|
6166
|
+
|
|
6167
|
+
if (options.quiet) return;
|
|
6168
|
+
console.log("");
|
|
6169
|
+
console.log("IOLA local mode готов:");
|
|
6170
|
+
console.log(` runtime: Python transformers/peft`);
|
|
6171
|
+
console.log(` model: ${IOLA_LOCAL_MODEL}`);
|
|
6172
|
+
console.log(` Hugging Face: ${repo}`);
|
|
6173
|
+
console.log(` cache: ${modelDir}`);
|
|
6174
|
+
console.log(" точные данные: https://apiiola.yasg.ru/api/v1/resolve-entity-field");
|
|
6175
|
+
}
|
|
6176
|
+
|
|
5987
6177
|
async function aiAsk(args, context = {}) {
|
|
5988
6178
|
const options = parseOptions(args);
|
|
5989
6179
|
const question = options._.join(" ").trim();
|
|
@@ -5995,9 +6185,9 @@ async function aiAsk(args, context = {}) {
|
|
|
5995
6185
|
const config = await loadConfig();
|
|
5996
6186
|
const providerConfig = await resolveUsableAiProfile(config, options);
|
|
5997
6187
|
if (providerConfig.provider === "codex") await assertPermission("codex");
|
|
5998
|
-
if (providerConfig.provider !== "ollama") await assertPermission("externalAi");
|
|
6188
|
+
if (providerConfig.provider !== "ollama" && providerConfig.provider !== "iola") await assertPermission("externalAi");
|
|
5999
6189
|
if (options["stream-json"]) options.events = true;
|
|
6000
|
-
if (options.tools && providerConfig.provider === "ollama") {
|
|
6190
|
+
if (providerConfig.provider === "iola" || (options.tools && providerConfig.provider === "ollama")) {
|
|
6001
6191
|
return localToolAsk(question, providerConfig, options);
|
|
6002
6192
|
}
|
|
6003
6193
|
applyRuntimeConfig(providerConfig, options.config);
|
|
@@ -6220,14 +6410,25 @@ function resolveAiProfile(config, options = {}) {
|
|
|
6220
6410
|
provider,
|
|
6221
6411
|
model: options.model || activeProfile.model || config.ai.model,
|
|
6222
6412
|
baseUrl: options["base-url"] || activeProfile.baseUrl || config.ai.baseUrl,
|
|
6413
|
+
repo: options.repo || activeProfile.repo,
|
|
6414
|
+
modelDir: options["model-dir"] || activeProfile.modelDir,
|
|
6223
6415
|
temperature: options.temperature || activeProfile.temperature,
|
|
6224
6416
|
};
|
|
6225
6417
|
}
|
|
6226
6418
|
|
|
6227
6419
|
async function localToolAsk(question, providerConfig, options) {
|
|
6228
6420
|
if (options["stream-json"]) options.events = true;
|
|
6421
|
+
const guarded = guardNonPublicQuestion(question);
|
|
6422
|
+
if (guarded) {
|
|
6423
|
+
if (!options.quiet) console.log(guarded);
|
|
6424
|
+
return guarded;
|
|
6425
|
+
}
|
|
6229
6426
|
await ensureLocalData();
|
|
6230
6427
|
const plan = await buildLocalToolPlan(question, providerConfig, options);
|
|
6428
|
+
if (plan.directAnswer) {
|
|
6429
|
+
if (!options.quiet) console.log(plan.directAnswer);
|
|
6430
|
+
return plan.directAnswer;
|
|
6431
|
+
}
|
|
6231
6432
|
const validated = validateToolPlan(plan, options);
|
|
6232
6433
|
if (options.plan) {
|
|
6233
6434
|
printToolPlan(validated);
|
|
@@ -6267,6 +6468,14 @@ async function localToolAsk(question, providerConfig, options) {
|
|
|
6267
6468
|
return answer;
|
|
6268
6469
|
}
|
|
6269
6470
|
|
|
6471
|
+
function guardNonPublicQuestion(question) {
|
|
6472
|
+
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
6473
|
+
if (/(зарплат|получа[ею]т|доход|домашн|паспорт|снилс|личн|персональн)/iu.test(normalized)) {
|
|
6474
|
+
return "Это поле не входит в открытые публичные данные.";
|
|
6475
|
+
}
|
|
6476
|
+
return "";
|
|
6477
|
+
}
|
|
6478
|
+
|
|
6270
6479
|
function printToolPlan(plan) {
|
|
6271
6480
|
console.log("План выполнения:");
|
|
6272
6481
|
plan.steps.forEach((step, index) => {
|
|
@@ -6276,6 +6485,17 @@ function printToolPlan(plan) {
|
|
|
6276
6485
|
|
|
6277
6486
|
async function buildLocalToolPlan(question, providerConfig, options) {
|
|
6278
6487
|
const mode = options.reasoning || "verify";
|
|
6488
|
+
|
|
6489
|
+
if (providerConfig.provider === "iola") {
|
|
6490
|
+
await ensureIolaModelFresh({
|
|
6491
|
+
repo: providerConfig.repo || IOLA_ROUTER_HF_REPO,
|
|
6492
|
+
modelDir: providerConfig.modelDir || IOLA_MODEL_DIR,
|
|
6493
|
+
quiet: true,
|
|
6494
|
+
});
|
|
6495
|
+
const raw = await callIolaLocal(providerConfig, [{ role: "user", content: question }]);
|
|
6496
|
+
return normalizeIolaRouterPlan(raw, question, options);
|
|
6497
|
+
}
|
|
6498
|
+
|
|
6279
6499
|
const prompt = [
|
|
6280
6500
|
"Ты планировщик CLI iola. Верни только JSON.",
|
|
6281
6501
|
`Доступные tools: ${availableToolNames(options).join(", ")}.`,
|
|
@@ -6298,6 +6518,27 @@ async function buildLocalToolPlan(question, providerConfig, options) {
|
|
|
6298
6518
|
}
|
|
6299
6519
|
}
|
|
6300
6520
|
|
|
6521
|
+
function normalizeIolaRouterPlan(raw, question, options = {}) {
|
|
6522
|
+
const payload = typeof raw === "string" ? parseJsonObject(raw) : raw;
|
|
6523
|
+
if (payload.action === "tool_call") {
|
|
6524
|
+
const tool = payload.tool === "get_entity_field" ? "resolve_entity_field" : payload.tool;
|
|
6525
|
+
return { steps: [{ tool, args: payload.args || {} }] };
|
|
6526
|
+
}
|
|
6527
|
+
if (payload.action === "direct_answer") {
|
|
6528
|
+
return { directAnswer: payload.answer || "" };
|
|
6529
|
+
}
|
|
6530
|
+
if (payload.action === "clarify") {
|
|
6531
|
+
return { directAnswer: payload.question || "Уточните запрос." };
|
|
6532
|
+
}
|
|
6533
|
+
if (payload.action === "refuse") {
|
|
6534
|
+
return { directAnswer: payload.reason === "field_not_public" ? "Это поле не входит в открытые публичные данные." : "Не могу выполнить этот запрос." };
|
|
6535
|
+
}
|
|
6536
|
+
if (options.reasoning === "vote") {
|
|
6537
|
+
return inferToolPlan(question, options);
|
|
6538
|
+
}
|
|
6539
|
+
throw new Error(`IOLA router вернул неподдерживаемое действие: ${payload.action || "unknown"}`);
|
|
6540
|
+
}
|
|
6541
|
+
|
|
6301
6542
|
function parseJsonObject(text) {
|
|
6302
6543
|
const match = String(text).match(/\{[\s\S]*\}/);
|
|
6303
6544
|
if (!match) throw new Error("JSON-план не найден.");
|
|
@@ -6347,6 +6588,50 @@ function validateToolPlan(plan, options = {}) {
|
|
|
6347
6588
|
return plan;
|
|
6348
6589
|
}
|
|
6349
6590
|
|
|
6591
|
+
async function searchPublicEntities(args = {}) {
|
|
6592
|
+
const payload = await postJson(`${await getApiBaseUrl()}/search-entities`, {
|
|
6593
|
+
layer: normalizeEntityLayer(args.layer),
|
|
6594
|
+
query: args.query || args.entity_name || args.name || "",
|
|
6595
|
+
limit: Number(args.limit || 10),
|
|
6596
|
+
filters: args.filters || undefined,
|
|
6597
|
+
});
|
|
6598
|
+
return normalizeItems(payload).map((item) => ({
|
|
6599
|
+
...(item.entity || item),
|
|
6600
|
+
score: item.score,
|
|
6601
|
+
layer: payload.layer || normalizeEntityLayer(args.layer),
|
|
6602
|
+
}));
|
|
6603
|
+
}
|
|
6604
|
+
|
|
6605
|
+
async function resolvePublicEntityField(args = {}) {
|
|
6606
|
+
const payload = await postJson(`${await getApiBaseUrl()}/resolve-entity-field`, {
|
|
6607
|
+
layer: normalizeEntityLayer(args.layer),
|
|
6608
|
+
entity_number: args.entity_number ?? args.number,
|
|
6609
|
+
entity_name: args.entity_name || args.name,
|
|
6610
|
+
inn: args.inn,
|
|
6611
|
+
field: normalizeEntityField(args.field),
|
|
6612
|
+
must_refute_user_value: args.must_refute_user_value,
|
|
6613
|
+
});
|
|
6614
|
+
return payload;
|
|
6615
|
+
}
|
|
6616
|
+
|
|
6617
|
+
function normalizeEntityLayer(layer) {
|
|
6618
|
+
const value = String(layer || "").toLocaleLowerCase("ru-RU");
|
|
6619
|
+
if (value === "school" || value === "schools" || value.includes("школ")) return "schools";
|
|
6620
|
+
if (value === "kindergarten" || value === "kindergartens" || value.includes("сад")) return "kindergartens";
|
|
6621
|
+
return value || "schools";
|
|
6622
|
+
}
|
|
6623
|
+
|
|
6624
|
+
function normalizeEntityField(field) {
|
|
6625
|
+
const value = String(field || "").toLocaleLowerCase("ru-RU");
|
|
6626
|
+
if (value === "director" || value === "head" || value.includes("директор") || value.includes("руковод")) return "head";
|
|
6627
|
+
if (value === "site" || value === "url" || value === "website" || value.includes("сайт")) return "website";
|
|
6628
|
+
if (value === "mail" || value === "email" || value.includes("почт")) return "email";
|
|
6629
|
+
if (value === "phone" || value.includes("тел")) return "phone";
|
|
6630
|
+
if (value === "address" || value.includes("адрес")) return "address";
|
|
6631
|
+
if (value === "license") return "license_status";
|
|
6632
|
+
return value || "name";
|
|
6633
|
+
}
|
|
6634
|
+
|
|
6350
6635
|
function availableToolNames(options = {}) {
|
|
6351
6636
|
const names = new Set(LOCAL_TOOLS);
|
|
6352
6637
|
for (const tool of getLocalMcpToolNames()) names.add(tool);
|
|
@@ -6366,6 +6651,13 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
6366
6651
|
if (step.tool === "search_data" || step.tool === "search_local") {
|
|
6367
6652
|
current = searchLocalRecords(step.args?.query || "", { dataset: step.args?.dataset || "all", limit: step.args?.limit || 20, fts: true });
|
|
6368
6653
|
outputs.push({ tool: step.tool, rows: current.length });
|
|
6654
|
+
} else if (step.tool === "search_entities") {
|
|
6655
|
+
current = await searchPublicEntities(step.args || {});
|
|
6656
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
6657
|
+
} else if (step.tool === "resolve_entity_field") {
|
|
6658
|
+
const resolved = await resolvePublicEntityField(step.args || {});
|
|
6659
|
+
current = Array.isArray(resolved) ? resolved : [resolved];
|
|
6660
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
6369
6661
|
} else if (step.tool === "get_card") {
|
|
6370
6662
|
const card = findCard(step.args?.query || "");
|
|
6371
6663
|
current = card ? [card] : [];
|
|
@@ -6514,7 +6806,13 @@ function formatToolResult(result, options) {
|
|
|
6514
6806
|
const exported = result.outputs.find((item) => item.output);
|
|
6515
6807
|
if (exported) return `Готово. Файл сохранен: ${exported.output}. Записей: ${exported.rows}`;
|
|
6516
6808
|
if (!result.rows.length) return "Данных не найдено.";
|
|
6517
|
-
return result.rows.slice(0, 10).map((row) =>
|
|
6809
|
+
return result.rows.slice(0, 10).map((row) => {
|
|
6810
|
+
if (row.ok && row.entity && row.field) {
|
|
6811
|
+
const name = row.entity.name || row.entity.inn || "организация";
|
|
6812
|
+
return `${name}: ${row.field} = ${row.value ?? "не указано"}`;
|
|
6813
|
+
}
|
|
6814
|
+
return `${row.name || row.check || row.inn || "строка"}: ${row.address || row.phone || row.email || row.website || row.count || ""}`;
|
|
6815
|
+
}).join("\n");
|
|
6518
6816
|
}
|
|
6519
6817
|
|
|
6520
6818
|
function applyRuntimeConfig(target, value) {
|
|
@@ -6906,6 +7204,10 @@ function buildSourceLines(dataContext) {
|
|
|
6906
7204
|
}
|
|
6907
7205
|
|
|
6908
7206
|
async function callAiProvider(config, messages) {
|
|
7207
|
+
if (config.provider === "iola") {
|
|
7208
|
+
return callIolaLocal(config, messages);
|
|
7209
|
+
}
|
|
7210
|
+
|
|
6909
7211
|
if (config.provider === "ollama") {
|
|
6910
7212
|
return callOllama(config, messages);
|
|
6911
7213
|
}
|
|
@@ -6925,6 +7227,155 @@ async function callAiProvider(config, messages) {
|
|
|
6925
7227
|
throw new Error(`Неизвестный AI-провайдер: ${config.provider}`);
|
|
6926
7228
|
}
|
|
6927
7229
|
|
|
7230
|
+
async function callIolaLocal(config, messages) {
|
|
7231
|
+
const runtime = await ensureIolaModelRuntime({ quiet: true });
|
|
7232
|
+
const repo = config.repo || IOLA_ROUTER_HF_REPO;
|
|
7233
|
+
const modelDir = config.modelDir || IOLA_MODEL_DIR;
|
|
7234
|
+
const payload = {
|
|
7235
|
+
repo,
|
|
7236
|
+
cache_dir: modelDir,
|
|
7237
|
+
messages,
|
|
7238
|
+
max_new_tokens: Number(config.maxNewTokens || 180),
|
|
7239
|
+
temperature: Number(config.temperature ?? 0),
|
|
7240
|
+
};
|
|
7241
|
+
const { stdout, stderr } = await runCommand(runtime.python, [IOLA_MODEL_RUNNER], {
|
|
7242
|
+
input: JSON.stringify(payload),
|
|
7243
|
+
env: {
|
|
7244
|
+
IOLA_ROUTER_HF_REPO: repo,
|
|
7245
|
+
IOLA_MODEL_DIR: modelDir,
|
|
7246
|
+
},
|
|
7247
|
+
});
|
|
7248
|
+
const text = stdout.trim();
|
|
7249
|
+
if (!text) {
|
|
7250
|
+
throw new Error(`IOLA local model вернула пустой ответ.${stderr ? `\n${stderr}` : ""}`);
|
|
7251
|
+
}
|
|
7252
|
+
return text;
|
|
7253
|
+
}
|
|
7254
|
+
|
|
7255
|
+
async function hasUsableIolaModel() {
|
|
7256
|
+
const state = readConfigLayerSync(getIolaModelStateFile(IOLA_MODEL_DIR));
|
|
7257
|
+
return Boolean(state?.repo && state?.revision && existsSync(IOLA_MODEL_DIR));
|
|
7258
|
+
}
|
|
7259
|
+
|
|
7260
|
+
async function ensureIolaModelFresh(options = {}) {
|
|
7261
|
+
const repo = options.repo || IOLA_ROUTER_HF_REPO;
|
|
7262
|
+
const modelDir = options.modelDir || IOLA_MODEL_DIR;
|
|
7263
|
+
await mkdir(modelDir, { recursive: true });
|
|
7264
|
+
const stateFile = getIolaModelStateFile(modelDir);
|
|
7265
|
+
const state = readConfigLayerSync(stateFile) || {};
|
|
7266
|
+
const remote = await getRemoteIolaModelRevision(repo).catch(() => null);
|
|
7267
|
+
const stale = options.force || state.repo !== repo || !state.revision || (remote?.sha && remote.sha !== state.revision);
|
|
7268
|
+
if (!stale) return state;
|
|
7269
|
+
|
|
7270
|
+
if (!options.quiet) {
|
|
7271
|
+
const reason = state.revision ? "обновляю" : "устанавливаю";
|
|
7272
|
+
console.log(`IOLA local model: ${reason} ${repo}`);
|
|
7273
|
+
console.log("Загрузка первой установки может занять несколько минут.");
|
|
7274
|
+
}
|
|
7275
|
+
|
|
7276
|
+
const runtime = await ensureIolaModelRuntime({ quiet: options.quiet });
|
|
7277
|
+
const { stdout } = await runCommand(runtime.python, [IOLA_MODEL_RUNNER, "--ensure"], {
|
|
7278
|
+
input: JSON.stringify({ repo, cache_dir: modelDir }),
|
|
7279
|
+
env: {
|
|
7280
|
+
IOLA_ROUTER_HF_REPO: repo,
|
|
7281
|
+
IOLA_MODEL_DIR: modelDir,
|
|
7282
|
+
},
|
|
7283
|
+
});
|
|
7284
|
+
const installed = parseJsonObject(stdout || "{}");
|
|
7285
|
+
const nextState = {
|
|
7286
|
+
repo,
|
|
7287
|
+
revision: installed.revision || remote?.sha || state.revision || `local-${Date.now()}`,
|
|
7288
|
+
installedAt: new Date().toISOString(),
|
|
7289
|
+
runtime: "transformers",
|
|
7290
|
+
};
|
|
7291
|
+
await mkdir(modelDir, { recursive: true });
|
|
7292
|
+
await writeFile(stateFile, `${JSON.stringify(nextState, null, 2)}\n`, "utf8");
|
|
7293
|
+
return nextState;
|
|
7294
|
+
}
|
|
7295
|
+
|
|
7296
|
+
function getIolaModelStateFile(modelDir = IOLA_MODEL_DIR) {
|
|
7297
|
+
return path.join(modelDir, "manifest.json");
|
|
7298
|
+
}
|
|
7299
|
+
|
|
7300
|
+
async function ensureIolaModelRuntime(options = {}) {
|
|
7301
|
+
const python = await getIolaRuntimePython();
|
|
7302
|
+
if (python && await checkIolaPythonDeps(python)) return { python };
|
|
7303
|
+
|
|
7304
|
+
const basePython = await findPythonCommand();
|
|
7305
|
+
if (!basePython) {
|
|
7306
|
+
throw new Error("Python не найден. Установите Python 3.11+ и повторите: iola ai setup iola --yes");
|
|
7307
|
+
}
|
|
7308
|
+
|
|
7309
|
+
await mkdir(IOLA_MODEL_RUNTIME_DIR, { recursive: true });
|
|
7310
|
+
if (!python) {
|
|
7311
|
+
if (!options.quiet) console.log(`Создаю Python runtime: ${IOLA_MODEL_RUNTIME_DIR}`);
|
|
7312
|
+
await runCommand(basePython.command, [...basePython.args, "-m", "venv", IOLA_MODEL_RUNTIME_DIR], { inherit: !options.quiet });
|
|
7313
|
+
}
|
|
7314
|
+
|
|
7315
|
+
const runtimePython = await getIolaRuntimePython();
|
|
7316
|
+
if (!runtimePython) {
|
|
7317
|
+
throw new Error("Не удалось создать Python runtime для локальной модели.");
|
|
7318
|
+
}
|
|
7319
|
+
|
|
7320
|
+
if (!options.quiet) console.log("Устанавливаю зависимости локальной модели: torch, transformers, peft.");
|
|
7321
|
+
await runCommand(runtimePython, ["-m", "pip", "install", "--upgrade", "pip"], { inherit: !options.quiet });
|
|
7322
|
+
await runCommand(runtimePython, ["-m", "pip", "install", "torch>=2.6.0", "transformers>=4.57.0,<5.0", "peft>=0.15.0", "accelerate>=1.8.0", "huggingface_hub>=0.34.0,<1.0", "hf_xet>=1.1.0", "safetensors>=0.4.0"], { inherit: !options.quiet });
|
|
7323
|
+
|
|
7324
|
+
if (!await checkIolaPythonDeps(runtimePython)) {
|
|
7325
|
+
throw new Error("Python-зависимости локальной модели не установились.");
|
|
7326
|
+
}
|
|
7327
|
+
return { python: runtimePython };
|
|
7328
|
+
}
|
|
7329
|
+
|
|
7330
|
+
async function getIolaRuntimePython() {
|
|
7331
|
+
const candidate = process.platform === "win32"
|
|
7332
|
+
? path.join(IOLA_MODEL_RUNTIME_DIR, "Scripts", "python.exe")
|
|
7333
|
+
: path.join(IOLA_MODEL_RUNTIME_DIR, "bin", "python");
|
|
7334
|
+
return existsSync(candidate) ? candidate : null;
|
|
7335
|
+
}
|
|
7336
|
+
|
|
7337
|
+
async function checkIolaPythonDeps(python) {
|
|
7338
|
+
try {
|
|
7339
|
+
await runCommand(python, [IOLA_MODEL_RUNNER, "--check-deps"]);
|
|
7340
|
+
return true;
|
|
7341
|
+
} catch {
|
|
7342
|
+
return false;
|
|
7343
|
+
}
|
|
7344
|
+
}
|
|
7345
|
+
|
|
7346
|
+
async function findPythonCommand() {
|
|
7347
|
+
const candidates = [
|
|
7348
|
+
{ command: process.env.IOLA_PYTHON, args: [] },
|
|
7349
|
+
{ command: "python", args: [] },
|
|
7350
|
+
{ command: "python3", args: [] },
|
|
7351
|
+
{ command: "py", args: ["-3"] },
|
|
7352
|
+
].filter((item) => item.command);
|
|
7353
|
+
|
|
7354
|
+
for (const candidate of candidates) {
|
|
7355
|
+
try {
|
|
7356
|
+
await runCommand(candidate.command, [...candidate.args, "--version"]);
|
|
7357
|
+
return candidate;
|
|
7358
|
+
} catch {
|
|
7359
|
+
// Try next candidate.
|
|
7360
|
+
}
|
|
7361
|
+
}
|
|
7362
|
+
return null;
|
|
7363
|
+
}
|
|
7364
|
+
|
|
7365
|
+
async function getRemoteIolaModelRevision(repo = IOLA_ROUTER_HF_REPO) {
|
|
7366
|
+
const repoPath = String(repo).split("/").map((part) => encodeURIComponent(part)).join("/");
|
|
7367
|
+
const response = await fetch(`https://huggingface.co/api/models/${repoPath}`, {
|
|
7368
|
+
headers: {
|
|
7369
|
+
accept: "application/json",
|
|
7370
|
+
"user-agent": "@iola_adm/iola-cli",
|
|
7371
|
+
},
|
|
7372
|
+
signal: AbortSignal.timeout(5000),
|
|
7373
|
+
});
|
|
7374
|
+
if (!response.ok) throw new Error(`Hugging Face model metadata failed: ${response.status} ${response.statusText}`);
|
|
7375
|
+
const payload = await response.json();
|
|
7376
|
+
return { sha: payload.sha || payload.lastModified || "", lastModified: payload.lastModified || "" };
|
|
7377
|
+
}
|
|
7378
|
+
|
|
6928
7379
|
async function callCodex(config, messages) {
|
|
6929
7380
|
const prompt = messages.map((message) => `${message.role.toUpperCase()}:\n${message.content}`).join("\n\n");
|
|
6930
7381
|
const outputFile = path.join(os.tmpdir(), `iola-codex-${process.pid}-${Date.now()}.txt`);
|
|
@@ -7262,6 +7713,9 @@ async function onboard(args = []) {
|
|
|
7262
7713
|
if (components.includes("workspace")) await handleWorkspace(["init"]);
|
|
7263
7714
|
if (components.includes("policy")) await handlePolicy(["use", "analyst"]);
|
|
7264
7715
|
if (components.includes("archive")) await ensureArchiveTool({ install: true });
|
|
7716
|
+
if (components.includes("iola")) {
|
|
7717
|
+
await setupIolaLocal(["--yes"]);
|
|
7718
|
+
}
|
|
7265
7719
|
if (components.includes("ollama")) {
|
|
7266
7720
|
await installOllamaIfMissing();
|
|
7267
7721
|
await setupOllama(["--yes"]);
|
|
@@ -7309,7 +7763,7 @@ async function chooseOnboardComponents(status = null) {
|
|
|
7309
7763
|
const map = {
|
|
7310
7764
|
1: "workspace",
|
|
7311
7765
|
2: "policy",
|
|
7312
|
-
3: "
|
|
7766
|
+
3: "iola",
|
|
7313
7767
|
4: "openai",
|
|
7314
7768
|
5: "openrouter",
|
|
7315
7769
|
6: "codex",
|
|
@@ -7317,6 +7771,7 @@ async function chooseOnboardComponents(status = null) {
|
|
|
7317
7771
|
8: "archive",
|
|
7318
7772
|
9: "index",
|
|
7319
7773
|
10: "browser",
|
|
7774
|
+
11: "ollama",
|
|
7320
7775
|
};
|
|
7321
7776
|
return [...selected].map((item) => map[item] || item).filter(Boolean);
|
|
7322
7777
|
} finally {
|
|
@@ -7338,6 +7793,7 @@ async function getOnboardComponentStatus() {
|
|
|
7338
7793
|
return {
|
|
7339
7794
|
workspace: workspaceReady,
|
|
7340
7795
|
policy: policyReady,
|
|
7796
|
+
iola: Boolean(readiness.iola),
|
|
7341
7797
|
ollama: Boolean(ollamaVersion && readiness.ollama),
|
|
7342
7798
|
openai: Boolean(readiness.openai),
|
|
7343
7799
|
openrouter: Boolean(readiness.openrouter),
|
|
@@ -7353,7 +7809,7 @@ function onboardComponentRows(status) {
|
|
|
7353
7809
|
const rows = [
|
|
7354
7810
|
["1", "workspace", "workspace и контекст", "рабочая папка, IOLA.md и .iola/context.md"],
|
|
7355
7811
|
["2", "policy", "policy analyst", "разрешения и профиль аналитика"],
|
|
7356
|
-
["3", "
|
|
7812
|
+
["3", "iola", "IOLA локальная модель", "локальная модель найдена"],
|
|
7357
7813
|
["4", "openai", "OpenAI API", "API-ключ сохранен или есть в env"],
|
|
7358
7814
|
["5", "openrouter", "OpenRouter API", "API-ключ сохранен или есть в env"],
|
|
7359
7815
|
["6", "codex", "Codex CLI", "CLI установлен и авторизация найдена"],
|
|
@@ -7361,6 +7817,7 @@ function onboardComponentRows(status) {
|
|
|
7361
7817
|
["8", "archive", "7-Zip / архивы", "архиватор найден"],
|
|
7362
7818
|
["9", "index", "Индекс локальных документов", "настраивается под выбранную папку"],
|
|
7363
7819
|
["10", "browser", "Browser runtime", "Playwright/Chromium установлен"],
|
|
7820
|
+
["11", "ollama", "Ollama", "опциональный локальный runtime"],
|
|
7364
7821
|
];
|
|
7365
7822
|
return rows.map(([number, key, title, hint]) => ({ number, key, title, hint, status: status[key] ? "готово" : "не настроено" }));
|
|
7366
7823
|
}
|
|
@@ -7369,12 +7826,13 @@ function defaultOnboardSelection(status) {
|
|
|
7369
7826
|
const defaults = [];
|
|
7370
7827
|
if (!status.workspace) defaults.push("1");
|
|
7371
7828
|
if (!status.policy) defaults.push("2");
|
|
7829
|
+
if (!status.iola) defaults.push("3");
|
|
7372
7830
|
if (!status.archive) defaults.push("8");
|
|
7373
7831
|
return defaults.length ? defaults : ["1", "2"];
|
|
7374
7832
|
}
|
|
7375
7833
|
|
|
7376
7834
|
function defaultOnboardComponents(status) {
|
|
7377
|
-
const map = { 1: "workspace", 2: "policy", 3: "
|
|
7835
|
+
const map = { 1: "workspace", 2: "policy", 3: "iola", 4: "openai", 5: "openrouter", 6: "codex", 7: "codex-mcp", 8: "archive", 9: "index", 10: "browser", 11: "ollama" };
|
|
7378
7836
|
return defaultOnboardSelection(status).map((item) => map[item]).filter(Boolean);
|
|
7379
7837
|
}
|
|
7380
7838
|
|
|
@@ -7383,12 +7841,12 @@ function parseOptions(args) {
|
|
|
7383
7841
|
|
|
7384
7842
|
for (let index = 0; index < args.length; index += 1) {
|
|
7385
7843
|
const arg = args[index];
|
|
7386
|
-
if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || arg === "--system" || arg === "--headed" || arg === "--headless" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--full" || arg === "--unread" || arg === "--once" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--append") {
|
|
7844
|
+
if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || arg === "--system" || arg === "--headed" || arg === "--headless" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--full" || arg === "--unread" || arg === "--once" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--optional" || arg === "--project" || arg === "--dry-run" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--append") {
|
|
7387
7845
|
result[arg.slice(2)] = true;
|
|
7388
7846
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
7389
7847
|
result.check = true;
|
|
7390
7848
|
result[arg.slice(2)] = true;
|
|
7391
|
-
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || arg === "--base-url" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--selector" || arg === "--url" || arg === "--timeout" || arg === "--wait" || arg === "--viewport" || arg === "--press" || arg === "--script" || arg === "--auth-url" || arg === "--token-url" || arg === "--userinfo-url" || arg === "--client-id" || arg === "--client-secret" || arg === "--redirect-host" || arg === "--redirect-port" || arg === "--redirect-path" || arg === "--debug-file") {
|
|
7849
|
+
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || arg === "--base-url" || arg === "--repo" || arg === "--model-dir" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--selector" || arg === "--url" || arg === "--timeout" || arg === "--wait" || arg === "--viewport" || arg === "--press" || arg === "--script" || arg === "--auth-url" || arg === "--token-url" || arg === "--userinfo-url" || arg === "--client-id" || arg === "--client-secret" || arg === "--redirect-host" || arg === "--redirect-port" || arg === "--redirect-path" || arg === "--debug-file") {
|
|
7392
7850
|
result[arg.slice(2)] = args[index + 1];
|
|
7393
7851
|
index += 1;
|
|
7394
7852
|
} else {
|
|
@@ -8663,7 +9121,7 @@ function recordUsage({ providerConfig, question, answer, sessionId, profile }) {
|
|
|
8663
9121
|
}
|
|
8664
9122
|
|
|
8665
9123
|
function estimateCost(providerConfig, tokens) {
|
|
8666
|
-
if (!providerConfig || providerConfig.provider === "ollama" || providerConfig.provider === "codex") return 0;
|
|
9124
|
+
if (!providerConfig || providerConfig.provider === "iola" || providerConfig.provider === "ollama" || providerConfig.provider === "codex") return 0;
|
|
8667
9125
|
const perMillion = providerConfig.provider === "openrouter" ? 0.25 : 0.4;
|
|
8668
9126
|
return Math.round((tokens / 1_000_000) * perMillion * 1_000_000) / 1_000_000;
|
|
8669
9127
|
}
|
|
@@ -9340,8 +9798,8 @@ function validateConfig(config) {
|
|
|
9340
9798
|
if (!config.ai?.profiles || typeof config.ai.profiles !== "object") errors.push("ai.profiles обязателен");
|
|
9341
9799
|
if (config.ai?.activeProfile && !config.ai.profiles?.[config.ai.activeProfile]) errors.push(`ai.activeProfile не найден в profiles: ${config.ai.activeProfile}`);
|
|
9342
9800
|
for (const [name, profile] of Object.entries(config.ai?.profiles || {})) {
|
|
9343
|
-
if (!["ollama", "openai", "openrouter", "codex"].includes(profile.provider)) errors.push(`ai.profiles.${name}.provider неизвестен`);
|
|
9344
|
-
if (profile.provider !== "codex" && !profile.baseUrl) errors.push(`ai.profiles.${name}.baseUrl обязателен`);
|
|
9801
|
+
if (!["iola", "ollama", "openai", "openrouter", "codex"].includes(profile.provider)) errors.push(`ai.profiles.${name}.provider неизвестен`);
|
|
9802
|
+
if (profile.provider !== "codex" && profile.provider !== "iola" && !profile.baseUrl) errors.push(`ai.profiles.${name}.baseUrl обязателен`);
|
|
9345
9803
|
}
|
|
9346
9804
|
for (const tool of Object.keys(config.permissions?.localTools || {})) {
|
|
9347
9805
|
if (!ALL_TOOL_ALIASES.includes(tool)) errors.push(`permissions.localTools.${tool} неизвестен`);
|
|
@@ -9358,7 +9816,7 @@ function configSchema() {
|
|
|
9358
9816
|
required: ["api", "ai"],
|
|
9359
9817
|
properties: {
|
|
9360
9818
|
api: { required: ["baseUrl", "mcpBaseUrl"] },
|
|
9361
|
-
ai: { required: ["activeProfile", "profiles"], providers: ["ollama", "openai", "openrouter", "codex"] },
|
|
9819
|
+
ai: { required: ["activeProfile", "profiles"], providers: ["iola", "ollama", "openai", "openrouter", "codex"] },
|
|
9362
9820
|
permissions: { localTools: ALL_LOCAL_TOOLS, runtime: ["readFiles", "writeFiles", "editFiles", "deleteFiles", "sync", "externalApi", "externalAi", "codex"] },
|
|
9363
9821
|
toolsets: { available: Object.keys(TOOLSETS) },
|
|
9364
9822
|
files: { modes: ["locked", "read-only", "workspace-write", "full-access"], approvals: ["never", "on-write", "on-danger", "always"] },
|
|
@@ -9373,7 +9831,7 @@ function getActiveProfileName(config) {
|
|
|
9373
9831
|
return config.ai.activeProfile;
|
|
9374
9832
|
}
|
|
9375
9833
|
|
|
9376
|
-
const provider = config.ai.provider === "ollama" ? "local" : config.ai.provider;
|
|
9834
|
+
const provider = config.ai.provider === "ollama" || config.ai.provider === "iola" ? "local" : config.ai.provider;
|
|
9377
9835
|
if (provider && config.ai.profiles?.[provider]) {
|
|
9378
9836
|
return provider;
|
|
9379
9837
|
}
|
|
@@ -9560,6 +10018,25 @@ async function fetchJson(url) {
|
|
|
9560
10018
|
return response.json();
|
|
9561
10019
|
}
|
|
9562
10020
|
|
|
10021
|
+
async function postJson(url, payload) {
|
|
10022
|
+
const response = await fetch(url, {
|
|
10023
|
+
method: "POST",
|
|
10024
|
+
headers: {
|
|
10025
|
+
accept: "application/json",
|
|
10026
|
+
"content-type": "application/json",
|
|
10027
|
+
"user-agent": "@iola_adm/iola-cli",
|
|
10028
|
+
},
|
|
10029
|
+
body: JSON.stringify(payload),
|
|
10030
|
+
});
|
|
10031
|
+
|
|
10032
|
+
if (!response.ok) {
|
|
10033
|
+
const text = await response.text().catch(() => "");
|
|
10034
|
+
throw new Error(`Request failed: ${response.status} ${response.statusText} (${url})${text ? `\n${text}` : ""}`);
|
|
10035
|
+
}
|
|
10036
|
+
|
|
10037
|
+
return response.json();
|
|
10038
|
+
}
|
|
10039
|
+
|
|
9563
10040
|
function parseJsonOrSse(text) {
|
|
9564
10041
|
const trimmed = String(text || "").trim();
|
|
9565
10042
|
if (!trimmed) return null;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
7
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
8
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
9
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def check_deps():
|
|
13
|
+
import torch # noqa: F401
|
|
14
|
+
import transformers # noqa: F401
|
|
15
|
+
import peft # noqa: F401
|
|
16
|
+
import huggingface_hub # noqa: F401
|
|
17
|
+
import hf_xet # noqa: F401
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_payload():
|
|
21
|
+
raw = sys.stdin.buffer.read().decode("utf-8").strip()
|
|
22
|
+
if not raw:
|
|
23
|
+
return {}
|
|
24
|
+
return json.loads(raw)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def model_revision(repo, token):
|
|
28
|
+
from huggingface_hub import HfApi
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
info = HfApi(token=token).model_info(repo)
|
|
32
|
+
return getattr(info, "sha", "") or ""
|
|
33
|
+
except Exception:
|
|
34
|
+
return ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_model(repo, cache_dir, token):
|
|
38
|
+
import torch
|
|
39
|
+
from peft import PeftConfig, PeftModel
|
|
40
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
41
|
+
|
|
42
|
+
peft_config = None
|
|
43
|
+
base_model = repo
|
|
44
|
+
try:
|
|
45
|
+
peft_config = PeftConfig.from_pretrained(repo, cache_dir=cache_dir, token=token)
|
|
46
|
+
base_model = peft_config.base_model_name_or_path
|
|
47
|
+
except Exception:
|
|
48
|
+
peft_config = None
|
|
49
|
+
|
|
50
|
+
tokenizer = AutoTokenizer.from_pretrained(base_model, cache_dir=cache_dir, token=token)
|
|
51
|
+
if tokenizer.pad_token is None:
|
|
52
|
+
tokenizer.pad_token = tokenizer.eos_token
|
|
53
|
+
|
|
54
|
+
dtype = torch.float16 if torch.cuda.is_available() else (torch.bfloat16 if peft_config is not None else torch.float32)
|
|
55
|
+
model = AutoModelForCausalLM.from_pretrained(
|
|
56
|
+
base_model,
|
|
57
|
+
cache_dir=cache_dir,
|
|
58
|
+
token=token,
|
|
59
|
+
torch_dtype=dtype,
|
|
60
|
+
device_map="auto" if torch.cuda.is_available() else None,
|
|
61
|
+
attn_implementation="sdpa",
|
|
62
|
+
)
|
|
63
|
+
if peft_config is not None:
|
|
64
|
+
model = PeftModel.from_pretrained(model, repo, cache_dir=cache_dir, token=token)
|
|
65
|
+
model = model.merge_and_unload()
|
|
66
|
+
|
|
67
|
+
model.eval()
|
|
68
|
+
return tokenizer, model
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def ensure_model(payload):
|
|
72
|
+
repo = payload.get("repo") or os.environ.get("IOLA_ROUTER_HF_REPO") or "LMSerg/iola-1b-router-2026-05-28-merged"
|
|
73
|
+
cache_dir = payload.get("cache_dir") or os.environ.get("IOLA_MODEL_DIR")
|
|
74
|
+
token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
|
|
75
|
+
if cache_dir:
|
|
76
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
77
|
+
|
|
78
|
+
tokenizer, model = load_model(repo, cache_dir, token)
|
|
79
|
+
del tokenizer
|
|
80
|
+
del model
|
|
81
|
+
print(json.dumps({"repo": repo, "revision": model_revision(repo, token)}, ensure_ascii=False))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def generate(payload):
|
|
85
|
+
repo = payload.get("repo") or os.environ.get("IOLA_ROUTER_HF_REPO") or "LMSerg/iola-1b-router-2026-05-28-merged"
|
|
86
|
+
cache_dir = payload.get("cache_dir") or os.environ.get("IOLA_MODEL_DIR")
|
|
87
|
+
token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
|
|
88
|
+
messages = payload.get("messages") or []
|
|
89
|
+
max_new_tokens = int(payload.get("max_new_tokens") or 180)
|
|
90
|
+
temperature = float(payload.get("temperature") or 0)
|
|
91
|
+
|
|
92
|
+
tokenizer, model = load_model(repo, cache_dir, token)
|
|
93
|
+
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
|
94
|
+
encoded = tokenizer(prompt, return_tensors="pt").to(model.device)
|
|
95
|
+
|
|
96
|
+
generate_kwargs = {
|
|
97
|
+
**encoded,
|
|
98
|
+
"max_new_tokens": max_new_tokens,
|
|
99
|
+
"do_sample": temperature > 0,
|
|
100
|
+
"pad_token_id": tokenizer.pad_token_id,
|
|
101
|
+
"eos_token_id": tokenizer.eos_token_id,
|
|
102
|
+
}
|
|
103
|
+
if temperature > 0:
|
|
104
|
+
generate_kwargs["temperature"] = temperature
|
|
105
|
+
|
|
106
|
+
import torch
|
|
107
|
+
|
|
108
|
+
with torch.no_grad():
|
|
109
|
+
output = model.generate(**generate_kwargs)
|
|
110
|
+
|
|
111
|
+
answer_ids = output[0][encoded["input_ids"].shape[1]:]
|
|
112
|
+
answer = tokenizer.decode(answer_ids, skip_special_tokens=True).strip()
|
|
113
|
+
print(answer)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def main():
|
|
117
|
+
parser = argparse.ArgumentParser()
|
|
118
|
+
parser.add_argument("--check-deps", action="store_true")
|
|
119
|
+
parser.add_argument("--ensure", action="store_true")
|
|
120
|
+
args = parser.parse_args()
|
|
121
|
+
|
|
122
|
+
if args.check_deps:
|
|
123
|
+
check_deps()
|
|
124
|
+
print("ok")
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
payload = load_payload()
|
|
128
|
+
if args.ensure:
|
|
129
|
+
ensure_model(payload)
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
generate(payload)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
main()
|
package/test/smoke-test.js
CHANGED
|
@@ -48,6 +48,7 @@ assertIncludes(help, "iola ask", "help");
|
|
|
48
48
|
const commands = await runCli(["commands"]);
|
|
49
49
|
assertIncludes(commands, "iola browser status|install|open|text|html|screenshot|pdf|click|type|eval", "commands");
|
|
50
50
|
assertIncludes(commands, "iola mcp list|status|install|remove|serve [--stdio]", "commands");
|
|
51
|
+
assertIncludes(commands, "iola uninstall --yes", "commands");
|
|
51
52
|
assertNotIncludes(commands, "Госуслуг", "commands");
|
|
52
53
|
assertNotIncludes(commands, "gosuslugi", "commands");
|
|
53
54
|
|
|
@@ -61,4 +62,9 @@ assertIncludes(skills, "open-data", "skills list");
|
|
|
61
62
|
assertIncludes(skills, "reports", "skills list");
|
|
62
63
|
assertNotIncludes(skills, "gosuslugi", "skills list");
|
|
63
64
|
|
|
65
|
+
const uninstallPlan = JSON.parse(await runCli(["uninstall", "--dry-run", "--json"]));
|
|
66
|
+
if (!Array.isArray(uninstallPlan.willDelete) || !uninstallPlan.willKeep.includes("Codex CLI")) {
|
|
67
|
+
throw new Error("uninstall dry-run should list delete targets and keep Codex CLI");
|
|
68
|
+
}
|
|
69
|
+
|
|
64
70
|
console.log("smoke tests passed");
|