@iola_adm/iola-cli 0.1.25 → 0.1.26

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
@@ -54,6 +54,10 @@ iola skills list
54
54
  iola tools toolsets
55
55
  iola files mode read-only
56
56
  iola files tree .
57
+ iola policy use analyst
58
+ iola tasks list
59
+ iola artifacts list
60
+ iola trace last
57
61
  iola context init
58
62
  iola cron list
59
63
  iola daemon status
@@ -82,6 +86,7 @@ iola version --check
82
86
  - [Локальный инструментальный агент](https://github.com/adm-iola/iola-cli/wiki/Локальный-инструментальный-агент)
83
87
  - [Skills и toolsets](https://github.com/adm-iola/iola-cli/wiki/Skills-и-toolsets)
84
88
  - [Локальные файлы](https://github.com/adm-iola/iola-cli/wiki/Локальные-файлы)
89
+ - [Рабочая среда агента](https://github.com/adm-iola/iola-cli/wiki/Рабочая-среда-агента)
85
90
  - [Daemon, RPC и cron](https://github.com/adm-iola/iola-cli/wiki/Daemon-RPC-и-cron)
86
91
  - [Контекст и память](https://github.com/adm-iola/iola-cli/wiki/Контекст-и-память)
87
92
  - [Команды](https://github.com/adm-iola/iola-cli/wiki/Команды)
@@ -95,6 +100,8 @@ iola version --check
95
100
  - локальный tool-agent для слабых моделей;
96
101
  - skills, toolsets, permissions, memory, hooks и готовые agents;
97
102
  - управляемые локальные файловые операции с режимами `locked`, `read-only`, `workspace-write`, `full-access`;
103
+ - планы выполнения, traces, tasks, artifacts, snapshots и policy-профили;
104
+ - экспорт отчетов в Excel/Word-совместимые файлы;
98
105
  - cron-задачи, локальный daemon и RPC для автоматизаций;
99
106
  - контекстные файлы `IOLA.md` и `.iola/context.md`;
100
107
  - интеграция с публичным MCP-сервером Йошкар-Олы.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
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
@@ -1,7 +1,7 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
3
3
  import { createServer } from "node:http";
4
- import { appendFile, copyFile, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
4
+ import { appendFile, copyFile, cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
7
  import readline from "node:readline/promises";
@@ -17,7 +17,7 @@ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
17
17
  const LAST_GOOD_CONFIG_FILE = path.join(CONFIG_DIR, "config.last-good.json");
18
18
  const SECRETS_FILE = path.join(CONFIG_DIR, "secrets.json");
19
19
  const DB_FILE = path.join(CONFIG_DIR, "iola.db");
20
- const DB_SCHEMA_VERSION = 4;
20
+ const DB_SCHEMA_VERSION = 5;
21
21
  const LOCAL_TOOLS = ["search_local", "get_card", "export_data", "run_report", "save_view"];
22
22
  const FILE_TOOLS = ["files_tree", "files_read", "files_search", "files_write", "files_patch"];
23
23
  const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS];
@@ -242,6 +242,13 @@ const COMMANDS = new Map([
242
242
  ["skills", handleSkills],
243
243
  ["tools", handleTools],
244
244
  ["files", handleFiles],
245
+ ["workspace", handleWorkspace],
246
+ ["tasks", handleTasks],
247
+ ["artifacts", handleArtifacts],
248
+ ["snapshot", handleSnapshot],
249
+ ["trace", handleTrace],
250
+ ["policy", handlePolicy],
251
+ ["export", handleExport],
245
252
  ["cron", handleCron],
246
253
  ["daemon", handleDaemon],
247
254
  ["rpc", handleRpc],
@@ -345,6 +352,13 @@ Usage:
345
352
  iola skills list|show|paths|enable|disable
346
353
  iola tools list|toolsets|enable|disable|profile
347
354
  iola files status|mode|approvals|tree|read|search|write|patch
355
+ iola workspace init|status|use|list
356
+ iola tasks list|add|done|run
357
+ iola artifacts list|show|open
358
+ iola snapshot create|list|restore
359
+ iola trace last|show
360
+ iola policy use safe|analyst|developer|full
361
+ iola export REPORT --format docx|xlsx --output FILE
348
362
  iola cron list|add|delete|run|tick
349
363
  iola daemon start|status
350
364
  iola rpc call METHOD [ARGS] [--json]
@@ -374,7 +388,7 @@ Usage:
374
388
  iola config set api.mcpBaseUrl URL
375
389
  iola config reset
376
390
  iola update
377
- iola ask TEXT [--profile NAME] [--model MODEL] [--tools] [--files] [--reasoning fast|verify|vote] [--output FILE] [--schema json|table] [--events] [--no-history] [--bare] [--quiet] [--no-color] [--fail-on-empty]
391
+ iola ask TEXT [--profile NAME] [--model MODEL] [--tools] [--files] [--plan] [--trace] [--reasoning fast|verify|vote] [--output FILE] [--schema json|table] [--events] [--no-history] [--bare] [--quiet] [--no-color] [--fail-on-empty]
378
392
  iola data LAYER [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
379
393
  iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
380
394
  iola ai context TEXT [--json]
@@ -590,6 +604,36 @@ async function handleAgentLine(line, state) {
590
604
  return false;
591
605
  }
592
606
 
607
+ if (command === "workspace") {
608
+ await handleWorkspace(args);
609
+ return false;
610
+ }
611
+
612
+ if (command === "tasks" || command === "todos") {
613
+ await handleTasks(args);
614
+ return false;
615
+ }
616
+
617
+ if (command === "artifacts") {
618
+ await handleArtifacts(args);
619
+ return false;
620
+ }
621
+
622
+ if (command === "snapshot") {
623
+ await handleSnapshot(args);
624
+ return false;
625
+ }
626
+
627
+ if (command === "trace") {
628
+ await handleTrace(args);
629
+ return false;
630
+ }
631
+
632
+ if (command === "policy") {
633
+ await handlePolicy(args);
634
+ return false;
635
+ }
636
+
593
637
  if (command === "cron") {
594
638
  await handleCron(args);
595
639
  return false;
@@ -713,6 +757,13 @@ async function handleAgentLine(line, state) {
713
757
  context: ["context", args],
714
758
  skills: ["skills", args],
715
759
  files: ["files", args],
760
+ workspace: ["workspace", args],
761
+ tasks: ["tasks", args],
762
+ todos: ["tasks", args],
763
+ artifacts: ["artifacts", args],
764
+ snapshot: ["snapshot", args],
765
+ trace: ["trace", args],
766
+ policy: ["policy", args],
716
767
  cron: ["cron", args],
717
768
  daemon: ["daemon", args],
718
769
  rpc: ["rpc", args],
@@ -761,6 +812,11 @@ function printAgentHelp() {
761
812
  /permissions
762
813
  /tools
763
814
  /files status
815
+ /workspace status
816
+ /tasks list
817
+ /artifacts list
818
+ /trace last
819
+ /policy use safe
764
820
  /cron list
765
821
  /daemon status
766
822
  /rpc call status
@@ -1491,6 +1547,7 @@ async function handleWiki(args) {
1491
1547
  ["Локальный инструментальный агент", `${base}/Локальный-инструментальный-агент`],
1492
1548
  ["Skills и toolsets", `${base}/Skills-и-toolsets`],
1493
1549
  ["Локальные файлы", `${base}/Локальные-файлы`],
1550
+ ["Рабочая среда агента", `${base}/Рабочая-среда-агента`],
1494
1551
  ["Daemon, RPC и cron", `${base}/Daemon-RPC-и-cron`],
1495
1552
  ["Контекст и память", `${base}/Контекст-и-память`],
1496
1553
  ["Команды", `${base}/Команды`],
@@ -1733,6 +1790,164 @@ async function handleFiles(args) {
1733
1790
  throw new Error("Команды files: status, mode MODE, approvals POLICY, tree [PATH], read FILE, search TEXT, write FILE --text TEXT, patch FILE --search OLD --replace NEW.");
1734
1791
  }
1735
1792
 
1793
+ async function handleWorkspace(args) {
1794
+ const [action = "status", nameOrPath] = args;
1795
+ const config = await loadConfig();
1796
+ if (action === "status") {
1797
+ printKeyValue({ root: resolveWorkspaceRoot(config), fileMode: config.files?.mode, approvals: config.files?.approvals });
1798
+ return;
1799
+ }
1800
+ if (action === "init") {
1801
+ await handleContext(["init"]);
1802
+ await mkdir(path.join(process.cwd(), ".iola", "skills"), { recursive: true });
1803
+ console.log(`Workspace готов: ${process.cwd()}`);
1804
+ return;
1805
+ }
1806
+ if (action === "list") {
1807
+ const rows = Object.entries(config.workspaces || {}).map(([name, value]) => ({ name, path: value.path }));
1808
+ printTable(rows, [["name", "Workspace"], ["path", "Путь"]]);
1809
+ return;
1810
+ }
1811
+ if (action === "use") {
1812
+ if (!nameOrPath) throw new Error("Пример: iola workspace use D:\\project");
1813
+ const root = path.resolve(nameOrPath);
1814
+ const name = path.basename(root);
1815
+ await saveConfig({ workspaces: { ...(config.workspaces || {}), [name]: { path: root } }, files: { ...(config.files || {}), workspaceRoot: root } });
1816
+ console.log(`Workspace выбран: ${root}`);
1817
+ return;
1818
+ }
1819
+ throw new Error("Команды workspace: init, status, list, use PATH.");
1820
+ }
1821
+
1822
+ async function handleTasks(args) {
1823
+ const [action = "list", idOrText, ...rest] = args;
1824
+ if (action === "list" || action === "ls") {
1825
+ printTable(listTasks(), [["id", "ID"], ["status", "Статус"], ["title", "Задача"], ["command", "Команда"]]);
1826
+ return;
1827
+ }
1828
+ if (action === "add") {
1829
+ const title = [idOrText, ...rest].filter(Boolean).join(" ");
1830
+ if (!title) throw new Error('Пример: iola tasks add "проверить школы"');
1831
+ const id = addTask(title);
1832
+ console.log(`Задача добавлена: ${id}`);
1833
+ return;
1834
+ }
1835
+ if (action === "done") {
1836
+ updateTaskStatus(Number(idOrText), "done");
1837
+ console.log(`Задача выполнена: ${idOrText}`);
1838
+ return;
1839
+ }
1840
+ if (action === "run") {
1841
+ const task = getTask(Number(idOrText));
1842
+ if (!task.command) throw new Error("У задачи нет команды. Добавьте command через SQLite пока не реализовано редактирование.");
1843
+ await main(splitCommandLine(task.command));
1844
+ updateTaskStatus(task.id, "done");
1845
+ return;
1846
+ }
1847
+ throw new Error("Команды tasks: list, add TEXT, done ID, run ID.");
1848
+ }
1849
+
1850
+ async function handleArtifacts(args) {
1851
+ const [action = "list", id] = args;
1852
+ if (action === "list" || action === "ls") {
1853
+ printTable(listArtifacts(), [["id", "ID"], ["kind", "Тип"], ["title", "Название"], ["file", "Файл"], ["created_at", "Дата"]]);
1854
+ return;
1855
+ }
1856
+ if (action === "show") {
1857
+ const artifact = getArtifact(Number(id));
1858
+ if (artifact.file && existsSync(artifact.file)) console.log(await readFile(artifact.file, "utf8"));
1859
+ else printJson(artifact);
1860
+ return;
1861
+ }
1862
+ if (action === "open") {
1863
+ const artifact = getArtifact(Number(id));
1864
+ if (!artifact.file) throw new Error("У artifact нет файла.");
1865
+ await openUrl(artifact.file);
1866
+ return;
1867
+ }
1868
+ throw new Error("Команды artifacts: list, show ID, open ID.");
1869
+ }
1870
+
1871
+ async function handleSnapshot(args) {
1872
+ const [action = "list", id] = args;
1873
+ if (action === "create") {
1874
+ const result = await createSnapshot();
1875
+ printKeyValue(result);
1876
+ return;
1877
+ }
1878
+ if (action === "list" || action === "ls") {
1879
+ printTable(listSnapshots(), [["id", "ID"], ["workspace", "Workspace"], ["path", "Папка"], ["created_at", "Дата"]]);
1880
+ return;
1881
+ }
1882
+ if (action === "restore") {
1883
+ await restoreSnapshot(Number(id));
1884
+ console.log(`Snapshot восстановлен: ${id}`);
1885
+ return;
1886
+ }
1887
+ throw new Error("Команды snapshot: create, list, restore ID.");
1888
+ }
1889
+
1890
+ async function handleTrace(args) {
1891
+ const [action = "last", id] = args;
1892
+ if (action === "last") {
1893
+ printTable(listTrace(Number(id || 20)), [["id", "ID"], ["run_id", "Run"], ["tool", "Tool"], ["status", "Статус"], ["summary", "Сводка"]]);
1894
+ return;
1895
+ }
1896
+ if (action === "show") {
1897
+ printJson(getTraceRun(id));
1898
+ return;
1899
+ }
1900
+ throw new Error("Команды trace: last [LIMIT], show RUN_ID.");
1901
+ }
1902
+
1903
+ async function handlePolicy(args) {
1904
+ const [action = "list", name] = args;
1905
+ const policies = {
1906
+ safe: { fileMode: "read-only", approvals: "always", toolProfile: "safe" },
1907
+ analyst: { fileMode: "read-only", approvals: "on-danger", toolsets: ["data-read", "reports", "sync", "ai", "local-files-read"] },
1908
+ developer: { fileMode: "workspace-write", approvals: "on-write", toolsets: ["data-read", "reports", "sync", "ai", "local-files-read", "local-files-write"] },
1909
+ full: { fileMode: "full-access", approvals: "on-danger", toolProfile: "full" },
1910
+ };
1911
+ if (action === "list") {
1912
+ printTable(Object.entries(policies).map(([policy, value]) => ({ policy, ...value, toolsets: value.toolsets?.join(", ") || value.toolProfile })), [["policy", "Policy"], ["fileMode", "Files"], ["approvals", "Approvals"], ["toolsets", "Toolsets"]]);
1913
+ return;
1914
+ }
1915
+ if (action === "use") {
1916
+ const policy = policies[name];
1917
+ if (!policy) throw new Error(`Policy неизвестна: ${Object.keys(policies).join(", ")}`);
1918
+ const config = await loadConfig();
1919
+ if (policy.toolProfile) {
1920
+ await handleTools(["profile", policy.toolProfile]);
1921
+ } else {
1922
+ await saveConfig({ toolsets: { ...(config.toolsets || {}), enabled: policy.toolsets } });
1923
+ }
1924
+ const next = await loadConfig();
1925
+ await saveConfig({ files: { ...(next.files || {}), mode: policy.fileMode, approvals: policy.approvals } });
1926
+ await setFilesMode(policy.fileMode, await loadConfig());
1927
+ console.log(`Policy применена: ${name}`);
1928
+ return;
1929
+ }
1930
+ throw new Error("Команды policy: list, use NAME.");
1931
+ }
1932
+
1933
+ async function handleExport(args) {
1934
+ const [name] = args;
1935
+ const options = parseOptions(args.slice(1));
1936
+ const format = options.format || "xlsx";
1937
+ const output = options.output || `${name || "iola-export"}.${format}`;
1938
+ await ensureLocalData();
1939
+ const rows = buildReportRows(name || "education-contacts");
1940
+ if (format === "xlsx") {
1941
+ await writeFile(output, toSpreadsheetXml(rows), "utf8");
1942
+ } else if (format === "docx" || format === "doc") {
1943
+ await writeFile(output, toWordHtml(name || "Отчет", rows), "utf8");
1944
+ } else {
1945
+ await outputData(rows, { output }, format);
1946
+ }
1947
+ saveArtifact("export", name || "export", output, { format, rows: rows.length });
1948
+ console.log(`Экспорт создан: ${output}`);
1949
+ }
1950
+
1736
1951
  async function handleCron(args) {
1737
1952
  const [action = "list", ...rest] = args;
1738
1953
  const options = parseOptions(rest);
@@ -2209,6 +2424,11 @@ async function handleView(args) {
2209
2424
  async function handleReport(args) {
2210
2425
  const [name] = args;
2211
2426
  await ensureLocalData();
2427
+ const options = parseOptions(args.slice(1));
2428
+ if (options.format === "docx" || options.format === "xlsx") {
2429
+ await handleExport([name || "education-contacts", "--format", options.format, "--output", options.output || `${name || "report"}.${options.format}`]);
2430
+ return;
2431
+ }
2212
2432
  if (name === "schools-summary") {
2213
2433
  printTable(getLocalSummaryRows("schools"), [["metric", "Показатель"], ["value", "Значение"]]);
2214
2434
  return;
@@ -2228,6 +2448,43 @@ async function handleReport(args) {
2228
2448
  throw new Error("Отчеты: schools-summary, education-contacts, missing-phones, licenses.");
2229
2449
  }
2230
2450
 
2451
+ function buildReportRows(name) {
2452
+ const reportName = name || "education-contacts";
2453
+ if (reportName === "schools-summary") return getLocalSummaryRows("schools");
2454
+ if (reportName === "missing-phones") return searchLocalRecords("", { dataset: "all", limit: 500 }).filter((item) => !item.phone || item.phone === "-");
2455
+ if (reportName === "licenses") return searchLocalRecords("", { dataset: "all", limit: 500 }).map((item) => ({ name: item.name, license_number: item.license_number, license_status: item.license_status }));
2456
+ return searchLocalRecords("", { dataset: "all", limit: 500 });
2457
+ }
2458
+
2459
+ function toSpreadsheetXml(rows) {
2460
+ const columns = Object.keys(rows[0] || { empty: "" });
2461
+ const cell = (value) => `<Cell><Data ss:Type="String">${escapeXml(value ?? "")}</Data></Cell>`;
2462
+ return `<?xml version="1.0"?>
2463
+ <Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
2464
+ <Worksheet ss:Name="IOLA"><Table>
2465
+ <Row>${columns.map(cell).join("")}</Row>
2466
+ ${rows.map((row) => `<Row>${columns.map((column) => cell(row[column])).join("")}</Row>`).join("\n")}
2467
+ </Table></Worksheet></Workbook>`;
2468
+ }
2469
+
2470
+ function toWordHtml(title, rows) {
2471
+ const columns = Object.keys(rows[0] || { empty: "" });
2472
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${escapeHtml(title)}</title></head><body>
2473
+ <h1>${escapeHtml(title)}</h1>
2474
+ <table border="1" cellspacing="0" cellpadding="4">
2475
+ <thead><tr>${columns.map((column) => `<th>${escapeHtml(column)}</th>`).join("")}</tr></thead>
2476
+ <tbody>${rows.map((row) => `<tr>${columns.map((column) => `<td>${escapeHtml(row[column] ?? "")}</td>`).join("")}</tr>`).join("\n")}</tbody>
2477
+ </table></body></html>`;
2478
+ }
2479
+
2480
+ function escapeXml(value) {
2481
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2482
+ }
2483
+
2484
+ function escapeHtml(value) {
2485
+ return escapeXml(value);
2486
+ }
2487
+
2231
2488
  async function handlePrivacy() {
2232
2489
  printKeyValue({
2233
2490
  config: CONFIG_FILE,
@@ -2906,6 +3163,38 @@ function initDatabase() {
2906
3163
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
2907
3164
  );
2908
3165
  CREATE INDEX IF NOT EXISTS idx_cron_jobs_enabled ON cron_jobs(enabled, last_run_at);
3166
+ CREATE TABLE IF NOT EXISTS tasks (
3167
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3168
+ title TEXT NOT NULL,
3169
+ command TEXT,
3170
+ status TEXT NOT NULL DEFAULT 'open',
3171
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
3172
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
3173
+ );
3174
+ CREATE TABLE IF NOT EXISTS artifacts (
3175
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3176
+ kind TEXT NOT NULL,
3177
+ title TEXT NOT NULL,
3178
+ file TEXT,
3179
+ meta_json TEXT NOT NULL DEFAULT '{}',
3180
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
3181
+ );
3182
+ CREATE TABLE IF NOT EXISTS tool_traces (
3183
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3184
+ run_id TEXT NOT NULL,
3185
+ tool TEXT NOT NULL,
3186
+ args_json TEXT NOT NULL,
3187
+ status TEXT NOT NULL,
3188
+ summary TEXT,
3189
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
3190
+ );
3191
+ CREATE INDEX IF NOT EXISTS idx_tool_traces_run_id ON tool_traces(run_id, id);
3192
+ CREATE TABLE IF NOT EXISTS snapshots (
3193
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3194
+ workspace TEXT NOT NULL,
3195
+ path TEXT NOT NULL,
3196
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
3197
+ );
2909
3198
  `);
2910
3199
  rebuildFtsIfEmpty(db);
2911
3200
  db.prepare(`
@@ -2958,6 +3247,8 @@ function getDbStatus() {
2958
3247
  const memory = db.prepare("SELECT COUNT(*) AS count FROM memory").get();
2959
3248
  const memorySuggestions = db.prepare("SELECT COUNT(*) AS count FROM memory_suggestions WHERE status = 'pending'").get();
2960
3249
  const cron = db.prepare("SELECT COUNT(*) AS count FROM cron_jobs").get();
3250
+ const tasks = db.prepare("SELECT COUNT(*) AS count FROM tasks WHERE status != 'done'").get();
3251
+ const artifacts = db.prepare("SELECT COUNT(*) AS count FROM artifacts").get();
2961
3252
  return {
2962
3253
  status: "ok",
2963
3254
  file: DB_FILE,
@@ -2969,6 +3260,8 @@ function getDbStatus() {
2969
3260
  memory: memory?.count ?? 0,
2970
3261
  memory_suggestions: memorySuggestions?.count ?? 0,
2971
3262
  cron_jobs: cron?.count ?? 0,
3263
+ open_tasks: tasks?.count ?? 0,
3264
+ artifacts: artifacts?.count ?? 0,
2972
3265
  };
2973
3266
  } finally {
2974
3267
  db.close();
@@ -4035,7 +4328,16 @@ async function localToolAsk(question, providerConfig, options) {
4035
4328
  await ensureLocalData();
4036
4329
  const plan = await buildLocalToolPlan(question, providerConfig, options);
4037
4330
  const validated = validateToolPlan(plan, options);
4038
- const result = await executeToolPlan(validated);
4331
+ if (options.plan) {
4332
+ printToolPlan(validated);
4333
+ const shouldRun = await confirm("Выполнить план? [y/N] ");
4334
+ if (!shouldRun) {
4335
+ saveArtifact("plan", question.slice(0, 80), "", { plan: validated });
4336
+ return "План построен, выполнение отменено.";
4337
+ }
4338
+ }
4339
+ const runId = `run-${Date.now()}-${Math.random().toString(16).slice(2)}`;
4340
+ const result = await executeToolPlan(validated, { ...options, runId });
4039
4341
  const answer = formatToolResult(result, options);
4040
4342
 
4041
4343
  if (!options["no-history"] && isFeatureEnabled("sqlite-history")) {
@@ -4049,7 +4351,8 @@ async function localToolAsk(question, providerConfig, options) {
4049
4351
  });
4050
4352
  }
4051
4353
 
4052
- emitEvent(options, "tool_plan", { plan: validated });
4354
+ emitEvent(options, "tool_plan", { plan: validated, runId });
4355
+ saveArtifact("tool-result", question.slice(0, 80), "", { runId, plan: validated, outputs: result.outputs });
4053
4356
  if (options.output) {
4054
4357
  await assertPermission("writeFiles");
4055
4358
  await writeFile(options.output, answer, "utf8");
@@ -4062,6 +4365,13 @@ async function localToolAsk(question, providerConfig, options) {
4062
4365
  return answer;
4063
4366
  }
4064
4367
 
4368
+ function printToolPlan(plan) {
4369
+ console.log("План выполнения:");
4370
+ plan.steps.forEach((step, index) => {
4371
+ console.log(`${index + 1}. ${step.tool} ${JSON.stringify(step.args || {})}`);
4372
+ });
4373
+ }
4374
+
4065
4375
  async function buildLocalToolPlan(question, providerConfig, options) {
4066
4376
  const mode = options.reasoning || "verify";
4067
4377
  const prompt = [
@@ -4138,50 +4448,62 @@ function availableToolNames(options = {}) {
4138
4448
  return options.files ? ALL_LOCAL_TOOLS : LOCAL_TOOLS;
4139
4449
  }
4140
4450
 
4141
- async function executeToolPlan(plan) {
4451
+ async function executeToolPlan(plan, options = {}) {
4142
4452
  let current = [];
4143
4453
  const outputs = [];
4144
4454
  for (const step of plan.steps) {
4455
+ let status = "ok";
4456
+ let summary = "";
4145
4457
  await assertPermission(step.tool);
4146
4458
  await runHooks("BeforeTool", { tool: step.tool, args: step.args || {} });
4147
- if (step.tool === "search_local") {
4148
- current = searchLocalRecords(step.args?.query || "", { dataset: step.args?.dataset || "all", limit: step.args?.limit || 20, fts: true });
4149
- outputs.push({ tool: step.tool, rows: current.length });
4150
- } else if (step.tool === "get_card") {
4151
- const card = findCard(step.args?.query || "");
4152
- current = card ? [card] : [];
4153
- outputs.push({ tool: step.tool, rows: current.length });
4154
- } else if (step.tool === "run_report") {
4155
- current = runQuality(step.args?.name || "all");
4156
- outputs.push({ tool: step.tool, rows: current.length });
4157
- } else if (step.tool === "save_view") {
4158
- saveView(step.args?.name, step.args?.dataset || "all", step.args?.args || []);
4159
- outputs.push({ tool: step.tool, saved: step.args?.name });
4160
- } else if (step.tool === "export_data") {
4161
- await assertPermission("writeFiles");
4162
- await runHooks("BeforeExport", { output: step.args?.output || "iola-export.csv", format: step.args?.format || "csv", rows: current.length });
4163
- const text = step.args?.format === "json" ? JSON.stringify(current, null, 2) : toCsv(current);
4164
- await writeFile(step.args?.output || "iola-export.csv", text, "utf8");
4165
- outputs.push({ tool: step.tool, output: step.args?.output || "iola-export.csv", rows: current.length });
4166
- } else if (step.tool === "files_tree") {
4167
- current = await filesTree(step.args?.path || ".", step.args || {});
4168
- outputs.push({ tool: step.tool, rows: current.length });
4169
- } else if (step.tool === "files_read") {
4170
- const text = await filesRead(step.args?.path || step.args?.file || ".", step.args || {});
4171
- current = [{ path: step.args?.path || step.args?.file || ".", text }];
4172
- outputs.push({ tool: step.tool, bytes: text.length });
4173
- } else if (step.tool === "files_search") {
4174
- current = await filesSearch(step.args?.query || "", { path: step.args?.path || ".", limit: step.args?.limit || 50 });
4175
- outputs.push({ tool: step.tool, rows: current.length });
4176
- } else if (step.tool === "files_write") {
4177
- await filesWrite(step.args?.path || step.args?.file, step.args?.text || "", { append: Boolean(step.args?.append) });
4178
- current = [{ path: step.args?.path || step.args?.file, status: "written" }];
4179
- outputs.push({ tool: step.tool, output: step.args?.path || step.args?.file, rows: 1 });
4180
- } else if (step.tool === "files_patch") {
4181
- const result = await filesPatch(step.args?.path || step.args?.file, step.args?.search || "", step.args?.replace || "");
4182
- current = [result];
4183
- outputs.push({ tool: step.tool, output: result.path, replacements: result.replacements });
4459
+ try {
4460
+ if (step.tool === "search_local") {
4461
+ current = searchLocalRecords(step.args?.query || "", { dataset: step.args?.dataset || "all", limit: step.args?.limit || 20, fts: true });
4462
+ outputs.push({ tool: step.tool, rows: current.length });
4463
+ } else if (step.tool === "get_card") {
4464
+ const card = findCard(step.args?.query || "");
4465
+ current = card ? [card] : [];
4466
+ outputs.push({ tool: step.tool, rows: current.length });
4467
+ } else if (step.tool === "run_report") {
4468
+ current = runQuality(step.args?.name || "all");
4469
+ outputs.push({ tool: step.tool, rows: current.length });
4470
+ } else if (step.tool === "save_view") {
4471
+ saveView(step.args?.name, step.args?.dataset || "all", step.args?.args || []);
4472
+ outputs.push({ tool: step.tool, saved: step.args?.name });
4473
+ } else if (step.tool === "export_data") {
4474
+ await assertPermission("writeFiles");
4475
+ await runHooks("BeforeExport", { output: step.args?.output || "iola-export.csv", format: step.args?.format || "csv", rows: current.length });
4476
+ const text = step.args?.format === "json" ? JSON.stringify(current, null, 2) : toCsv(current);
4477
+ await writeFile(step.args?.output || "iola-export.csv", text, "utf8");
4478
+ saveArtifact("export", step.args?.output || "iola-export.csv", step.args?.output || "iola-export.csv", { rows: current.length });
4479
+ outputs.push({ tool: step.tool, output: step.args?.output || "iola-export.csv", rows: current.length });
4480
+ } else if (step.tool === "files_tree") {
4481
+ current = await filesTree(step.args?.path || ".", step.args || {});
4482
+ outputs.push({ tool: step.tool, rows: current.length });
4483
+ } else if (step.tool === "files_read") {
4484
+ const text = await filesRead(step.args?.path || step.args?.file || ".", step.args || {});
4485
+ current = [{ path: step.args?.path || step.args?.file || ".", text }];
4486
+ outputs.push({ tool: step.tool, bytes: text.length });
4487
+ } else if (step.tool === "files_search") {
4488
+ current = await filesSearch(step.args?.query || "", { path: step.args?.path || ".", limit: step.args?.limit || 50 });
4489
+ outputs.push({ tool: step.tool, rows: current.length });
4490
+ } else if (step.tool === "files_write") {
4491
+ await filesWrite(step.args?.path || step.args?.file, step.args?.text || "", { append: Boolean(step.args?.append) });
4492
+ current = [{ path: step.args?.path || step.args?.file, status: "written" }];
4493
+ outputs.push({ tool: step.tool, output: step.args?.path || step.args?.file, rows: 1 });
4494
+ } else if (step.tool === "files_patch") {
4495
+ const result = await filesPatch(step.args?.path || step.args?.file, step.args?.search || "", step.args?.replace || "");
4496
+ current = [result];
4497
+ outputs.push({ tool: step.tool, output: result.path, replacements: result.replacements });
4498
+ }
4499
+ summary = `rows=${current.length}`;
4500
+ } catch (error) {
4501
+ status = "error";
4502
+ summary = error instanceof Error ? error.message : String(error);
4503
+ recordToolTrace(options.runId || "manual", step.tool, step.args || {}, status, summary);
4504
+ throw error;
4184
4505
  }
4506
+ recordToolTrace(options.runId || "manual", step.tool, step.args || {}, status, summary);
4185
4507
  await runHooks("AfterTool", { tool: step.tool, rows: current.length });
4186
4508
  }
4187
4509
  return { rows: current, outputs };
@@ -4738,7 +5060,7 @@ function parseOptions(args) {
4738
5060
 
4739
5061
  for (let index = 0; index < args.length; index += 1) {
4740
5062
  const arg = args[index];
4741
- if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--append") {
5063
+ if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--append") {
4742
5064
  result[arg.slice(2)] = true;
4743
5065
  } else if (arg === "--check" || arg === "--upgrade-node") {
4744
5066
  result.check = true;
@@ -5313,6 +5635,155 @@ async function maybeConfirmFileOperation(operation, target, preview) {
5313
5635
  if (!ok) throw new Error("Файловая операция отменена.");
5314
5636
  }
5315
5637
 
5638
+ function listTasks() {
5639
+ initDatabase();
5640
+ const db = openDatabase();
5641
+ try {
5642
+ return db.prepare("SELECT id, title, COALESCE(command, '-') AS command, status FROM tasks ORDER BY status, id DESC LIMIT 100").all();
5643
+ } finally {
5644
+ db.close();
5645
+ }
5646
+ }
5647
+
5648
+ function addTask(title, command = "") {
5649
+ initDatabase();
5650
+ const db = openDatabase();
5651
+ try {
5652
+ const result = db.prepare("INSERT INTO tasks(title, command) VALUES (?, ?)").run(title, command);
5653
+ return Number(result.lastInsertRowid);
5654
+ } finally {
5655
+ db.close();
5656
+ }
5657
+ }
5658
+
5659
+ function getTask(id) {
5660
+ initDatabase();
5661
+ const db = openDatabase();
5662
+ try {
5663
+ const row = db.prepare("SELECT * FROM tasks WHERE id = ?").get(id);
5664
+ if (!row) throw new Error(`Задача не найдена: ${id}`);
5665
+ return row;
5666
+ } finally {
5667
+ db.close();
5668
+ }
5669
+ }
5670
+
5671
+ function updateTaskStatus(id, status) {
5672
+ initDatabase();
5673
+ const db = openDatabase();
5674
+ try {
5675
+ db.prepare("UPDATE tasks SET status = ?, updated_at = datetime('now') WHERE id = ?").run(status, id);
5676
+ } finally {
5677
+ db.close();
5678
+ }
5679
+ }
5680
+
5681
+ function saveArtifact(kind, title, file = "", meta = {}) {
5682
+ initDatabase();
5683
+ const db = openDatabase();
5684
+ try {
5685
+ const result = db.prepare("INSERT INTO artifacts(kind, title, file, meta_json) VALUES (?, ?, ?, ?)").run(kind, title || kind, file || "", JSON.stringify(meta));
5686
+ return Number(result.lastInsertRowid);
5687
+ } finally {
5688
+ db.close();
5689
+ }
5690
+ }
5691
+
5692
+ function listArtifacts() {
5693
+ initDatabase();
5694
+ const db = openDatabase();
5695
+ try {
5696
+ return db.prepare("SELECT id, kind, title, file, created_at FROM artifacts ORDER BY id DESC LIMIT 100").all();
5697
+ } finally {
5698
+ db.close();
5699
+ }
5700
+ }
5701
+
5702
+ function getArtifact(id) {
5703
+ initDatabase();
5704
+ const db = openDatabase();
5705
+ try {
5706
+ const row = db.prepare("SELECT * FROM artifacts WHERE id = ?").get(id);
5707
+ if (!row) throw new Error(`Artifact не найден: ${id}`);
5708
+ return row;
5709
+ } finally {
5710
+ db.close();
5711
+ }
5712
+ }
5713
+
5714
+ function recordToolTrace(runId, tool, args, status, summary) {
5715
+ initDatabase();
5716
+ const db = openDatabase();
5717
+ try {
5718
+ db.prepare("INSERT INTO tool_traces(run_id, tool, args_json, status, summary) VALUES (?, ?, ?, ?, ?)").run(runId, tool, JSON.stringify(args), status, summary || "");
5719
+ } finally {
5720
+ db.close();
5721
+ }
5722
+ }
5723
+
5724
+ function listTrace(limit = 20) {
5725
+ initDatabase();
5726
+ const db = openDatabase();
5727
+ try {
5728
+ return db.prepare("SELECT id, run_id, tool, status, summary, created_at FROM tool_traces ORDER BY id DESC LIMIT ?").all(limit);
5729
+ } finally {
5730
+ db.close();
5731
+ }
5732
+ }
5733
+
5734
+ function getTraceRun(runId) {
5735
+ initDatabase();
5736
+ const db = openDatabase();
5737
+ try {
5738
+ return db.prepare("SELECT * FROM tool_traces WHERE run_id = ? ORDER BY id ASC").all(runId);
5739
+ } finally {
5740
+ db.close();
5741
+ }
5742
+ }
5743
+
5744
+ async function createSnapshot() {
5745
+ const config = await loadConfig();
5746
+ const workspace = resolveWorkspaceRoot(config);
5747
+ const snapshotsDir = path.join(CONFIG_DIR, "snapshots");
5748
+ await mkdir(snapshotsDir, { recursive: true });
5749
+ const target = path.join(snapshotsDir, `snapshot-${Date.now()}`);
5750
+ await cp(workspace, target, {
5751
+ recursive: true,
5752
+ filter: (source) => !isBlockedPathForConfig(source, config) && !source.includes(`${path.sep}node_modules${path.sep}`),
5753
+ });
5754
+ initDatabase();
5755
+ const db = openDatabase();
5756
+ try {
5757
+ const result = db.prepare("INSERT INTO snapshots(workspace, path) VALUES (?, ?)").run(workspace, target);
5758
+ return { id: Number(result.lastInsertRowid), workspace, path: target };
5759
+ } finally {
5760
+ db.close();
5761
+ }
5762
+ }
5763
+
5764
+ function listSnapshots() {
5765
+ initDatabase();
5766
+ const db = openDatabase();
5767
+ try {
5768
+ return db.prepare("SELECT id, workspace, path, created_at FROM snapshots ORDER BY id DESC LIMIT 50").all();
5769
+ } finally {
5770
+ db.close();
5771
+ }
5772
+ }
5773
+
5774
+ async function restoreSnapshot(id) {
5775
+ initDatabase();
5776
+ const db = openDatabase();
5777
+ let row;
5778
+ try {
5779
+ row = db.prepare("SELECT * FROM snapshots WHERE id = ?").get(id);
5780
+ } finally {
5781
+ db.close();
5782
+ }
5783
+ if (!row) throw new Error(`Snapshot не найден: ${id}`);
5784
+ await cp(row.path, row.workspace, { recursive: true, force: true });
5785
+ }
5786
+
5316
5787
  function unifiedPreview(before, after) {
5317
5788
  const beforeLines = before.split(/\r?\n/);
5318
5789
  const afterLines = after.split(/\r?\n/);
package/wiki/Home.md CHANGED
@@ -30,6 +30,7 @@ iola ask "найди школу 29"
30
30
  - [Локальный инструментальный агент](Локальный-инструментальный-агент)
31
31
  - [Skills и toolsets](Skills-и-toolsets)
32
32
  - [Локальные файлы](Локальные-файлы)
33
+ - [Рабочая среда агента](Рабочая-среда-агента)
33
34
  - [Daemon, RPC и cron](Daemon-RPC-и-cron)
34
35
  - [Контекст и память](Контекст-и-память)
35
36
  - [Команды](Команды)
@@ -51,6 +51,12 @@ iola files status
51
51
  iola files mode read-only
52
52
  iola files tree .
53
53
  iola files search "Петрова" --path .
54
+ iola policy use analyst
55
+ iola tasks list
56
+ iola artifacts list
57
+ iola snapshot list
58
+ iola trace last
59
+ iola export education-contacts --format xlsx --output contacts.xlsx
54
60
  iola memory suggest
55
61
  ```
56
62
 
@@ -0,0 +1,67 @@
1
+ # Рабочая среда агента
2
+
3
+ `iola-cli` поддерживает рабочие пространства, задачи, артефакты, snapshots, trace и policy-профили.
4
+
5
+ План перед выполнением:
6
+
7
+ ```bash
8
+ iola ask "найди файлы со школами и сделай отчет" --profile local --tools --files --plan
9
+ ```
10
+
11
+ Policy:
12
+
13
+ ```bash
14
+ iola policy list
15
+ iola policy use safe
16
+ iola policy use analyst
17
+ iola policy use developer
18
+ iola policy use full
19
+ ```
20
+
21
+ Workspace:
22
+
23
+ ```bash
24
+ iola workspace init
25
+ iola workspace status
26
+ iola workspace use D:\project
27
+ iola workspace list
28
+ ```
29
+
30
+ Tasks:
31
+
32
+ ```bash
33
+ iola tasks add "проверить качество данных"
34
+ iola tasks list
35
+ iola tasks done 1
36
+ ```
37
+
38
+ Artifacts:
39
+
40
+ ```bash
41
+ iola artifacts list
42
+ iola artifacts show 1
43
+ iola artifacts open 1
44
+ ```
45
+
46
+ Snapshots:
47
+
48
+ ```bash
49
+ iola snapshot create
50
+ iola snapshot list
51
+ iola snapshot restore 1
52
+ ```
53
+
54
+ Trace:
55
+
56
+ ```bash
57
+ iola trace last
58
+ iola trace show RUN_ID
59
+ ```
60
+
61
+ Export:
62
+
63
+ ```bash
64
+ iola export education-contacts --format xlsx --output contacts.xlsx
65
+ iola report missing-phones --format docx --output missing-phones.docx
66
+ ```
67
+