@iola_adm/iola-cli 0.1.59 → 0.1.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -86,6 +86,9 @@ iola review config
86
86
  iola browser status
87
87
  iola gosuslugi status
88
88
  iola gosuslugi connect
89
+ iola gosuslugi whoami
90
+ iola gosuslugi debt
91
+ iola gosuslugi notifications --unread
89
92
  ```
90
93
 
91
94
  Локальная модель через Ollama:
@@ -134,6 +137,7 @@ iola version --check
134
137
  - дополнительные stdio MCP-серверы можно добавить в `~/.iola/config.json` в раздел `mcp.servers`;
135
138
  - браузерный runtime через Playwright: чтение страниц, скриншоты, PDF, клики, ввод и eval;
136
139
  - личное локальное подключение Госуслуг через отдельный браузерный профиль на ПК пользователя;
140
+ - read-only tools Госуслуг для агента: ФИО, дата рождения, задолженности и уведомления;
137
141
  - управляемые локальные файловые операции с режимами `locked`, `read-only`, `workspace-write`, `full-access`;
138
142
  - планы выполнения, traces, tasks, artifacts, snapshots и policy-профили;
139
143
  - экспорт отчетов в Excel/Word-совместимые файлы;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.59",
3
+ "version": "0.1.60",
4
4
  "description": "CLI и AI-агент городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: gosuslugi
3
+ description: Работа с личным аккаунтом Госуслуг пользователя через локальный браузерный профиль.
4
+ ---
5
+
6
+ Используй этот skill только когда пользователь явно спрашивает про свой личный аккаунт Госуслуг: ФИО, дату рождения, задолженности, штрафы, налоги, платежи, уведомления или госпочту.
7
+
8
+ Доступные read-only tools:
9
+
10
+ - `gosuslugi_whoami` - получить ФИО, дату рождения и краткие данные профиля.
11
+ - `gosuslugi_debt` - получить задолженности и суммы к оплате.
12
+ - `gosuslugi_notifications` - получить уведомления, включая новые непрочитанные.
13
+
14
+ Не выполняй юридически значимые действия автоматически. Оплата, отправка заявлений, изменение персональных данных, удаление учетной записи, согласия и отметка уведомлений прочитанными требуют отдельного явного подтверждения пользователя.
15
+
16
+ Если сохраненная сессия Госуслуг протухла или требуется повторная двухфакторная аутентификация, попроси пользователя выполнить `iola gosuslugi connect`.
package/src/cli.js CHANGED
@@ -27,9 +27,10 @@ const LOCAL_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "local.json");
27
27
  const BROWSER_RUNTIME_DIR = path.join(CONFIG_DIR, "browser-runtime");
28
28
  const BROWSER_RUNTIME_PACKAGE = path.join(BROWSER_RUNTIME_DIR, "node_modules", "playwright", "package.json");
29
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");
30
31
  const GOSUSLUGI_DEFAULT_URL = "https://www.gosuslugi.ru/";
31
32
  const INDEXABLE_EXTENSIONS = /\.(md|txt|csv|json|html|docx|xlsx|pptx|pdf)$/i;
32
- const LOCAL_TOOLS = ["search_data", "get_card", "export_report", "file_read", "browser_open"];
33
+ const LOCAL_TOOLS = ["search_data", "get_card", "export_report", "file_read", "browser_open", "gosuslugi_whoami", "gosuslugi_debt", "gosuslugi_notifications"];
33
34
  const LEGACY_LOCAL_TOOLS = ["search_local", "export_data", "run_report", "save_view"];
34
35
  const FILE_TOOLS = ["files_tree", "files_read", "files_search", "files_write", "files_patch"];
35
36
  const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS];
@@ -180,6 +181,9 @@ const DEFAULT_AI_CONFIG = {
180
181
  export_report: true,
181
182
  file_read: false,
182
183
  browser_open: true,
184
+ gosuslugi_whoami: true,
185
+ gosuslugi_debt: true,
186
+ gosuslugi_notifications: true,
183
187
  files_tree: false,
184
188
  files_read: false,
185
189
  files_search: false,
@@ -210,7 +214,7 @@ const DEFAULT_AI_CONFIG = {
210
214
  suggestions: true,
211
215
  },
212
216
  skills: {
213
- enabled: ["open-data", "reports", "local-model", "local-files", "browser-agent"],
217
+ enabled: ["open-data", "reports", "local-model", "local-files", "browser-agent", "gosuslugi"],
214
218
  },
215
219
  daemon: {
216
220
  host: "127.0.0.1",
@@ -284,6 +288,8 @@ const SLASH_COMMANDS = [
284
288
  { command: "/features list", description: "feature flags" },
285
289
  { command: "/gosuslugi status", description: "личное подключение Госуслуг" },
286
290
  { command: "/gosuslugi connect", description: "открыть личный вход Госуслуг" },
291
+ { command: "/gosuslugi debt", description: "задолженности Госуслуг" },
292
+ { command: "/gosuslugi notifications", description: "уведомления Госуслуг" },
287
293
  { command: "/wiki", description: "ссылки на документацию" },
288
294
  { command: "/context list", description: "локальный контекст проекта" },
289
295
  { command: "/skills list", description: "skills" },
@@ -517,7 +523,7 @@ Usage:
517
523
  iola fork SESSION_ID [TEXT]
518
524
  iola features list|enable|disable
519
525
  iola settings list|get|validate|doctor|init
520
- iola gosuslugi terms|consent|status|connect|open|text|screenshot|logout|configure|login|userinfo
526
+ iola gosuslugi terms|consent|status|connect|open|text|screenshot|whoami|debt|notifications|mark-read|logout|configure|login|userinfo
521
527
  iola wiki [open|links]
522
528
  iola context list|show|init
523
529
  iola skills list|show|paths|enable|disable|bundles|bundle|doctor
@@ -2309,6 +2315,32 @@ async function handleGosuslugi(args) {
2309
2315
  return;
2310
2316
  }
2311
2317
 
2318
+ if (action === "whoami" || action === "profile") {
2319
+ const result = await gosuslugiWhoami(options);
2320
+ if (options.json) printJson(result);
2321
+ else printKeyValue(result.summary);
2322
+ return;
2323
+ }
2324
+
2325
+ if (action === "debt" || action === "debts" || action === "payments") {
2326
+ const result = await gosuslugiDebt(options);
2327
+ if (options.json) printJson(result);
2328
+ else printGosuslugiDebt(result);
2329
+ return;
2330
+ }
2331
+
2332
+ if (action === "notifications" || action === "notices") {
2333
+ const result = await gosuslugiNotifications(options);
2334
+ if (options.json) printJson(result);
2335
+ else printGosuslugiNotifications(result);
2336
+ return;
2337
+ }
2338
+
2339
+ if (action === "mark-read") {
2340
+ await gosuslugiMarkNotificationsRead(options);
2341
+ return;
2342
+ }
2343
+
2312
2344
  if (action === "configure") {
2313
2345
  const current = await loadConfig();
2314
2346
  const next = {
@@ -2357,7 +2389,7 @@ async function handleGosuslugi(args) {
2357
2389
  return;
2358
2390
  }
2359
2391
 
2360
- throw new Error("Команды gosuslugi: terms, consent, status, connect, open, text, screenshot, logout, configure, login, userinfo.");
2392
+ throw new Error("Команды gosuslugi: terms, consent, status, connect, open, text, screenshot, whoami, debt, notifications, mark-read, logout, configure, login, userinfo.");
2361
2393
  }
2362
2394
 
2363
2395
  function targetOrDefault(args, options = {}) {
@@ -6010,6 +6042,12 @@ async function aiAsk(args, context = {}) {
6010
6042
  throw new Error('Текст вопроса обязателен. Пример: iola ai ask "Какие школы есть на улице Петрова?"');
6011
6043
  }
6012
6044
 
6045
+ if (!options.bare && isGosuslugiPersonalIntent(question)) {
6046
+ const answer = await answerGosuslugiQuestion(question, options);
6047
+ if (!options.quiet) console.log(answer);
6048
+ return answer;
6049
+ }
6050
+
6013
6051
  const config = await loadConfig();
6014
6052
  const providerConfig = await resolveUsableAiProfile(config, options);
6015
6053
  if (providerConfig.provider === "codex") await assertPermission("codex");
@@ -6178,7 +6216,7 @@ async function buildLocalToolPlan(question, providerConfig, options) {
6178
6216
  "Ты планировщик CLI iola. Верни только JSON.",
6179
6217
  `Доступные tools: ${availableToolNames(options).join(", ")}.`,
6180
6218
  "Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
6181
- "Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
6219
+ "Минимальные 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}.",
6182
6220
  "MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
6183
6221
  "Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
6184
6222
  `Вопрос: ${question}`,
@@ -6208,6 +6246,12 @@ function inferToolPlan(question, options = {}) {
6208
6246
  const steps = [];
6209
6247
  if (normalized.includes("без телефона")) {
6210
6248
  steps.push({ tool: "export_report", args: { name: "missing-phones" } });
6249
+ } else if (/(уведомлен|сообщени|госпочт|непрочитан)/iu.test(normalized)) {
6250
+ steps.push({ tool: "gosuslugi_notifications", args: { unread: /непрочитан|нов/iu.test(normalized), limit: 15 } });
6251
+ } else if (/(задолж|долг|штраф|налог|к оплате|платеж|платёж)/iu.test(normalized)) {
6252
+ steps.push({ tool: "gosuslugi_debt", args: {} });
6253
+ } else if (/(фио|дата рождения|профиль|кто я)/iu.test(normalized) && /госуслуг/iu.test(normalized)) {
6254
+ steps.push({ tool: "gosuslugi_whoami", args: {} });
6211
6255
  } else {
6212
6256
  const query = normalized.match(/петрова|школ[а-яё ]*\d+|сад[а-яё ]*\d+|лицей[а-яё ]*\d+/iu)?.[0] || question;
6213
6257
  steps.push({ tool: "search_data", args: { dataset, query, limit: 20 } });
@@ -6297,6 +6341,18 @@ async function executeToolPlan(plan, options = {}) {
6297
6341
  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" });
6298
6342
  current = [{ url: step.args?.url, text }];
6299
6343
  outputs.push({ tool: step.tool, rows: 1 });
6344
+ } else if (step.tool === "gosuslugi_whoami") {
6345
+ const result = await gosuslugiWhoami(step.args || {});
6346
+ current = [result.summary];
6347
+ outputs.push({ tool: step.tool, rows: 1 });
6348
+ } else if (step.tool === "gosuslugi_debt") {
6349
+ const result = await gosuslugiDebt(step.args || {});
6350
+ current = [{ total: result.total, amount: result.amount, debts: result.debts }];
6351
+ outputs.push({ tool: step.tool, rows: result.debts.length });
6352
+ } else if (step.tool === "gosuslugi_notifications") {
6353
+ const result = await gosuslugiNotifications(step.args || {});
6354
+ current = [{ total: result.total, unread: result.unread, items: result.items }];
6355
+ outputs.push({ tool: step.tool, rows: result.items.length });
6300
6356
  } else if (String(step.tool || "").startsWith("mcp:")) {
6301
6357
  const result = await callConfiguredMcpTool(step.tool, step.args || {});
6302
6358
  current = Array.isArray(result) ? result : [result];
@@ -7094,7 +7150,7 @@ function parseOptions(args) {
7094
7150
 
7095
7151
  for (let index = 0; index < args.length; index += 1) {
7096
7152
  const arg = args[index];
7097
- if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || arg === "--system" || arg === "--headed" || arg === "--headless" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--append") {
7153
+ if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || arg === "--system" || arg === "--headed" || arg === "--headless" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--full" || arg === "--unread" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--append") {
7098
7154
  result[arg.slice(2)] = true;
7099
7155
  } else if (arg === "--check" || arg === "--upgrade-node") {
7100
7156
  result.check = true;
@@ -7335,7 +7391,8 @@ async function buildSkillsText(config, question = "", options = {}) {
7335
7391
  const chunks = [];
7336
7392
  const selected = selectSkillsForPrompt(config, question, options);
7337
7393
  for (const skill of listSkills(config)) {
7338
- if (!skill.enabled || !selected.has(skill.name)) continue;
7394
+ const active = skill.enabled || (skill.name === "gosuslugi" && config.gosuslugi?.enabled);
7395
+ if (!active || !selected.has(skill.name)) continue;
7339
7396
  const text = await readFile(skill.file, "utf8");
7340
7397
  chunks.push(`## Skill: ${skill.name}\n${stripFrontmatter(text).trim()}`);
