@iola_adm/iola-cli 0.1.30 → 0.1.31

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
@@ -74,6 +74,7 @@ iola budget status
74
74
  iola subagents list
75
75
  iola trajectory last
76
76
  iola review config
77
+ iola browser status
77
78
  ```
78
79
 
79
80
  Локальная модель через Ollama:
@@ -100,6 +101,7 @@ iola version --check
100
101
  - [Локальные файлы](https://github.com/adm-iola/iola-cli/wiki/Локальные-файлы)
101
102
  - [Рабочая среда агента](https://github.com/adm-iola/iola-cli/wiki/Рабочая-среда-агента)
102
103
  - [Платформа агента](https://github.com/adm-iola/iola-cli/wiki/Платформа-агента)
104
+ - [Браузерный агент](https://github.com/adm-iola/iola-cli/wiki/Браузерный-агент)
103
105
  - [Расширения и локальные данные](https://github.com/adm-iola/iola-cli/wiki/Расширения-и-локальные-данные)
104
106
  - [Архивы и мастер настройки](https://github.com/adm-iola/iola-cli/wiki/Архивы-и-мастер-настройки)
105
107
  - [Daemon, RPC и cron](https://github.com/adm-iola/iola-cli/wiki/Daemon-RPC-и-cron)
@@ -116,6 +118,7 @@ iola version --check
116
118
  - skills, toolsets, permissions, memory, hooks и готовые agents;
117
119
  - subagents, skill bundles, layered settings, usage/budget accounting и trajectory export;
118
120
  - полноценный локальный MCP server по stdio/http: tools, resources и prompts;
121
+ - браузерный runtime через Playwright: чтение страниц, скриншоты, PDF, клики, ввод и eval;
119
122
  - управляемые локальные файловые операции с режимами `locked`, `read-only`, `workspace-write`, `full-access`;
120
123
  - планы выполнения, traces, tasks, artifacts, snapshots и policy-профили;
121
124
  - экспорт отчетов в Excel/Word-совместимые файлы;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "description": "CLI и AI-агент для работы с открытыми данными городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
package/src/cli.js CHANGED
@@ -22,6 +22,8 @@ const DB_SCHEMA_VERSION = 8;
22
22
  const PROJECT_IOLA_DIR = path.join(process.cwd(), ".iola");
23
23
  const PROJECT_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "config.json");
24
24
  const LOCAL_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "local.json");
25
+ const BROWSER_RUNTIME_DIR = path.join(CONFIG_DIR, "browser-runtime");
26
+ const BROWSER_RUNTIME_PACKAGE = path.join(BROWSER_RUNTIME_DIR, "node_modules", "playwright", "package.json");
25
27
  const INDEXABLE_EXTENSIONS = /\.(md|txt|csv|json|html|docx|xlsx|pptx|pdf)$/i;
26
28
  const LOCAL_TOOLS = ["search_local", "get_card", "export_data", "run_report", "save_view"];
27
29
  const FILE_TOOLS = ["files_tree", "files_read", "files_search", "files_write", "files_patch"];
@@ -271,6 +273,7 @@ const COMMANDS = new Map([
271
273
  ["index", handleIndex],
272
274
  ["reports", handleReports],
273
275
  ["plugins", handlePlugins],
276
+ ["browser", handleBrowser],
274
277
  ["workspace", handleWorkspace],
275
278
  ["tasks", handleTasks],
276
279
  ["artifacts", handleArtifacts],
@@ -396,6 +399,7 @@ Usage:
396
399
  iola index folder|status|search
397
400
  iola reports list|run
398
401
  iola plugins list|install|run|remove
402
+ iola browser status|install|open|text|html|screenshot|pdf|click|type|eval
399
403
  iola workspace init|status|use|list
400
404
  iola tasks list|add|done|run
401
405
  iola artifacts list|show|open
@@ -1709,6 +1713,7 @@ async function handleWiki(args) {
1709
1713
  ["Локальные файлы", `${base}/Локальные-файлы`],
1710
1714
  ["Рабочая среда агента", `${base}/Рабочая-среда-агента`],
1711
1715
  ["Платформа агента", `${base}/Платформа-агента`],
1716
+ ["Браузерный агент", `${base}/Браузерный-агент`],
1712
1717
  ["Расширения и локальные данные", `${base}/Расширения-и-локальные-данные`],
1713
1718
  ["Архивы и мастер настройки", `${base}/Архивы-и-мастер-настройки`],
1714
1719
  ["Daemon, RPC и cron", `${base}/Daemon-RPC-и-cron`],
@@ -2209,6 +2214,94 @@ async function handlePlugins(args) {
2209
2214
  throw new Error("Команды plugins: list, install NAME --command CMD, run NAME, remove NAME.");
2210
2215
  }
2211
2216
 
2217
+ async function handleBrowser(args) {
2218
+ const [action = "status", target, ...rest] = args;
2219
+ const options = parseOptions(rest);
2220
+
2221
+ if (action === "status") {
2222
+ printKeyValue(await getBrowserStatus());
2223
+ return;
2224
+ }
2225
+
2226
+ if (action === "install") {
2227
+ await installBrowserRuntime();
2228
+ printKeyValue(await getBrowserStatus());
2229
+ return;
2230
+ }
2231
+
2232
+ if (action === "open") {
2233
+ const url = target || options.url;
2234
+ if (!url) throw new Error("Пример: iola browser open https://example.com");
2235
+ if (options.system) {
2236
+ await openUrl(url);
2237
+ return;
2238
+ }
2239
+ await runBrowserAutomation("open", { url, headed: options.headless ? false : true, waitMs: Number(options.wait || 600000) });
2240
+ return;
2241
+ }
2242
+
2243
+ if (action === "text" || action === "html") {
2244
+ const url = target || options.url;
2245
+ if (!url) throw new Error(`Пример: iola browser ${action} https://example.com`);
2246
+ const result = await runBrowserAutomation(action, browserParams(url, options));
2247
+ if (options.output) {
2248
+ await writeFile(options.output, result, "utf8");
2249
+ console.log(`Файл сохранен: ${options.output}`);
2250
+ } else {
2251
+ console.log(result);
2252
+ }
2253
+ return;
2254
+ }
2255
+
2256
+ if (action === "screenshot" || action === "pdf") {
2257
+ const url = target || options.url;
2258
+ if (!url) throw new Error(`Пример: iola browser ${action} https://example.com --output page.${action === "pdf" ? "pdf" : "png"}`);
2259
+ const output = options.output || path.join(process.cwd(), action === "pdf" ? "browser-page.pdf" : "browser-page.png");
2260
+ await runBrowserAutomation(action, { ...browserParams(url, options), output: path.resolve(output) });
2261
+ saveArtifact(action === "pdf" ? "browser-pdf" : "browser-screenshot", url, path.resolve(output), { url });
2262
+ console.log(`Файл сохранен: ${output}`);
2263
+ return;
2264
+ }
2265
+
2266
+ if (action === "click") {
2267
+ const url = target || options.url;
2268
+ if (!url || !options.selector) throw new Error('Пример: iola browser click https://example.com --selector "button" --output after.png');
2269
+ const result = await runBrowserAutomation("click", { ...browserParams(url, options), selector: options.selector, output: options.output ? path.resolve(options.output) : "" });
2270
+ if (result) console.log(result);
2271
+ return;
2272
+ }
2273
+
2274
+ if (action === "type") {
2275
+ const url = target || options.url;
2276
+ if (!url || !options.selector || options.text === undefined) throw new Error('Пример: iola browser type https://example.com --selector "#q" --text "школа 29"');
2277
+ const result = await runBrowserAutomation("type", { ...browserParams(url, options), selector: options.selector, text: options.text, press: options.press || "", output: options.output ? path.resolve(options.output) : "" });
2278
+ if (result) console.log(result);
2279
+ return;
2280
+ }
2281
+
2282
+ if (action === "eval") {
2283
+ const url = target || options.url;
2284
+ const script = options.script || rest.join(" ");
2285
+ if (!url || !script) throw new Error('Пример: iola browser eval https://example.com --script "document.title"');
2286
+ const result = await runBrowserAutomation("eval", { ...browserParams(url, options), script });
2287
+ console.log(result);
2288
+ return;
2289
+ }
2290
+
2291
+ throw new Error("Команды browser: status, install, open URL, text URL, html URL, screenshot URL --output FILE, pdf URL --output FILE, click URL --selector SEL, type URL --selector SEL --text TEXT, eval URL --script JS.");
2292
+ }
2293
+
2294
+ function browserParams(url, options = {}) {
2295
+ return {
2296
+ url,
2297
+ headed: Boolean(options.headed),
2298
+ timeout: Number(options.timeout || 30000),
2299
+ waitMs: Number(options.wait || 0),
2300
+ selector: options.selector || "",
2301
+ viewport: options.viewport || "1366x768",
2302
+ };
2303
+ }
2304
+
2212
2305
  async function handleWorkspace(args) {
2213
2306
  const [action = "status", nameOrPath] = args;
2214
2307
  const config = await loadConfig();
@@ -5903,12 +5996,12 @@ function parseOptions(args) {
5903
5996
 
5904
5997
  for (let index = 0; index < args.length; index += 1) {
5905
5998
  const arg = args[index];
5906
- if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || 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") {
5999
+ 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") {
5907
6000
  result[arg.slice(2)] = true;
5908
6001
  } else if (arg === "--check" || arg === "--upgrade-node") {
5909
6002
  result.check = true;
5910
6003
  result[arg.slice(2)] = true;
5911
- } else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || arg === "--base-url" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--debug-file") {
6004
+ } else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || arg === "--base-url" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--selector" || arg === "--url" || arg === "--timeout" || arg === "--wait" || arg === "--viewport" || arg === "--press" || arg === "--script" || arg === "--debug-file") {
5912
6005
  result[arg.slice(2)] = args[index + 1];
5913
6006
  index += 1;
5914
6007
  } else {
@@ -6285,6 +6378,105 @@ async function installCodexIfMissing() {
6285
6378
  await runCommand("npm", ["install", "-g", "@openai/codex"], { inherit: true });
6286
6379
  }
6287
6380
 
6381
+ async function getBrowserStatus() {
6382
+ const installed = existsSync(BROWSER_RUNTIME_PACKAGE);
6383
+ let playwright = "не установлен";
6384
+ if (installed) {
6385
+ try {
6386
+ playwright = JSON.parse(await readFile(BROWSER_RUNTIME_PACKAGE, "utf8")).version || "installed";
6387
+ } catch {
6388
+ playwright = "installed";
6389
+ }
6390
+ }
6391
+ return {
6392
+ runtime: BROWSER_RUNTIME_DIR,
6393
+ playwright,
6394
+ installed: installed ? "yes" : "no",
6395
+ install_command: "iola browser install",
6396
+ chromium: installed ? "managed by Playwright" : "not installed",
6397
+ };
6398
+ }
6399
+
6400
+ async function installBrowserRuntime() {
6401
+ await mkdir(BROWSER_RUNTIME_DIR, { recursive: true });
6402
+ const packageFile = path.join(BROWSER_RUNTIME_DIR, "package.json");
6403
+ if (!existsSync(packageFile)) {
6404
+ await writeFile(packageFile, `${JSON.stringify({ private: true, type: "module", dependencies: {} }, null, 2)}\n`, "utf8");
6405
+ }
6406
+ console.log(`Устанавливаю Playwright runtime: ${BROWSER_RUNTIME_DIR}`);
6407
+ await runPackageManager("npm", ["install", "playwright@latest"], { inherit: true, cwd: BROWSER_RUNTIME_DIR });
6408
+ await runPackageManager("npx", ["playwright", "install", "chromium"], { inherit: true, cwd: BROWSER_RUNTIME_DIR });
6409
+ }
6410
+
6411
+ function runPackageManager(command, args, options = {}) {
6412
+ if (process.platform === "win32") {
6413
+ return runCommand(process.env.ComSpec || "cmd.exe", ["/d", "/c", [command, ...args].join(" ")], options);
6414
+ }
6415
+ return runCommand(command, args, options);
6416
+ }
6417
+
6418
+ async function ensureBrowserRuntime() {
6419
+ if (existsSync(BROWSER_RUNTIME_PACKAGE)) return;
6420
+ throw new Error("Browser runtime не установлен. Запустите: iola browser install");
6421
+ }
6422
+
6423
+ async function runBrowserAutomation(action, params) {
6424
+ await ensureBrowserRuntime();
6425
+ const scriptFile = path.join(BROWSER_RUNTIME_DIR, `iola-browser-${Date.now()}-${Math.random().toString(16).slice(2)}.mjs`);
6426
+ await writeFile(scriptFile, browserAutomationScript(action, params), "utf8");
6427
+ try {
6428
+ const { stdout } = await runCommand(process.execPath, [scriptFile], { cwd: BROWSER_RUNTIME_DIR });
6429
+ return stdout.trim();
6430
+ } finally {
6431
+ await rm(scriptFile, { force: true }).catch(() => {});
6432
+ }
6433
+ }
6434
+
6435
+ function browserAutomationScript(action, params) {
6436
+ return `
6437
+ import { chromium } from "playwright";
6438
+ const action = ${JSON.stringify(action)};
6439
+ const params = ${JSON.stringify(params)};
6440
+ const [width, height] = String(params.viewport || "1366x768").split("x").map(Number);
6441
+ const browser = await chromium.launch({ headless: !params.headed });
6442
+ const page = await browser.newPage({ viewport: { width: width || 1366, height: height || 768 } });
6443
+ page.setDefaultTimeout(params.timeout || 30000);
6444
+ try {
6445
+ await page.goto(params.url, { waitUntil: "domcontentloaded", timeout: params.timeout || 30000 });
6446
+ if (params.waitMs) await page.waitForTimeout(params.waitMs);
6447
+ if (action === "open") {
6448
+ if (params.waitMs > 0) await page.waitForTimeout(params.waitMs);
6449
+ else if (!page.context().browser()?.isConnected()) {}
6450
+ } else if (action === "text") {
6451
+ console.log((await page.locator("body").innerText()).trim());
6452
+ } else if (action === "html") {
6453
+ console.log(await page.content());
6454
+ } else if (action === "screenshot") {
6455
+ await page.screenshot({ path: params.output, fullPage: true });
6456
+ } else if (action === "pdf") {
6457
+ await page.pdf({ path: params.output, format: "A4", printBackground: true });
6458
+ } else if (action === "click") {
6459
+ await page.locator(params.selector).first().click();
6460
+ if (params.waitMs) await page.waitForTimeout(params.waitMs);
6461
+ if (params.output) await page.screenshot({ path: params.output, fullPage: true });
6462
+ console.log((await page.locator("body").innerText()).trim().slice(0, 4000));
6463
+ } else if (action === "type") {
6464
+ const locator = page.locator(params.selector).first();
6465
+ await locator.fill(params.text || "");
6466
+ if (params.press) await locator.press(params.press);
6467
+ if (params.waitMs) await page.waitForTimeout(params.waitMs);
6468
+ if (params.output) await page.screenshot({ path: params.output, fullPage: true });
6469
+ console.log((await page.locator("body").innerText()).trim().slice(0, 4000));
6470
+ } else if (action === "eval") {
6471
+ const value = await page.evaluate(new Function("return (" + params.script + ")"));
6472
+ console.log(typeof value === "string" ? value : JSON.stringify(value, null, 2));
6473
+ }
6474
+ } finally {
6475
+ await browser.close();
6476
+ }
6477
+ `;
6478
+ }
6479
+
6288
6480
  async function probeEndpoint(url) {
6289
6481
  try {
6290
6482
  const response = await fetch(url, { headers: { accept: "application/json" } });
@@ -6468,6 +6660,8 @@ function mcpTools() {
6468
6660
  { name: "files.search", description: "Поиск текста в файлах workspace.", inputSchema: schema({ query: { type: "string" }, path: { type: "string" }, limit: { type: "number" } }) },
6469
6661
  { name: "index.search", description: "Поиск по индексу локальных документов.", inputSchema: schema({ query: { type: "string" }, limit: { type: "number" } }) },
6470
6662
  { name: "report", description: "Запуск встроенного отчета.", inputSchema: schema({ name: { type: "string" }, format: { type: "string" }, output: { type: "string" } }) },
6663
+ { name: "browser.text", description: "Открыть страницу в headless Chromium и вернуть видимый текст.", inputSchema: schema({ url: { type: "string" }, waitMs: { type: "number" } }) },
6664
+ { name: "browser.screenshot", description: "Сделать скриншот страницы через Chromium.", inputSchema: schema({ url: { type: "string" }, output: { type: "string" }, waitMs: { type: "number" } }) },
6471
6665
  ];
6472
6666
  }
6473
6667
 
@@ -6497,6 +6691,14 @@ async function callMcpTool(name, args = {}) {
6497
6691
  await handleExport([args.name || "education-contacts", "--format", args.format || "xlsx", "--output", output]);
6498
6692
  return { output };
6499
6693
  }
6694
+ if (name === "browser.text") {
6695
+ return runBrowserAutomation("text", { url: args.url, waitMs: Number(args.waitMs || 0), timeout: Number(args.timeout || 30000), viewport: args.viewport || "1366x768" });
6696
+ }
6697
+ if (name === "browser.screenshot") {
6698
+ const output = path.resolve(args.output || "browser-page.png");
6699
+ await runBrowserAutomation("screenshot", { url: args.url, output, waitMs: Number(args.waitMs || 0), timeout: Number(args.timeout || 30000), viewport: args.viewport || "1366x768" });
6700
+ return { output };
6701
+ }
6500
6702
  return executeRpc(name, { ...args, _: [] });
6501
6703
  }
6502
6704
 
package/wiki/Home.md CHANGED
@@ -32,6 +32,7 @@ iola ask "найди школу 29"
32
32
  - [Локальные файлы](Локальные-файлы)
33
33
  - [Рабочая среда агента](Рабочая-среда-агента)
34
34
  - [Платформа агента](Платформа-агента)
35
+ - [Браузерный агент](Браузерный-агент)
35
36
  - [Расширения и локальные данные](Расширения-и-локальные-данные)
36
37
  - [Архивы и мастер настройки](Архивы-и-мастер-настройки)
37
38
  - [Daemon, RPC и cron](Daemon-RPC-и-cron)
@@ -0,0 +1,68 @@
1
+ # Браузерный агент
2
+
3
+ `iola-cli` умеет подключать локальный браузерный runtime на Playwright. Сам npm-пакет остается легким: Chromium и Playwright ставятся отдельно при первом включении браузера.
4
+
5
+ ## Установка
6
+
7
+ ```bash
8
+ iola browser status
9
+ iola browser install
10
+ ```
11
+
12
+ Runtime хранится в `~/.iola/browser-runtime`.
13
+
14
+ ## Базовые команды
15
+
16
+ ```bash
17
+ iola browser open https://example.com
18
+ iola browser text https://example.com
19
+ iola browser html https://example.com --output page.html
20
+ iola browser screenshot https://example.com --output page.png
21
+ iola browser pdf https://example.com --output page.pdf
22
+ ```
23
+
24
+ `open` запускает видимый Chromium и держит его открытым. По умолчанию окно живет 10 минут. Можно задать время:
25
+
26
+ ```bash
27
+ iola browser open https://example.com --wait 30000
28
+ ```
29
+
30
+ ## Действия на странице
31
+
32
+ Клик:
33
+
34
+ ```bash
35
+ iola browser click https://example.com --selector "button" --output after-click.png
36
+ ```
37
+
38
+ Ввод:
39
+
40
+ ```bash
41
+ iola browser type https://example.com --selector "#q" --text "школа 29" --press Enter --wait 2000 --output search.png
42
+ ```
43
+
44
+ Выполнение JavaScript-выражения:
45
+
46
+ ```bash
47
+ iola browser eval https://example.com --script "document.title"
48
+ ```
49
+
50
+ ## MCP
51
+
52
+ Локальный MCP server также отдает браузерные tools:
53
+
54
+ - `browser.text`;
55
+ - `browser.screenshot`.
56
+
57
+ Перед использованием этих tools нужно один раз выполнить:
58
+
59
+ ```bash
60
+ iola browser install
61
+ ```
62
+
63
+ ## Режимы
64
+
65
+ - Для чтения страниц используйте `text`, `html`, `screenshot`, `pdf`.
66
+ - Для действий используйте `click` и `type`.
67
+ - Для ручной авторизации или просмотра используйте `open`.
68
+
@@ -74,6 +74,10 @@ iola import file data.csv --dataset custom
74
74
  iola index folder ./docs
75
75
  iola reports list
76
76
  iola plugins list
77
+ iola browser status
78
+ iola browser install
79
+ iola browser text https://example.com
80
+ iola browser screenshot https://example.com --output page.png
77
81
  iola mcp serve
78
82
  iola mcp serve --stdio
79
83
  iola memory suggest