@iola_adm/iola-cli 0.1.64 → 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/src/cli.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { execFile, spawn } from "node:child_process";
2
- import { createHash, randomBytes } from "node:crypto";
3
2
  import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
4
3
  import { createServer } from "node:http";
5
4
  import { appendFile, copyFile, cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
@@ -26,31 +25,14 @@ const PROJECT_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "config.json");
26
25
  const LOCAL_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "local.json");
27
26
  const BROWSER_RUNTIME_DIR = path.join(CONFIG_DIR, "browser-runtime");
28
27
  const BROWSER_RUNTIME_PACKAGE = path.join(BROWSER_RUNTIME_DIR, "node_modules", "playwright", "package.json");
29
- const GOSUSLUGI_BROWSER_PROFILE_DIR = path.join(CONFIG_DIR, "gosuslugi-browser-profile");
30
- const GOSUSLUGI_BROWSER_LOCK_DIR = path.join(CONFIG_DIR, "gosuslugi-browser-profile.lock");
31
- const GOSUSLUGI_DEFAULT_URL = "https://www.gosuslugi.ru/";
32
28
  const INDEXABLE_EXTENSIONS = /\.(md|txt|csv|json|html|docx|xlsx|pptx|pdf)$/i;
33
- const LOCAL_TOOLS = ["search_data", "get_card", "export_report", "file_read", "browser_open", "gosuslugi_whoami", "gosuslugi_debt", "gosuslugi_notifications"];
29
+ const LOCAL_TOOLS = ["search_data", "get_card", "export_report", "file_read", "browser_open"];
34
30
  const LEGACY_LOCAL_TOOLS = ["search_local", "export_data", "run_report", "save_view"];
35
31
  const FILE_TOOLS = ["files_tree", "files_read", "files_search", "files_write", "files_patch"];
36
32
  const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS];
37
33
  const ALL_TOOL_ALIASES = [...ALL_LOCAL_TOOLS, ...LEGACY_LOCAL_TOOLS];
38
34
  const HOOK_EVENTS = ["SessionStart", "BeforeTool", "AfterTool", "PreToolUse", "PostToolUse", "OnError", "AfterSync", "BeforeExport", "SessionEnd"];
39
35
  const DAEMON_PORT = Number(process.env.IOLA_DAEMON_PORT || 18790);
40
- const GOSUSLUGI_CONSENT_VERSION = "2026-05-26-personal-local-v1";
41
- const GOSUSLUGI_CONSENT_TEXT = `Подключение личных Госуслуг
42
-
43
- Вы подключаете личную учетную запись Госуслуг к локальному CLI-агенту iola-cli на этом компьютере.
44
-
45
- Нажимая "Да", вы подтверждаете, что:
46
- - используете собственную учетную запись Госуслуг;
47
- - понимаете, что все действия, выполненные через CLI-агента после подключения, считаются действиями владельца этой учетной записи;
48
- - разрешаете iola-cli локально сохранить данные доступа, необходимые для повторного входа или выполнения запросов от вашего имени;
49
- - понимаете, что данные доступа хранятся только на этом компьютере в локальном хранилище пользователя и не передаются разработчикам CLI, администрации города или третьим лицам;
50
- - обязуетесь не подключать чужие учетные записи и не передавать локальные файлы доступа другим лицам;
51
- - понимаете, что перед юридически значимыми действиями, отправкой заявлений, оплатой, подписанием или изменением персональных данных CLI должен запросить отдельное подтверждение.
52
-
53
- Продолжить подключение?`;
54
36
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
55
37
  const BUILTIN_SKILLS_DIR = path.resolve(__dirname, "..", "skills");
56
38
  const USER_SKILLS_DIR = path.join(CONFIG_DIR, "skills");
@@ -131,19 +113,6 @@ const DEFAULT_AI_CONFIG = {
131
113
  baseUrl: "https://apiiola.yasg.ru/api/v1",
132
114
  mcpBaseUrl: "https://apiiola.yasg.ru",
133
115
  },