7341
7398
  }
@@ -7351,6 +7408,7 @@ function selectSkillsForPrompt(config, question = "", options = {}) {
7351
7408
  if (enabled.has("reports") && /(отчет|отчёт|выгруз|csv|xlsx|качество|провер)/iu.test(normalized)) selected.add("reports");
7352
7409
  if (enabled.has("local-files") && (options.files || /(файл|папк|readme|документ|архив)/iu.test(normalized))) selected.add("local-files");
7353
7410
  if (enabled.has("browser-agent") && /(браузер|сайт|страниц|url|https?:\/\/)/iu.test(normalized)) selected.add("browser-agent");
7411
+ if (enabled.has("gosuslugi") && /(госуслуг|задолж|долг|штраф|налог|к оплате|платеж|платёж|уведомлен|госпочт|фио|дата рождения)/iu.test(normalized)) selected.add("gosuslugi");
7354
7412
  return selected;
7355
7413
  }
7356
7414
 
@@ -7646,9 +7704,226 @@ async function gosuslugiBrowserScreenshot(url = GOSUSLUGI_DEFAULT_URL, outputFil
7646
7704
  });
7647
7705
  }
7648
7706
 
7707
+ async function gosuslugiWhoami(options = {}) {
7708
+ const data = await gosuslugiBrowserApiJson({
7709
+ pageUrl: "https://lk.gosuslugi.ru/settings/account",
7710
+ endpoint: "https://www.gosuslugi.ru/api/lk/v1/users/data",
7711
+ waitMs: Number(options.wait || 3000),
7712
+ });
7713
+ const person = data.person?.person || data.person || data;
7714
+ const summary = {
7715
+ fio: [data.lastName || person.lastName, data.firstName || person.firstName, data.middleName || person.middleName].filter(Boolean).join(" ") || data.formattedName || "-",
7716
+ birthDate: person.birthDate || data.birthDate || "-",
7717
+ status: data.assuranceLevel === "AL20" || person.trusted ? "Подтвержденная учетная запись" : data.assuranceLevel || "-",
7718
+ phone: options.full ? (data.personMobilePhone || data.mobile || "-") : maskPhone(data.personMobilePhone || data.mobile || ""),
7719
+ email: options.full ? (data.personEMail || data.personEmail || data.email || "-") : maskEmail(data.personEMail || data.personEmail || data.email || ""),
7720
+ snils: options.full ? (person.snils || data.personSnils || data.snils || "-") : maskDocument(person.snils || data.personSnils || data.snils || ""),
7721
+ inn: options.full ? (person.inn || data.personINN || data.inn || "-") : maskDocument(person.inn || data.personINN || data.inn || ""),
7722
+ };
7723
+ return {
7724
+ summary,
7725
+ raw: options.full ? redactGosuslugiSensitive(data, { keepPersonal: true }) : undefined,
7726
+ };
7727
+ }
7728
+
7729
+ async function gosuslugiDebt(options = {}) {
7730
+ const data = await gosuslugiBrowserApiJson({
7731
+ pageUrl: "https://www.gosuslugi.ru/pay/forPayment",
7732
+ endpoint: "https://www.gosuslugi.ru/api/pay/v2/informer/fetch",
7733
+ waitMs: Number(options.wait || 5000),
7734
+ });
7735
+ const groups = Array.isArray(data.groups) ? data.groups : [];
7736
+ const debts = groups.flatMap((group) => (group.bills || []).map((bill) => ({
7737
+ group: group.name || group.code || "-",
7738
+ caption: bill.caption || "-",
7739
+ amount: Number(bill.amount || 0),
7740
+ billDate: bill.billDate || "-",
7741
+ supplier: bill.supplierFullName || "-",
7742
+ document: bill.document?.typeName ? `${bill.document.typeName} ${bill.document.number || ""}`.trim() : "-",
7743
+ })));
7744
+ return {
7745
+ total: Number(data.summary?.total || debts.length || 0),
7746
+ amount: Number(data.summary?.amount || debts.reduce((sum, item) => sum + item.amount, 0)),
7747
+ groups: groups.map((group) => ({ name: group.name, code: group.code, total: group.summary?.total || 0, amount: group.summary?.amount || 0 })),
7748
+ debts,
7749
+ };
7750
+ }
7751
+
7752
+ async function gosuslugiNotifications(options = {}) {
7753
+ 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";
7754
+ const pageSize = Number(options.limit || 15);
7755
+ const unread = options.unread ? "true" : "false";
7756
+ const counters = await gosuslugiBrowserApiJson({
7757
+ pageUrl: `https://lk.gosuslugi.ru/notifications?type=${types}`,
7758
+ endpoint: `https://www.gosuslugi.ru/api/lk/v1/feeds/counters?types=${types},PARTNERS&isArchive=false`,
7759
+ waitMs: Number(options.wait || 3000),
7760
+ });
7761
+ const feed = await gosuslugiBrowserApiJson({
7762
+ pageUrl: `https://lk.gosuslugi.ru/notifications?type=${types}`,
7763
+ endpoint: `https://www.gosuslugi.ru/api/lk/v1/feeds/?unread=${unread}&isArchive=false&isHide=false&types=${types}&pageSize=${pageSize}&status=&startDate=&lastFeedId=&lastFeedDate=&q=`,
7764
+ waitMs: Number(options.wait || 3000),
7765
+ });
7766
+ const items = (feed.items || []).map((item) => ({
7767
+ id: item.id,
7768
+ unread: Boolean(item.unread),
7769
+ date: item.date || "-",
7770
+ type: item.feedType || "-",
7771
+ title: item.title || "-",
7772
+ subtitle: item.subTitle || "-",
7773
+ status: item.status || "-",
7774
+ summary: summarizeNotificationData(item.data),
7775
+ }));
7776
+ return {
7777
+ total: counters.total || feed.items?.length || 0,
7778
+ unread: counters.unread || items.filter((item) => item.unread).length,
7779
+ counters: counters.counter || [],
7780
+ hasMore: Boolean(feed.hasMore),
7781
+ items,
7782
+ };
7783
+ }
7784
+
7785
+ async function gosuslugiMarkNotificationsRead(options = {}) {
7786
+ await requireGosuslugiConsent();
7787
+ if (!options.yes) {
7788
+ const ok = await confirm("Отметить уведомления Госуслуг прочитанными? Это изменит состояние личного кабинета. [y/N] ");
7789
+ if (!ok) {
7790
+ console.log("Операция отменена.");
7791
+ return;
7792
+ }
7793
+ }
7794
+ await gosuslugiBrowserClickText({
7795
+ 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",
7796
+ text: "Прочитать все",
7797
+ waitMs: Number(options.wait || 5000),
7798
+ });
7799
+ console.log("Команда отметки прочитанным выполнена. Проверьте статус: iola gosuslugi notifications --unread");
7800
+ }
7801
+
7802
+ function printGosuslugiDebt(result) {
7803
+ printKeyValue({
7804
+ total: result.total,
7805
+ amount: `${formatRub(result.amount)} Р`,
7806
+ });
7807
+ if (!result.debts.length) {
7808
+ console.log("Задолженности не найдены.");
7809
+ return;
7810
+ }
7811
+ printTable(result.debts.map((item) => ({
7812
+ group: item.group,
7813
+ amount: `${formatRub(item.amount)} Р`,
7814
+ date: item.billDate,
7815
+ caption: item.caption,
7816
+ })), [
7817
+ ["group", "Группа"],
7818
+ ["amount", "Сумма"],
7819
+ ["date", "Дата"],
7820
+ ["caption", "Описание"],
7821
+ ]);
7822
+ }
7823
+
7824
+ function printGosuslugiNotifications(result) {
7825
+ printKeyValue({ total: result.total, unread: result.unread, hasMore: result.hasMore ? "yes" : "no" });
7826
+ printTable(result.items.map((item) => ({
7827
+ unread: item.unread ? "new" : "read",
7828
+ date: item.date,
7829
+ type: item.type,
7830
+ title: item.title,
7831
+ subtitle: item.subtitle,
7832
+ summary: item.summary,
7833
+ })), [
7834
+ ["unread", "Статус"],
7835
+ ["date", "Дата"],
7836
+ ["type", "Тип"],
7837
+ ["title", "Заголовок"],
7838
+ ["subtitle", "Подзаголовок"],
7839
+ ["summary", "Детали"],
7840
+ ]);
7841
+ }
7842
+
7843
+ function summarizeNotificationData(data) {
7844
+ if (!data || typeof data !== "object") return "";
7845
+ const snippets = Array.isArray(data.snippets) ? data.snippets : [];
7846
+ if (snippets.length) {
7847
+ const first = snippets[0];
7848
+ return [first.orgName, first.address, first.date].filter(Boolean).join(" | ");
7849
+ }
7850
+ return [data.messageType, data.messageUuid, data.orderId, data.passCodeEpguCode].filter(Boolean).join(" | ");
7851
+ }
7852
+
7853
+ function formatRub(value) {
7854
+ return Number(value || 0).toLocaleString("ru-RU", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
7855
+ }
7856
+
7857
+ function isGosuslugiPersonalIntent(question) {
7858
+ const normalized = String(question || "").toLocaleLowerCase("ru-RU");
7859
+ return /(госуслуг|задолж|долг|штраф|налог|к оплате|платеж|платёж|уведомлен|госпочт|фио|дата рождения)/iu.test(normalized);
7860
+ }
7861
+
7862
+ async function answerGosuslugiQuestion(question, options = {}) {
7863
+ const normalized = String(question || "").toLocaleLowerCase("ru-RU");
7864
+ if (/(уведомлен|сообщени|госпочт|непрочитан)/iu.test(normalized)) {
7865
+ const result = await gosuslugiNotifications({ unread: /непрочитан|нов/iu.test(normalized), limit: options.limit || 10 });
7866
+ const lines = [`На Госуслугах: всего уведомлений ${result.total}, непрочитанных ${result.unread}.`];
7867
+ const items = result.items.slice(0, Number(options.limit || 5));
7868
+ if (items.length) {
7869
+ lines.push("");
7870
+ for (const item of items) {
7871
+ lines.push(`- ${item.unread ? "новое" : "прочитано"}: ${item.title} — ${item.subtitle} (${item.date})`);
7872
+ }
7873
+ }
7874
+ return lines.join("\n");
7875
+ }
7876
+ if (/(задолж|долг|штраф|налог|к оплате|платеж|платёж)/iu.test(normalized)) {
7877
+ const result = await gosuslugiDebt(options);
7878
+ if (!result.debts.length) return "На Госуслугах задолженности к оплате не найдены.";
7879
+ const lines = [`На Госуслугах найдено задолженностей: ${result.total}. Общая сумма: ${formatRub(result.amount)} Р.`];
7880
+ for (const item of result.debts) {
7881
+ lines.push(`- ${item.group}: ${formatRub(item.amount)} Р — ${item.caption}`);
7882
+ }
7883
+ return lines.join("\n");
7884
+ }
7885
+ const result = await gosuslugiWhoami(options);
7886
+ return [
7887
+ `ФИО: ${result.summary.fio}`,
7888
+ `Дата рождения: ${result.summary.birthDate}`,
7889
+ `Статус: ${result.summary.status}`,
7890
+ ].join("\n");
7891
+ }
7892
+
7893
+ function maskPhone(value) {
7894
+ const text = String(value || "");
7895
+ return text.replace(/(\+?\d)([\d\s()-]{4,})(\d{2})$/u, "$1***$3") || "-";
7896
+ }
7897
+
7898
+ function maskEmail(value) {
7899
+ const text = String(value || "");
7900
+ const [name, domain] = text.split("@");
7901
+ if (!name || !domain) return text || "-";
7902
+ return `${name.slice(0, 2)}***@${domain}`;
7903
+ }
7904
+
7905
+ function maskDocument(value) {
7906
+ const digits = String(value || "").replace(/\D+/g, "");
7907
+ if (!digits) return "-";
7908
+ return `***${digits.slice(-4)}`;
7909
+ }
7910
+
7911
+ function redactGosuslugiSensitive(value, options = {}) {
7912
+ if (Array.isArray(value)) return value.map((item) => redactGosuslugiSensitive(item, options));
7913
+ if (!value || typeof value !== "object") return value;
7914
+ const result = {};
7915
+ for (const [key, item] of Object.entries(value)) {
7916
+ if (/token|cookie|session|password|secret|jwt|auth/i.test(key)) result[key] = "[redacted]";
7917
+ else if (!options.keepPersonal && /(snils|inn|passport|number|series|address|mobile|email|phone)/i.test(key)) result[key] = "[redacted]";
7918
+ else result[key] = redactGosuslugiSensitive(item, options);
7919
+ }
7920
+ return result;
7921
+ }
7922
+
7649
7923
  async function runPersistentBrowserAutomation(action, params) {
7650
7924
  await ensureBrowserRuntime();
7651
7925
  await mkdir(params.userDataDir, { recursive: true });
7926
+ const releaseLock = params.userDataDir === GOSUSLUGI_BROWSER_PROFILE_DIR ? await acquireDirectoryLock(GOSUSLUGI_BROWSER_LOCK_DIR, 180000) : async () => {};
7652
7927
  const scriptFile = path.join(BROWSER_RUNTIME_DIR, `iola-browser-profile-${Date.now()}-${Math.random().toString(16).slice(2)}.mjs`);
7653
7928
  await writeFile(scriptFile, persistentBrowserAutomationScript(action, params), "utf8");
7654
7929
  try {
@@ -7657,9 +7932,57 @@ async function runPersistentBrowserAutomation(action, params) {
7657
7932
  return result.stdout?.trim() || "";
7658
7933
  } finally {
7659
7934
  await rm(scriptFile, { force: true }).catch(() => {});
7935
+ await releaseLock();
7936
+ }
7937
+ }
7938
+
7939
+ async function acquireDirectoryLock(lockDir, timeoutMs = 60000) {
7940
+ const started = Date.now();
7941
+ while (true) {
7942
+ try {
7943
+ await mkdir(lockDir, { recursive: false });
7944
+ await writeFile(path.join(lockDir, "owner.json"), JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }, null, 2), "utf8").catch(() => {});
7945
+ return async () => {
7946
+ await rm(lockDir, { recursive: true, force: true }).catch(() => {});
7947
+ };
7948
+ } catch {
7949
+ if (Date.now() - started > timeoutMs) {
7950
+ throw new Error("Браузерный профиль Госуслуг занят другим процессом. Закройте окно Госуслуг или повторите команду позже.");
7951
+ }
7952
+ await sleep(1000);
7953
+ }
7660
7954
  }
7661
7955
  }
