@iola_adm/iola-cli 0.1.25 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.js CHANGED
@@ -1,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 = 6;
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,18 @@ const COMMANDS = new Map([
242
242
  ["skills", handleSkills],
243
243
  ["tools", handleTools],
244
244
  ["files", handleFiles],
245
+ ["changes", handleChanges],
246
+ ["import", handleImport],
247
+ ["index", handleIndex],
248
+ ["reports", handleReports],
249
+ ["plugins", handlePlugins],
250
+ ["workspace", handleWorkspace],
251
+ ["tasks", handleTasks],
252
+ ["artifacts", handleArtifacts],
253
+ ["snapshot", handleSnapshot],
254
+ ["trace", handleTrace],
255
+ ["policy", handlePolicy],
256
+ ["export", handleExport],
245
257
  ["cron", handleCron],
246
258
  ["daemon", handleDaemon],
247
259
  ["rpc", handleRpc],
@@ -277,6 +289,7 @@ const COMMANDS = new Map([
277
289
  ["search", searchAll],
278
290
  ["mcp-info", showMcpInfo],
279
291
  ["setup", setupClient],
292
+ ["onboard", onboard],
280
293
  ]);
281
294
 
282
295
  export async function main(argv) {
@@ -345,6 +358,18 @@ Usage:
345
358
  iola skills list|show|paths|enable|disable
346
359
  iola tools list|toolsets|enable|disable|profile
347
360
  iola files status|mode|approvals|tree|read|search|write|patch
361
+ iola changes list|show|apply|discard
362
+ iola import file|folder
363
+ iola index folder|status|search
364
+ iola reports list|run
365
+ iola plugins list|install|run|remove
366
+ iola workspace init|status|use|list
367
+ iola tasks list|add|done|run
368
+ iola artifacts list|show|open
369
+ iola snapshot create|list|restore
370
+ iola trace last|show
371
+ iola policy use safe|analyst|developer|full
372
+ iola export REPORT --format docx|xlsx --output FILE
348
373
  iola cron list|add|delete|run|tick
349
374
  iola daemon start|status
350
375
  iola rpc call METHOD [ARGS] [--json]
@@ -352,7 +377,7 @@ Usage:
352
377
  iola memory show|add|set|clear|export
353
378
  iola hooks list|add|delete|run
354
379
  iola agents list|run
355
- iola mcp list|status|install|remove
380
+ iola mcp list|status|install|remove|serve
356
381
  iola cache status|warm|clear
357
382
  iola sync [--dataset schools|kindergartens]
358
383
  iola sync status
@@ -374,7 +399,7 @@ Usage:
374
399
  iola config set api.mcpBaseUrl URL
375
400
  iola config reset
376
401
  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]
402
+ 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
403
  iola data LAYER [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
379
404
  iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
380
405
  iola ai context TEXT [--json]
@@ -399,6 +424,7 @@ Usage:
399
424
  iola search TEXT [--limit 5] [--format table|json|csv]
400
425
  iola mcp-info [--json]
401
426
  iola setup codex
427
+ iola onboard
402
428
  iola version
403
429
 
404
430
  Environment:
@@ -590,6 +616,56 @@ async function handleAgentLine(line, state) {
590
616
  return false;
591
617
  }
592
618
 
619
+ if (command === "changes") {
620
+ await handleChanges(args);
621
+ return false;
622
+ }
623
+
624
+ if (command === "index") {
625
+ await handleIndex(args);
626
+ return false;
627
+ }
628
+
629
+ if (command === "reports") {
630
+ await handleReports(args);
631
+ return false;
632
+ }
633
+
634
+ if (command === "plugins") {
635
+ await handlePlugins(args);
636
+ return false;
637
+ }
638
+
639
+ if (command === "workspace") {
640
+ await handleWorkspace(args);
641
+ return false;
642
+ }
643
+
644
+ if (command === "tasks" || command === "todos") {
645
+ await handleTasks(args);
646
+ return false;
647
+ }
648
+
649
+ if (command === "artifacts") {
650
+ await handleArtifacts(args);
651
+ return false;
652
+ }
653
+
654
+ if (command === "snapshot") {
655
+ await handleSnapshot(args);
656
+ return false;
657
+ }
658
+
659
+ if (command === "trace") {
660
+ await handleTrace(args);
661
+ return false;
662
+ }
663
+
664
+ if (command === "policy") {
665
+ await handlePolicy(args);
666
+ return false;
667
+ }
668
+
593
669
  if (command === "cron") {
594
670
  await handleCron(args);
595
671
  return false;
@@ -713,6 +789,17 @@ async function handleAgentLine(line, state) {
713
789
  context: ["context", args],
714
790
  skills: ["skills", args],
715
791
  files: ["files", args],
792
+ changes: ["changes", args],
793
+ index: ["index", args],
794
+ reports: ["reports", args],
795
+ plugins: ["plugins", args],
796
+ workspace: ["workspace", args],
797
+ tasks: ["tasks", args],
798
+ todos: ["tasks", args],
799
+ artifacts: ["artifacts", args],
800
+ snapshot: ["snapshot", args],
801
+ trace: ["trace", args],
802
+ policy: ["policy", args],
716
803
  cron: ["cron", args],
717
804
  daemon: ["daemon", args],
718
805
  rpc: ["rpc", args],
@@ -761,6 +848,15 @@ function printAgentHelp() {
761
848
  /permissions
762
849
  /tools
763
850
  /files status
851
+ /changes list
852
+ /index status
853
+ /reports list
854
+ /plugins list
855
+ /workspace status
856
+ /tasks list
857
+ /artifacts list
858
+ /trace last
859
+ /policy use safe
764
860
  /cron list
765
861
  /daemon status
766
862
  /rpc call status
@@ -1491,6 +1587,8 @@ async function handleWiki(args) {
1491
1587
  ["Локальный инструментальный агент", `${base}/Локальный-инструментальный-агент`],
1492
1588
  ["Skills и toolsets", `${base}/Skills-и-toolsets`],
1493
1589
  ["Локальные файлы", `${base}/Локальные-файлы`],
1590
+ ["Рабочая среда агента", `${base}/Рабочая-среда-агента`],
1591
+ ["Расширения и локальные данные", `${base}/Расширения-и-локальные-данные`],
1494
1592
  ["Daemon, RPC и cron", `${base}/Daemon-RPC-и-cron`],
1495
1593
  ["Контекст и память", `${base}/Контекст-и-память`],
1496
1594
  ["Команды", `${base}/Команды`],
@@ -1717,22 +1815,314 @@ async function handleFiles(args) {
1717
1815
  if (!target) throw new Error('Пример: iola files write report.md --text "..."');
1718
1816
  const text = options.text ?? rest.join(" ");
1719
1817
  if (!text) throw new Error('Для записи нужен --text "..." или текст после пути.');
1720
- await filesWrite(target, text, { append: Boolean(options.append) });
1721
- console.log(`Файл записан: ${target}`);
1818
+ if (options.stage) {
1819
+ const id = await stageFileChange("write", target, text);
1820
+ console.log(`Изменение подготовлено: ${id}`);
1821
+ } else {
1822
+ await filesWrite(target, text, { append: Boolean(options.append) });
1823
+ console.log(`Файл записан: ${target}`);
1824
+ }
1722
1825
  return;
1723
1826
  }
1724
1827
 
1725
1828
  if (action === "patch") {
1726
1829
  if (!target) throw new Error('Пример: iola files patch README.md --search old --replace new');
1727
1830
  if (!options.search || options.replace === undefined) throw new Error("Для patch нужны --search и --replace.");
1728
- const result = await filesPatch(target, options.search, options.replace);
1729
- printKeyValue(result);
1831
+ if (options.stage) {
1832
+ const current = await filesRead(target);
1833
+ const next = current.split(options.search).join(options.replace);
1834
+ const id = await stageFileChange("patch", target, next, current);
1835
+ console.log(`Изменение подготовлено: ${id}`);
1836
+ } else {
1837
+ const result = await filesPatch(target, options.search, options.replace);
1838
+ printKeyValue(result);
1839
+ }
1730
1840
  return;
1731
1841
  }
1732
1842
 
1733
1843
  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
1844
  }
1735
1845
 
1846
+ async function handleChanges(args) {
1847
+ const [action = "list", id] = args;
1848
+ if (action === "list" || action === "ls") {
1849
+ printTable(listChanges(), [["id", "ID"], ["kind", "Тип"], ["target", "Файл"], ["status", "Статус"], ["created_at", "Дата"]]);
1850
+ return;
1851
+ }
1852
+ if (action === "show") {
1853
+ const change = getChange(Number(id));
1854
+ console.log(unifiedPreview(change.before_text || "", change.after_text || ""));
1855
+ return;
1856
+ }
1857
+ if (action === "apply") {
1858
+ await applyChange(Number(id));
1859
+ console.log(`Изменение применено: ${id}`);
1860
+ return;
1861
+ }
1862
+ if (action === "discard") {
1863
+ updateChangeStatus(Number(id), "discarded");
1864
+ console.log(`Изменение отклонено: ${id}`);
1865
+ return;
1866
+ }
1867
+ throw new Error("Команды changes: list, show ID, apply ID, discard ID.");
1868
+ }
1869
+
1870
+ async function handleImport(args) {
1871
+ const [action, target, ...rest] = args;
1872
+ const options = parseOptions(rest);
1873
+ if (action === "file") {
1874
+ if (!target) throw new Error("Пример: iola import file data.csv --dataset custom");
1875
+ const dataset = options.dataset || path.basename(target, path.extname(target));
1876
+ const count = await importDataFile(target, dataset);
1877
+ console.log(`Импортировано записей: ${count}, dataset=${dataset}`);
1878
+ return;
1879
+ }
1880
+ if (action === "folder") {
1881
+ if (!target) throw new Error("Пример: iola import folder ./data");
1882
+ const rows = await filesTree(target, { depth: 1, limit: 200 });
1883
+ let total = 0;
1884
+ for (const row of rows.filter((item) => item.type === "file" && /\.(json|csv)$/i.test(item.path))) {
1885
+ total += await importDataFile(row.path, options.dataset || path.basename(row.path, path.extname(row.path)));
1886
+ }
1887
+ console.log(`Импортировано записей: ${total}`);
1888
+ return;
1889
+ }
1890
+ throw new Error("Команды import: file PATH --dataset NAME, folder PATH.");
1891
+ }
1892
+
1893
+ async function handleIndex(args) {
1894
+ const [action = "status", target, ...rest] = args;
1895
+ const options = parseOptions(rest);
1896
+ if (action === "status") {
1897
+ printKeyValue(getIndexStatus());
1898
+ return;
1899
+ }
1900
+ if (action === "folder") {
1901
+ if (!target) throw new Error("Пример: iola index folder ./docs");
1902
+ const count = await indexFolder(target, options);
1903
+ console.log(`Проиндексировано документов: ${count}`);
1904
+ return;
1905
+ }
1906
+ if (action === "search") {
1907
+ const query = [target, ...rest].filter(Boolean).join(" ");
1908
+ if (!query) throw new Error('Пример: iola index search "школа 29"');
1909
+ printTable(searchDocs(query, Number(options.limit || 20)), [["file", "Файл"], ["title", "Название"], ["snippet", "Фрагмент"]]);
1910
+ return;
1911
+ }
1912
+ throw new Error("Команды index: status, folder PATH, search TEXT.");
1913
+ }
1914
+
1915
+ async function handleReports(args) {
1916
+ const [action = "list", name, ...rest] = args;
1917
+ const packs = {
1918
+ "education-passport": ["education-contacts", "licenses"],
1919
+ "data-quality-pack": ["schools-summary", "missing-phones"],
1920
+ };
1921
+ if (action === "list") {
1922
+ printTable(Object.entries(packs).map(([pack, reports]) => ({ pack, reports: reports.join(", ") })), [["pack", "Пакет"], ["reports", "Отчеты"]]);
1923
+ return;
1924
+ }
1925
+ if (action === "run") {
1926
+ if (!packs[name]) throw new Error(`Пакет неизвестен: ${Object.keys(packs).join(", ")}`);
1927
+ const options = parseOptions(rest);
1928
+ const dir = options.output || path.join(process.cwd(), `iola-report-${name}-${Date.now()}`);
1929
+ await mkdir(dir, { recursive: true });
1930
+ for (const report of packs[name]) {
1931
+ await handleExport([report, "--format", "xlsx", "--output", path.join(dir, `${report}.xlsx`)]);
1932
+ await handleExport([report, "--format", "docx", "--output", path.join(dir, `${report}.docx`)]);
1933
+ }
1934
+ saveArtifact("report-pack", name, dir, { reports: packs[name] });
1935
+ console.log(`Пакет отчетов создан: ${dir}`);
1936
+ return;
1937
+ }
1938
+ throw new Error("Команды reports: list, run NAME [--output DIR].");
1939
+ }
1940
+
1941
+ async function handlePlugins(args) {
1942
+ const [action = "list", name, ...rest] = args;
1943
+ if (action === "list" || action === "ls") {
1944
+ printTable(listPlugins(), [["name", "Plugin"], ["source", "Источник"], ["command", "Команда"]]);
1945
+ return;
1946
+ }
1947
+ if (action === "install") {
1948
+ const options = parseOptions(rest);
1949
+ if (!name) throw new Error("Пример: iola plugins install my-plugin --command \"iola quality\"");
1950
+ savePlugin(name, options.source || name, options.command || "");
1951
+ console.log(`Plugin установлен: ${name}`);
1952
+ return;
1953
+ }
1954
+ if (action === "run") {
1955
+ const plugin = getPlugin(name);
1956
+ if (!plugin.command) throw new Error("У plugin нет command.");
1957
+ await main(splitCommandLine(plugin.command));
1958
+ return;
1959
+ }
1960
+ if (action === "remove" || action === "delete") {
1961
+ deletePlugin(name);
1962
+ console.log(`Plugin удален: ${name}`);
1963
+ return;
1964
+ }
1965
+ throw new Error("Команды plugins: list, install NAME --command CMD, run NAME, remove NAME.");
1966
+ }
1967
+
1968
+ async function handleWorkspace(args) {
1969
+ const [action = "status", nameOrPath] = args;
1970
+ const config = await loadConfig();
1971
+ if (action === "status") {
1972
+ printKeyValue({ root: resolveWorkspaceRoot(config), fileMode: config.files?.mode, approvals: config.files?.approvals });
1973
+ return;
1974
+ }
1975
+ if (action === "init") {
1976
+ await handleContext(["init"]);
1977
+ await mkdir(path.join(process.cwd(), ".iola", "skills"), { recursive: true });
1978
+ console.log(`Workspace готов: ${process.cwd()}`);
1979
+ return;
1980
+ }
1981
+ if (action === "list") {
1982
+ const rows = Object.entries(config.workspaces || {}).map(([name, value]) => ({ name, path: value.path }));
1983
+ printTable(rows, [["name", "Workspace"], ["path", "Путь"]]);
1984
+ return;
1985
+ }
1986
+ if (action === "use") {
1987
+ if (!nameOrPath) throw new Error("Пример: iola workspace use D:\\project");
1988
+ const root = path.resolve(nameOrPath);
1989
+ const name = path.basename(root);
1990
+ await saveConfig({ workspaces: { ...(config.workspaces || {}), [name]: { path: root } }, files: { ...(config.files || {}), workspaceRoot: root } });
1991
+ console.log(`Workspace выбран: ${root}`);
1992
+ return;
1993
+ }
1994
+ throw new Error("Команды workspace: init, status, list, use PATH.");
1995
+ }
1996
+
1997
+ async function handleTasks(args) {
1998
+ const [action = "list", idOrText, ...rest] = args;
1999
+ if (action === "list" || action === "ls") {
2000
+ printTable(listTasks(), [["id", "ID"], ["status", "Статус"], ["title", "Задача"], ["command", "Команда"]]);
2001
+ return;
2002
+ }
2003
+ if (action === "add") {
2004
+ const title = [idOrText, ...rest].filter(Boolean).join(" ");
2005
+ if (!title) throw new Error('Пример: iola tasks add "проверить школы"');
2006
+ const id = addTask(title);
2007
+ console.log(`Задача добавлена: ${id}`);
2008
+ return;
2009
+ }
2010
+ if (action === "done") {
2011
+ updateTaskStatus(Number(idOrText), "done");
2012
+ console.log(`Задача выполнена: ${idOrText}`);
2013
+ return;
2014
+ }
2015
+ if (action === "run") {
2016
+ const task = getTask(Number(idOrText));
2017
+ if (!task.command) throw new Error("У задачи нет команды. Добавьте command через SQLite пока не реализовано редактирование.");
2018
+ await main(splitCommandLine(task.command));
2019
+ updateTaskStatus(task.id, "done");
2020
+ return;
2021
+ }
2022
+ throw new Error("Команды tasks: list, add TEXT, done ID, run ID.");
2023
+ }
2024
+
2025
+ async function handleArtifacts(args) {
2026
+ const [action = "list", id] = args;
2027
+ if (action === "list" || action === "ls") {
2028
+ printTable(listArtifacts(), [["id", "ID"], ["kind", "Тип"], ["title", "Название"], ["file", "Файл"], ["created_at", "Дата"]]);
2029
+ return;
2030
+ }
2031
+ if (action === "show") {
2032
+ const artifact = getArtifact(Number(id));
2033
+ if (artifact.file && existsSync(artifact.file)) console.log(await readFile(artifact.file, "utf8"));
2034
+ else printJson(artifact);
2035
+ return;
2036
+ }
2037
+ if (action === "open") {
2038
+ const artifact = getArtifact(Number(id));
2039
+ if (!artifact.file) throw new Error("У artifact нет файла.");
2040
+ await openUrl(artifact.file);
2041
+ return;
2042
+ }
2043
+ throw new Error("Команды artifacts: list, show ID, open ID.");
2044
+ }
2045
+
2046
+ async function handleSnapshot(args) {
2047
+ const [action = "list", id] = args;
2048
+ if (action === "create") {
2049
+ const result = await createSnapshot();
2050
+ printKeyValue(result);
2051
+ return;
2052
+ }
2053
+ if (action === "list" || action === "ls") {
2054
+ printTable(listSnapshots(), [["id", "ID"], ["workspace", "Workspace"], ["path", "Папка"], ["created_at", "Дата"]]);
2055
+ return;
2056
+ }
2057
+ if (action === "restore") {
2058
+ await restoreSnapshot(Number(id));
2059
+ console.log(`Snapshot восстановлен: ${id}`);
2060
+ return;
2061
+ }
2062
+ throw new Error("Команды snapshot: create, list, restore ID.");
2063
+ }
2064
+
2065
+ async function handleTrace(args) {
2066
+ const [action = "last", id] = args;
2067
+ if (action === "last") {
2068
+ printTable(listTrace(Number(id || 20)), [["id", "ID"], ["run_id", "Run"], ["tool", "Tool"], ["status", "Статус"], ["summary", "Сводка"]]);
2069
+ return;
2070
+ }
2071
+ if (action === "show") {
2072
+ printJson(getTraceRun(id));
2073
+ return;
2074
+ }
2075
+ throw new Error("Команды trace: last [LIMIT], show RUN_ID.");
2076
+ }
2077
+
2078
+ async function handlePolicy(args) {
2079
+ const [action = "list", name] = args;
2080
+ const policies = {
2081
+ safe: { fileMode: "read-only", approvals: "always", toolProfile: "safe" },
2082
+ analyst: { fileMode: "read-only", approvals: "on-danger", toolsets: ["data-read", "reports", "sync", "ai", "local-files-read"] },
2083
+ developer: { fileMode: "workspace-write", approvals: "on-write", toolsets: ["data-read", "reports", "sync", "ai", "local-files-read", "local-files-write"] },
2084
+ full: { fileMode: "full-access", approvals: "on-danger", toolProfile: "full" },
2085
+ };
2086
+ if (action === "list") {
2087
+ printTable(Object.entries(policies).map(([policy, value]) => ({ policy, ...value, toolsets: value.toolsets?.join(", ") || value.toolProfile })), [["policy", "Policy"], ["fileMode", "Files"], ["approvals", "Approvals"], ["toolsets", "Toolsets"]]);
2088
+ return;
2089
+ }
2090
+ if (action === "use") {
2091
+ const policy = policies[name];
2092
+ if (!policy) throw new Error(`Policy неизвестна: ${Object.keys(policies).join(", ")}`);
2093
+ const config = await loadConfig();
2094
+ if (policy.toolProfile) {
2095
+ await handleTools(["profile", policy.toolProfile]);
2096
+ } else {
2097
+ await saveConfig({ toolsets: { ...(config.toolsets || {}), enabled: policy.toolsets } });
2098
+ }
2099
+ const next = await loadConfig();
2100
+ await saveConfig({ files: { ...(next.files || {}), mode: policy.fileMode, approvals: policy.approvals } });
2101
+ await setFilesMode(policy.fileMode, await loadConfig());
2102
+ console.log(`Policy применена: ${name}`);
2103
+ return;
2104
+ }
2105
+ throw new Error("Команды policy: list, use NAME.");
2106
+ }
2107
+
2108
+ async function handleExport(args) {
2109
+ const [name] = args;
2110
+ const options = parseOptions(args.slice(1));
2111
+ const format = options.format || "xlsx";
2112
+ const output = options.output || `${name || "iola-export"}.${format}`;
2113
+ await ensureLocalData();
2114
+ const rows = buildReportRows(name || "education-contacts");
2115
+ if (format === "xlsx") {
2116
+ await writeFile(output, toSpreadsheetXml(rows), "utf8");
2117
+ } else if (format === "docx" || format === "doc") {
2118
+ await writeFile(output, toWordHtml(name || "Отчет", rows), "utf8");
2119
+ } else {
2120
+ await outputData(rows, { output }, format);
2121
+ }
2122
+ saveArtifact("export", name || "export", output, { format, rows: rows.length });
2123
+ console.log(`Экспорт создан: ${output}`);
2124
+ }
2125
+
1736
2126
  async function handleCron(args) {
1737
2127
  const [action = "list", ...rest] = args;
1738
2128
  const options = parseOptions(rest);
@@ -2094,7 +2484,13 @@ async function handleMcp(args) {
2094
2484
  return;
2095
2485
  }
2096
2486
 
2097
- throw new Error("Команды mcp: status, list, install codex, remove codex.");
2487
+ if (action === "serve") {
2488
+ const config = await loadConfig();
2489
+ await startMcpServer(config.daemon?.host || "127.0.0.1", Number(config.daemon?.port || DAEMON_PORT) + 1);
2490
+ return;
2491
+ }
2492
+
2493
+ throw new Error("Команды mcp: status, list, install codex, remove codex, serve.");
2098
2494
  }
2099
2495
 
2100
2496
  async function handleCache(args) {
@@ -2209,6 +2605,11 @@ async function handleView(args) {
2209
2605
  async function handleReport(args) {
2210
2606
  const [name] = args;
2211
2607
  await ensureLocalData();
2608
+ const options = parseOptions(args.slice(1));
2609
+ if (options.format === "docx" || options.format === "xlsx") {
2610
+ await handleExport([name || "education-contacts", "--format", options.format, "--output", options.output || `${name || "report"}.${options.format}`]);
2611
+ return;
2612
+ }
2212
2613
  if (name === "schools-summary") {
2213
2614
  printTable(getLocalSummaryRows("schools"), [["metric", "Показатель"], ["value", "Значение"]]);
2214
2615
  return;
@@ -2228,6 +2629,43 @@ async function handleReport(args) {
2228
2629
  throw new Error("Отчеты: schools-summary, education-contacts, missing-phones, licenses.");
2229
2630
  }
2230
2631
 
2632
+ function buildReportRows(name) {
2633
+ const reportName = name || "education-contacts";
2634
+ if (reportName === "schools-summary") return getLocalSummaryRows("schools");
2635
+ if (reportName === "missing-phones") return searchLocalRecords("", { dataset: "all", limit: 500 }).filter((item) => !item.phone || item.phone === "-");
2636
+ if (reportName === "licenses") return searchLocalRecords("", { dataset: "all", limit: 500 }).map((item) => ({ name: item.name, license_number: item.license_number, license_status: item.license_status }));
2637
+ return searchLocalRecords("", { dataset: "all", limit: 500 });
2638
+ }
2639
+
2640
+ function toSpreadsheetXml(rows) {
2641
+ const columns = Object.keys(rows[0] || { empty: "" });
2642
+ const cell = (value) => `<Cell><Data ss:Type="String">${escapeXml(value ?? "")}</Data></Cell>`;
2643
+ return `<?xml version="1.0"?>
2644
+ <Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
2645
+ <Worksheet ss:Name="IOLA"><Table>
2646
+ <Row>${columns.map(cell).join("")}</Row>
2647
+ ${rows.map((row) => `<Row>${columns.map((column) => cell(row[column])).join("")}</Row>`).join("\n")}
2648
+ </Table></Worksheet></Workbook>`;
2649
+ }
2650
+
2651
+ function toWordHtml(title, rows) {
2652
+ const columns = Object.keys(rows[0] || { empty: "" });
2653
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${escapeHtml(title)}</title></head><body>
2654
+ <h1>${escapeHtml(title)}</h1>
2655
+ <table border="1" cellspacing="0" cellpadding="4">
2656
+ <thead><tr>${columns.map((column) => `<th>${escapeHtml(column)}</th>`).join("")}</tr></thead>
2657
+ <tbody>${rows.map((row) => `<tr>${columns.map((column) => `<td>${escapeHtml(row[column] ?? "")}</td>`).join("")}</tr>`).join("\n")}</tbody>
2658
+ </table></body></html>`;
2659
+ }
2660
+
2661
+ function escapeXml(value) {
2662
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2663
+ }
2664
+
2665
+ function escapeHtml(value) {
2666
+ return escapeXml(value);
2667
+ }
2668
+
2231
2669
  async function handlePrivacy() {
2232
2670
  printKeyValue({
2233
2671
  config: CONFIG_FILE,
@@ -2906,6 +3344,71 @@ function initDatabase() {
2906
3344
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
2907
3345
  );
2908
3346
  CREATE INDEX IF NOT EXISTS idx_cron_jobs_enabled ON cron_jobs(enabled, last_run_at);
3347
+ CREATE TABLE IF NOT EXISTS tasks (
3348
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3349
+ title TEXT NOT NULL,
3350
+ command TEXT,
3351
+ status TEXT NOT NULL DEFAULT 'open',
3352
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
3353
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
3354
+ );
3355
+ CREATE TABLE IF NOT EXISTS artifacts (
3356
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3357
+ kind TEXT NOT NULL,
3358
+ title TEXT NOT NULL,
3359
+ file TEXT,
3360
+ meta_json TEXT NOT NULL DEFAULT '{}',
3361
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
3362
+ );
3363
+ CREATE TABLE IF NOT EXISTS tool_traces (
3364
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3365
+ run_id TEXT NOT NULL,
3366
+ tool TEXT NOT NULL,
3367
+ args_json TEXT NOT NULL,
3368
+ status TEXT NOT NULL,
3369
+ summary TEXT,
3370
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
3371
+ );
3372
+ CREATE INDEX IF NOT EXISTS idx_tool_traces_run_id ON tool_traces(run_id, id);
3373
+ CREATE TABLE IF NOT EXISTS snapshots (
3374
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3375
+ workspace TEXT NOT NULL,
3376
+ path TEXT NOT NULL,
3377
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
3378
+ );
3379
+ CREATE TABLE IF NOT EXISTS pending_changes (
3380
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3381
+ kind TEXT NOT NULL,
3382
+ target TEXT NOT NULL,
3383
+ before_text TEXT,
3384
+ after_text TEXT NOT NULL,
3385
+ status TEXT NOT NULL DEFAULT 'pending',
3386
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
3387
+ applied_at TEXT
3388
+ );
3389
+ CREATE TABLE IF NOT EXISTS custom_records (
3390
+ dataset TEXT NOT NULL,
3391
+ record_key TEXT NOT NULL,
3392
+ record_json TEXT NOT NULL,
3393
+ searchable_text TEXT NOT NULL,
3394
+ imported_at TEXT NOT NULL DEFAULT (datetime('now')),
3395
+ PRIMARY KEY(dataset, record_key)
3396
+ );
3397
+ CREATE VIRTUAL TABLE IF NOT EXISTS custom_records_fts USING fts5(dataset, record_key, searchable_text);
3398
+ CREATE TABLE IF NOT EXISTS doc_index (
3399
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3400
+ file TEXT NOT NULL,
3401
+ title TEXT,
3402
+ content TEXT NOT NULL,
3403
+ indexed_at TEXT NOT NULL DEFAULT (datetime('now'))
3404
+ );
3405
+ CREATE VIRTUAL TABLE IF NOT EXISTS doc_index_fts USING fts5(file, title, content);
3406
+ CREATE TABLE IF NOT EXISTS plugins (
3407
+ name TEXT PRIMARY KEY,
3408
+ source TEXT NOT NULL,
3409
+ command TEXT,
3410
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
3411
+ );
2909
3412
  `);
2910
3413
  rebuildFtsIfEmpty(db);
2911
3414
  db.prepare(`
@@ -2958,6 +3461,10 @@ function getDbStatus() {
2958
3461
  const memory = db.prepare("SELECT COUNT(*) AS count FROM memory").get();
2959
3462
  const memorySuggestions = db.prepare("SELECT COUNT(*) AS count FROM memory_suggestions WHERE status = 'pending'").get();
2960
3463
  const cron = db.prepare("SELECT COUNT(*) AS count FROM cron_jobs").get();
3464
+ const tasks = db.prepare("SELECT COUNT(*) AS count FROM tasks WHERE status != 'done'").get();
3465
+ const artifacts = db.prepare("SELECT COUNT(*) AS count FROM artifacts").get();
3466
+ const docs = db.prepare("SELECT COUNT(*) AS count FROM doc_index").get();
3467
+ const custom = db.prepare("SELECT COUNT(*) AS count FROM custom_records").get();
2961
3468
  return {
2962
3469
  status: "ok",
2963
3470
  file: DB_FILE,
@@ -2969,6 +3476,10 @@ function getDbStatus() {
2969
3476
  memory: memory?.count ?? 0,
2970
3477
  memory_suggestions: memorySuggestions?.count ?? 0,
2971
3478
  cron_jobs: cron?.count ?? 0,
3479
+ open_tasks: tasks?.count ?? 0,
3480
+ artifacts: artifacts?.count ?? 0,
3481
+ indexed_docs: docs?.count ?? 0,
3482
+ custom_records: custom?.count ?? 0,
2972
3483
  };
2973
3484
  } finally {
2974
3485
  db.close();
@@ -4035,7 +4546,16 @@ async function localToolAsk(question, providerConfig, options) {
4035
4546
  await ensureLocalData();
4036
4547
  const plan = await buildLocalToolPlan(question, providerConfig, options);
4037
4548
  const validated = validateToolPlan(plan, options);
4038
- const result = await executeToolPlan(validated);
4549
+ if (options.plan) {
4550
+ printToolPlan(validated);
4551
+ const shouldRun = await confirm("Выполнить план? [y/N] ");
4552
+ if (!shouldRun) {
4553
+ saveArtifact("plan", question.slice(0, 80), "", { plan: validated });
4554
+ return "План построен, выполнение отменено.";
4555
+ }
4556
+ }
4557
+ const runId = `run-${Date.now()}-${Math.random().toString(16).slice(2)}`;
4558
+ const result = await executeToolPlan(validated, { ...options, runId });
4039
4559
  const answer = formatToolResult(result, options);
4040
4560
 
4041
4561
  if (!options["no-history"] && isFeatureEnabled("sqlite-history")) {
@@ -4049,7 +4569,8 @@ async function localToolAsk(question, providerConfig, options) {
4049
4569
  });
4050
4570
  }
4051
4571
 
4052
- emitEvent(options, "tool_plan", { plan: validated });
4572
+ emitEvent(options, "tool_plan", { plan: validated, runId });
4573
+ saveArtifact("tool-result", question.slice(0, 80), "", { runId, plan: validated, outputs: result.outputs });
4053
4574
  if (options.output) {
4054
4575
  await assertPermission("writeFiles");
4055
4576
  await writeFile(options.output, answer, "utf8");
@@ -4062,6 +4583,13 @@ async function localToolAsk(question, providerConfig, options) {
4062
4583
  return answer;
4063
4584
  }
4064
4585
 
4586
+ function printToolPlan(plan) {
4587
+ console.log("План выполнения:");
4588
+ plan.steps.forEach((step, index) => {
4589
+ console.log(`${index + 1}. ${step.tool} ${JSON.stringify(step.args || {})}`);
4590
+ });
4591
+ }
4592
+
4065
4593
  async function buildLocalToolPlan(question, providerConfig, options) {
4066
4594
  const mode = options.reasoning || "verify";
4067
4595
  const prompt = [
@@ -4138,50 +4666,62 @@ function availableToolNames(options = {}) {
4138
4666
  return options.files ? ALL_LOCAL_TOOLS : LOCAL_TOOLS;
4139
4667
  }
4140
4668
 
4141
- async function executeToolPlan(plan) {
4669
+ async function executeToolPlan(plan, options = {}) {
4142
4670
  let current = [];
4143
4671
  const outputs = [];
4144
4672
  for (const step of plan.steps) {
4673
+ let status = "ok";
4674
+ let summary = "";
4145
4675
  await assertPermission(step.tool);
4146
4676
  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 });
4677
+ try {
4678
+ if (step.tool === "search_local") {
4679
+ current = searchLocalRecords(step.args?.query || "", { dataset: step.args?.dataset || "all", limit: step.args?.limit || 20, fts: true });
4680
+ outputs.push({ tool: step.tool, rows: current.length });
4681
+ } else if (step.tool === "get_card") {
4682
+ const card = findCard(step.args?.query || "");
4683
+ current = card ? [card] : [];
4684
+ outputs.push({ tool: step.tool, rows: current.length });
4685
+ } else if (step.tool === "run_report") {
4686
+ current = runQuality(step.args?.name || "all");
4687
+ outputs.push({ tool: step.tool, rows: current.length });
4688
+ } else if (step.tool === "save_view") {
4689
+ saveView(step.args?.name, step.args?.dataset || "all", step.args?.args || []);
4690
+ outputs.push({ tool: step.tool, saved: step.args?.name });
4691
+ } else if (step.tool === "export_data") {
4692
+ await assertPermission("writeFiles");
4693
+ await runHooks("BeforeExport", { output: step.args?.output || "iola-export.csv", format: step.args?.format || "csv", rows: current.length });
4694
+ const text = step.args?.format === "json" ? JSON.stringify(current, null, 2) : toCsv(current);
4695
+ await writeFile(step.args?.output || "iola-export.csv", text, "utf8");
4696
+ saveArtifact("export", step.args?.output || "iola-export.csv", step.args?.output || "iola-export.csv", { rows: current.length });
4697
+ outputs.push({ tool: step.tool, output: step.args?.output || "iola-export.csv", rows: current.length });
4698
+ } else if (step.tool === "files_tree") {
4699
+ current = await filesTree(step.args?.path || ".", step.args || {});
4700
+ outputs.push({ tool: step.tool, rows: current.length });
4701
+ } else if (step.tool === "files_read") {
4702
+ const text = await filesRead(step.args?.path || step.args?.file || ".", step.args || {});
4703
+ current = [{ path: step.args?.path || step.args?.file || ".", text }];
4704
+ outputs.push({ tool: step.tool, bytes: text.length });
4705
+ } else if (step.tool === "files_search") {
4706
+ current = await filesSearch(step.args?.query || "", { path: step.args?.path || ".", limit: step.args?.limit || 50 });
4707
+ outputs.push({ tool: step.tool, rows: current.length });
4708
+ } else if (step.tool === "files_write") {
4709
+ await filesWrite(step.args?.path || step.args?.file, step.args?.text || "", { append: Boolean(step.args?.append) });
4710
+ current = [{ path: step.args?.path || step.args?.file, status: "written" }];
4711
+ outputs.push({ tool: step.tool, output: step.args?.path || step.args?.file, rows: 1 });
4712
+ } else if (step.tool === "files_patch") {
4713
+ const result = await filesPatch(step.args?.path || step.args?.file, step.args?.search || "", step.args?.replace || "");
4714
+ current = [result];
4715
+ outputs.push({ tool: step.tool, output: result.path, replacements: result.replacements });
4716
+ }
4717
+ summary = `rows=${current.length}`;
4718
+ } catch (error) {
4719
+ status = "error";
4720
+ summary = error instanceof Error ? error.message : String(error);
4721
+ recordToolTrace(options.runId || "manual", step.tool, step.args || {}, status, summary);
4722
+ throw error;
4184
4723
  }
4724
+ recordToolTrace(options.runId || "manual", step.tool, step.args || {}, status, summary);
4185
4725
  await runHooks("AfterTool", { tool: step.tool, rows: current.length });
4186
4726
  }
4187
4727
  return { rows: current, outputs };
@@ -4733,17 +5273,29 @@ async function setupClient(args) {
4733
5273
  console.log("Codex MCP и skill установлены.");
4734
5274
  }
4735
5275
 
5276
+ async function onboard(args = []) {
5277
+ const options = parseOptions(args);
5278
+ showBanner();
5279
+ initDatabase();
5280
+ await handleConfig(["validate"]);
5281
+ await doctor(["--summary"]);
5282
+ if (options.yes || await confirm("Инициализировать workspace? [Y/n] ")) await handleWorkspace(["init"]);
5283
+ if (options.yes || await confirm("Применить policy analyst? [Y/n] ")) await handlePolicy(["use", "analyst"]);
5284
+ if (options.yes || await confirm("Настроить AI сейчас? [y/N] ")) await aiSetup([]);
5285
+ console.log("Onboard завершен.");
5286
+ }
5287
+
4736
5288
  function parseOptions(args) {
4737
5289
  const result = { _: [] };
4738
5290
 
4739
5291
  for (let index = 0; index < args.length; index += 1) {
4740
5292
  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") {
5293
+ 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 === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--append") {
4742
5294
  result[arg.slice(2)] = true;
4743
5295
  } else if (arg === "--check" || arg === "--upgrade-node") {
4744
5296
  result.check = true;
4745
5297
  result[arg.slice(2)] = true;
4746
- } 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 === "--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") {
5298
+ } 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 === "--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") {
4747
5299
  result[arg.slice(2)] = args[index + 1];
4748
5300
  index += 1;
4749
5301
  } else {
@@ -5110,6 +5662,34 @@ async function startDaemon(host, port) {
5110
5662
  await new Promise(() => {});
5111
5663
  }
5112
5664
 
5665
+ async function startMcpServer(host, port) {
5666
+ const server = createServer(async (req, res) => {
5667
+ try {
5668
+ res.setHeader("content-type", "application/json; charset=utf-8");
5669
+ if (req.method !== "POST") {
5670
+ res.end(JSON.stringify({ name: "iola-local-mcp", tools: ["status", "search", "card", "quality", "files.search", "index.search"] }));
5671
+ return;
5672
+ }
5673
+ const payload = JSON.parse(await readRequestBody(req) || "{}");
5674
+ const method = payload.method === "tools/call" ? payload.params?.name : payload.method;
5675
+ const args = payload.params?.arguments || payload.params || {};
5676
+ let result;
5677
+ if (method === "index.search") result = searchDocs(args.query || "", Number(args.limit || 20));
5678
+ else result = await executeRpc(method, { ...args, _: [] });
5679
+ res.end(JSON.stringify({ jsonrpc: "2.0", id: payload.id || null, result }));
5680
+ } catch (error) {
5681
+ res.statusCode = 500;
5682
+ res.end(JSON.stringify({ jsonrpc: "2.0", id: null, error: { message: error instanceof Error ? error.message : String(error) } }));
5683
+ }
5684
+ });
5685
+ await new Promise((resolve, reject) => {
5686
+ server.once("error", reject);
5687
+ server.listen(port, host, resolve);
5688
+ });
5689
+ console.log(`iola local MCP запущен: http://${host}:${port}`);
5690
+ await new Promise(() => {});
5691
+ }
5692
+
5113
5693
  function readRequestBody(req) {
5114
5694
  return new Promise((resolve, reject) => {
5115
5695
  let body = "";
@@ -5313,6 +5893,357 @@ async function maybeConfirmFileOperation(operation, target, preview) {
5313
5893
  if (!ok) throw new Error("Файловая операция отменена.");
5314
5894
  }
5315
5895
 
5896
+ function listTasks() {
5897
+ initDatabase();
5898
+ const db = openDatabase();
5899
+ try {
5900
+ return db.prepare("SELECT id, title, COALESCE(command, '-') AS command, status FROM tasks ORDER BY status, id DESC LIMIT 100").all();
5901
+ } finally {
5902
+ db.close();
5903
+ }
5904
+ }
5905
+
5906
+ function addTask(title, command = "") {
5907
+ initDatabase();
5908
+ const db = openDatabase();
5909
+ try {
5910
+ const result = db.prepare("INSERT INTO tasks(title, command) VALUES (?, ?)").run(title, command);
5911
+ return Number(result.lastInsertRowid);
5912
+ } finally {
5913
+ db.close();
5914
+ }
5915
+ }
5916
+
5917
+ function getTask(id) {
5918
+ initDatabase();
5919
+ const db = openDatabase();
5920
+ try {
5921
+ const row = db.prepare("SELECT * FROM tasks WHERE id = ?").get(id);
5922
+ if (!row) throw new Error(`Задача не найдена: ${id}`);
5923
+ return row;
5924
+ } finally {
5925
+ db.close();
5926
+ }
5927
+ }
5928
+
5929
+ function updateTaskStatus(id, status) {
5930
+ initDatabase();
5931
+ const db = openDatabase();
5932
+ try {
5933
+ db.prepare("UPDATE tasks SET status = ?, updated_at = datetime('now') WHERE id = ?").run(status, id);
5934
+ } finally {
5935
+ db.close();
5936
+ }
5937
+ }
5938
+
5939
+ function saveArtifact(kind, title, file = "", meta = {}) {
5940
+ initDatabase();
5941
+ const db = openDatabase();
5942
+ try {
5943
+ const result = db.prepare("INSERT INTO artifacts(kind, title, file, meta_json) VALUES (?, ?, ?, ?)").run(kind, title || kind, file || "", JSON.stringify(meta));
5944
+ return Number(result.lastInsertRowid);
5945
+ } finally {
5946
+ db.close();
5947
+ }
5948
+ }
5949
+
5950
+ function listArtifacts() {
5951
+ initDatabase();
5952
+ const db = openDatabase();
5953
+ try {
5954
+ return db.prepare("SELECT id, kind, title, file, created_at FROM artifacts ORDER BY id DESC LIMIT 100").all();
5955
+ } finally {
5956
+ db.close();
5957
+ }
5958
+ }
5959
+
5960
+ function getArtifact(id) {
5961
+ initDatabase();
5962
+ const db = openDatabase();
5963
+ try {
5964
+ const row = db.prepare("SELECT * FROM artifacts WHERE id = ?").get(id);
5965
+ if (!row) throw new Error(`Artifact не найден: ${id}`);
5966
+ return row;
5967
+ } finally {
5968
+ db.close();
5969
+ }
5970
+ }
5971
+
5972
+ function recordToolTrace(runId, tool, args, status, summary) {
5973
+ initDatabase();
5974
+ const db = openDatabase();
5975
+ try {
5976
+ db.prepare("INSERT INTO tool_traces(run_id, tool, args_json, status, summary) VALUES (?, ?, ?, ?, ?)").run(runId, tool, JSON.stringify(args), status, summary || "");
5977
+ } finally {
5978
+ db.close();
5979
+ }
5980
+ }
5981
+
5982
+ function listTrace(limit = 20) {
5983
+ initDatabase();
5984
+ const db = openDatabase();
5985
+ try {
5986
+ return db.prepare("SELECT id, run_id, tool, status, summary, created_at FROM tool_traces ORDER BY id DESC LIMIT ?").all(limit);
5987
+ } finally {
5988
+ db.close();
5989
+ }
5990
+ }
5991
+
5992
+ function getTraceRun(runId) {
5993
+ initDatabase();
5994
+ const db = openDatabase();
5995
+ try {
5996
+ return db.prepare("SELECT * FROM tool_traces WHERE run_id = ? ORDER BY id ASC").all(runId);
5997
+ } finally {
5998
+ db.close();
5999
+ }
6000
+ }
6001
+
6002
+ async function createSnapshot() {
6003
+ const config = await loadConfig();
6004
+ const workspace = resolveWorkspaceRoot(config);
6005
+ const snapshotsDir = path.join(CONFIG_DIR, "snapshots");
6006
+ await mkdir(snapshotsDir, { recursive: true });
6007
+ const target = path.join(snapshotsDir, `snapshot-${Date.now()}`);
6008
+ await cp(workspace, target, {
6009
+ recursive: true,
6010
+ filter: (source) => !isBlockedPathForConfig(source, config) && !source.includes(`${path.sep}node_modules${path.sep}`),
6011
+ });
6012
+ initDatabase();
6013
+ const db = openDatabase();
6014
+ try {
6015
+ const result = db.prepare("INSERT INTO snapshots(workspace, path) VALUES (?, ?)").run(workspace, target);
6016
+ return { id: Number(result.lastInsertRowid), workspace, path: target };
6017
+ } finally {
6018
+ db.close();
6019
+ }
6020
+ }
6021
+
6022
+ function listSnapshots() {
6023
+ initDatabase();
6024
+ const db = openDatabase();
6025
+ try {
6026
+ return db.prepare("SELECT id, workspace, path, created_at FROM snapshots ORDER BY id DESC LIMIT 50").all();
6027
+ } finally {
6028
+ db.close();
6029
+ }
6030
+ }
6031
+
6032
+ async function restoreSnapshot(id) {
6033
+ initDatabase();
6034
+ const db = openDatabase();
6035
+ let row;
6036
+ try {
6037
+ row = db.prepare("SELECT * FROM snapshots WHERE id = ?").get(id);
6038
+ } finally {
6039
+ db.close();
6040
+ }
6041
+ if (!row) throw new Error(`Snapshot не найден: ${id}`);
6042
+ await cp(row.path, row.workspace, { recursive: true, force: true });
6043
+ }
6044
+
6045
+ async function stageFileChange(kind, target, afterText, beforeText = null) {
6046
+ const { resolved, relative } = await resolveFileTarget(target, kind === "patch" ? "edit" : "write");
6047
+ const before = beforeText ?? (existsSync(resolved) ? await readFile(resolved, "utf8").catch(() => "") : "");
6048
+ initDatabase();
6049
+ const db = openDatabase();
6050
+ try {
6051
+ const result = db.prepare("INSERT INTO pending_changes(kind, target, before_text, after_text) VALUES (?, ?, ?, ?)").run(kind, relative, before, afterText);
6052
+ return Number(result.lastInsertRowid);
6053
+ } finally {
6054
+ db.close();
6055
+ }
6056
+ }
6057
+
6058
+ function listChanges() {
6059
+ initDatabase();
6060
+ const db = openDatabase();
6061
+ try {
6062
+ return db.prepare("SELECT id, kind, target, status, created_at FROM pending_changes ORDER BY id DESC LIMIT 100").all();
6063
+ } finally {
6064
+ db.close();
6065
+ }
6066
+ }
6067
+
6068
+ function getChange(id) {
6069
+ initDatabase();
6070
+ const db = openDatabase();
6071
+ try {
6072
+ const row = db.prepare("SELECT * FROM pending_changes WHERE id = ?").get(id);
6073
+ if (!row) throw new Error(`Изменение не найдено: ${id}`);
6074
+ return row;
6075
+ } finally {
6076
+ db.close();
6077
+ }
6078
+ }
6079
+
6080
+ function updateChangeStatus(id, status) {
6081
+ initDatabase();
6082
+ const db = openDatabase();
6083
+ try {
6084
+ db.prepare("UPDATE pending_changes SET status = ?, applied_at = CASE WHEN ? = 'applied' THEN datetime('now') ELSE applied_at END WHERE id = ?").run(status, status, id);
6085
+ } finally {
6086
+ db.close();
6087
+ }
6088
+ }
6089
+
6090
+ async function applyChange(id) {
6091
+ const change = getChange(id);
6092
+ if (change.status !== "pending") throw new Error(`Изменение уже не pending: ${change.status}`);
6093
+ await filesWrite(change.target, change.after_text);
6094
+ updateChangeStatus(id, "applied");
6095
+ }
6096
+
6097
+ async function importDataFile(target, dataset) {
6098
+ const text = await filesRead(target, { maxBytes: 5_000_000 });
6099
+ const ext = path.extname(target).toLocaleLowerCase("ru-RU");
6100
+ let rows = [];
6101
+ if (ext === ".json") {
6102
+ const parsed = JSON.parse(text);
6103
+ rows = Array.isArray(parsed) ? parsed : normalizeItems(parsed);
6104
+ } else if (ext === ".csv") {
6105
+ rows = parseCsv(text);
6106
+ } else {
6107
+ throw new Error("Поддерживается импорт JSON и CSV.");
6108
+ }
6109
+ saveCustomRecords(dataset, rows);
6110
+ return rows.length;
6111
+ }
6112
+
6113
+ function parseCsv(text) {
6114
+ const lines = text.split(/\r?\n/).filter(Boolean);
6115
+ const headers = splitCsvLine(lines.shift() || "");
6116
+ return lines.map((line) => {
6117
+ const values = splitCsvLine(line);
6118
+ return Object.fromEntries(headers.map((header, index) => [header, values[index] || ""]));
6119
+ });
6120
+ }
6121
+
6122
+ function splitCsvLine(line) {
6123
+ const result = [];
6124
+ let current = "";
6125
+ let quote = false;
6126
+ for (let index = 0; index < line.length; index += 1) {
6127
+ const char = line[index];
6128
+ if (char === '"') quote = !quote;
6129
+ else if (char === "," && !quote) {
6130
+ result.push(current);
6131
+ current = "";
6132
+ } else current += char;
6133
+ }
6134
+ result.push(current);
6135
+ return result.map((value) => value.trim());
6136
+ }
6137
+
6138
+ function saveCustomRecords(dataset, rows) {
6139
+ initDatabase();
6140
+ const db = openDatabase();
6141
+ try {
6142
+ const insert = db.prepare("INSERT INTO custom_records(dataset, record_key, record_json, searchable_text) VALUES (?, ?, ?, ?) ON CONFLICT(dataset, record_key) DO UPDATE SET record_json = excluded.record_json, searchable_text = excluded.searchable_text, imported_at = datetime('now')");
6143
+ const insertFts = db.prepare("INSERT INTO custom_records_fts(dataset, record_key, searchable_text) VALUES (?, ?, ?)");
6144
+ db.prepare("DELETE FROM custom_records_fts WHERE dataset = ?").run(dataset);
6145
+ rows.forEach((row, index) => {
6146
+ const key = String(row.id || row.inn || index + 1);
6147
+ const json = JSON.stringify(row);
6148
+ const text = json.toLocaleLowerCase("ru-RU");
6149
+ insert.run(dataset, key, json, text);
6150
+ insertFts.run(dataset, key, text);
6151
+ });
6152
+ } finally {
6153
+ db.close();
6154
+ }
6155
+ }
6156
+
6157
+ async function indexFolder(target, options = {}) {
6158
+ const rows = await filesTree(target, { depth: Number(options.depth || 5), limit: Number(options.limit || 1000) });
6159
+ let count = 0;
6160
+ for (const row of rows.filter((item) => item.type === "file" && /\.(md|txt|csv|json|html)$/i.test(item.path))) {
6161
+ try {
6162
+ const text = await filesRead(row.path, { maxBytes: 1_000_000 });
6163
+ saveIndexedDoc(row.path, path.basename(row.path), text);
6164
+ count += 1;
6165
+ } catch {
6166
+ // Skip unreadable files.
6167
+ }
6168
+ }
6169
+ return count;
6170
+ }
6171
+
6172
+ function saveIndexedDoc(file, title, content) {
6173
+ initDatabase();
6174
+ const db = openDatabase();
6175
+ try {
6176
+ const result = db.prepare("INSERT INTO doc_index(file, title, content) VALUES (?, ?, ?)").run(file, title, content);
6177
+ db.prepare("INSERT INTO doc_index_fts(rowid, file, title, content) VALUES (?, ?, ?, ?)").run(Number(result.lastInsertRowid), file, title, content);
6178
+ } finally {
6179
+ db.close();
6180
+ }
6181
+ }
6182
+
6183
+ function getIndexStatus() {
6184
+ initDatabase();
6185
+ const db = openDatabase();
6186
+ try {
6187
+ const docs = db.prepare("SELECT COUNT(*) AS count FROM doc_index").get();
6188
+ return { docs: docs?.count || 0 };
6189
+ } finally {
6190
+ db.close();
6191
+ }
6192
+ }
6193
+
6194
+ function searchDocs(query, limit = 20) {
6195
+ initDatabase();
6196
+ const db = openDatabase();
6197
+ try {
6198
+ const rows = db.prepare("SELECT file, title, snippet(doc_index_fts, 2, '[', ']', '...', 16) AS snippet FROM doc_index_fts WHERE doc_index_fts MATCH ? LIMIT ?").all(toFtsQuery(query), limit);
6199
+ return rows;
6200
+ } finally {
6201
+ db.close();
6202
+ }
6203
+ }
6204
+
6205
+ function listPlugins() {
6206
+ initDatabase();
6207
+ const db = openDatabase();
6208
+ try {
6209
+ return db.prepare("SELECT name, source, COALESCE(command, '-') AS command FROM plugins ORDER BY name").all();
6210
+ } finally {
6211
+ db.close();
6212
+ }
6213
+ }
6214
+
6215
+ function savePlugin(name, source, command) {
6216
+ initDatabase();
6217
+ const db = openDatabase();
6218
+ try {
6219
+ db.prepare("INSERT INTO plugins(name, source, command) VALUES (?, ?, ?) ON CONFLICT(name) DO UPDATE SET source = excluded.source, command = excluded.command").run(name, source, command);
6220
+ } finally {
6221
+ db.close();
6222
+ }
6223
+ }
6224
+
6225
+ function getPlugin(name) {
6226
+ initDatabase();
6227
+ const db = openDatabase();
6228
+ try {
6229
+ const row = db.prepare("SELECT * FROM plugins WHERE name = ?").get(name);
6230
+ if (!row) throw new Error(`Plugin не найден: ${name}`);
6231
+ return row;
6232
+ } finally {
6233
+ db.close();
6234
+ }
6235
+ }
6236
+
6237
+ function deletePlugin(name) {
6238
+ initDatabase();
6239
+ const db = openDatabase();
6240
+ try {
6241
+ db.prepare("DELETE FROM plugins WHERE name = ?").run(name);
6242
+ } finally {
6243
+ db.close();
6244
+ }
6245
+ }
6246
+
5316
6247
  function unifiedPreview(before, after) {
5317
6248
  const beforeLines = before.split(/\r?\n/);
5318
6249
  const afterLines = after.split(/\r?\n/);
@@ -5360,6 +6291,9 @@ async function executeRpc(method, options = {}) {
5360
6291
  if (method === "files.search") {
5361
6292
  return filesSearch(options.query || options.search || "", options);
5362
6293
  }
6294
+ if (method === "index.search") {
6295
+ return searchDocs(options.query || options.search || "", Number(options.limit || 20));
6296
+ }
5363
6297
  throw new Error(`RPC method неизвестен: ${method}. Доступно: status, search, card, quality, sync.`);
5364
6298
  }
5365
6299