134
- gosuslugi: {
135
- enabled: false,
136
- mode: "personal-browser",
137
- authUrl: "",
138
- tokenUrl: "",
139
- userinfoUrl: "",
140
- clientId: "",
141
- clientSecret: "",
142
- scope: "openid",
143
- redirectHost: "127.0.0.1",
144
- redirectPort: 18791,
145
- redirectPath: "/gosuslugi/callback",
146
- },
147
116
  ai: {
148
117
  activeProfile: "local",
149
118
  provider: "ollama",
@@ -181,9 +150,6 @@ const DEFAULT_AI_CONFIG = {
181
150
  export_report: true,
182
151
  file_read: false,
183
152
  browser_open: true,
184
- gosuslugi_whoami: true,
185
- gosuslugi_debt: true,
186
- gosuslugi_notifications: true,
187
153
  files_tree: false,
188
154
  files_read: false,
189
155
  files_search: false,
@@ -214,7 +180,7 @@ const DEFAULT_AI_CONFIG = {
214
180
  suggestions: true,
215
181
  },
216
182
  skills: {
217
- enabled: ["open-data", "reports", "local-model", "local-files", "browser-agent", "gosuslugi"],
183
+ enabled: ["open-data", "reports", "local-model", "local-files", "browser-agent"],
218
184
  },
219
185
  daemon: {
220
186
  host: "127.0.0.1",
@@ -286,11 +252,6 @@ const SLASH_COMMANDS = [
286
252
  { command: "/sessions", description: "AI-сессии" },
287
253
  { command: "/resume SESSION_ID", description: "продолжить сессию" },
288
254
  { command: "/features list", description: "feature flags" },
289
- { command: "/gosuslugi status", description: "личное подключение Госуслуг" },
290
- { command: "/gosuslugi connect", description: "открыть личный вход Госуслуг" },
291
- { command: "/gosuslugi debt", description: "задолженности Госуслуг" },
292
- { command: "/gosuslugi notifications", description: "уведомления Госуслуг" },
293
- { command: "/gosuslugi keepalive", description: "проверка сессии каждые 30 минут" },
294
255
  { command: "/wiki", description: "ссылки на документацию" },
295
256
  { command: "/context list", description: "локальный контекст проекта" },
296
257
  { command: "/skills list", description: "skills" },
@@ -328,9 +289,14 @@ const SLASH_COMMANDS = [
328
289
  { command: "/search лицей --limit 3", description: "поиск" },
329
290
  { command: "/mcp-info", description: "публичный MCP" },
330
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-модель" },
331
295
  { command: "/models openrouter --search qwen", description: "модели" },
332
296
  { command: "/ai doctor", description: "AI diagnostics" },
333
297
  { command: "/ai setup ollama", description: "настройка Ollama" },
298
+ { command: "/use codex", description: "выбрать Codex CLI" },
299
+ { command: "/use local", description: "выбрать локальный профиль" },
334
300
  { command: "/use openai", description: "выбрать OpenAI" },
335
301
  { command: "/use ollama", description: "выбрать Ollama" },
336
302
  { command: "/key status", description: "API-ключи" },
@@ -361,7 +327,6 @@ const COMMANDS = new Map([
361
327
  ["fork", forkSession],
362
328
  ["features", handleFeatures],
363
329
  ["settings", handleSettings],
364
- ["gosuslugi", handleGosuslugi],
365
330
  ["wiki", handleWiki],
366
331
  ["context", handleContext],
367
332
  ["skills", handleSkills],
@@ -489,7 +454,6 @@ async function showHelp() {
489
454
  iola agent интерактивный режим
490
455
  iola ai setup настройка AI-профиля
491
456
  iola browser status браузерный runtime
492
- iola gosuslugi status личное подключение Госуслуг
493
457
  iola mcp status MCP-подключение
494
458
  iola doctor диагностика
495
459
  iola wiki документация
@@ -524,7 +488,6 @@ Usage:
524
488
  iola fork SESSION_ID [TEXT]
525
489
  iola features list|enable|disable
526
490
  iola settings list|get|validate|doctor|init
527
- iola gosuslugi terms|consent|status|check|keepalive|install-keepalive|keepalive-status|uninstall-keepalive|connect|open|text|screenshot|whoami|debt|notifications|mark-read|logout|configure|login|userinfo
528
491
  iola wiki [open|links]
529
492
  iola context list|show|init
530
493
  iola skills list|show|paths|enable|disable|bundles|bundle|doctor
@@ -803,10 +766,11 @@ async function startAgentReadline() {
803
766
  }
804
767
 
805
768
  async function startAgentRawInput() {
806
- 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 };
807
770
  const wasRaw = input.isRaw;
808
771
  activateRawInput(input);
809
772
 
773
+ await refreshAgentAiStatus(state);
810
774
  const render = () => renderAgentInput(state);
811
775
  render();
812
776
 
@@ -827,13 +791,19 @@ async function startAgentRawInput() {
827
791
  }
828
792
  if (key?.name === "up" && state.slashOpen) {
829
793
  const matches = currentSlashMatches(state);
830
- state.selected = Math.max(0, Math.min(matches.length - 1, state.selected - 1));
794
+ const nextSelected = Math.max(0, state.selected - 1);
795
+ state.selected = nextSelected;
796
+ if (state.selected < state.slashOffset) state.slashOffset = state.selected;
831
797
  render();
832
798
  continue;
833
799
  }
834
800
  if (key?.name === "down" && state.slashOpen) {
835
801
  const matches = currentSlashMatches(state);
836
- state.selected = Math.max(0, Math.min(matches.length - 1, state.selected + 1));
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)));
837
807
  render();
838
808
  continue;
839
809
  }
@@ -861,6 +831,7 @@ async function startAgentRawInput() {
861
831
  const shouldExit = await handleAgentLine(line, state);
862
832
  stopActivity();
863
833
  flushPendingAgentOutput(state);
834
+ await refreshAgentAiStatus(state);
864
835
  if (!shouldExit) restoreRawInput();
865
836
  if (shouldExit) break;
866
837
  } catch (error) {
@@ -1059,10 +1030,6 @@ async function handleAgentLine(line, state) {
1059
1030
  return false;
1060
1031
  }
1061
1032
 
1062
- if (command === "gosuslugi") {
1063
- await handleGosuslugi(args);
1064
- return false;
1065
- }
1066
1033
 
1067
1034
  if (command === "workspace") {
1068
1035
  await handleWorkspace(args);
@@ -1164,6 +1131,11 @@ async function handleAgentLine(line, state) {
1164
1131
  return false;
1165
1132
  }
1166
1133
 
1134
+ if (command === "model") {
1135
+ await slashModelMenu(args);
1136
+ return false;
1137
+ }
1138
+
1167
1139
  if (command === "use") {
1168
1140
  await useAiProvider(args);
1169
1141
  return false;
@@ -1213,7 +1185,6 @@ async function handleAgentLine(line, state) {
1213
1185
  resume: ["resume", args],
1214
1186
  fork: ["fork", args],
1215
1187
  features: ["features", args],
1216
- gosuslugi: ["gosuslugi", args],
1217
1188
  wiki: ["wiki", args],
1218
1189
  context: ["context", args],
1219
1190
  skills: ["skills", args],
@@ -1279,8 +1250,9 @@ function printAgentHelp() {
1279
1250
 
1280
1251
  function printSlashMenu(filter = "", options = {}) {
1281
1252
  const normalized = String(filter || "").replace(/^\//, "");
1253
+ const limit = options.limit === undefined ? Infinity : Number(options.limit);
1282
1254
  const rows = getSlashCommandMatches(normalized)
1283
- .slice(0, Number(options.limit || 30))
1255
+ .slice(0, limit)
1284
1256
  .map((item) => ({ command: item.command, description: item.description }));
1285
1257
  if (rows.length === 0) {
1286
1258
  console.log(`Нет slash-команд по фильтру: ${filter}`);
@@ -1308,11 +1280,16 @@ function getSlashCommandMatches(filter = "") {
1308
1280
  function updateSlashState(state) {
1309
1281
  state.slashOpen = state.buffer.startsWith("/");
1310
1282
  state.selected = 0;
1283
+ state.slashOffset = 0;
1311
1284
  }
1312
1285
 
1313
1286
  function currentSlashMatches(state) {
1314
1287
  if (!state.buffer.startsWith("/")) return [];
1315
- return getSlashCommandMatches(state.buffer.slice(1)).slice(0, 10);
1288
+ return getSlashCommandMatches(state.buffer.slice(1));
1289
+ }
1290
+
1291
+ function getSlashVisibleLimit() {
1292
+ return 10;
1316
1293
  }
1317
1294
 
1318
1295
  function renderAgentInput(state) {
@@ -1320,20 +1297,25 @@ function renderAgentInput(state) {
1320
1297
  const prompt = "> ";
1321
1298
  const lines = state.buffer.split("\n");
1322
1299
  const inputLines = [`${prompt}${lines[0] || ""}`, ...lines.slice(1).map((line) => ` ${line}`)];
1323
- const cwdLine = colorMuted(` ${process.cwd()}`);
1300
+ const cwdLine = colorMuted(` ${buildAgentStatusLine(state)}`);
1324
1301
  const menuLines = [];
1325
1302
  if (state.slashOpen) {
1326
1303
  const matches = currentSlashMatches(state);
1327
1304
  if (matches.length === 0) {
1328
1305
  menuLines.push(" нет команд");
1329
1306
  } else {
1330
- for (let index = 0; index < matches.length; index += 1) {
1331
- const selected = index === state.selected;
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;
1332
1313
  const marker = selected ? ">" : " ";
1333
- const row = `${marker} ${matches[index].command.padEnd(24)} ${matches[index].description}`;
1314
+ const row = `${marker} ${visibleMatches[index].command.padEnd(24)} ${visibleMatches[index].description}`;
1334
1315
  menuLines.push(selected ? colorSlashSelection(row) : ` ${row.slice(2)}`);
1335
1316
  }
1336
- menuLines.push(" ↑/↓ выбрать Enter вставить/выполнить Esc закрыть");
1317
+ const shownTo = Math.min(offset + visibleLimit, matches.length);
1318
+ menuLines.push(` ↑/↓ выбрать • Enter выполнить • Esc закрыть • ${offset + 1}-${shownTo} из ${matches.length}`);
1337
1319
  }
1338
1320
  }
1339
1321
 
@@ -1447,6 +1429,35 @@ function printAgentHistory(history) {
1447
1429
  }
1448
1430
  }
1449
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
+
1450
1461
  function compactAgentHistory(history) {
1451
1462
  if (history.length <= 8) return history;
1452
1463
  const summary = history.slice(0, -6)
@@ -2245,185 +2256,6 @@ async function handleSettings(args) {
2245
2256
  throw new Error("Команды settings: list, get [KEY], validate, doctor, init.");
2246
2257
  }
2247
2258
 
2248
- async function handleGosuslugi(args) {
2249
- const [action = "status", ...rest] = args;
2250
- const options = parseOptions(rest);
2251
-
2252
- if (action === "terms") {
2253
- console.log(GOSUSLUGI_CONSENT_TEXT);
2254
- return;
2255
- }
2256
-
2257
- if (action === "consent") {
2258
- await acceptGosuslugiConsent(options);
2259
- return;
2260
- }
2261
-
2262
- if (action === "status") {
2263
- const config = await loadConfig();
2264
- const secrets = await loadSecrets();
2265
- const tokens = secrets.gosuslugi?.tokens || null;
2266
- const browserSession = secrets.gosuslugiBrowser || null;
2267
- const consent = secrets.gosuslugiConsent || null;
2268
- printKeyValue({
2269
- mode: config.gosuslugi?.mode || "personal-browser",
2270
- enabled: config.gosuslugi?.enabled ? "yes" : "no",
2271
- browserProfile: GOSUSLUGI_BROWSER_PROFILE_DIR,
2272
- browserProfileExists: existsSync(GOSUSLUGI_BROWSER_PROFILE_DIR) ? "yes" : "no",
2273
- browserConnected: browserSession?.connectedAt ? "yes" : "unknown",
2274
- browserConnectedAt: browserSession?.connectedAt || "-",
2275
- oauthConfigured: isGosuslugiConfigured(config) ? "yes" : "no",
2276
- consent: consent?.version === GOSUSLUGI_CONSENT_VERSION ? "accepted" : "not accepted",
2277
- consentAt: consent?.acceptedAt || "-",
2278
- clientId: config.gosuslugi?.clientId ? maskSecret(config.gosuslugi.clientId) : "-",
2279
- authUrl: config.gosuslugi?.authUrl || "-",
2280
- tokenUrl: config.gosuslugi?.tokenUrl || "-",
2281
- userinfoUrl: config.gosuslugi?.userinfoUrl || "-",
2282
- redirectUri: gosuslugiRedirectUri(config),
2283
- connected: tokens?.access_token ? "yes" : "no",
2284
- savedAt: secrets.gosuslugi?.savedAt || "-",
2285
- expiresAt: secrets.gosuslugi?.expiresAt || "-",
2286
- });
2287
- return;
2288
- }
2289
-
2290
- if (action === "check") {
2291
- const result = await gosuslugiCheck(options);
2292
- if (options.json) printJson(result);
2293
- else printKeyValue(result);
2294
- return;
2295
- }
2296
-
2297
- if (action === "keepalive") {
2298
- await gosuslugiKeepalive(options);
2299
- return;
2300
- }
2301
-
2302
- if (action === "install-keepalive") {
2303
- await installGosuslugiKeepaliveTask(options);
2304
- return;
2305
- }
2306
-
2307
- if (action === "uninstall-keepalive") {
2308
- await uninstallGosuslugiKeepaliveTask(options);
2309
- return;
2310
- }
2311
-
2312
- if (action === "keepalive-status") {
2313
- await printGosuslugiKeepaliveTaskStatus(options);
2314
- return;
2315
- }
2316
-
2317
- if (action === "connect") {
2318
- await gosuslugiBrowserConnect(options);
2319
- return;
2320
- }
2321
-
2322
- if (action === "open") {
2323
- await gosuslugiBrowserOpen(targetOrDefault(rest, options), options);
2324
- return;
2325
- }
2326
-
2327
- if (action === "text") {
2328
- const result = await gosuslugiBrowserReadText(targetOrDefault(rest, options), options);
2329
- if (options.output) {
2330
- await writeFile(path.resolve(options.output), result, "utf8");
2331
- console.log(`Файл сохранен: ${path.resolve(options.output)}`);
2332
- } else {
2333
- console.log(result);
2334
- }
2335
- return;
2336
- }
2337
-
2338
- if (action === "screenshot") {
2339
- const outputFile = path.resolve(options.output || "gosuslugi-page.png");
2340
- await gosuslugiBrowserScreenshot(targetOrDefault(rest, options), outputFile, options);
2341
- saveArtifact("gosuslugi-screenshot", targetOrDefault(rest, options), outputFile, { url: targetOrDefault(rest, options) });
2342
- console.log(`Файл сохранен: ${outputFile}`);
2343
- return;
2344
- }
2345
-
2346
- if (action === "whoami" || action === "profile") {
2347
- const result = await gosuslugiWhoami(options);
2348
- if (options.json) printJson(result);
2349
- else printKeyValue(result.summary);
2350
- return;
2351
- }
2352
-
2353
- if (action === "debt" || action === "debts" || action === "payments") {
2354
- const result = await gosuslugiDebt(options);
2355
- if (options.json) printJson(result);
2356
- else printGosuslugiDebt(result);
2357
- return;
2358
- }
2359
-
2360
- if (action === "notifications" || action === "notices") {
2361
- const result = await gosuslugiNotifications(options);
2362
- if (options.json) printJson(result);
2363
- else printGosuslugiNotifications(result);
2364
- return;
2365
- }
2366
-
2367
- if (action === "mark-read") {
2368
- await gosuslugiMarkNotificationsRead(options);
2369
- return;
2370
- }
2371
-
2372
- if (action === "configure") {
2373
- const current = await loadConfig();
2374
- const next = {
2375
- ...(current.gosuslugi || {}),
2376
- enabled: true,
2377
- mode: "personal-local",
2378
- authUrl: options["auth-url"] || current.gosuslugi?.authUrl || "",
2379
- tokenUrl: options["token-url"] || current.gosuslugi?.tokenUrl || "",
2380
- userinfoUrl: options["userinfo-url"] || current.gosuslugi?.userinfoUrl || "",
2381
- clientId: options["client-id"] || current.gosuslugi?.clientId || "",
2382
- clientSecret: options["client-secret"] || current.gosuslugi?.clientSecret || "",
2383
- scope: options.scope || current.gosuslugi?.scope || "openid",
2384
- redirectHost: options["redirect-host"] || current.gosuslugi?.redirectHost || "127.0.0.1",
2385
- redirectPort: Number(options["redirect-port"] || current.gosuslugi?.redirectPort || 18791),
2386
- redirectPath: options["redirect-path"] || current.gosuslugi?.redirectPath || "/gosuslugi/callback",
2387
- };
2388
- await saveConfig({ gosuslugi: next });
2389
- console.log("Настройки личного локального подключения Госуслуг сохранены.");
2390
- console.log(`Redirect URI: ${gosuslugiRedirectUri({ gosuslugi: next })}`);
2391
- return;
2392
- }
2393
-
2394
- if (action === "login") {
2395
- const result = await gosuslugiLogin(options);
2396
- printKeyValue(result);
2397
- return;
2398
- }
2399
-
2400
- if (action === "logout") {
2401
- const secrets = await loadSecrets();
2402
- delete secrets.gosuslugi;
2403
- delete secrets.gosuslugiBrowser;
2404
- await saveSecrets(secrets);
2405
- if (options.profile || options.all) {
2406
- await rm(GOSUSLUGI_BROWSER_PROFILE_DIR, { recursive: true, force: true }).catch(() => {});
2407
- console.log("Локальный браузерный профиль Госуслуг удален.");
2408
- }
2409
- console.log("Локальное подключение Госуслуг удалено.");
2410
- return;
2411
- }
2412
-
2413
- if (action === "userinfo" || action === "me") {
2414
- const result = await gosuslugiUserinfo(options);
2415
- if (options.json) printJson(result);
2416
- else printKeyValue(flattenObjectForPrint(result));
2417
- return;
2418
- }
2419
-
2420
- throw new Error("Команды gosuslugi: terms, consent, status, check, keepalive, install-keepalive, keepalive-status, uninstall-keepalive, connect, open, text, screenshot, whoami, debt, notifications, mark-read, logout, configure, login, userinfo.");
2421
- }
2422
-
2423
- function targetOrDefault(args, options = {}) {
2424
- return options.url || args.find((item) => !item.startsWith("--")) || GOSUSLUGI_DEFAULT_URL;
2425
- }
2426
-
2427
2259
  async function handleWiki(args) {
2428
2260
  const [action = "links"] = args;
2429
2261
  const base = "https://github.com/adm-iola/iola-cli/wiki";
@@ -2439,7 +2271,6 @@ async function handleWiki(args) {
2439
2271
  ["Рабочая среда агента", `${base}/Рабочая-среда-агента`],
2440
2272
  ["Платформа агента", `${base}/Платформа-агента`],
2441
2273
  ["Браузерный агент", `${base}/Браузерный-агент`],
2442
- ["Подключение Госуслуг", `${base}/Подключение-Госуслуг`],
2443
2274
  ["Расширения и локальные данные", `${base}/Расширения-и-локальные-данные`],
2444
2275
  ["Архивы и мастер настройки", `${base}/Архивы-и-мастер-настройки`],
2445
2276
  ["Daemon, RPC и cron", `${base}/Daemon-RPC-и-cron`],
@@ -3376,189 +3207,6 @@ async function openUrl(url) {
3376
3207
  await runCommand("xdg-open", [url], { inherit: false });
3377
3208
  }
3378
3209
 
3379
- async function gosuslugiLogin(options = {}) {
3380
- const config = await loadConfig();
3381
- if (!isGosuslugiConfigured(config)) {
3382
- throw new Error("Личное подключение не настроено. Пример: iola gosuslugi configure --auth-url URL --token-url URL --client-id ID --scope openid");
3383
- }
3384
- await ensureGosuslugiConsent(options);
3385
-
3386
- const state = randomUrlSafe(24);
3387
- const codeVerifier = randomUrlSafe(64);
3388
- const codeChallenge = base64Url(createHash("sha256").update(codeVerifier).digest());
3389
- const redirectUri = gosuslugiRedirectUri(config);
3390
- const callback = waitForOAuthCallback(config.gosuslugi, state, Number(options.timeout || 180000));
3391
- const authUrl = new URL(config.gosuslugi.authUrl);
3392
- authUrl.searchParams.set("response_type", "code");
3393
- authUrl.searchParams.set("client_id", config.gosuslugi.clientId);
3394
- authUrl.searchParams.set("redirect_uri", redirectUri);
3395
- authUrl.searchParams.set("scope", config.gosuslugi.scope || "openid");
3396
- authUrl.searchParams.set("state", state);
3397
- authUrl.searchParams.set("code_challenge", codeChallenge);
3398
- authUrl.searchParams.set("code_challenge_method", "S256");
3399
-
3400
- console.log("Открываю экран входа Госуслуг в браузере для личного локального подключения.");
3401
- console.log("После входа CLI примет callback на локальном адресе и сохранит данные доступа только на этом компьютере.");
3402
- await openUrl(authUrl.toString());
3403
- const params = await callback;
3404
- if (params.error) throw new Error(`Госуслуги вернули ошибку: ${params.error} ${params.error_description || ""}`.trim());
3405
- if (!params.code) throw new Error("Authorization code не получен.");
3406
-
3407
- const tokens = await exchangeGosuslugiCode(config, {
3408
- code: params.code,
3409
- codeVerifier,
3410
- redirectUri,
3411
- });
3412
- const secrets = await loadSecrets();
3413
- const now = new Date();
3414
- const expiresAt = tokens.expires_in ? new Date(now.getTime() + Number(tokens.expires_in) * 1000).toISOString() : "";
3415
- secrets.gosuslugi = {
3416
- savedAt: now.toISOString(),
3417
- expiresAt,
3418
- tokens,
3419
- };
3420
- await saveSecrets(secrets);
3421
- return {
3422
- connected: "yes",
3423
- savedAt: secrets.gosuslugi.savedAt,
3424
- expiresAt: expiresAt || "-",
3425
- tokenType: tokens.token_type || "-",
3426
- scope: tokens.scope || config.gosuslugi.scope || "-",
3427
- };
3428
- }
3429
-
3430
- async function acceptGosuslugiConsent(options = {}) {
3431
- console.log(GOSUSLUGI_CONSENT_TEXT);
3432
- if (!options.yes) {
3433
- const accepted = await confirm("Да, подключить личные Госуслуги к локальному iola-cli? [y/N] ");
3434
- if (!accepted) {
3435
- throw new Error("Подключение Госуслуг отменено пользователем.");
3436
- }
3437
- }
3438
- const secrets = await loadSecrets();
3439
- secrets.gosuslugiConsent = {
3440
- version: GOSUSLUGI_CONSENT_VERSION,
3441
- acceptedAt: new Date().toISOString(),
3442
- user: os.userInfo().username,
3443
- host: os.hostname(),
3444
- };
3445
- await saveSecrets(secrets);
3446
- console.log("Согласие сохранено локально.");
3447
- }
3448
-
3449
- async function ensureGosuslugiConsent(options = {}) {
3450
- const secrets = await loadSecrets();
3451
- if (secrets.gosuslugiConsent?.version === GOSUSLUGI_CONSENT_VERSION) return;
3452
- await acceptGosuslugiConsent(options);
3453
- }
3454
-
3455
- async function requireGosuslugiConsent() {
3456
- await ensureGosuslugiConsent();
3457
- }
3458
-
3459
- function waitForOAuthCallback(settings, expectedState, timeoutMs) {
3460
- const host = settings.redirectHost || "127.0.0.1";
3461
- const port = Number(settings.redirectPort || 18791);
3462
- const callbackPath = settings.redirectPath || "/gosuslugi/callback";
3463
- return new Promise((resolve, reject) => {
3464
- const timer = setTimeout(() => {
3465
- server.close(() => {});
3466
- reject(new Error("Истекло время ожидания входа через Госуслуги."));
3467
- }, timeoutMs);
3468
- const server = createServer((req, res) => {
3469
- const url = new URL(req.url || "/", `http://${host}:${port}`);
3470
- if (url.pathname !== callbackPath) {
3471
- res.statusCode = 404;
3472
- res.end("Not found");
3473
- return;
3474
- }
3475
- const params = Object.fromEntries(url.searchParams.entries());
3476
- if (params.state !== expectedState) {
3477
- res.statusCode = 400;
3478
- res.end("Invalid state");
3479
- clearTimeout(timer);
3480
- server.close(() => {});
3481
- reject(new Error("OAuth state не совпал. Вход отменен."));
3482
- return;
3483
- }
3484
- res.setHeader("content-type", "text/html; charset=utf-8");
3485
- res.end("<!doctype html><meta charset=\"utf-8\"><title>iola</title><body>Вход выполнен. Можно закрыть это окно и вернуться в терминал.</body>");
3486
- clearTimeout(timer);
3487
- server.close(() => resolve(params));
3488
- });
3489
- server.once("error", (error) => {
3490
- clearTimeout(timer);
3491
- reject(error);
3492
- });
3493
- server.listen(port, host);
3494
- });
3495
- }
3496
-
3497
- async function exchangeGosuslugiCode(config, { code, codeVerifier, redirectUri }) {
3498
- const body = new URLSearchParams();
3499
- body.set("grant_type", "authorization_code");
3500
- body.set("code", code);
3501
- body.set("redirect_uri", redirectUri);
3502
- body.set("client_id", config.gosuslugi.clientId);
3503
- body.set("code_verifier", codeVerifier);
3504
- body.set("client_mode", config.gosuslugi.mode || "personal-local");
3505
- if (config.gosuslugi.clientSecret) body.set("client_secret", config.gosuslugi.clientSecret);
3506
-
3507
- const response = await fetch(config.gosuslugi.tokenUrl, {
3508
- method: "POST",
3509
- headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
3510
- body,
3511
- });
3512
- const text = await response.text();
3513
- let payload = {};
3514
- try {
3515
- payload = text ? JSON.parse(text) : {};
3516
- } catch {
3517
- payload = { raw: text };
3518
- }
3519
- if (!response.ok) {
3520
- throw new Error(`Token endpoint вернул ${response.status}: ${JSON.stringify(payload)}`);
3521
- }
3522
- return payload;
3523
- }
3524
-
3525
- async function gosuslugiUserinfo() {
3526
- const config = await loadConfig();
3527
- const secrets = await loadSecrets();
3528
- const accessToken = secrets.gosuslugi?.tokens?.access_token;
3529
- if (!accessToken) throw new Error("Госуслуги не подключены. Запустите: iola gosuslugi login");
3530
- if (!config.gosuslugi?.userinfoUrl) throw new Error("userinfoUrl не настроен.");
3531
- const response = await fetch(config.gosuslugi.userinfoUrl, {
3532
- headers: { authorization: `Bearer ${accessToken}`, accept: "application/json" },
3533
- });
3534
- const text = await response.text();
3535
- let payload = {};
3536
- try {
3537
- payload = text ? JSON.parse(text) : {};
3538
- } catch {
3539
- payload = { raw: text };
3540
- }
3541
- if (!response.ok) throw new Error(`Userinfo endpoint вернул ${response.status}: ${JSON.stringify(payload)}`);
3542
- return payload;
3543
- }
3544
-
3545
- function isGosuslugiConfigured(config) {
3546
- return Boolean(config.gosuslugi?.authUrl && config.gosuslugi?.tokenUrl && config.gosuslugi?.clientId);
3547
- }
3548
-
3549
- function gosuslugiRedirectUri(config) {
3550
- const settings = config.gosuslugi || DEFAULT_AI_CONFIG.gosuslugi;
3551
- return `http://${settings.redirectHost || "127.0.0.1"}:${Number(settings.redirectPort || 18791)}${settings.redirectPath || "/gosuslugi/callback"}`;
3552
- }
3553
-
3554
- function randomUrlSafe(bytes) {
3555
- return base64Url(randomBytes(bytes));
3556
- }
3557
-
3558
- function base64Url(buffer) {
3559
- return Buffer.from(buffer).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
3560
- }
3561
-
3562
3210
  function maskSecret(value) {
3563
3211
  const text = String(value || "");
3564
3212
  if (text.length <= 8) return text ? "***" : "-";
@@ -4677,6 +4325,171 @@ async function useAiProvider(args) {
4677
4325
  console.log(`AI-провайдер переключен: ${provider}, профиль: ${profileName}, модель: ${defaultModel}`);
4678
4326
  }
4679
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
+
4680
4493
  async function aiContext(args) {
4681
4494
  const options = parseOptions(args);
4682
4495
  const query = options._.join(" ").trim();
@@ -6075,12 +5888,6 @@ async function aiAsk(args, context = {}) {
6075
5888
  throw new Error('Текст вопроса обязателен. Пример: iola ai ask "Какие школы есть на улице Петрова?"');
6076
5889
  }
6077
5890
 
6078
- if (!options.bare && isGosuslugiPersonalIntent(question)) {
6079
- const answer = await answerGosuslugiQuestion(question, options);
6080
- if (!options.quiet) console.log(answer);
6081
- return answer;
6082
- }
6083
-
6084
5891
  const config = await loadConfig();
6085
5892
  const providerConfig = await resolveUsableAiProfile(config, options);
6086
5893
  if (providerConfig.provider === "codex") await assertPermission("codex");
@@ -6249,7 +6056,7 @@ async function buildLocalToolPlan(question, providerConfig, options) {
6249
6056
  "Ты планировщик CLI iola. Верни только JSON.",
6250
6057
  `Доступные tools: ${availableToolNames(options).join(", ")}.`,
6251
6058
  "Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
6252
- "Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}, gosuslugi_whoami {}, gosuslugi_debt {}, gosuslugi_notifications {unread,limit}.",
6059
+ "Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
6253
6060
  "MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
6254
6061
  "Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
6255
6062
  `Вопрос: ${question}`,
@@ -6279,12 +6086,6 @@ function inferToolPlan(question, options = {}) {
6279
6086
  const steps = [];
6280
6087
  if (normalized.includes("без телефона")) {
6281
6088
  steps.push({ tool: "export_report", args: { name: "missing-phones" } });
6282
- } else if (/(уведомлен|сообщени|госпочт|непрочитан)/iu.test(normalized)) {
6283
- steps.push({ tool: "gosuslugi_notifications", args: { unread: /непрочитан|нов/iu.test(normalized), limit: 15 } });
6284
- } else if (/(задолж|долг|штраф|налог|к оплате|платеж|платёж)/iu.test(normalized)) {
6285
- steps.push({ tool: "gosuslugi_debt", args: {} });
6286
- } else if (/(фио|дата рождения|профиль|кто я)/iu.test(normalized) && /госуслуг/iu.test(normalized)) {
6287
- steps.push({ tool: "gosuslugi_whoami", args: {} });
6288
6089
  } else {
6289
6090
  const query = normalized.match(/петрова|школ[а-яё ]*\d+|сад[а-яё ]*\d+|лицей[а-яё ]*\d+/iu)?.[0] || question;
6290
6091
  steps.push({ tool: "search_data", args: { dataset, query, limit: 20 } });
@@ -6374,18 +6175,6 @@ async function executeToolPlan(plan, options = {}) {
6374
6175
  const text = await runBrowserAutomation("text", { url: step.args?.url, waitMs: Number(step.args?.waitMs || 0), timeout: Number(step.args?.timeout || 30000), viewport: step.args?.viewport || "1366x768" });
6375
6176
  current = [{ url: step.args?.url, text }];
6376
6177
  outputs.push({ tool: step.tool, rows: 1 });
6377
- } else if (step.tool === "gosuslugi_whoami") {
6378
- const result = await gosuslugiWhoami(step.args || {});
6379
- current = [result.summary];
6380
- outputs.push({ tool: step.tool, rows: 1 });
6381
- } else if (step.tool === "gosuslugi_debt") {
6382
- const result = await gosuslugiDebt(step.args || {});
6383
- current = [{ total: result.total, amount: result.amount, debts: result.debts }];
6384
- outputs.push({ tool: step.tool, rows: result.debts.length });
6385
- } else if (step.tool === "gosuslugi_notifications") {
6386
- const result = await gosuslugiNotifications(step.args || {});
6387
- current = [{ total: result.total, unread: result.unread, items: result.items }];
6388
- outputs.push({ tool: step.tool, rows: result.items.length });
6389
6178
  } else if (String(step.tool || "").startsWith("mcp:")) {
6390
6179
  const result = await callConfiguredMcpTool(step.tool, step.args || {});
6391
6180
  current = Array.isArray(result) ? result : [result];
@@ -7122,17 +6911,6 @@ async function onboard(args = []) {
7122
6911
  if (status.installed === "yes") console.log("Browser runtime уже установлен.");
7123
6912
  else await installBrowserRuntime();
7124
6913
  }
7125
- if (components.includes("gosuslugi")) {
7126
- if (process.stdin.isTTY) await handleGosuslugi(["consent"]);
7127
- else await handleGosuslugi(["terms"]);
7128
- await ensureBrowserRuntimeForGosuslugi();
7129
- if (process.stdin.isTTY && await confirm("Открыть Госуслуги для входа сейчас? [Y/n] ")) {
7130
- await gosuslugiBrowserConnect({ yes: true });
7131
- await installGosuslugiKeepaliveTask({ interval: "30m" });
7132
- } else {
7133
- console.log("Подключить личные Госуслуги позже: iola gosuslugi connect");
7134
- }
7135
- }
7136
6914
  if (components.includes("index")) {
7137
6915
  await setFilesMode("read-only", await loadConfig());
7138
6916
  console.log("Индекс документов можно запустить командой: iola index folder ./docs");
@@ -7166,7 +6944,6 @@ async function chooseOnboardComponents(status = null) {
7166
6944
  8: "archive",
7167
6945
  9: "index",
7168
6946
  10: "browser",
7169
- 11: "gosuslugi",
7170
6947
  };
7171
6948
  return [...selected].map((item) => map[item] || item).filter(Boolean);
7172
6949
  } finally {
@@ -7175,18 +6952,16 @@ async function chooseOnboardComponents(status = null) {
7175
6952
  }
7176
6953
 
7177
6954
  async function getOnboardComponentStatus() {
7178
- const [config, readiness, browser, archive, codexVersion, ollamaVersion, secrets] = await Promise.all([
6955
+ const [config, readiness, browser, archive, codexVersion, ollamaVersion] = await Promise.all([
7179
6956
  loadConfig(),
7180
6957
  getAiReadiness(),
7181
6958
  getBrowserStatus(),
7182
6959
  findCommand(["7z", "7zz", "7za"], ["--help"]).catch(() => null),
7183
6960
  getCommandVersion("codex", ["--version"]),
7184
6961
  getOllamaVersion(),
7185
- loadSecrets(),
7186
6962
  ]);
7187
6963
  const workspaceReady = existsSync(PROJECT_CONTEXT_FILE) || existsSync(PROJECT_CONTEXT_DIR_FILE) || existsSync(PROJECT_IOLA_DIR);
7188
6964
  const policyReady = (config.toolsets?.enabled || []).includes("analyst");
7189
- const gosuslugiReady = Boolean(config.gosuslugi?.enabled && existsSync(GOSUSLUGI_BROWSER_PROFILE_DIR) && secrets.gosuslugiBrowser?.connectedAt);
7190
6965
  return {
7191
6966
  workspace: workspaceReady,
7192
6967
  policy: policyReady,
@@ -7198,7 +6973,6 @@ async function getOnboardComponentStatus() {
7198
6973
  archive: Boolean(archive),
7199
6974
  index: false,
7200
6975
  browser: browser.installed === "yes",
7201
- gosuslugi: gosuslugiReady,
7202
6976
  };
7203
6977
  }
7204
6978
 
@@ -7214,7 +6988,6 @@ function onboardComponentRows(status) {
7214
6988
  ["8", "archive", "7-Zip / архивы", "архиватор найден"],
7215
6989
  ["9", "index", "Индекс локальных документов", "настраивается под выбранную папку"],
7216
6990
  ["10", "browser", "Browser runtime", "Playwright/Chromium установлен"],
7217
- ["11", "gosuslugi", "Личное подключение Госуслуг", "профиль и keepalive"],
7218
6991
  ];
7219
6992
  return rows.map(([number, key, title, hint]) => ({ number, key, title, hint, status: status[key] ? "готово" : "не настроено" }));
7220
6993
  }
@@ -7228,7 +7001,7 @@ function defaultOnboardSelection(status) {
7228
7001
  }
7229
7002
 
7230
7003
  function defaultOnboardComponents(status) {
7231
- const map = { 1: "workspace", 2: "policy", 3: "ollama", 4: "openai", 5: "openrouter", 6: "codex", 7: "codex-mcp", 8: "archive", 9: "index", 10: "browser", 11: "gosuslugi" };
7004
+ const map = { 1: "workspace", 2: "policy", 3: "ollama", 4: "openai", 5: "openrouter", 6: "codex", 7: "codex-mcp", 8: "archive", 9: "index", 10: "browser" };
7232
7005
  return defaultOnboardSelection(status).map((item) => map[item]).filter(Boolean);
7233
7006
  }
7234
7007
 
@@ -7478,8 +7251,7 @@ async function buildSkillsText(config, question = "", options = {}) {
7478
7251
  const chunks = [];
7479
7252
  const selected = selectSkillsForPrompt(config, question, options);
7480
7253
  for (const skill of listSkills(config)) {
7481
- const active = skill.enabled || (skill.name === "gosuslugi" && config.gosuslugi?.enabled);
7482
- if (!active || !selected.has(skill.name)) continue;
7254
+ if (!skill.enabled || !selected.has(skill.name)) continue;
7483
7255
  const text = await readFile(skill.file, "utf8");
7484
7256
  chunks.push(`## Skill: ${skill.name}\n${stripFrontmatter(text).trim()}`);
7485
7257
  }
@@ -7495,7 +7267,6 @@ function selectSkillsForPrompt(config, question = "", options = {}) {
7495
7267
  if (enabled.has("reports") && /(отчет|отчёт|выгруз|csv|xlsx|качество|провер)/iu.test(normalized)) selected.add("reports");
7496
7268
  if (enabled.has("local-files") && (options.files || /(файл|папк|readme|документ|архив)/iu.test(normalized))) selected.add("local-files");
7497
7269
  if (enabled.has("browser-agent") && /(браузер|сайт|страниц|url|https?:\/\/)/iu.test(normalized)) selected.add("browser-agent");
7498
- if (enabled.has("gosuslugi") && /(госуслуг|задолж|долг|штраф|налог|к оплате|платеж|платёж|уведомлен|госпочт|фио|дата рождения)/iu.test(normalized)) selected.add("gosuslugi");
7499
7270
  return selected;
7500
7271
  }
7501
7272
 
@@ -7719,534 +7490,6 @@ async function runBrowserAutomation(action, params) {
7719
7490
  }
7720
7491
  }
7721
7492
 
7722
- async function ensureBrowserRuntimeForGosuslugi() {
7723
- if (existsSync(BROWSER_RUNTIME_PACKAGE)) return;
7724
- console.log("Browser runtime не установлен. Устанавливаю Playwright/Chromium для локального браузерного профиля.");
7725
- await installBrowserRuntime();
7726
- }
7727
-
7728
- async function gosuslugiBrowserConnect(options = {}) {
7729
- await ensureGosuslugiConsent({ yes: options.yes });
7730
- await ensureBrowserRuntimeForGosuslugi();
7731
- await saveConfig({ gosuslugi: { ...(await loadConfig()).gosuslugi, enabled: true, mode: "personal-browser" } });
7732
- const url = options.url || GOSUSLUGI_DEFAULT_URL;
7733
- console.log(`Открываю Госуслуги в отдельном локальном профиле: ${GOSUSLUGI_BROWSER_PROFILE_DIR}`);
7734
- console.log("Авторизуйтесь в открывшемся окне. Когда закончите, закройте окно браузера.");
7735
- await runPersistentBrowserAutomation("open", {
7736
- url,
7737
- userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
7738
- headed: true,
7739
- waitMs: Number(options.wait || 0),
7740
- timeout: Number(options.timeout || 120000),
7741
- viewport: options.viewport || "1366x768",
7742
- });
7743
- const secrets = await loadSecrets();
7744
- secrets.gosuslugiBrowser = {
7745
- mode: "personal-browser",
7746
- profileDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
7747
- connectedAt: new Date().toISOString(),
7748
- lastUrl: url,
7749
- };
7750
- await saveSecrets(secrets);
7751
- console.log("Локальный браузерный профиль Госуслуг сохранен.");
7752
- }
7753
-
7754
- async function gosuslugiBrowserOpen(url = GOSUSLUGI_DEFAULT_URL, options = {}) {
7755
- await requireGosuslugiConsent();
7756
- await ensureBrowserRuntimeForGosuslugi();
7757
- await runPersistentBrowserAutomation("open", {
7758
- url,
7759
- userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
7760
- headed: true,
7761
- waitMs: Number(options.wait || 0),
7762
- timeout: Number(options.timeout || 120000),
7763
- viewport: options.viewport || "1366x768",
7764
- });
7765
- }
7766
-
7767
- async function gosuslugiBrowserReadText(url = GOSUSLUGI_DEFAULT_URL, options = {}) {
7768
- await requireGosuslugiConsent();
7769
- await ensureBrowserRuntimeForGosuslugi();
7770
- return runPersistentBrowserAutomation("text", {
7771
- url,
7772
- userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
7773
- headed: Boolean(options.headed),
7774
- waitMs: Number(options.wait || 3000),
7775
- timeout: Number(options.timeout || 60000),
7776
- viewport: options.viewport || "1366x768",
7777
- });
7778
- }
7779
-
7780
- async function gosuslugiBrowserScreenshot(url = GOSUSLUGI_DEFAULT_URL, outputFile, options = {}) {
7781
- await requireGosuslugiConsent();
7782
- await ensureBrowserRuntimeForGosuslugi();
7783
- await runPersistentBrowserAutomation("screenshot", {
7784
- url,
7785
- output: outputFile,
7786
- userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
7787
- headed: Boolean(options.headed),
7788
- waitMs: Number(options.wait || 3000),
7789
- timeout: Number(options.timeout || 60000),
7790
- viewport: options.viewport || "1366x768",
7791
- });
7792
- }
7793
-
7794
- async function gosuslugiWhoami(options = {}) {
7795
- const data = await gosuslugiBrowserApiJson({
7796
- pageUrl: "https://lk.gosuslugi.ru/settings/account",
7797
- endpoint: "https://www.gosuslugi.ru/api/lk/v1/users/data",
7798
- waitMs: Number(options.wait || 3000),
7799
- });
7800
- const person = data.person?.person || data.person || data;
7801
- const summary = {
7802
- fio: [data.lastName || person.lastName, data.firstName || person.firstName, data.middleName || person.middleName].filter(Boolean).join(" ") || data.formattedName || "-",
7803
- birthDate: person.birthDate || data.birthDate || "-",
7804
- status: data.assuranceLevel === "AL20" || person.trusted ? "Подтвержденная учетная запись" : data.assuranceLevel || "-",
7805
- phone: options.full ? (data.personMobilePhone || data.mobile || "-") : maskPhone(data.personMobilePhone || data.mobile || ""),
7806
- email: options.full ? (data.personEMail || data.personEmail || data.email || "-") : maskEmail(data.personEMail || data.personEmail || data.email || ""),
7807
- snils: options.full ? (person.snils || data.personSnils || data.snils || "-") : maskDocument(person.snils || data.personSnils || data.snils || ""),
7808
- inn: options.full ? (person.inn || data.personINN || data.inn || "-") : maskDocument(person.inn || data.personINN || data.inn || ""),
7809
- };
7810
- return {
7811
- summary,
7812
- raw: options.full ? redactGosuslugiSensitive(data, { keepPersonal: true }) : undefined,
7813
- };
7814
- }
7815
-
7816
- async function gosuslugiDebt(options = {}) {
7817
- const data = await gosuslugiBrowserApiJson({
7818
- pageUrl: "https://www.gosuslugi.ru/pay/forPayment",
7819
- endpoint: "https://www.gosuslugi.ru/api/pay/v2/informer/fetch",
7820
- waitMs: Number(options.wait || 5000),
7821
- });
7822
- const groups = Array.isArray(data.groups) ? data.groups : [];
7823
- const debts = groups.flatMap((group) => (group.bills || []).map((bill) => ({
7824
- group: group.name || group.code || "-",
7825
- caption: bill.caption || "-",
7826
- amount: Number(bill.amount || 0),
7827
- billDate: bill.billDate || "-",
7828
- supplier: bill.supplierFullName || "-",
7829
- document: bill.document?.typeName ? `${bill.document.typeName} ${bill.document.number || ""}`.trim() : "-",
7830
- })));
7831
- return {
7832
- total: Number(data.summary?.total || debts.length || 0),
7833
- amount: Number(data.summary?.amount || debts.reduce((sum, item) => sum + item.amount, 0)),
7834
- groups: groups.map((group) => ({ name: group.name, code: group.code, total: group.summary?.total || 0, amount: group.summary?.amount || 0 })),
7835
- debts,
7836
- };
7837
- }
7838
-
7839
- async function gosuslugiNotifications(options = {}) {
7840
- const types = "ORDER,EQUEUE,PAYMENT,GEPS,BIOMETRICS,ACCOUNT,ACCOUNT_CHILD,PROFILE,APPEAL,CLAIM,ELECTION_INFO,COMPLEX_ORDER,FEEDBACK,ORGANIZATION,BUSINESSMAN,ESIGNATURE,KND_APPEAL,LINKED_ACCOUNT,SIGN,GOSQR,INFO,PERMISSION,LICENSING,LICENSING_APPEAL,CONSTRUCTOR";
7841
- const pageSize = Number(options.limit || 15);
7842
- const unread = options.unread ? "true" : "false";
7843
- const counters = await gosuslugiBrowserApiJson({
7844
- pageUrl: `https://lk.gosuslugi.ru/notifications?type=${types}`,
7845
- endpoint: `https://www.gosuslugi.ru/api/lk/v1/feeds/counters?types=${types},PARTNERS&isArchive=false`,
7846
- waitMs: Number(options.wait || 3000),
7847
- });
7848
- const feed = await gosuslugiBrowserApiJson({
7849
- pageUrl: `https://lk.gosuslugi.ru/notifications?type=${types}`,
7850
- endpoint: `https://www.gosuslugi.ru/api/lk/v1/feeds/?unread=${unread}&isArchive=false&isHide=false&types=${types}&pageSize=${pageSize}&status=&startDate=&lastFeedId=&lastFeedDate=&q=`,
7851
- waitMs: Number(options.wait || 3000),
7852
- });
7853
- const items = (feed.items || []).map((item) => ({
7854
- id: item.id,
7855
- unread: Boolean(item.unread),
7856
- date: item.date || "-",
7857
- type: item.feedType || "-",
7858
- title: item.title || "-",
7859
- subtitle: item.subTitle || "-",
7860
- status: item.status || "-",
7861
- summary: summarizeNotificationData(item.data),
7862
- }));
7863
- return {
7864
- total: counters.total || feed.items?.length || 0,
7865
- unread: counters.unread || items.filter((item) => item.unread).length,
7866
- counters: counters.counter || [],
7867
- hasMore: Boolean(feed.hasMore),
7868
- items,
7869
- };
7870
- }
7871
-
7872
- async function gosuslugiMarkNotificationsRead(options = {}) {
7873
- await requireGosuslugiConsent();
7874
- if (!options.yes) {
7875
- const ok = await confirm("Отметить уведомления Госуслуг прочитанными? Это изменит состояние личного кабинета. [y/N] ");
7876
- if (!ok) {
7877
- console.log("Операция отменена.");
7878
- return;
7879
- }
7880
- }
7881
- await gosuslugiBrowserClickText({
7882
- pageUrl: "https://lk.gosuslugi.ru/notifications?type=ORDER,EQUEUE,PAYMENT,GEPS,BIOMETRICS,ACCOUNT,ACCOUNT_CHILD,PROFILE,APPEAL,CLAIM,ELECTION_INFO,COMPLEX_ORDER,FEEDBACK,ORGANIZATION,BUSINESSMAN,ESIGNATURE,KND_APPEAL,LINKED_ACCOUNT,SIGN,GOSQR,INFO,PERMISSION,LICENSING,LICENSING_APPEAL,CONSTRUCTOR",
7883
- text: "Прочитать все",
7884
- waitMs: Number(options.wait || 5000),
7885
- });
7886
- console.log("Команда отметки прочитанным выполнена. Проверьте статус: iola gosuslugi notifications --unread");
7887
- }
7888
-
7889
- async function gosuslugiCheck(options = {}) {
7890
- try {
7891
- const result = await gosuslugiWhoami({ wait: options.wait || 2000 });
7892
- return {
7893
- status: "ok",
7894
- authorized: "yes",
7895
- fio: result.summary.fio,
7896
- checkedAt: new Date().toISOString(),
7897
- nextAction: "-",
7898
- };
7899
- } catch (error) {
7900
- const message = error instanceof Error ? error.message : String(error);
7901
- const result = {
7902
- status: "needs-login",
7903
- authorized: "unknown",
7904
- checkedAt: new Date().toISOString(),
7905
- nextAction: "iola gosuslugi connect",
7906
- error: message,
7907
- };
7908
- if (!options.silent) {
7909
- console.error("Сессия Госуслуг недоступна или требует повторный вход.");
7910
- console.error("Запустите: iola gosuslugi connect");
7911
- }
7912
- return result;
7913
- }
7914
- }
7915
-
7916
- async function gosuslugiKeepalive(options = {}) {
7917
- const intervalMs = parseDurationMs(options.interval || "30m");
7918
- const once = Boolean(options.once);
7919
- console.log(`Gosuslugi keepalive запущен. Интервал: ${Math.round(intervalMs / 60000)} мин.`);
7920
- console.log("Остановить: Ctrl+C");
7921
- while (true) {
7922
- const result = await gosuslugiCheck({ silent: true });
7923
- const line = result.status === "ok"
7924
- ? `[${result.checkedAt}] Госуслуги: сессия активна (${result.fio || "-"})`
7925
- : `[${result.checkedAt}] Госуслуги: нужен повторный вход. Запустите: iola gosuslugi connect`;
7926
- console.log(line);
7927
- if (once) return;
7928
- await sleep(intervalMs);
7929
- }
7930
- }
7931
-
7932
- function gosuslugiKeepaliveTaskName() {
7933
- return "iola-gosuslugi-keepalive";
7934
- }
7935
-
7936
- function gosuslugiKeepaliveLogFile() {
7937
- return path.join(CONFIG_DIR, "gosuslugi-keepalive.log");
7938
- }
7939
-
7940
- function cliEntrypointFile() {
7941
- return path.resolve(__dirname, "..", "bin", "iola.js");
7942
- }
7943
-
7944
- async function installGosuslugiKeepaliveTask(options = {}) {
7945
- const intervalMinutes = Math.max(1, Math.round(parseDurationMs(options.interval || "30m") / 60000));
7946
- if (process.platform === "win32") {
7947
- await installWindowsGosuslugiKeepaliveTask(intervalMinutes);
7948
- return;
7949
- }
7950
- const id = addCronJob(`каждые ${intervalMinutes} минут`, "gosuslugi check --silent");
7951
- console.log(`Локальная cron-задача добавлена: ${id}`);
7952
- console.log("Для автоматического выполнения настройте системный планировщик на запуск: iola cron tick");
7953
- }
7954
-
7955
- async function installWindowsGosuslugiKeepaliveTask(intervalMinutes) {
7956
- await mkdir(CONFIG_DIR, { recursive: true });
7957
- const taskName = gosuslugiKeepaliveTaskName();
7958
- const logFile = gosuslugiKeepaliveLogFile();
7959
- const script = path.join(CONFIG_DIR, "gosuslugi-keepalive-task.cmd");
7960
- const command = `"${process.execPath}" --no-warnings "${cliEntrypointFile()}" gosuslugi check --silent >> "${logFile}" 2>&1`;
7961
- await writeFile(script, `@echo off\r\n${command}\r\n`, "utf8");
7962
- await runCommand("schtasks.exe", [
7963
- "/Create",
7964
- "/TN", taskName,
7965
- "/SC", "MINUTE",
7966
- "/MO", String(intervalMinutes),
7967
- "/TR", script,
7968
- "/F",
7969
- ]);
7970
- console.log(`Windows Task Scheduler задача создана: ${taskName}`);
7971
- console.log(`Интервал: ${intervalMinutes} мин.`);
7972
- console.log(`Лог: ${logFile}`);
7973
- console.log("Проверить: iola gosuslugi keepalive-status");
7974
- }
7975
-
7976
- async function uninstallGosuslugiKeepaliveTask() {
7977
- if (process.platform === "win32") {
7978
- await runCommand("schtasks.exe", ["/Delete", "/TN", gosuslugiKeepaliveTaskName(), "/F"]).catch(() => {});
7979
- console.log(`Windows Task Scheduler задача удалена: ${gosuslugiKeepaliveTaskName()}`);
7980
- return;
7981
- }
7982
- console.log("Для не-Windows удалите локальную cron-задачу вручную: iola cron list, затем iola cron delete ID.");
7983
- }
7984
-
7985
- async function printGosuslugiKeepaliveTaskStatus(options = {}) {
7986
- if (process.platform === "win32") {
7987
- try {
7988
- const { stdout } = await runCommand("schtasks.exe", ["/Query", "/TN", gosuslugiKeepaliveTaskName(), "/FO", "LIST"]);
7989
- console.log(stdout.trim());
7990
- } catch {
7991
- console.log(`Задача не найдена: ${gosuslugiKeepaliveTaskName()}`);
7992
- }
7993
- if (existsSync(gosuslugiKeepaliveLogFile())) {
7994
- console.log("");
7995
- console.log(`Лог: ${gosuslugiKeepaliveLogFile()}`);
7996
- }
7997
- return;
7998
- }
7999
- const rows = listCronJobs().filter((job) => String(job.command).includes("gosuslugi check"));
8000
- if (options.json) printJson(rows);
8001
- else printTable(rows, [["id", "ID"], ["enabled", "Вкл"], ["schedule_text", "Расписание"], ["command", "Команда"], ["last_run_at", "Последний запуск"]]);
8002
- }
8003
-
8004
- function parseDurationMs(value) {
8005
- const text = String(value || "30m").trim().toLocaleLowerCase("ru-RU");
8006
- const match = text.match(/^(\d+(?:[.,]\d+)?)(ms|s|m|h|мин|минут|час|часа|часов)?$/u);
8007
- if (!match) throw new Error("Интервал задается как 30m, 1800s или 1h.");
8008
- const amount = Number(match[1].replace(",", "."));
8009
- const unit = match[2] || "m";
8010
- if (unit === "ms") return Math.max(1000, amount);
8011
- if (unit === "s") return Math.max(1000, amount * 1000);
8012
- if (unit === "h" || unit.startsWith("час")) return Math.max(1000, amount * 60 * 60 * 1000);
8013
- return Math.max(1000, amount * 60 * 1000);
8014
- }
8015
-
8016
- function printGosuslugiDebt(result) {
8017
- printKeyValue({
8018
- total: result.total,
8019
- amount: `${formatRub(result.amount)} Р`,
8020
- });
8021
- if (!result.debts.length) {
8022
- console.log("Задолженности не найдены.");
8023
- return;
8024
- }
8025
- printTable(result.debts.map((item) => ({
8026
- group: item.group,
8027
- amount: `${formatRub(item.amount)} Р`,
8028
- date: item.billDate,
8029
- caption: item.caption,
8030
- })), [
8031
- ["group", "Группа"],
8032
- ["amount", "Сумма"],
8033
- ["date", "Дата"],
8034
- ["caption", "Описание"],
8035
- ]);
8036
- }
8037
-
8038
- function printGosuslugiNotifications(result) {
8039
- printKeyValue({ total: result.total, unread: result.unread, hasMore: result.hasMore ? "yes" : "no" });
8040
- printTable(result.items.map((item) => ({
8041
- unread: item.unread ? "new" : "read",
8042
- date: item.date,
8043
- type: item.type,
8044
- title: item.title,
8045
- subtitle: item.subtitle,
8046
- summary: item.summary,
8047
- })), [
8048
- ["unread", "Статус"],
8049
- ["date", "Дата"],
8050
- ["type", "Тип"],
8051
- ["title", "Заголовок"],
8052
- ["subtitle", "Подзаголовок"],
8053
- ["summary", "Детали"],
8054
- ]);
8055
- }
8056
-
8057
- function summarizeNotificationData(data) {
8058
- if (!data || typeof data !== "object") return "";
8059
- const snippets = Array.isArray(data.snippets) ? data.snippets : [];
8060
- if (snippets.length) {
8061
- const first = snippets[0];
8062
- return [first.orgName, first.address, first.date].filter(Boolean).join(" | ");
8063
- }
8064
- return [data.messageType, data.messageUuid, data.orderId, data.passCodeEpguCode].filter(Boolean).join(" | ");
8065
- }
8066
-
8067
- function formatRub(value) {
8068
- return Number(value || 0).toLocaleString("ru-RU", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
8069
- }
8070
-
8071
- function isGosuslugiPersonalIntent(question) {
8072
- const normalized = String(question || "").toLocaleLowerCase("ru-RU");
8073
- return /(госуслуг|задолж|долг|штраф|налог|к оплате|платеж|платёж|уведомлен|госпочт|фио|дата рождения)/iu.test(normalized);
8074
- }
8075
-
8076
- async function answerGosuslugiQuestion(question, options = {}) {
8077
- const normalized = String(question || "").toLocaleLowerCase("ru-RU");
8078
- if (/(уведомлен|сообщени|госпочт|непрочитан)/iu.test(normalized)) {
8079
- const result = await gosuslugiNotifications({ unread: /непрочитан|нов/iu.test(normalized), limit: options.limit || 10 });
8080
- const lines = [`На Госуслугах: всего уведомлений ${result.total}, непрочитанных ${result.unread}.`];
8081
- const items = result.items.slice(0, Number(options.limit || 5));
8082
- if (items.length) {
8083
- lines.push("");
8084
- for (const item of items) {
8085
- lines.push(`- ${item.unread ? "новое" : "прочитано"}: ${item.title} — ${item.subtitle} (${item.date})`);
8086
- }
8087
- }
8088
- return lines.join("\n");
8089
- }
8090
- if (/(задолж|долг|штраф|налог|к оплате|платеж|платёж)/iu.test(normalized)) {
8091
- const result = await gosuslugiDebt(options);
8092
- if (!result.debts.length) return "На Госуслугах задолженности к оплате не найдены.";
8093
- const lines = [`На Госуслугах найдено задолженностей: ${result.total}. Общая сумма: ${formatRub(result.amount)} Р.`];
8094
- for (const item of result.debts) {
8095
- lines.push(`- ${item.group}: ${formatRub(item.amount)} Р — ${item.caption}`);
8096
- }
8097
- return lines.join("\n");
8098
- }
8099
- const result = await gosuslugiWhoami(options);
8100
- return [
8101
- `ФИО: ${result.summary.fio}`,
8102
- `Дата рождения: ${result.summary.birthDate}`,
8103
- `Статус: ${result.summary.status}`,
8104
- ].join("\n");
8105
- }
8106
-
8107
- function maskPhone(value) {
8108
- const text = String(value || "");
8109
- return text.replace(/(\+?\d)([\d\s()-]{4,})(\d{2})$/u, "$1***$3") || "-";
8110
- }
8111
-
8112
- function maskEmail(value) {
8113
- const text = String(value || "");
8114
- const [name, domain] = text.split("@");
8115
- if (!name || !domain) return text || "-";
8116
- return `${name.slice(0, 2)}***@${domain}`;
8117
- }
8118
-
8119
- function maskDocument(value) {
8120
- const digits = String(value || "").replace(/\D+/g, "");
8121
- if (!digits) return "-";
8122
- return `***${digits.slice(-4)}`;
8123
- }
8124
-
8125
- function redactGosuslugiSensitive(value, options = {}) {
8126
- if (Array.isArray(value)) return value.map((item) => redactGosuslugiSensitive(item, options));
8127
- if (!value || typeof value !== "object") return value;
8128
- const result = {};
8129
- for (const [key, item] of Object.entries(value)) {
8130
- if (/token|cookie|session|password|secret|jwt|auth/i.test(key)) result[key] = "[redacted]";
8131
- else if (!options.keepPersonal && /(snils|inn|passport|number|series|address|mobile|email|phone)/i.test(key)) result[key] = "[redacted]";
8132
- else result[key] = redactGosuslugiSensitive(item, options);
8133
- }
8134
- return result;
8135
- }
8136
-
8137
- async function runPersistentBrowserAutomation(action, params) {
8138
- await ensureBrowserRuntime();
8139
- await mkdir(params.userDataDir, { recursive: true });
8140
- const releaseLock = params.userDataDir === GOSUSLUGI_BROWSER_PROFILE_DIR ? await acquireDirectoryLock(GOSUSLUGI_BROWSER_LOCK_DIR, 180000) : async () => {};
8141
- const scriptFile = path.join(BROWSER_RUNTIME_DIR, `iola-browser-profile-${Date.now()}-${Math.random().toString(16).slice(2)}.mjs`);
8142
- await writeFile(scriptFile, persistentBrowserAutomationScript(action, params), "utf8");
8143
- try {
8144
- const options = action === "open" ? { cwd: BROWSER_RUNTIME_DIR, inherit: true } : { cwd: BROWSER_RUNTIME_DIR };
8145
- const result = await runCommand(process.execPath, [scriptFile], options);
8146
- return result.stdout?.trim() || "";
8147
- } finally {
8148
- await rm(scriptFile, { force: true }).catch(() => {});
8149
- await releaseLock();
8150
- }
8151
- }
8152
-
8153
- async function acquireDirectoryLock(lockDir, timeoutMs = 60000) {
8154
- const started = Date.now();
8155
- while (true) {
8156
- try {
8157
- await mkdir(lockDir, { recursive: false });
8158
- await writeFile(path.join(lockDir, "owner.json"), JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }, null, 2), "utf8").catch(() => {});
8159
- return async () => {
8160
- await rm(lockDir, { recursive: true, force: true }).catch(() => {});
8161
- };
8162
- } catch {
8163
- if (Date.now() - started > timeoutMs) {
8164
- throw new Error("Браузерный профиль Госуслуг занят другим процессом. Закройте окно Госуслуг или повторите команду позже.");
8165
- }
8166
- await sleep(1000);
8167
- }
8168
- }
8169
- }
8170
-
8171
- async function gosuslugiBrowserApiJson(params) {
8172
- await requireGosuslugiConsent();
8173
- await ensureBrowserRuntimeForGosuslugi();
8174
- const raw = await runPersistentBrowserAutomation("api-json", {
8175
- pageUrl: params.pageUrl || GOSUSLUGI_DEFAULT_URL,
8176
- endpoint: params.endpoint,
8177
- userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
8178
- headed: params.headed !== false,
8179
- waitMs: Number(params.waitMs || 0),
8180
- timeout: Number(params.timeout || 60000),
8181
- viewport: params.viewport || "1366x768",
8182
- });
8183
- return JSON.parse(raw);
8184
- }
8185
-
8186
- async function gosuslugiBrowserClickText(params) {
8187
- await requireGosuslugiConsent();
8188
- await ensureBrowserRuntimeForGosuslugi();
8189
- return runPersistentBrowserAutomation("click-text", {
8190
- pageUrl: params.pageUrl || GOSUSLUGI_DEFAULT_URL,
8191
- text: params.text,
8192
- userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
8193
- headed: true,
8194
- waitMs: Number(params.waitMs || 3000),
8195
- timeout: Number(params.timeout || 60000),
8196
- viewport: params.viewport || "1366x768",
8197
- });
8198
- }
8199
-
8200
- function persistentBrowserAutomationScript(action, params) {
8201
- return `
8202
- import { chromium } from "playwright";
8203
- const action = ${JSON.stringify(action)};
8204
- const params = ${JSON.stringify(params)};
8205
- const [width, height] = String(params.viewport || "1366x768").split("x").map(Number);
8206
- const context = await chromium.launchPersistentContext(params.userDataDir, {
8207
- headless: !params.headed,
8208
- viewport: { width: width || 1366, height: height || 768 },
8209
- });
8210
- context.setDefaultTimeout(params.timeout || 60000);
8211
- const page = context.pages()[0] || await context.newPage();
8212
- try {
8213
- await page.goto(params.url || params.pageUrl, { waitUntil: "domcontentloaded", timeout: params.timeout || 60000 });
8214
- if (params.waitMs) await page.waitForTimeout(params.waitMs);
8215
- if (action === "open") {
8216
- if (params.headed) {
8217
- page.on("close", async () => {
8218
- await context.close().catch(() => {});
8219
- });
8220
- while (!page.isClosed()) {
8221
- await page.waitForTimeout(1000).catch(() => {});
8222
- }
8223
- }
8224
- } else if (action === "text") {
8225
- console.log((await page.locator("body").innerText()).trim());
8226
- } else if (action === "screenshot") {
8227
- await page.screenshot({ path: params.output, fullPage: true });
8228
- } else if (action === "api-json") {
8229
- const data = await page.evaluate(async (endpoint) => {
8230
- const response = await fetch(endpoint, {
8231
- credentials: "include",
8232
- headers: { accept: "application/json" },
8233
- });
8234
- const text = await response.text();
8235
- if (!response.ok) throw new Error(response.status + " " + response.statusText + ": " + text.slice(0, 500));
8236
- return JSON.parse(text);
8237
- }, params.endpoint);
8238
- console.log(JSON.stringify(data));
8239
- } else if (action === "click-text") {
8240
- await page.getByText(params.text, { exact: true }).first().click();
8241
- if (params.waitMs) await page.waitForTimeout(params.waitMs);
8242
- console.log((await page.locator("body").innerText()).trim().slice(0, 4000));
8243
- }
8244
- } finally {
8245
- await context.close().catch(() => {});
8246
- }
8247
- `;
8248
- }
8249
-
8250
7493
  function browserAutomationScript(action, params) {
8251
7494
  return `
8252
7495
  import { chromium } from "playwright";
@@ -8477,9 +7720,6 @@ function mcpTools() {
8477
7720
  { name: "report", description: "Запуск встроенного отчета.", inputSchema: schema({ name: { type: "string" }, format: { type: "string" }, output: { type: "string" } }) },
8478
7721
  { name: "browser.text", description: "Открыть страницу в headless Chromium и вернуть видимый текст.", inputSchema: schema({ url: { type: "string" }, waitMs: { type: "number" } }) },
8479
7722
  { name: "browser.screenshot", description: "Сделать скриншот страницы через Chromium.", inputSchema: schema({ url: { type: "string" }, output: { type: "string" }, waitMs: { type: "number" } }) },
8480
- { name: "gosuslugi.whoami", description: "Прочитать ФИО и дату рождения из личного профиля Госуслуг через локальный браузерный профиль.", inputSchema: schema({ full: { type: "boolean" } }) },
8481
- { name: "gosuslugi.debt", description: "Прочитать задолженности и платежи к оплате на Госуслугах.", inputSchema: schema() },
8482
- { name: "gosuslugi.notifications", description: "Прочитать уведомления Госуслуг.", inputSchema: schema({ unread: { type: "boolean" }, limit: { type: "number" } }) },
8483
7723
  ];
8484
7724
  }
8485
7725
 
@@ -8517,9 +7757,6 @@ async function callMcpTool(name, args = {}) {
8517
7757
  await runBrowserAutomation("screenshot", { url: args.url, output, waitMs: Number(args.waitMs || 0), timeout: Number(args.timeout || 30000), viewport: args.viewport || "1366x768" });
8518
7758
  return { output };
8519
7759
  }
8520
- if (name === "gosuslugi.whoami") return gosuslugiWhoami(args);
8521
- if (name === "gosuslugi.debt") return gosuslugiDebt(args);
8522
- if (name === "gosuslugi.notifications") return gosuslugiNotifications(args);
8523
7760
  return executeRpc(name, { ...args, _: [] });
8524
7761
  }
8525
7762
 
@@ -9598,10 +8835,6 @@ function mergeConfig(base, override) {
9598
8835
  ...base.api,
9599
8836
  ...(override.api || {}),
9600
8837
  },
9601
- gosuslugi: {
9602
- ...base.gosuslugi,
9603
- ...(override.gosuslugi || {}),
9604
- },
9605
8838
  ai: {
9606
8839
  ...base.ai,
9607
8840
  ...(override.ai || {}),
@@ -9684,11 +8917,6 @@ function validateConfig(config) {
9684
8917
  for (const toolset of config.toolsets?.enabled || []) {
9685
8918
  if (!TOOLSETS[toolset]) errors.push(`toolsets.enabled содержит неизвестный toolset: ${toolset}`);
9686
8919
  }
9687
- if (config.gosuslugi?.enabled && !isGosuslugiConfigured(config)) {
9688
- if ((config.gosuslugi?.mode || "personal-browser") !== "personal-browser") {
9689
- errors.push("gosuslugi включен в OAuth/OIDC-режиме, но authUrl/tokenUrl/clientId не заполнены");
9690
- }
9691
- }
9692
8920
  return errors;
9693
8921
  }
9694
8922
 
@@ -9698,7 +8926,6 @@ function configSchema() {
9698
8926
  required: ["api", "ai"],
9699
8927
  properties: {
9700
8928
  api: { required: ["baseUrl", "mcpBaseUrl"] },
9701
- gosuslugi: { modes: ["personal-browser", "personal-local"], browserProfile: GOSUSLUGI_BROWSER_PROFILE_DIR, oauthRequiredWhenEnabled: ["authUrl", "tokenUrl", "clientId"], optional: ["userinfoUrl", "clientSecret", "scope", "redirectHost", "redirectPort", "redirectPath"] },
9702
8929
  ai: { required: ["activeProfile", "profiles"], providers: ["ollama", "openai", "openrouter", "codex"] },
9703
8930
  permissions: { localTools: ALL_LOCAL_TOOLS, runtime: ["readFiles", "writeFiles", "editFiles", "deleteFiles", "sync", "externalApi", "externalAi", "codex"] },
9704
8931
  toolsets: { available: Object.keys(TOOLSETS) },