7662
7956
 
7957
+ async function gosuslugiBrowserApiJson(params) {
7958
+ await requireGosuslugiConsent();
7959
+ await ensureBrowserRuntimeForGosuslugi();
7960
+ const raw = await runPersistentBrowserAutomation("api-json", {
7961
+ pageUrl: params.pageUrl || GOSUSLUGI_DEFAULT_URL,
7962
+ endpoint: params.endpoint,
7963
+ userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
7964
+ headed: params.headed !== false,
7965
+ waitMs: Number(params.waitMs || 0),
7966
+ timeout: Number(params.timeout || 60000),
7967
+ viewport: params.viewport || "1366x768",
7968
+ });
7969
+ return JSON.parse(raw);
7970
+ }
7971
+
7972
+ async function gosuslugiBrowserClickText(params) {
7973
+ await requireGosuslugiConsent();
7974
+ await ensureBrowserRuntimeForGosuslugi();
7975
+ return runPersistentBrowserAutomation("click-text", {
7976
+ pageUrl: params.pageUrl || GOSUSLUGI_DEFAULT_URL,
7977
+ text: params.text,
7978
+ userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
7979
+ headed: true,
7980
+ waitMs: Number(params.waitMs || 3000),
7981
+ timeout: Number(params.timeout || 60000),
7982
+ viewport: params.viewport || "1366x768",
7983
+ });
7984
+ }
7985
+
7663
7986
  function persistentBrowserAutomationScript(action, params) {
7664
7987
  return `
7665
7988
  import { chromium } from "playwright";
@@ -7673,7 +7996,7 @@ const context = await chromium.launchPersistentContext(params.userDataDir, {
7673
7996
  context.setDefaultTimeout(params.timeout || 60000);
7674
7997
  const page = context.pages()[0] || await context.newPage();
7675
7998
  try {
7676
- await page.goto(params.url, { waitUntil: "domcontentloaded", timeout: params.timeout || 60000 });
7999
+ await page.goto(params.url || params.pageUrl, { waitUntil: "domcontentloaded", timeout: params.timeout || 60000 });
7677
8000
  if (params.waitMs) await page.waitForTimeout(params.waitMs);
7678
8001
  if (action === "open") {
7679
8002
  if (params.headed) {
@@ -7688,6 +8011,21 @@ try {
7688
8011
  console.log((await page.locator("body").innerText()).trim());
7689
8012
  } else if (action === "screenshot") {
7690
8013
  await page.screenshot({ path: params.output, fullPage: true });
8014
+ } else if (action === "api-json") {
8015
+ const data = await page.evaluate(async (endpoint) => {
8016
+ const response = await fetch(endpoint, {
8017
+ credentials: "include",
8018
+ headers: { accept: "application/json" },
8019
+ });
8020
+ const text = await response.text();
8021
+ if (!response.ok) throw new Error(response.status + " " + response.statusText + ": " + text.slice(0, 500));
8022
+ return JSON.parse(text);
8023
+ }, params.endpoint);
8024
+ console.log(JSON.stringify(data));
8025
+ } else if (action === "click-text") {
8026
+ await page.getByText(params.text, { exact: true }).first().click();
8027
+ if (params.waitMs) await page.waitForTimeout(params.waitMs);
8028
+ console.log((await page.locator("body").innerText()).trim().slice(0, 4000));
7691
8029
  }
7692
8030
  } finally {
7693
8031
  await context.close().catch(() => {});
@@ -7925,6 +8263,9 @@ function mcpTools() {
7925
8263
  { name: "report", description: "Запуск встроенного отчета.", inputSchema: schema({ name: { type: "string" }, format: { type: "string" }, output: { type: "string" } }) },
7926
8264
  { name: "browser.text", description: "Открыть страницу в headless Chromium и вернуть видимый текст.", inputSchema: schema({ url: { type: "string" }, waitMs: { type: "number" } }) },
7927
8265
  { name: "browser.screenshot", description: "Сделать скриншот страницы через Chromium.", inputSchema: schema({ url: { type: "string" }, output: { type: "string" }, waitMs: { type: "number" } }) },
8266
+ { name: "gosuslugi.whoami", description: "Прочитать ФИО и дату рождения из личного профиля Госуслуг через локальный браузерный профиль.", inputSchema: schema({ full: { type: "boolean" } }) },
8267
+ { name: "gosuslugi.debt", description: "Прочитать задолженности и платежи к оплате на Госуслугах.", inputSchema: schema() },
8268
+ { name: "gosuslugi.notifications", description: "Прочитать уведомления Госуслуг.", inputSchema: schema({ unread: { type: "boolean" }, limit: { type: "number" } }) },
7928
8269
  ];
7929
8270
  }
7930
8271
 
@@ -7962,6 +8303,9 @@ async function callMcpTool(name, args = {}) {
7962
8303
  await runBrowserAutomation("screenshot", { url: args.url, output, waitMs: Number(args.waitMs || 0), timeout: Number(args.timeout || 30000), viewport: args.viewport || "1366x768" });
7963
8304
  return { output };
7964
8305
  }
8306
+ if (name === "gosuslugi.whoami") return gosuslugiWhoami(args);
8307
+ if (name === "gosuslugi.debt") return gosuslugiDebt(args);
8308
+ if (name === "gosuslugi.notifications") return gosuslugiNotifications(args);
7965
8309
  return executeRpc(name, { ...args, _: [] });
7966
8310
  }
7967
8311
 
@@ -9198,6 +9542,10 @@ function setConfigValue(config, key, value) {
9198
9542
  current[parts.at(-1)] = value;
9199
9543
  }
9200
9544
 
9545
+ function sleep(ms) {
9546
+ return new Promise((resolve) => setTimeout(resolve, ms));
9547
+ }
9548
+
9201
9549
  async function loadSecrets() {
9202
9550
  try {
9203
9551
  return JSON.parse(await readFile(SECRETS_FILE, "utf8"));
@@ -101,6 +101,10 @@ iola gosuslugi connect
101
101
  iola gosuslugi open
102
102
  iola gosuslugi text https://www.gosuslugi.ru/
103
103
  iola gosuslugi screenshot https://www.gosuslugi.ru/ --output gosuslugi.png
104
+ iola gosuslugi whoami
105
+ iola gosuslugi debt
106
+ iola gosuslugi notifications --unread
107
+ iola gosuslugi mark-read
104
108
  iola gosuslugi logout --all
105
109
  iola gosuslugi configure --auth-url URL --token-url URL --client-id ID --scope openid
106
110
  iola gosuslugi login
@@ -45,6 +45,35 @@ iola gosuslugi text https://www.gosuslugi.ru/
45
45
  iola gosuslugi screenshot https://www.gosuslugi.ru/ --output gosuslugi.png
46
46
  ```
47
47
 
48
+ ## Read-only данные
49
+
50
+ Краткие данные профиля:
51
+
52
+ ```bash
53
+ iola gosuslugi whoami
54
+ ```
55
+
56
+ Задолженности и платежи к оплате:
57
+
58
+ ```bash
59
+ iola gosuslugi debt
60
+ ```
61
+
62
+ Уведомления:
63
+
64
+ ```bash
65
+ iola gosuslugi notifications
66
+ iola gosuslugi notifications --unread
67
+ ```
68
+
69
+ Отметить уведомления прочитанными можно только отдельной командой с подтверждением:
70
+
71
+ ```bash
72
+ iola gosuslugi mark-read
73
+ ```
74
+
75
+ AI-агент может использовать read-only tools Госуслуг, если пользователь спрашивает про ФИО, дату рождения, задолженности, штрафы, налоги, платежи или уведомления.
76
+
48
77
  ## Статус
49
78
 
50
79
  ```bash
@@ -68,6 +97,7 @@ iola gosuslugi logout --all
68
97
  - CLI работает только с тем, что доступно пользователю в локальном браузере.
69
98
  - CLI не извлекает cookies, session tokens или внутренние токены Госуслуг.
70
99
  - Юридически значимые действия должны требовать отдельного подтверждения пользователя.
100
+ - Сессия браузерного профиля может протухнуть. Если Госуслуги попросят повторный вход или 2FA, выполните `iola gosuslugi connect`.
71
101
  - Если Госуслуги попросят повторный вход, код или подтверждение, пользователь проходит его сам в открытом окне.
72
102
 
73
103
  OAuth/OIDC-команды `configure`, `login`, `userinfo` оставлены для случая, если у пользователя есть официально зарегистрированное подключение ЕСИА.