@iola_adm/iola-cli 0.1.4 → 0.1.6
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 +60 -0
- package/package.json +1 -1
- package/src/cli.js +421 -4
package/README.md
CHANGED
|
@@ -75,8 +75,12 @@ iola help
|
|
|
75
75
|
|
|
76
76
|
```bash
|
|
77
77
|
iola banner
|
|
78
|
+
iola agent
|
|
78
79
|
iola ai doctor
|
|
79
80
|
iola ai setup ollama
|
|
81
|
+
iola ai ask "Какие школы есть на улице Петрова?"
|
|
82
|
+
iola ai setup openai --model gpt-4.1-mini
|
|
83
|
+
iola ai setup openrouter --model openai/gpt-4.1-mini
|
|
80
84
|
iola health
|
|
81
85
|
iola layers
|
|
82
86
|
iola schools --limit 10
|
|
@@ -91,6 +95,62 @@ iola setup codex
|
|
|
91
95
|
По умолчанию команды выводят компактную таблицу. Для полного ответа API
|
|
92
96
|
используйте `--json`.
|
|
93
97
|
|
|
98
|
+
## Интерактивный режим
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
iola agent
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Внутри agent доступны slash-команды:
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
/help
|
|
108
|
+
/health
|
|
109
|
+
/layers
|
|
110
|
+
/schools --limit 10
|
|
111
|
+
/schools get --inn 1215067180
|
|
112
|
+
/kindergartens --search 29
|
|
113
|
+
/search лицей --limit 3
|
|
114
|
+
/mcp-info
|
|
115
|
+
/ai doctor
|
|
116
|
+
/model
|
|
117
|
+
/provider
|
|
118
|
+
/config
|
|
119
|
+
/history
|
|
120
|
+
/clear
|
|
121
|
+
/exit
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Обычный текст без `/` в `iola agent` отправляется в настроенный AI-провайдер.
|
|
125
|
+
|
|
126
|
+
## AI-запросы
|
|
127
|
+
|
|
128
|
+
Локальная модель через Ollama:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
iola ai setup ollama
|
|
132
|
+
iola ai ask "Какие школы есть на улице Петрова?"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
OpenAI:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
set OPENAI_API_KEY=...
|
|
139
|
+
iola ai setup openai --model gpt-4.1-mini
|
|
140
|
+
iola ai ask "Найди школу 29"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
OpenRouter:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
set OPENROUTER_API_KEY=...
|
|
147
|
+
iola ai setup openrouter --model openai/gpt-4.1-mini
|
|
148
|
+
iola ai ask "Покажи контакты лицея"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
AI-ответ строится по контексту из публичного API. Если данных в контексте
|
|
152
|
+
недостаточно, ассистент должен сообщить об этом, а не выдумывать сведения.
|
|
153
|
+
|
|
94
154
|
## Назначение
|
|
95
155
|
|
|
96
156
|
Первый релиз CLI дает прямой терминальный доступ к открытым данным и командам
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import readline from "node:readline/promises";
|
|
@@ -9,6 +9,13 @@ const API_BASE_URL = process.env.IOLA_API_BASE_URL || "https://apiiola.yasg.ru/a
|
|
|
9
9
|
const MCP_BASE_URL = process.env.IOLA_MCP_BASE_URL || "https://apiiola.yasg.ru";
|
|
10
10
|
const CONFIG_DIR = path.join(os.homedir(), ".iola");
|
|
11
11
|
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
12
|
+
const DEFAULT_AI_CONFIG = {
|
|
13
|
+
ai: {
|
|
14
|
+
provider: "ollama",
|
|
15
|
+
model: "llama3.2:1b",
|
|
16
|
+
baseUrl: "http://127.0.0.1:11434",
|
|
17
|
+
},
|
|
18
|
+
};
|
|
12
19
|
const BANNER = `\x1b[38;5;45m┌────────────────────────────────────────────────────────────────────────────┐
|
|
13
20
|
│\x1b[38;5;51m ____ _ ___ ____ ____ ___ _____ _ _______ \x1b[38;5;45m│
|
|
14
21
|
│\x1b[38;5;51m / ___| | |_ _| | _ \\| _ \\ / _ \\| ____| |/ /_ _| \x1b[38;5;45m│
|
|
@@ -27,6 +34,7 @@ const COMMANDS = new Map([
|
|
|
27
34
|
["help", showHelp],
|
|
28
35
|
["version", showVersion],
|
|
29
36
|
["banner", showBanner],
|
|
37
|
+
["agent", startAgent],
|
|
30
38
|
["ai", handleAi],
|
|
31
39
|
["health", checkHealth],
|
|
32
40
|
["layers", listLayers],
|
|
@@ -54,6 +62,8 @@ async function showHelp() {
|
|
|
54
62
|
|
|
55
63
|
Usage:
|
|
56
64
|
iola banner
|
|
65
|
+
iola agent
|
|
66
|
+
iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
|
|
57
67
|
iola ai doctor [--json]
|
|
58
68
|
iola ai setup
|
|
59
69
|
iola ai setup ollama [--yes] [--model MODEL]
|
|
@@ -74,6 +84,168 @@ Environment:
|
|
|
74
84
|
`);
|
|
75
85
|
}
|
|
76
86
|
|
|
87
|
+
async function startAgent() {
|
|
88
|
+
showBanner();
|
|
89
|
+
console.log("Интерактивный режим. Введите /help для списка команд, /exit для выхода.");
|
|
90
|
+
|
|
91
|
+
const rl = readline.createInterface({ input, output, prompt: "iola> " });
|
|
92
|
+
const state = {
|
|
93
|
+
history: [],
|
|
94
|
+
};
|
|
95
|
+
let closed = false;
|
|
96
|
+
rl.on("close", () => {
|
|
97
|
+
closed = true;
|
|
98
|
+
});
|
|
99
|
+
safePrompt(rl);
|
|
100
|
+
|
|
101
|
+
for await (const rawLine of rl) {
|
|
102
|
+
const line = rawLine.trim();
|
|
103
|
+
|
|
104
|
+
if (!line) {
|
|
105
|
+
safePrompt(rl, closed);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const shouldExit = await handleAgentLine(line, state);
|
|
111
|
+
if (shouldExit) {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
safePrompt(rl, closed);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!closed) {
|
|
122
|
+
rl.close();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function handleAgentLine(line, state) {
|
|
127
|
+
if (!line.startsWith("/")) {
|
|
128
|
+
const answer = await aiAsk([line], { history: state.history });
|
|
129
|
+
state.history.push({ role: "user", content: line });
|
|
130
|
+
state.history.push({ role: "assistant", content: answer });
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const [command, ...args] = splitCommandLine(line.slice(1));
|
|
135
|
+
|
|
136
|
+
if (command === "exit" || command === "quit") {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (command === "help") {
|
|
141
|
+
printAgentHelp();
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (command === "clear") {
|
|
146
|
+
state.history = [];
|
|
147
|
+
console.log("История agent-сессии очищена.");
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (command === "history") {
|
|
152
|
+
printAgentHistory(state.history);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (command === "config") {
|
|
157
|
+
await printAiConfig();
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (command === "provider") {
|
|
162
|
+
await printAiConfigField("provider");
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (command === "model") {
|
|
167
|
+
await printAiConfigField("model");
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (command === "banner") {
|
|
172
|
+
showBanner();
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (command === "ai") {
|
|
177
|
+
await handleAi(args);
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const mapped = {
|
|
182
|
+
health: ["health", args],
|
|
183
|
+
layers: ["layers", args],
|
|
184
|
+
schools: ["schools", args],
|
|
185
|
+
kindergartens: ["kindergartens", args],
|
|
186
|
+
search: ["search", args],
|
|
187
|
+
"mcp-info": ["mcp-info", args],
|
|
188
|
+
setup: ["setup", args],
|
|
189
|
+
}[command];
|
|
190
|
+
|
|
191
|
+
if (!mapped) {
|
|
192
|
+
console.log(`Неизвестная slash-команда: /${command}`);
|
|
193
|
+
printAgentHelp();
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const [cliCommand, cliArgs] = mapped;
|
|
198
|
+
await COMMANDS.get(cliCommand)(cliArgs);
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function printAgentHelp() {
|
|
203
|
+
console.log(`Slash-команды:
|
|
204
|
+
/help
|
|
205
|
+
/health
|
|
206
|
+
/layers
|
|
207
|
+
/schools --limit 10
|
|
208
|
+
/schools get --inn 1215067180
|
|
209
|
+
/kindergartens --search 29
|
|
210
|
+
/kindergartens get --inn 1215077421
|
|
211
|
+
/search лицей --limit 3
|
|
212
|
+
/mcp-info
|
|
213
|
+
/ai doctor
|
|
214
|
+
/ai setup ollama
|
|
215
|
+
/config
|
|
216
|
+
/provider
|
|
217
|
+
/model
|
|
218
|
+
/history
|
|
219
|
+
/clear
|
|
220
|
+
/banner
|
|
221
|
+
/exit
|
|
222
|
+
|
|
223
|
+
Обычный текст без slash-команды отправляется в настроенный AI-провайдер.`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function printAgentHistory(history) {
|
|
227
|
+
if (history.length === 0) {
|
|
228
|
+
console.log("История пуста.");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const item of history.slice(-10)) {
|
|
233
|
+
console.log(`${item.role}: ${item.content}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function safePrompt(rl, closed = false) {
|
|
238
|
+
if (closed) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
rl.prompt();
|
|
244
|
+
} catch {
|
|
245
|
+
// The input stream can close while an async slash-command is still running.
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
77
249
|
function showBanner() {
|
|
78
250
|
if (process.stdout.isTTY && process.env.NO_COLOR !== "1") {
|
|
79
251
|
console.log(BANNER);
|
|
@@ -112,14 +284,22 @@ async function handleAi(args) {
|
|
|
112
284
|
if (subcommand === "help") {
|
|
113
285
|
showBanner();
|
|
114
286
|
console.log(`AI-команды:
|
|
287
|
+
iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
|
|
115
288
|
iola ai doctor [--json]
|
|
116
289
|
iola ai setup
|
|
117
290
|
iola ai setup ollama [--yes] [--model MODEL]
|
|
291
|
+
iola ai setup openai [--model MODEL]
|
|
292
|
+
iola ai setup openrouter [--model MODEL]
|
|
118
293
|
|
|
119
294
|
Локальная настройка сохраняется в ${CONFIG_FILE}`);
|
|
120
295
|
return;
|
|
121
296
|
}
|
|
122
297
|
|
|
298
|
+
if (subcommand === "ask") {
|
|
299
|
+
await aiAsk(rest);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
123
303
|
if (subcommand === "doctor") {
|
|
124
304
|
await aiDoctor(rest);
|
|
125
305
|
return;
|
|
@@ -162,8 +342,17 @@ async function aiSetup(args) {
|
|
|
162
342
|
}
|
|
163
343
|
|
|
164
344
|
if (provider === "openai" || provider === "openrouter") {
|
|
165
|
-
|
|
166
|
-
|
|
345
|
+
const options = parseOptions(args.slice(1));
|
|
346
|
+
const model = options.model || (provider === "openai" ? "gpt-4.1-mini" : "openai/gpt-4.1-mini");
|
|
347
|
+
await saveConfig({
|
|
348
|
+
ai: {
|
|
349
|
+
provider,
|
|
350
|
+
model,
|
|
351
|
+
baseUrl: provider === "openai" ? "https://api.openai.com/v1" : "https://openrouter.ai/api/v1",
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
console.log(`AI-профиль ${provider} сохранен в ${CONFIG_FILE}`);
|
|
355
|
+
console.log(`Ключ задайте через переменную окружения ${provider === "openai" ? "OPENAI_API_KEY" : "OPENROUTER_API_KEY"}.`);
|
|
167
356
|
return;
|
|
168
357
|
}
|
|
169
358
|
|
|
@@ -236,6 +425,167 @@ async function setupOllama(args) {
|
|
|
236
425
|
console.log(`Готово. Локальный AI-профиль сохранен в ${CONFIG_FILE}`);
|
|
237
426
|
}
|
|
238
427
|
|
|
428
|
+
async function aiAsk(args, context = {}) {
|
|
429
|
+
const options = parseOptions(args);
|
|
430
|
+
const question = options._.join(" ").trim();
|
|
431
|
+
|
|
432
|
+
if (!question) {
|
|
433
|
+
throw new Error('Текст вопроса обязателен. Пример: iola ai ask "Какие школы есть на улице Петрова?"');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const config = await loadConfig();
|
|
437
|
+
const provider = options.provider || config.ai.provider;
|
|
438
|
+
const model = options.model || config.ai.model;
|
|
439
|
+
const providerConfig = {
|
|
440
|
+
...config.ai,
|
|
441
|
+
provider,
|
|
442
|
+
model,
|
|
443
|
+
};
|
|
444
|
+
const dataContext = await buildDataContext(question);
|
|
445
|
+
const messages = buildAiMessages(question, dataContext, context.history || []);
|
|
446
|
+
const answer = await callAiProvider(providerConfig, messages);
|
|
447
|
+
|
|
448
|
+
console.log(answer);
|
|
449
|
+
return answer;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function buildDataContext(question) {
|
|
453
|
+
const [layers, schools, kindergartens] = await Promise.all([
|
|
454
|
+
fetchJson(`${MCP_BASE_URL}/mcp-version`),
|
|
455
|
+
fetchJson(`${API_BASE_URL}/schools?limit=100&offset=0`),
|
|
456
|
+
fetchJson(`${API_BASE_URL}/kindergartens?limit=100&offset=0`),
|
|
457
|
+
]);
|
|
458
|
+
const queryTerms = extractSearchTerms(question);
|
|
459
|
+
const schoolItems = findRelevantItems(normalizeItems(schools), queryTerms).slice(0, 8);
|
|
460
|
+
const kindergartenItems = findRelevantItems(normalizeItems(kindergartens), queryTerms).slice(0, 8);
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
layers: layers.data_layers || [],
|
|
464
|
+
schools: schoolItems.map(selectPublicSummary),
|
|
465
|
+
kindergartens: kindergartenItems.map(selectPublicSummary),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function extractSearchTerms(question) {
|
|
470
|
+
const normalized = question
|
|
471
|
+
.toLocaleLowerCase("ru-RU")
|
|
472
|
+
.replace(/[^\p{L}\p{N}\s.-]/gu, " ")
|
|
473
|
+
.split(/\s+/)
|
|
474
|
+
.map((term) => term.trim())
|
|
475
|
+
.filter(Boolean)
|
|
476
|
+
.filter((term) => !["какие", "какая", "какой", "есть", "найди", "покажи", "контакты", "адрес", "телефон", "школы", "школа", "сад", "детский", "детские", "сады", "улица", "ул"].includes(term));
|
|
477
|
+
|
|
478
|
+
return normalized.length > 0 ? normalized : [question];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function findRelevantItems(items, terms) {
|
|
482
|
+
return items
|
|
483
|
+
.map((item) => ({
|
|
484
|
+
item,
|
|
485
|
+
score: scoreItem(item, terms),
|
|
486
|
+
}))
|
|
487
|
+
.filter((entry) => entry.score > 0)
|
|
488
|
+
.sort((left, right) => right.score - left.score)
|
|
489
|
+
.map((entry) => entry.item);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function scoreItem(item, terms) {
|
|
493
|
+
const text = JSON.stringify(selectPublicSummary(item)).toLocaleLowerCase("ru-RU");
|
|
494
|
+
return terms.reduce((score, term) => score + (text.includes(term.toLocaleLowerCase("ru-RU")) ? 1 : 0), 0);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function buildAiMessages(question, dataContext, history) {
|
|
498
|
+
const system = [
|
|
499
|
+
"Ты терминальный AI-ассистент CLI-проекта Йошкар-Олы.",
|
|
500
|
+
"Отвечай на русском языке.",
|
|
501
|
+
"Используй только данные из переданного контекста.",
|
|
502
|
+
"Если в контексте нет нужных сведений, прямо напиши, что данных недостаточно.",
|
|
503
|
+
"Не выдумывай адреса, телефоны, лицензии и руководителей.",
|
|
504
|
+
"Отвечай кратко и по делу.",
|
|
505
|
+
].join(" ");
|
|
506
|
+
const contextText = JSON.stringify(dataContext, null, 2);
|
|
507
|
+
const recentHistory = history.slice(-6);
|
|
508
|
+
|
|
509
|
+
return [
|
|
510
|
+
{ role: "system", content: system },
|
|
511
|
+
...recentHistory,
|
|
512
|
+
{
|
|
513
|
+
role: "user",
|
|
514
|
+
content: `Контекст открытых данных городского округа "Город Йошкар-Ола":\n${contextText}\n\nВопрос пользователя: ${question}`,
|
|
515
|
+
},
|
|
516
|
+
];
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async function callAiProvider(config, messages) {
|
|
520
|
+
if (config.provider === "ollama") {
|
|
521
|
+
return callOllama(config, messages);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (config.provider === "openai") {
|
|
525
|
+
return callOpenAiCompatible(config, messages, process.env.OPENAI_API_KEY, "OpenAI");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (config.provider === "openrouter") {
|
|
529
|
+
return callOpenAiCompatible(config, messages, process.env.OPENROUTER_API_KEY, "OpenRouter");
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
throw new Error(`Неизвестный AI-провайдер: ${config.provider}`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function callOllama(config, messages) {
|
|
536
|
+
let response;
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
response = await fetch(`${config.baseUrl || "http://127.0.0.1:11434"}/api/chat`, {
|
|
540
|
+
method: "POST",
|
|
541
|
+
headers: { "content-type": "application/json" },
|
|
542
|
+
body: JSON.stringify({
|
|
543
|
+
model: config.model || "llama3.2:1b",
|
|
544
|
+
messages,
|
|
545
|
+
stream: false,
|
|
546
|
+
}),
|
|
547
|
+
});
|
|
548
|
+
} catch {
|
|
549
|
+
throw new Error("Ollama недоступен. Запустите Ollama и проверьте: ollama --version");
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (!response.ok) {
|
|
553
|
+
throw new Error(`Ollama request failed: ${response.status} ${response.statusText}. Проверьте "ollama serve" и модель.`);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const payload = await response.json();
|
|
557
|
+
return payload.message?.content || "";
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function callOpenAiCompatible(config, messages, apiKey, providerName) {
|
|
561
|
+
if (!apiKey) {
|
|
562
|
+
throw new Error(`${providerName} API key не найден. Задайте ${providerName === "OpenAI" ? "OPENAI_API_KEY" : "OPENROUTER_API_KEY"}.`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const response = await fetch(`${config.baseUrl}/chat/completions`, {
|
|
566
|
+
method: "POST",
|
|
567
|
+
headers: {
|
|
568
|
+
authorization: `Bearer ${apiKey}`,
|
|
569
|
+
"content-type": "application/json",
|
|
570
|
+
"http-referer": "https://github.com/adm-iola/iola-cli",
|
|
571
|
+
"x-title": "iola-cli",
|
|
572
|
+
},
|
|
573
|
+
body: JSON.stringify({
|
|
574
|
+
model: config.model,
|
|
575
|
+
messages,
|
|
576
|
+
temperature: 0.2,
|
|
577
|
+
}),
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
if (!response.ok) {
|
|
581
|
+
const text = await response.text();
|
|
582
|
+
throw new Error(`${providerName} request failed: ${response.status} ${response.statusText}\n${text}`);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const payload = await response.json();
|
|
586
|
+
return payload.choices?.[0]?.message?.content || "";
|
|
587
|
+
}
|
|
588
|
+
|
|
239
589
|
async function listLayers(args) {
|
|
240
590
|
const options = parseOptions(args);
|
|
241
591
|
const info = await fetchJson(`${MCP_BASE_URL}/mcp-version`);
|
|
@@ -375,7 +725,7 @@ function parseOptions(args) {
|
|
|
375
725
|
const arg = args[index];
|
|
376
726
|
if (arg === "--json" || arg === "--yes") {
|
|
377
727
|
result[arg.slice(2)] = true;
|
|
378
|
-
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--inn") {
|
|
728
|
+
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--inn" || arg === "--model" || arg === "--provider") {
|
|
379
729
|
result[arg.slice(2)] = args[index + 1];
|
|
380
730
|
index += 1;
|
|
381
731
|
} else {
|
|
@@ -386,6 +736,40 @@ function parseOptions(args) {
|
|
|
386
736
|
return result;
|
|
387
737
|
}
|
|
388
738
|
|
|
739
|
+
function splitCommandLine(line) {
|
|
740
|
+
const result = [];
|
|
741
|
+
let current = "";
|
|
742
|
+
let quote = null;
|
|
743
|
+
|
|
744
|
+
for (const char of line) {
|
|
745
|
+
if ((char === "\"" || char === "'") && quote === null) {
|
|
746
|
+
quote = char;
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (char === quote) {
|
|
751
|
+
quote = null;
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (/\s/.test(char) && quote === null) {
|
|
756
|
+
if (current) {
|
|
757
|
+
result.push(current);
|
|
758
|
+
current = "";
|
|
759
|
+
}
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
current += char;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (current) {
|
|
767
|
+
result.push(current);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return result;
|
|
771
|
+
}
|
|
772
|
+
|
|
389
773
|
function filterItems(items, query) {
|
|
390
774
|
const normalized = query.toLocaleLowerCase("ru-RU");
|
|
391
775
|
return items.filter((item) => JSON.stringify(item).toLocaleLowerCase("ru-RU").includes(normalized));
|
|
@@ -568,6 +952,39 @@ async function saveConfig(value) {
|
|
|
568
952
|
await writeFile(CONFIG_FILE, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
569
953
|
}
|
|
570
954
|
|
|
955
|
+
async function loadConfig() {
|
|
956
|
+
try {
|
|
957
|
+
const text = await readFile(CONFIG_FILE, "utf8");
|
|
958
|
+
return mergeConfig(DEFAULT_AI_CONFIG, JSON.parse(text));
|
|
959
|
+
} catch {
|
|
960
|
+
return DEFAULT_AI_CONFIG;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function mergeConfig(base, override) {
|
|
965
|
+
return {
|
|
966
|
+
...base,
|
|
967
|
+
...override,
|
|
968
|
+
ai: {
|
|
969
|
+
...base.ai,
|
|
970
|
+
...(override.ai || {}),
|
|
971
|
+
},
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
async function printAiConfig() {
|
|
976
|
+
const config = await loadConfig();
|
|
977
|
+
printJson({
|
|
978
|
+
file: CONFIG_FILE,
|
|
979
|
+
ai: config.ai,
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
async function printAiConfigField(field) {
|
|
984
|
+
const config = await loadConfig();
|
|
985
|
+
console.log(config.ai[field] || "-");
|
|
986
|
+
}
|
|
987
|
+
|
|
571
988
|
function runCommand(command, args, options = {}) {
|
|
572
989
|
return new Promise((resolve, reject) => {
|
|
573
990
|
const child = execFile(command, args, {
|