@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/README.md +13 -0
- package/package.json +1 -1
- package/src/cli.js +985 -51
- package/wiki/Home.md +2 -0
- package/wiki//320/232/320/276/320/274/320/260/320/275/320/264/321/213.md +12 -0
- package/wiki//320/240/320/260/320/261/320/276/321/207/320/260/321/217-/321/201/321/200/320/265/320/264/320/260-/320/260/320/263/320/265/320/275/321/202/320/260.md +67 -0
- package/wiki//320/240/320/260/321/201/321/210/320/270/321/200/320/265/320/275/320/270/321/217-/320/270-/320/273/320/276/320/272/320/260/320/273/321/214/320/275/321/213/320/265-/320/264/320/260/320/275/320/275/321/213/320/265.md +54 -0
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 =
|
|
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
|
-
|
|
1721
|
-
|
|
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
|
-
|
|
1729
|
-
|
|
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
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
-
|
|
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
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
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
|
|