@iola_adm/iola-cli 0.1.65 → 0.1.66
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/package.json +3 -2
- package/src/cli.js +233 -10
- package/test/smoke-test.js +64 -0
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.66",
|
|
4
4
|
"description": "CLI и AI-агент городского округа Йошкар-Ола.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/adm-iola/iola-cli#readme",
|
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"postinstall": "node --no-warnings bin/iola.js db init --silent && node --no-warnings bin/iola.js browser install",
|
|
20
20
|
"start": "node --no-warnings bin/iola.js",
|
|
21
|
-
"test": "node --no-warnings --check bin/iola.js && node --no-warnings --check src/cli.js"
|
|
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
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"bin",
|
|
25
25
|
"src",
|
|
26
|
+
"test",
|
|
26
27
|
"skills",
|
|
27
28
|
"wiki",
|
|
28
29
|
"docs/assets/readme-header.png",
|
package/src/cli.js
CHANGED
|
@@ -289,9 +289,14 @@ const SLASH_COMMANDS = [
|
|
|
289
289
|
{ command: "/search лицей --limit 3", description: "поиск" },
|
|
290
290
|
{ command: "/mcp-info", description: "публичный MCP" },
|
|
291
291
|
{ command: "/profiles", description: "AI-профили" },
|
|
292
|
+
{ command: "/model", description: "переключить AI: local/API/Codex" },
|
|
293
|
+
{ command: "/model codex", description: "выбрать модель Codex" },
|
|
294
|
+
{ command: "/model api", description: "выбрать API-модель" },
|
|
292
295
|
{ command: "/models openrouter --search qwen", description: "модели" },
|
|
293
296
|
{ command: "/ai doctor", description: "AI diagnostics" },
|
|
294
297
|
{ command: "/ai setup ollama", description: "настройка Ollama" },
|
|
298
|
+
{ command: "/use codex", description: "выбрать Codex CLI" },
|
|
299
|
+
{ command: "/use local", description: "выбрать локальный профиль" },
|
|
295
300
|
{ command: "/use openai", description: "выбрать OpenAI" },
|
|
296
301
|
{ command: "/use ollama", description: "выбрать Ollama" },
|
|
297
302
|
{ command: "/key status", description: "API-ключи" },
|
|
@@ -761,10 +766,11 @@ async function startAgentReadline() {
|
|
|
761
766
|
}
|
|
762
767
|
|
|
763
768
|
async function startAgentRawInput() {
|
|
764
|
-
const state = { history: [], buffer: "", selected: 0, slashOpen: false, running: false, renderedInputLines: 0, rawMode: true, pendingOutput: "" };
|
|
769
|
+
const state = { history: [], buffer: "", selected: 0, slashOffset: 0, slashOpen: false, running: false, renderedInputLines: 0, rawMode: true, pendingOutput: "", aiStatus: null };
|
|
765
770
|
const wasRaw = input.isRaw;
|
|
766
771
|
activateRawInput(input);
|
|
767
772
|
|
|
773
|
+
await refreshAgentAiStatus(state);
|
|
768
774
|
const render = () => renderAgentInput(state);
|
|
769
775
|
render();
|
|
770
776
|
|
|
@@ -785,13 +791,19 @@ async function startAgentRawInput() {
|
|
|
785
791
|
}
|
|
786
792
|
if (key?.name === "up" && state.slashOpen) {
|
|
787
793
|
const matches = currentSlashMatches(state);
|
|
788
|
-
|
|
794
|
+
const nextSelected = Math.max(0, state.selected - 1);
|
|
795
|
+
state.selected = nextSelected;
|
|
796
|
+
if (state.selected < state.slashOffset) state.slashOffset = state.selected;
|
|
789
797
|
render();
|
|
790
798
|
continue;
|
|
791
799
|
}
|
|
792
800
|
if (key?.name === "down" && state.slashOpen) {
|
|
793
801
|
const matches = currentSlashMatches(state);
|
|
794
|
-
|
|
802
|
+
const visibleLimit = getSlashVisibleLimit();
|
|
803
|
+
const nextSelected = Math.min(matches.length - 1, state.selected + 1);
|
|
804
|
+
state.selected = Math.max(0, nextSelected);
|
|
805
|
+
if (state.selected >= state.slashOffset + visibleLimit) state.slashOffset = state.selected - visibleLimit + 1;
|
|
806
|
+
state.slashOffset = Math.max(0, Math.min(state.slashOffset, Math.max(0, matches.length - visibleLimit)));
|
|
795
807
|
render();
|
|
796
808
|
continue;
|
|
797
809
|
}
|
|
@@ -819,6 +831,7 @@ async function startAgentRawInput() {
|
|
|
819
831
|
const shouldExit = await handleAgentLine(line, state);
|
|
820
832
|
stopActivity();
|
|
821
833
|
flushPendingAgentOutput(state);
|
|
834
|
+
await refreshAgentAiStatus(state);
|
|
822
835
|
if (!shouldExit) restoreRawInput();
|
|
823
836
|
if (shouldExit) break;
|
|
824
837
|
} catch (error) {
|
|
@@ -1118,6 +1131,11 @@ async function handleAgentLine(line, state) {
|
|
|
1118
1131
|
return false;
|
|
1119
1132
|
}
|
|
1120
1133
|
|
|
1134
|
+
if (command === "model") {
|
|
1135
|
+
await slashModelMenu(args);
|
|
1136
|
+
return false;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1121
1139
|
if (command === "use") {
|
|
1122
1140
|
await useAiProvider(args);
|
|
1123
1141
|
return false;
|
|
@@ -1232,8 +1250,9 @@ function printAgentHelp() {
|
|
|
1232
1250
|
|
|
1233
1251
|
function printSlashMenu(filter = "", options = {}) {
|
|
1234
1252
|
const normalized = String(filter || "").replace(/^\//, "");
|
|
1253
|
+
const limit = options.limit === undefined ? Infinity : Number(options.limit);
|
|
1235
1254
|
const rows = getSlashCommandMatches(normalized)
|
|
1236
|
-
.slice(0,
|
|
1255
|
+
.slice(0, limit)
|
|
1237
1256
|
.map((item) => ({ command: item.command, description: item.description }));
|
|
1238
1257
|
if (rows.length === 0) {
|
|
1239
1258
|
console.log(`Нет slash-команд по фильтру: ${filter}`);
|
|
@@ -1261,11 +1280,16 @@ function getSlashCommandMatches(filter = "") {
|
|
|
1261
1280
|
function updateSlashState(state) {
|
|
1262
1281
|
state.slashOpen = state.buffer.startsWith("/");
|
|
1263
1282
|
state.selected = 0;
|
|
1283
|
+
state.slashOffset = 0;
|
|
1264
1284
|
}
|
|
1265
1285
|
|
|
1266
1286
|
function currentSlashMatches(state) {
|
|
1267
1287
|
if (!state.buffer.startsWith("/")) return [];
|
|
1268
|
-
return getSlashCommandMatches(state.buffer.slice(1))
|
|
1288
|
+
return getSlashCommandMatches(state.buffer.slice(1));
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function getSlashVisibleLimit() {
|
|
1292
|
+
return 10;
|
|
1269
1293
|
}
|
|
1270
1294
|
|
|
1271
1295
|
function renderAgentInput(state) {
|
|
@@ -1273,20 +1297,25 @@ function renderAgentInput(state) {
|
|
|
1273
1297
|
const prompt = "> ";
|
|
1274
1298
|
const lines = state.buffer.split("\n");
|
|
1275
1299
|
const inputLines = [`${prompt}${lines[0] || ""}`, ...lines.slice(1).map((line) => ` ${line}`)];
|
|
1276
|
-
const cwdLine = colorMuted(` ${
|
|
1300
|
+
const cwdLine = colorMuted(` ${buildAgentStatusLine(state)}`);
|
|
1277
1301
|
const menuLines = [];
|
|
1278
1302
|
if (state.slashOpen) {
|
|
1279
1303
|
const matches = currentSlashMatches(state);
|
|
1280
1304
|
if (matches.length === 0) {
|
|
1281
1305
|
menuLines.push(" нет команд");
|
|
1282
1306
|
} else {
|
|
1283
|
-
|
|
1284
|
-
|
|
1307
|
+
const visibleLimit = getSlashVisibleLimit();
|
|
1308
|
+
const offset = Math.max(0, Math.min(state.slashOffset || 0, Math.max(0, matches.length - visibleLimit)));
|
|
1309
|
+
const visibleMatches = matches.slice(offset, offset + visibleLimit);
|
|
1310
|
+
for (let index = 0; index < visibleMatches.length; index += 1) {
|
|
1311
|
+
const absoluteIndex = offset + index;
|
|
1312
|
+
const selected = absoluteIndex === state.selected;
|
|
1285
1313
|
const marker = selected ? ">" : " ";
|
|
1286
|
-
const row = `${marker} ${
|
|
1314
|
+
const row = `${marker} ${visibleMatches[index].command.padEnd(24)} ${visibleMatches[index].description}`;
|
|
1287
1315
|
menuLines.push(selected ? colorSlashSelection(row) : ` ${row.slice(2)}`);
|
|
1288
1316
|
}
|
|
1289
|
-
|
|
1317
|
+
const shownTo = Math.min(offset + visibleLimit, matches.length);
|
|
1318
|
+
menuLines.push(` ↑/↓ выбрать • Enter выполнить • Esc закрыть • ${offset + 1}-${shownTo} из ${matches.length}`);
|
|
1290
1319
|
}
|
|
1291
1320
|
}
|
|
1292
1321
|
|
|
@@ -1400,6 +1429,35 @@ function printAgentHistory(history) {
|
|
|
1400
1429
|
}
|
|
1401
1430
|
}
|
|
1402
1431
|
|
|
1432
|
+
async function refreshAgentAiStatus(state) {
|
|
1433
|
+
try {
|
|
1434
|
+
const config = await loadConfig();
|
|
1435
|
+
const name = getActiveProfileName(config);
|
|
1436
|
+
const profile = config.ai.profiles?.[name] || {
|
|
1437
|
+
provider: config.ai.provider,
|
|
1438
|
+
model: config.ai.model,
|
|
1439
|
+
baseUrl: config.ai.baseUrl,
|
|
1440
|
+
};
|
|
1441
|
+
state.aiStatus = { name, provider: profile.provider || "-", model: profile.model || "-" };
|
|
1442
|
+
} catch {
|
|
1443
|
+
state.aiStatus = null;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
function buildAgentStatusLine(state) {
|
|
1448
|
+
const cwd = process.cwd();
|
|
1449
|
+
const ai = state.aiStatus;
|
|
1450
|
+
if (!ai) return cwd;
|
|
1451
|
+
const kind = {
|
|
1452
|
+
ollama: "локальная",
|
|
1453
|
+
openai: "API",
|
|
1454
|
+
openrouter: "API",
|
|
1455
|
+
codex: "Codex",
|
|
1456
|
+
}[ai.provider] || ai.provider;
|
|
1457
|
+
const model = ai.model && ai.model !== "-" ? ` • ${ai.model}` : "";
|
|
1458
|
+
return `${cwd} | AI: ${kind}${model} (${ai.name})`;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1403
1461
|
function compactAgentHistory(history) {
|
|
1404
1462
|
if (history.length <= 8) return history;
|
|
1405
1463
|
const summary = history.slice(0, -6)
|
|
@@ -4267,6 +4325,171 @@ async function useAiProvider(args) {
|
|
|
4267
4325
|
console.log(`AI-провайдер переключен: ${provider}, профиль: ${profileName}, модель: ${defaultModel}`);
|
|
4268
4326
|
}
|
|
4269
4327
|
|
|
4328
|
+
async function slashModelMenu(args = []) {
|
|
4329
|
+
const [target, maybeModel] = args;
|
|
4330
|
+
const normalizedTarget = normalizeModelMenuTarget(target);
|
|
4331
|
+
|
|
4332
|
+
if (normalizedTarget && maybeModel) {
|
|
4333
|
+
const directTarget = normalizedTarget === "api" ? await getDefaultApiProviderForModelSwitch() : normalizedTarget;
|
|
4334
|
+
await switchModelTarget(directTarget, maybeModel);
|
|
4335
|
+
return;
|
|
4336
|
+
}
|
|
4337
|
+
|
|
4338
|
+
const selectedTarget = normalizedTarget || await chooseModelTarget();
|
|
4339
|
+
if (!selectedTarget) return;
|
|
4340
|
+
|
|
4341
|
+
await openModelTargetMenu(selectedTarget);
|
|
4342
|
+
}
|
|
4343
|
+
|
|
4344
|
+
function normalizeModelMenuTarget(value = "") {
|
|
4345
|
+
const normalized = String(value || "").trim().toLocaleLowerCase("ru-RU");
|
|
4346
|
+
if (!normalized) return "";
|
|
4347
|
+
if (["local", "локальная", "локально", "ollama"].includes(normalized)) return "local";
|
|
4348
|
+
if (["api", "апи"].includes(normalized)) return "api";
|
|
4349
|
+
if (normalized === "openai") return "openai";
|
|
4350
|
+
if (normalized === "openrouter" || normalized === "router") return "openrouter";
|
|
4351
|
+
if (["codex", "кодекс"].includes(normalized)) return "codex";
|
|
4352
|
+
return "";
|
|
4353
|
+
}
|
|
4354
|
+
|
|
4355
|
+
async function chooseModelTarget() {
|
|
4356
|
+
console.log("Выберите AI-подключение:");
|
|
4357
|
+
console.log(" 1. Локальная модель (Ollama)");
|
|
4358
|
+
console.log(" 2. API (OpenAI/OpenRouter)");
|
|
4359
|
+
console.log(" 3. Codex CLI");
|
|
4360
|
+
console.log(" 0. Отмена");
|
|
4361
|
+
|
|
4362
|
+
const answer = await askText("Номер: ");
|
|
4363
|
+
return { 1: "local", 2: "api", 3: "codex" }[answer.trim()] || "";
|
|
4364
|
+
}
|
|
4365
|
+
|
|
4366
|
+
async function openModelTargetMenu(target) {
|
|
4367
|
+
if (target === "local") {
|
|
4368
|
+
const model = await chooseAiModel("ollama");
|
|
4369
|
+
if (model) await switchModelTarget("local", model);
|
|
4370
|
+
return;
|
|
4371
|
+
}
|
|
4372
|
+
|
|
4373
|
+
if (target === "codex") {
|
|
4374
|
+
const model = await chooseAiModel("codex");
|
|
4375
|
+
if (model) await switchModelTarget("codex", model);
|
|
4376
|
+
return;
|
|
4377
|
+
}
|
|
4378
|
+
|
|
4379
|
+
if (target === "openai" || target === "openrouter") {
|
|
4380
|
+
const model = await chooseAiModel(target);
|
|
4381
|
+
if (model) await switchModelTarget(target, model);
|
|
4382
|
+
return;
|
|
4383
|
+
}
|
|
4384
|
+
|
|
4385
|
+
const provider = await chooseApiProvider();
|
|
4386
|
+
if (!provider) return;
|
|
4387
|
+
const model = await chooseAiModel(provider);
|
|
4388
|
+
if (model) await switchModelTarget(provider, model);
|
|
4389
|
+
}
|
|
4390
|
+
|
|
4391
|
+
async function chooseApiProvider() {
|
|
4392
|
+
const config = await loadConfig();
|
|
4393
|
+
const apiProfiles = Object.entries(config.ai.profiles || {})
|
|
4394
|
+
.filter(([, profile]) => profile.provider === "openai" || profile.provider === "openrouter")
|
|
4395
|
+
.map(([name, profile]) => ({ id: profile.provider, label: `${name}: ${profile.provider} (${profile.model || "-"})` }));
|
|
4396
|
+
const choices = [
|
|
4397
|
+
...apiProfiles,
|
|
4398
|
+
{ id: "openai", label: "OpenAI API" },
|
|
4399
|
+
{ id: "openrouter", label: "OpenRouter API" },
|
|
4400
|
+
].filter((item, index, array) => array.findIndex((candidate) => candidate.id === item.id) === index);
|
|
4401
|
+
|
|
4402
|
+
console.log("Выберите API-подключение:");
|
|
4403
|
+
choices.forEach((item, index) => console.log(` ${index + 1}. ${item.label}`));
|
|
4404
|
+
console.log(" 0. Отмена");
|
|
4405
|
+
|
|
4406
|
+
const answer = Number(await askText("Номер: "));
|
|
4407
|
+
return choices[answer - 1]?.id || "";
|
|
4408
|
+
}
|
|
4409
|
+
|
|
4410
|
+
async function getDefaultApiProviderForModelSwitch() {
|
|
4411
|
+
const config = await loadConfig();
|
|
4412
|
+
const activeProfile = config.ai.profiles?.[getActiveProfileName(config)];
|
|
4413
|
+
if (activeProfile?.provider === "openai" || activeProfile?.provider === "openrouter") return activeProfile.provider;
|
|
4414
|
+
const apiProfile = Object.values(config.ai.profiles || {}).find((profile) => profile.provider === "openai" || profile.provider === "openrouter");
|
|
4415
|
+
return apiProfile?.provider || "openai";
|
|
4416
|
+
}
|
|
4417
|
+
|
|
4418
|
+
async function chooseAiModel(provider) {
|
|
4419
|
+
let search = "";
|
|
4420
|
+
if (provider === "openrouter" || provider === "openai") {
|
|
4421
|
+
search = (await askText("Фильтр моделей (Enter - без фильтра): ")).trim();
|
|
4422
|
+
}
|
|
4423
|
+
|
|
4424
|
+
let models;
|
|
4425
|
+
try {
|
|
4426
|
+
models = await listAiModels(provider);
|
|
4427
|
+
} catch (error) {
|
|
4428
|
+
console.log(error instanceof Error ? error.message : String(error));
|
|
4429
|
+
return "";
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
let filtered = search
|
|
4433
|
+
? models.filter((model) => model.id.toLocaleLowerCase("ru-RU").includes(search.toLocaleLowerCase("ru-RU")))
|
|
4434
|
+
: models;
|
|
4435
|
+
|
|
4436
|
+
if (filtered.length === 0) {
|
|
4437
|
+
console.log("Модели не найдены.");
|
|
4438
|
+
return "";
|
|
4439
|
+
}
|
|
4440
|
+
|
|
4441
|
+
const limit = 25;
|
|
4442
|
+
if (filtered.length > limit) {
|
|
4443
|
+
filtered = filtered.slice(0, limit);
|
|
4444
|
+
console.log(`Показаны первые ${limit} моделей. Для точного выбора запустите /model и задайте фильтр.`);
|
|
4445
|
+
}
|
|
4446
|
+
|
|
4447
|
+
console.log("Выберите модель:");
|
|
4448
|
+
filtered.forEach((model, index) => console.log(` ${index + 1}. ${model.id}${model.note ? ` - ${model.note}` : ""}`));
|
|
4449
|
+
console.log(" 0. Отмена");
|
|
4450
|
+
|
|
4451
|
+
const answer = Number(await askText("Номер: "));
|
|
4452
|
+
return filtered[answer - 1]?.id || "";
|
|
4453
|
+
}
|
|
4454
|
+
|
|
4455
|
+
async function switchModelTarget(target, model) {
|
|
4456
|
+
const config = await loadConfig();
|
|
4457
|
+
const provider = target === "local" ? "ollama" : target;
|
|
4458
|
+
const profileName = provider === "ollama" ? "local" : provider;
|
|
4459
|
+
const currentProfile = config.ai.profiles?.[profileName] || buildProfileFromOptions(provider, { model });
|
|
4460
|
+
const profile = {
|
|
4461
|
+
...currentProfile,
|
|
4462
|
+
provider,
|
|
4463
|
+
model,
|
|
4464
|
+
};
|
|
4465
|
+
|
|
4466
|
+
await saveConfig({
|
|
4467
|
+
ai: {
|
|
4468
|
+
...config.ai,
|
|
4469
|
+
activeProfile: profileName,
|
|
4470
|
+
provider,
|
|
4471
|
+
model,
|
|
4472
|
+
baseUrl: profile.baseUrl || config.ai.baseUrl,
|
|
4473
|
+
profiles: {
|
|
4474
|
+
...(config.ai.profiles || {}),
|
|
4475
|
+
[profileName]: profile,
|
|
4476
|
+
},
|
|
4477
|
+
},
|
|
4478
|
+
});
|
|
4479
|
+
|
|
4480
|
+
console.log(`Активная модель: ${profileName} (${provider}, ${model})`);
|
|
4481
|
+
}
|
|
4482
|
+
|
|
4483
|
+
async function askText(question) {
|
|
4484
|
+
if (!process.stdin.isTTY) return "";
|
|
4485
|
+
const rl = readline.createInterface({ input, output });
|
|
4486
|
+
try {
|
|
4487
|
+
return await rl.question(question);
|
|
4488
|
+
} finally {
|
|
4489
|
+
rl.close();
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4492
|
+
|
|
4270
4493
|
async function aiContext(args) {
|
|
4271
4494
|
const options = parseOptions(args);
|
|
4272
4495
|
const query = options._.join(" ").trim();
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
+
const binPath = resolve(rootDir, "bin", "iola.js");
|
|
8
|
+
const packageJson = JSON.parse(await readFile(resolve(rootDir, "package.json"), "utf8"));
|
|
9
|
+
|
|
10
|
+
function runCli(args) {
|
|
11
|
+
return new Promise((resolvePromise, reject) => {
|
|
12
|
+
execFile(
|
|
13
|
+
process.execPath,
|
|
14
|
+
["--no-warnings", binPath, ...args],
|
|
15
|
+
{ cwd: rootDir, encoding: "utf8", timeout: 15_000 },
|
|
16
|
+
(error, stdout, stderr) => {
|
|
17
|
+
if (error) {
|
|
18
|
+
reject(new Error(`iola ${args.join(" ")} failed\n${stdout}${stderr}`));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
resolvePromise(stdout);
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function assertIncludes(text, expected, label) {
|
|
28
|
+
if (!text.includes(expected)) {
|
|
29
|
+
throw new Error(`${label} should include ${JSON.stringify(expected)}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function assertNotIncludes(text, unexpected, label) {
|
|
34
|
+
if (text.includes(unexpected)) {
|
|
35
|
+
throw new Error(`${label} should not include ${JSON.stringify(unexpected)}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const version = (await runCli(["version"])).trim();
|
|
40
|
+
if (version !== packageJson.version) {
|
|
41
|
+
throw new Error(`version command returned ${version}, expected ${packageJson.version}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const help = await runCli(["--help"]);
|
|
45
|
+
assertIncludes(help, "iola master", "help");
|
|
46
|
+
assertIncludes(help, "iola ask", "help");
|
|
47
|
+
|
|
48
|
+
const commands = await runCli(["commands"]);
|
|
49
|
+
assertIncludes(commands, "iola browser status|install|open|text|html|screenshot|pdf|click|type|eval", "commands");
|
|
50
|
+
assertIncludes(commands, "iola mcp list|status|install|remove|serve [--stdio]", "commands");
|
|
51
|
+
assertNotIncludes(commands, "Госуслуг", "commands");
|
|
52
|
+
assertNotIncludes(commands, "gosuslugi", "commands");
|
|
53
|
+
|
|
54
|
+
const schema = JSON.parse(await runCli(["config", "schema"]));
|
|
55
|
+
if (!schema.properties?.api || !schema.properties?.ai) {
|
|
56
|
+
throw new Error("config schema should expose api and ai sections");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const skills = await runCli(["skills", "list"]);
|
|
60
|
+
assertIncludes(skills, "open-data", "skills list");
|
|
61
|
+
assertIncludes(skills, "reports", "skills list");
|
|
62
|
+
assertNotIncludes(skills, "gosuslugi", "skills list");
|
|
63
|
+
|
|
64
|
+
console.log("smoke tests passed");
|