@iola_adm/iola-cli 0.1.26 → 0.1.28
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 +7 -0
- package/package.json +1 -1
- package/src/cli.js +619 -10
- package/wiki/Home.md +1 -0
- package/wiki//320/232/320/276/320/274/320/260/320/275/320/264/321/213.md +6 -0
- package/wiki//320/233/320/276/320/272/320/260/320/273/321/214/320/275/321/213/320/265-/321/204/320/260/320/271/320/273/321/213.md +12 -1
- 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 +65 -0
package/README.md
CHANGED
|
@@ -58,6 +58,10 @@ iola policy use analyst
|
|
|
58
58
|
iola tasks list
|
|
59
59
|
iola artifacts list
|
|
60
60
|
iola trace last
|
|
61
|
+
iola changes list
|
|
62
|
+
iola index status
|
|
63
|
+
iola reports list
|
|
64
|
+
iola plugins list
|
|
61
65
|
iola context init
|
|
62
66
|
iola cron list
|
|
63
67
|
iola daemon status
|
|
@@ -87,6 +91,7 @@ iola version --check
|
|
|
87
91
|
- [Skills и toolsets](https://github.com/adm-iola/iola-cli/wiki/Skills-и-toolsets)
|
|
88
92
|
- [Локальные файлы](https://github.com/adm-iola/iola-cli/wiki/Локальные-файлы)
|
|
89
93
|
- [Рабочая среда агента](https://github.com/adm-iola/iola-cli/wiki/Рабочая-среда-агента)
|
|
94
|
+
- [Расширения и локальные данные](https://github.com/adm-iola/iola-cli/wiki/Расширения-и-локальные-данные)
|
|
90
95
|
- [Daemon, RPC и cron](https://github.com/adm-iola/iola-cli/wiki/Daemon-RPC-и-cron)
|
|
91
96
|
- [Контекст и память](https://github.com/adm-iola/iola-cli/wiki/Контекст-и-память)
|
|
92
97
|
- [Команды](https://github.com/adm-iola/iola-cli/wiki/Команды)
|
|
@@ -102,6 +107,8 @@ iola version --check
|
|
|
102
107
|
- управляемые локальные файловые операции с режимами `locked`, `read-only`, `workspace-write`, `full-access`;
|
|
103
108
|
- планы выполнения, traces, tasks, artifacts, snapshots и policy-профили;
|
|
104
109
|
- экспорт отчетов в Excel/Word-совместимые файлы;
|
|
110
|
+
- staged changes, импорт локальных CSV/JSON, индекс локальных документов, report packs, plugins и локальный MCP endpoint;
|
|
111
|
+
- чтение и индексирование `.docx`, `.xlsx`, `.pptx`, `.pdf`, `.md`, `.txt`, `.csv`, `.json`, `.html`;
|
|
105
112
|
- cron-задачи, локальный daemon и RPC для автоматизаций;
|
|
106
113
|
- контекстные файлы `IOLA.md` и `.iola/context.md`;
|
|
107
114
|
- интеграция с публичным MCP-сервером Йошкар-Олы.
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import readline from "node:readline/promises";
|
|
|
8
8
|
import { stdin as input, stdout as output } from "node:process";
|
|
9
9
|
import { DatabaseSync } from "node:sqlite";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { inflateRawSync, inflateSync } from "node:zlib";
|
|
11
12
|
|
|
12
13
|
const API_BASE_URL = process.env.IOLA_API_BASE_URL || "https://apiiola.yasg.ru/api/v1";
|
|
13
14
|
const MCP_BASE_URL = process.env.IOLA_MCP_BASE_URL || "https://apiiola.yasg.ru";
|
|
@@ -17,7 +18,8 @@ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
|
17
18
|
const LAST_GOOD_CONFIG_FILE = path.join(CONFIG_DIR, "config.last-good.json");
|
|
18
19
|
const SECRETS_FILE = path.join(CONFIG_DIR, "secrets.json");
|
|
19
20
|
const DB_FILE = path.join(CONFIG_DIR, "iola.db");
|
|
20
|
-
const DB_SCHEMA_VERSION =
|
|
21
|
+
const DB_SCHEMA_VERSION = 7;
|
|
22
|
+
const INDEXABLE_EXTENSIONS = /\.(md|txt|csv|json|html|docx|xlsx|pptx|pdf)$/i;
|
|
21
23
|
const LOCAL_TOOLS = ["search_local", "get_card", "export_data", "run_report", "save_view"];
|
|
22
24
|
const FILE_TOOLS = ["files_tree", "files_read", "files_search", "files_write", "files_patch"];
|
|
23
25
|
const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS];
|
|
@@ -242,6 +244,11 @@ const COMMANDS = new Map([
|
|
|
242
244
|
["skills", handleSkills],
|
|
243
245
|
["tools", handleTools],
|
|
244
246
|
["files", handleFiles],
|
|
247
|
+
["changes", handleChanges],
|
|
248
|
+
["import", handleImport],
|
|
249
|
+
["index", handleIndex],
|
|
250
|
+
["reports", handleReports],
|
|
251
|
+
["plugins", handlePlugins],
|
|
245
252
|
["workspace", handleWorkspace],
|
|
246
253
|
["tasks", handleTasks],
|
|
247
254
|
["artifacts", handleArtifacts],
|
|
@@ -284,6 +291,7 @@ const COMMANDS = new Map([
|
|
|
284
291
|
["search", searchAll],
|
|
285
292
|
["mcp-info", showMcpInfo],
|
|
286
293
|
["setup", setupClient],
|
|
294
|
+
["onboard", onboard],
|
|
287
295
|
]);
|
|
288
296
|
|
|
289
297
|
export async function main(argv) {
|
|
@@ -352,6 +360,11 @@ Usage:
|
|
|
352
360
|
iola skills list|show|paths|enable|disable
|
|
353
361
|
iola tools list|toolsets|enable|disable|profile
|
|
354
362
|
iola files status|mode|approvals|tree|read|search|write|patch
|
|
363
|
+
iola changes list|show|apply|discard
|
|
364
|
+
iola import file|folder
|
|
365
|
+
iola index folder|status|search
|
|
366
|
+
iola reports list|run
|
|
367
|
+
iola plugins list|install|run|remove
|
|
355
368
|
iola workspace init|status|use|list
|
|
356
369
|
iola tasks list|add|done|run
|
|
357
370
|
iola artifacts list|show|open
|
|
@@ -366,7 +379,7 @@ Usage:
|
|
|
366
379
|
iola memory show|add|set|clear|export
|
|
367
380
|
iola hooks list|add|delete|run
|
|
368
381
|
iola agents list|run
|
|
369
|
-
iola mcp list|status|install|remove
|
|
382
|
+
iola mcp list|status|install|remove|serve
|
|
370
383
|
iola cache status|warm|clear
|
|
371
384
|
iola sync [--dataset schools|kindergartens]
|
|
372
385
|
iola sync status
|
|
@@ -413,6 +426,7 @@ Usage:
|
|
|
413
426
|
iola search TEXT [--limit 5] [--format table|json|csv]
|
|
414
427
|
iola mcp-info [--json]
|
|
415
428
|
iola setup codex
|
|
429
|
+
iola onboard
|
|
416
430
|
iola version
|
|
417
431
|
|
|
418
432
|
Environment:
|
|
@@ -604,6 +618,26 @@ async function handleAgentLine(line, state) {
|
|
|
604
618
|
return false;
|
|
605
619
|
}
|
|
606
620
|
|
|
621
|
+
if (command === "changes") {
|
|
622
|
+
await handleChanges(args);
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (command === "index") {
|
|
627
|
+
await handleIndex(args);
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (command === "reports") {
|
|
632
|
+
await handleReports(args);
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (command === "plugins") {
|
|
637
|
+
await handlePlugins(args);
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
|
|
607
641
|
if (command === "workspace") {
|
|
608
642
|
await handleWorkspace(args);
|
|
609
643
|
return false;
|
|
@@ -757,6 +791,10 @@ async function handleAgentLine(line, state) {
|
|
|
757
791
|
context: ["context", args],
|
|
758
792
|
skills: ["skills", args],
|
|
759
793
|
files: ["files", args],
|
|
794
|
+
changes: ["changes", args],
|
|
795
|
+
index: ["index", args],
|
|
796
|
+
reports: ["reports", args],
|
|
797
|
+
plugins: ["plugins", args],
|
|
760
798
|
workspace: ["workspace", args],
|
|
761
799
|
tasks: ["tasks", args],
|
|
762
800
|
todos: ["tasks", args],
|
|
@@ -812,6 +850,10 @@ function printAgentHelp() {
|
|
|
812
850
|
/permissions
|
|
813
851
|
/tools
|
|
814
852
|
/files status
|
|
853
|
+
/changes list
|
|
854
|
+
/index status
|
|
855
|
+
/reports list
|
|
856
|
+
/plugins list
|
|
815
857
|
/workspace status
|
|
816
858
|
/tasks list
|
|
817
859
|
/artifacts list
|
|
@@ -1548,6 +1590,7 @@ async function handleWiki(args) {
|
|
|
1548
1590
|
["Skills и toolsets", `${base}/Skills-и-toolsets`],
|
|
1549
1591
|
["Локальные файлы", `${base}/Локальные-файлы`],
|
|
1550
1592
|
["Рабочая среда агента", `${base}/Рабочая-среда-агента`],
|
|
1593
|
+
["Расширения и локальные данные", `${base}/Расширения-и-локальные-данные`],
|
|
1551
1594
|
["Daemon, RPC и cron", `${base}/Daemon-RPC-и-cron`],
|
|
1552
1595
|
["Контекст и память", `${base}/Контекст-и-память`],
|
|
1553
1596
|
["Команды", `${base}/Команды`],
|
|
@@ -1774,22 +1817,156 @@ async function handleFiles(args) {
|
|
|
1774
1817
|
if (!target) throw new Error('Пример: iola files write report.md --text "..."');
|
|
1775
1818
|
const text = options.text ?? rest.join(" ");
|
|
1776
1819
|
if (!text) throw new Error('Для записи нужен --text "..." или текст после пути.');
|
|
1777
|
-
|
|
1778
|
-
|
|
1820
|
+
if (options.stage) {
|
|
1821
|
+
const id = await stageFileChange("write", target, text);
|
|
1822
|
+
console.log(`Изменение подготовлено: ${id}`);
|
|
1823
|
+
} else {
|
|
1824
|
+
await filesWrite(target, text, { append: Boolean(options.append) });
|
|
1825
|
+
console.log(`Файл записан: ${target}`);
|
|
1826
|
+
}
|
|
1779
1827
|
return;
|
|
1780
1828
|
}
|
|
1781
1829
|
|
|
1782
1830
|
if (action === "patch") {
|
|
1783
1831
|
if (!target) throw new Error('Пример: iola files patch README.md --search old --replace new');
|
|
1784
1832
|
if (!options.search || options.replace === undefined) throw new Error("Для patch нужны --search и --replace.");
|
|
1785
|
-
|
|
1786
|
-
|
|
1833
|
+
if (options.stage) {
|
|
1834
|
+
const current = await filesRead(target);
|
|
1835
|
+
const next = current.split(options.search).join(options.replace);
|
|
1836
|
+
const id = await stageFileChange("patch", target, next, current);
|
|
1837
|
+
console.log(`Изменение подготовлено: ${id}`);
|
|
1838
|
+
} else {
|
|
1839
|
+
const result = await filesPatch(target, options.search, options.replace);
|
|
1840
|
+
printKeyValue(result);
|
|
1841
|
+
}
|
|
1787
1842
|
return;
|
|
1788
1843
|
}
|
|
1789
1844
|
|
|
1790
1845
|
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.");
|
|
1791
1846
|
}
|
|
1792
1847
|
|
|
1848
|
+
async function handleChanges(args) {
|
|
1849
|
+
const [action = "list", id] = args;
|
|
1850
|
+
if (action === "list" || action === "ls") {
|
|
1851
|
+
printTable(listChanges(), [["id", "ID"], ["kind", "Тип"], ["target", "Файл"], ["status", "Статус"], ["created_at", "Дата"]]);
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
if (action === "show") {
|
|
1855
|
+
const change = getChange(Number(id));
|
|
1856
|
+
console.log(unifiedPreview(change.before_text || "", change.after_text || ""));
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
if (action === "apply") {
|
|
1860
|
+
await applyChange(Number(id));
|
|
1861
|
+
console.log(`Изменение применено: ${id}`);
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
if (action === "discard") {
|
|
1865
|
+
updateChangeStatus(Number(id), "discarded");
|
|
1866
|
+
console.log(`Изменение отклонено: ${id}`);
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
throw new Error("Команды changes: list, show ID, apply ID, discard ID.");
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
async function handleImport(args) {
|
|
1873
|
+
const [action, target, ...rest] = args;
|
|
1874
|
+
const options = parseOptions(rest);
|
|
1875
|
+
if (action === "file") {
|
|
1876
|
+
if (!target) throw new Error("Пример: iola import file data.csv --dataset custom");
|
|
1877
|
+
const dataset = options.dataset || path.basename(target, path.extname(target));
|
|
1878
|
+
const count = await importDataFile(target, dataset);
|
|
1879
|
+
console.log(`Импортировано записей: ${count}, dataset=${dataset}`);
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
if (action === "folder") {
|
|
1883
|
+
if (!target) throw new Error("Пример: iola import folder ./data");
|
|
1884
|
+
const rows = await filesTree(target, { depth: 1, limit: 200 });
|
|
1885
|
+
let total = 0;
|
|
1886
|
+
for (const row of rows.filter((item) => item.type === "file" && /\.(json|csv)$/i.test(item.path))) {
|
|
1887
|
+
total += await importDataFile(row.path, options.dataset || path.basename(row.path, path.extname(row.path)));
|
|
1888
|
+
}
|
|
1889
|
+
console.log(`Импортировано записей: ${total}`);
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
throw new Error("Команды import: file PATH --dataset NAME, folder PATH.");
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
async function handleIndex(args) {
|
|
1896
|
+
const [action = "status", target, ...rest] = args;
|
|
1897
|
+
const options = parseOptions(rest);
|
|
1898
|
+
if (action === "status") {
|
|
1899
|
+
printKeyValue(getIndexStatus());
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
if (action === "folder") {
|
|
1903
|
+
if (!target) throw new Error("Пример: iola index folder ./docs");
|
|
1904
|
+
const count = await indexFolder(target, options);
|
|
1905
|
+
console.log(`Проиндексировано документов: ${count}`);
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
if (action === "search") {
|
|
1909
|
+
const query = [target, ...rest].filter(Boolean).join(" ");
|
|
1910
|
+
if (!query) throw new Error('Пример: iola index search "школа 29"');
|
|
1911
|
+
printTable(searchDocs(query, Number(options.limit || 20)), [["file", "Файл"], ["title", "Название"], ["snippet", "Фрагмент"]]);
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
throw new Error("Команды index: status, folder PATH, search TEXT.");
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
async function handleReports(args) {
|
|
1918
|
+
const [action = "list", name, ...rest] = args;
|
|
1919
|
+
const packs = {
|
|
1920
|
+
"education-passport": ["education-contacts", "licenses"],
|
|
1921
|
+
"data-quality-pack": ["schools-summary", "missing-phones"],
|
|
1922
|
+
};
|
|
1923
|
+
if (action === "list") {
|
|
1924
|
+
printTable(Object.entries(packs).map(([pack, reports]) => ({ pack, reports: reports.join(", ") })), [["pack", "Пакет"], ["reports", "Отчеты"]]);
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
if (action === "run") {
|
|
1928
|
+
if (!packs[name]) throw new Error(`Пакет неизвестен: ${Object.keys(packs).join(", ")}`);
|
|
1929
|
+
const options = parseOptions(rest);
|
|
1930
|
+
const dir = options.output || path.join(process.cwd(), `iola-report-${name}-${Date.now()}`);
|
|
1931
|
+
await mkdir(dir, { recursive: true });
|
|
1932
|
+
for (const report of packs[name]) {
|
|
1933
|
+
await handleExport([report, "--format", "xlsx", "--output", path.join(dir, `${report}.xlsx`)]);
|
|
1934
|
+
await handleExport([report, "--format", "docx", "--output", path.join(dir, `${report}.docx`)]);
|
|
1935
|
+
}
|
|
1936
|
+
saveArtifact("report-pack", name, dir, { reports: packs[name] });
|
|
1937
|
+
console.log(`Пакет отчетов создан: ${dir}`);
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
throw new Error("Команды reports: list, run NAME [--output DIR].");
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
async function handlePlugins(args) {
|
|
1944
|
+
const [action = "list", name, ...rest] = args;
|
|
1945
|
+
if (action === "list" || action === "ls") {
|
|
1946
|
+
printTable(listPlugins(), [["name", "Plugin"], ["source", "Источник"], ["command", "Команда"]]);
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
if (action === "install") {
|
|
1950
|
+
const options = parseOptions(rest);
|
|
1951
|
+
if (!name) throw new Error("Пример: iola plugins install my-plugin --command \"iola quality\"");
|
|
1952
|
+
savePlugin(name, options.source || name, options.command || "");
|
|
1953
|
+
console.log(`Plugin установлен: ${name}`);
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
if (action === "run") {
|
|
1957
|
+
const plugin = getPlugin(name);
|
|
1958
|
+
if (!plugin.command) throw new Error("У plugin нет command.");
|
|
1959
|
+
await main(splitCommandLine(plugin.command));
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
if (action === "remove" || action === "delete") {
|
|
1963
|
+
deletePlugin(name);
|
|
1964
|
+
console.log(`Plugin удален: ${name}`);
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
throw new Error("Команды plugins: list, install NAME --command CMD, run NAME, remove NAME.");
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1793
1970
|
async function handleWorkspace(args) {
|
|
1794
1971
|
const [action = "status", nameOrPath] = args;
|
|
1795
1972
|
const config = await loadConfig();
|
|
@@ -2309,7 +2486,13 @@ async function handleMcp(args) {
|
|
|
2309
2486
|
return;
|
|
2310
2487
|
}
|
|
2311
2488
|
|
|
2312
|
-
|
|
2489
|
+
if (action === "serve") {
|
|
2490
|
+
const config = await loadConfig();
|
|
2491
|
+
await startMcpServer(config.daemon?.host || "127.0.0.1", Number(config.daemon?.port || DAEMON_PORT) + 1);
|
|
2492
|
+
return;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
throw new Error("Команды mcp: status, list, install codex, remove codex, serve.");
|
|
2313
2496
|
}
|
|
2314
2497
|
|
|
2315
2498
|
async function handleCache(args) {
|
|
@@ -3195,6 +3378,39 @@ function initDatabase() {
|
|
|
3195
3378
|
path TEXT NOT NULL,
|
|
3196
3379
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
3197
3380
|
);
|
|
3381
|
+
CREATE TABLE IF NOT EXISTS pending_changes (
|
|
3382
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3383
|
+
kind TEXT NOT NULL,
|
|
3384
|
+
target TEXT NOT NULL,
|
|
3385
|
+
before_text TEXT,
|
|
3386
|
+
after_text TEXT NOT NULL,
|
|
3387
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
3388
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
3389
|
+
applied_at TEXT
|
|
3390
|
+
);
|
|
3391
|
+
CREATE TABLE IF NOT EXISTS custom_records (
|
|
3392
|
+
dataset TEXT NOT NULL,
|
|
3393
|
+
record_key TEXT NOT NULL,
|
|
3394
|
+
record_json TEXT NOT NULL,
|
|
3395
|
+
searchable_text TEXT NOT NULL,
|
|
3396
|
+
imported_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
3397
|
+
PRIMARY KEY(dataset, record_key)
|
|
3398
|
+
);
|
|
3399
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS custom_records_fts USING fts5(dataset, record_key, searchable_text);
|
|
3400
|
+
CREATE TABLE IF NOT EXISTS doc_index (
|
|
3401
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3402
|
+
file TEXT NOT NULL,
|
|
3403
|
+
title TEXT,
|
|
3404
|
+
content TEXT NOT NULL,
|
|
3405
|
+
indexed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
3406
|
+
);
|
|
3407
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS doc_index_fts USING fts5(file, title, content);
|
|
3408
|
+
CREATE TABLE IF NOT EXISTS plugins (
|
|
3409
|
+
name TEXT PRIMARY KEY,
|
|
3410
|
+
source TEXT NOT NULL,
|
|
3411
|
+
command TEXT,
|
|
3412
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
3413
|
+
);
|
|
3198
3414
|
`);
|
|
3199
3415
|
rebuildFtsIfEmpty(db);
|
|
3200
3416
|
db.prepare(`
|
|
@@ -3249,6 +3465,8 @@ function getDbStatus() {
|
|
|
3249
3465
|
const cron = db.prepare("SELECT COUNT(*) AS count FROM cron_jobs").get();
|
|
3250
3466
|
const tasks = db.prepare("SELECT COUNT(*) AS count FROM tasks WHERE status != 'done'").get();
|
|
3251
3467
|
const artifacts = db.prepare("SELECT COUNT(*) AS count FROM artifacts").get();
|
|
3468
|
+
const docs = db.prepare("SELECT COUNT(*) AS count FROM doc_index").get();
|
|
3469
|
+
const custom = db.prepare("SELECT COUNT(*) AS count FROM custom_records").get();
|
|
3252
3470
|
return {
|
|
3253
3471
|
status: "ok",
|
|
3254
3472
|
file: DB_FILE,
|
|
@@ -3262,6 +3480,8 @@ function getDbStatus() {
|
|
|
3262
3480
|
cron_jobs: cron?.count ?? 0,
|
|
3263
3481
|
open_tasks: tasks?.count ?? 0,
|
|
3264
3482
|
artifacts: artifacts?.count ?? 0,
|
|
3483
|
+
indexed_docs: docs?.count ?? 0,
|
|
3484
|
+
custom_records: custom?.count ?? 0,
|
|
3265
3485
|
};
|
|
3266
3486
|
} finally {
|
|
3267
3487
|
db.close();
|
|
@@ -5055,17 +5275,29 @@ async function setupClient(args) {
|
|
|
5055
5275
|
console.log("Codex MCP и skill установлены.");
|
|
5056
5276
|
}
|
|
5057
5277
|
|
|
5278
|
+
async function onboard(args = []) {
|
|
5279
|
+
const options = parseOptions(args);
|
|
5280
|
+
showBanner();
|
|
5281
|
+
initDatabase();
|
|
5282
|
+
await handleConfig(["validate"]);
|
|
5283
|
+
await doctor(["--summary"]);
|
|
5284
|
+
if (options.yes || await confirm("Инициализировать workspace? [Y/n] ")) await handleWorkspace(["init"]);
|
|
5285
|
+
if (options.yes || await confirm("Применить policy analyst? [Y/n] ")) await handlePolicy(["use", "analyst"]);
|
|
5286
|
+
if (options.yes || await confirm("Настроить AI сейчас? [y/N] ")) await aiSetup([]);
|
|
5287
|
+
console.log("Onboard завершен.");
|
|
5288
|
+
}
|
|
5289
|
+
|
|
5058
5290
|
function parseOptions(args) {
|
|
5059
5291
|
const result = { _: [] };
|
|
5060
5292
|
|
|
5061
5293
|
for (let index = 0; index < args.length; index += 1) {
|
|
5062
5294
|
const arg = args[index];
|
|
5063
|
-
if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--append") {
|
|
5295
|
+
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") {
|
|
5064
5296
|
result[arg.slice(2)] = true;
|
|
5065
5297
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
5066
5298
|
result.check = true;
|
|
5067
5299
|
result[arg.slice(2)] = true;
|
|
5068
|
-
} 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") {
|
|
5300
|
+
} 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") {
|
|
5069
5301
|
result[arg.slice(2)] = args[index + 1];
|
|
5070
5302
|
index += 1;
|
|
5071
5303
|
} else {
|
|
@@ -5432,6 +5664,34 @@ async function startDaemon(host, port) {
|
|
|
5432
5664
|
await new Promise(() => {});
|
|
5433
5665
|
}
|
|
5434
5666
|
|
|
5667
|
+
async function startMcpServer(host, port) {
|
|
5668
|
+
const server = createServer(async (req, res) => {
|
|
5669
|
+
try {
|
|
5670
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
5671
|
+
if (req.method !== "POST") {
|
|
5672
|
+
res.end(JSON.stringify({ name: "iola-local-mcp", tools: ["status", "search", "card", "quality", "files.search", "index.search"] }));
|
|
5673
|
+
return;
|
|
5674
|
+
}
|
|
5675
|
+
const payload = JSON.parse(await readRequestBody(req) || "{}");
|
|
5676
|
+
const method = payload.method === "tools/call" ? payload.params?.name : payload.method;
|
|
5677
|
+
const args = payload.params?.arguments || payload.params || {};
|
|
5678
|
+
let result;
|
|
5679
|
+
if (method === "index.search") result = searchDocs(args.query || "", Number(args.limit || 20));
|
|
5680
|
+
else result = await executeRpc(method, { ...args, _: [] });
|
|
5681
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", id: payload.id || null, result }));
|
|
5682
|
+
} catch (error) {
|
|
5683
|
+
res.statusCode = 500;
|
|
5684
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", id: null, error: { message: error instanceof Error ? error.message : String(error) } }));
|
|
5685
|
+
}
|
|
5686
|
+
});
|
|
5687
|
+
await new Promise((resolve, reject) => {
|
|
5688
|
+
server.once("error", reject);
|
|
5689
|
+
server.listen(port, host, resolve);
|
|
5690
|
+
});
|
|
5691
|
+
console.log(`iola local MCP запущен: http://${host}:${port}`);
|
|
5692
|
+
await new Promise(() => {});
|
|
5693
|
+
}
|
|
5694
|
+
|
|
5435
5695
|
function readRequestBody(req) {
|
|
5436
5696
|
return new Promise((resolve, reject) => {
|
|
5437
5697
|
let body = "";
|
|
@@ -5575,7 +5835,7 @@ async function filesRead(target, options = {}) {
|
|
|
5575
5835
|
if (!info.isFile()) throw new Error(`Это не файл: ${target}`);
|
|
5576
5836
|
const maxBytes = Number(options.maxBytes || config.files?.maxReadBytes || 200000);
|
|
5577
5837
|
if (info.size > maxBytes) throw new Error(`Файл слишком большой: ${info.size} байт. Лимит: ${maxBytes}`);
|
|
5578
|
-
return
|
|
5838
|
+
return extractReadableText(resolved);
|
|
5579
5839
|
}
|
|
5580
5840
|
|
|
5581
5841
|
async function filesSearch(query, options = {}) {
|
|
@@ -5624,6 +5884,150 @@ async function filesPatch(target, search, replace) {
|
|
|
5624
5884
|
return { path: relative, replacements };
|
|
5625
5885
|
}
|
|
5626
5886
|
|
|
5887
|
+
async function extractReadableText(file) {
|
|
5888
|
+
const ext = path.extname(file).toLocaleLowerCase("ru-RU");
|
|
5889
|
+
if (ext === ".docx") return extractDocxText(await readFile(file));
|
|
5890
|
+
if (ext === ".xlsx") return extractXlsxText(await readFile(file));
|
|
5891
|
+
if (ext === ".pptx") return extractPptxText(await readFile(file));
|
|
5892
|
+
if (ext === ".pdf") return extractPdfText(await readFile(file));
|
|
5893
|
+
return readFile(file, "utf8");
|
|
5894
|
+
}
|
|
5895
|
+
|
|
5896
|
+
function extractDocxText(buffer) {
|
|
5897
|
+
const entries = readZipEntries(buffer);
|
|
5898
|
+
const documentXml = entries.get("word/document.xml") || "";
|
|
5899
|
+
const footnotes = [...entries.entries()].filter(([name]) => name.startsWith("word/") && /footnotes|endnotes|comments/.test(name)).map(([, text]) => text).join("\n");
|
|
5900
|
+
return xmlToText(`${documentXml}\n${footnotes}`);
|
|
5901
|
+
}
|
|
5902
|
+
|
|
5903
|
+
function extractXlsxText(buffer) {
|
|
5904
|
+
const entries = readZipEntries(buffer);
|
|
5905
|
+
const sharedStrings = parseSharedStrings(entries.get("xl/sharedStrings.xml") || "");
|
|
5906
|
+
const chunks = [];
|
|
5907
|
+
for (const [name, xml] of entries.entries()) {
|
|
5908
|
+
if (!/^xl\/worksheets\/sheet\d+\.xml$/i.test(name)) continue;
|
|
5909
|
+
chunks.push(name);
|
|
5910
|
+
const resolved = xml.replace(/<c[^>]*t="s"[^>]*>[\s\S]*?<v>(\d+)<\/v>[\s\S]*?<\/c>/g, (_, index) => ` ${sharedStrings[Number(index)] || ""} `);
|
|
5911
|
+
chunks.push(xmlToText(resolved));
|
|
5912
|
+
}
|
|
5913
|
+
return normalizeExtractedText(chunks.join("\n"));
|
|
5914
|
+
}
|
|
5915
|
+
|
|
5916
|
+
function extractPptxText(buffer) {
|
|
5917
|
+
const entries = readZipEntries(buffer);
|
|
5918
|
+
const slides = [...entries.entries()]
|
|
5919
|
+
.filter(([name]) => /^ppt\/slides\/slide\d+\.xml$/i.test(name))
|
|
5920
|
+
.sort(([left], [right]) => left.localeCompare(right, undefined, { numeric: true }));
|
|
5921
|
+
return normalizeExtractedText(slides.map(([name, xml]) => `${name}\n${xmlToText(xml)}`).join("\n\n"));
|
|
5922
|
+
}
|
|
5923
|
+
|
|
5924
|
+
function extractPdfText(buffer) {
|
|
5925
|
+
const latin = buffer.toString("latin1");
|
|
5926
|
+
const chunks = [];
|
|
5927
|
+
const streamPattern = /<<(?:.|\r|\n)*?>>\s*stream\r?\n([\s\S]*?)\r?\nendstream/g;
|
|
5928
|
+
let match;
|
|
5929
|
+
while ((match = streamPattern.exec(latin))) {
|
|
5930
|
+
const dictionary = latin.slice(Math.max(0, match.index - 500), match.index + 500);
|
|
5931
|
+
let data = Buffer.from(match[1], "latin1");
|
|
5932
|
+
if (/FlateDecode/.test(dictionary)) {
|
|
5933
|
+
try {
|
|
5934
|
+
data = inflateSync(data);
|
|
5935
|
+
} catch {
|
|
5936
|
+
try {
|
|
5937
|
+
data = inflateRawSync(data);
|
|
5938
|
+
} catch {
|
|
5939
|
+
// Leave compressed stream unreadable.
|
|
5940
|
+
}
|
|
5941
|
+
}
|
|
5942
|
+
}
|
|
5943
|
+
chunks.push(extractPdfStrings(data.toString("latin1")));
|
|
5944
|
+
}
|
|
5945
|
+
chunks.push(extractPdfStrings(latin));
|
|
5946
|
+
return normalizeExtractedText(chunks.join("\n"));
|
|
5947
|
+
}
|
|
5948
|
+
|
|
5949
|
+
function extractPdfStrings(text) {
|
|
5950
|
+
const strings = [];
|
|
5951
|
+
for (const match of text.matchAll(/\(([^()\\]*(?:\\.[^()\\]*)*)\)\s*T[jJ]?/g)) {
|
|
5952
|
+
strings.push(unescapePdfString(match[1]));
|
|
5953
|
+
}
|
|
5954
|
+
for (const match of text.matchAll(/\[([\s\S]*?)\]\s*TJ/g)) {
|
|
5955
|
+
for (const item of match[1].matchAll(/\(([^()\\]*(?:\\.[^()\\]*)*)\)/g)) {
|
|
5956
|
+
strings.push(unescapePdfString(item[1]));
|
|
5957
|
+
}
|
|
5958
|
+
}
|
|
5959
|
+
return strings.join(" ");
|
|
5960
|
+
}
|
|
5961
|
+
|
|
5962
|
+
function unescapePdfString(value) {
|
|
5963
|
+
const unescaped = value
|
|
5964
|
+
.replace(/\\n/g, "\n")
|
|
5965
|
+
.replace(/\\r/g, "\r")
|
|
5966
|
+
.replace(/\\t/g, "\t")
|
|
5967
|
+
.replace(/\\([()\\])/g, "$1")
|
|
5968
|
+
.replace(/\\(\d{3})/g, (_, octal) => String.fromCharCode(parseInt(octal, 8)));
|
|
5969
|
+
return decodePossiblyUtf8(unescaped);
|
|
5970
|
+
}
|
|
5971
|
+
|
|
5972
|
+
function decodePossiblyUtf8(value) {
|
|
5973
|
+
const decoded = Buffer.from(value, "latin1").toString("utf8");
|
|
5974
|
+
return decoded.includes("\uFFFD") ? value : decoded;
|
|
5975
|
+
}
|
|
5976
|
+
|
|
5977
|
+
function readZipEntries(buffer) {
|
|
5978
|
+
const entries = new Map();
|
|
5979
|
+
let offset = 0;
|
|
5980
|
+
while (offset < buffer.length - 30) {
|
|
5981
|
+
const signature = buffer.readUInt32LE(offset);
|
|
5982
|
+
if (signature !== 0x04034b50) {
|
|
5983
|
+
offset += 1;
|
|
5984
|
+
continue;
|
|
5985
|
+
}
|
|
5986
|
+
const method = buffer.readUInt16LE(offset + 8);
|
|
5987
|
+
const compressedSize = buffer.readUInt32LE(offset + 18);
|
|
5988
|
+
const fileNameLength = buffer.readUInt16LE(offset + 26);
|
|
5989
|
+
const extraLength = buffer.readUInt16LE(offset + 28);
|
|
5990
|
+
const nameStart = offset + 30;
|
|
5991
|
+
const name = buffer.subarray(nameStart, nameStart + fileNameLength).toString("utf8");
|
|
5992
|
+
const dataStart = nameStart + fileNameLength + extraLength;
|
|
5993
|
+
const dataEnd = dataStart + compressedSize;
|
|
5994
|
+
const compressed = buffer.subarray(dataStart, dataEnd);
|
|
5995
|
+
try {
|
|
5996
|
+
const data = method === 8 ? inflateRawSync(compressed) : compressed;
|
|
5997
|
+
entries.set(name.replace(/\\/g, "/"), data.toString("utf8"));
|
|
5998
|
+
} catch {
|
|
5999
|
+
// Skip unreadable ZIP entry.
|
|
6000
|
+
}
|
|
6001
|
+
offset = dataEnd;
|
|
6002
|
+
}
|
|
6003
|
+
return entries;
|
|
6004
|
+
}
|
|
6005
|
+
|
|
6006
|
+
function parseSharedStrings(xml) {
|
|
6007
|
+
return [...xml.matchAll(/<si[\s\S]*?<\/si>/g)].map((match) => xmlToText(match[0]));
|
|
6008
|
+
}
|
|
6009
|
+
|
|
6010
|
+
function xmlToText(xml) {
|
|
6011
|
+
return normalizeExtractedText(String(xml)
|
|
6012
|
+
.replace(/<w:tab\/>/g, "\t")
|
|
6013
|
+
.replace(/<w:br\/>|<a:br\/>|<\/w:p>|<\/a:p>|<\/row>/g, "\n")
|
|
6014
|
+
.replace(/<[^>]+>/g, " ")
|
|
6015
|
+
.replace(/"/g, "\"")
|
|
6016
|
+
.replace(/'/g, "'")
|
|
6017
|
+
.replace(/</g, "<")
|
|
6018
|
+
.replace(/>/g, ">")
|
|
6019
|
+
.replace(/&/g, "&"));
|
|
6020
|
+
}
|
|
6021
|
+
|
|
6022
|
+
function normalizeExtractedText(text) {
|
|
6023
|
+
return String(text)
|
|
6024
|
+
.replace(/\u0000/g, "")
|
|
6025
|
+
.replace(/[ \t]+/g, " ")
|
|
6026
|
+
.replace(/\s*\n\s*/g, "\n")
|
|
6027
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
6028
|
+
.trim();
|
|
6029
|
+
}
|
|
6030
|
+
|
|
5627
6031
|
async function maybeConfirmFileOperation(operation, target, preview) {
|
|
5628
6032
|
const config = await loadConfig();
|
|
5629
6033
|
const approvals = config.files?.approvals || "on-write";
|
|
@@ -5784,6 +6188,208 @@ async function restoreSnapshot(id) {
|
|
|
5784
6188
|
await cp(row.path, row.workspace, { recursive: true, force: true });
|
|
5785
6189
|
}
|
|
5786
6190
|
|
|
6191
|
+
async function stageFileChange(kind, target, afterText, beforeText = null) {
|
|
6192
|
+
const { resolved, relative } = await resolveFileTarget(target, kind === "patch" ? "edit" : "write");
|
|
6193
|
+
const before = beforeText ?? (existsSync(resolved) ? await readFile(resolved, "utf8").catch(() => "") : "");
|
|
6194
|
+
initDatabase();
|
|
6195
|
+
const db = openDatabase();
|
|
6196
|
+
try {
|
|
6197
|
+
const result = db.prepare("INSERT INTO pending_changes(kind, target, before_text, after_text) VALUES (?, ?, ?, ?)").run(kind, relative, before, afterText);
|
|
6198
|
+
return Number(result.lastInsertRowid);
|
|
6199
|
+
} finally {
|
|
6200
|
+
db.close();
|
|
6201
|
+
}
|
|
6202
|
+
}
|
|
6203
|
+
|
|
6204
|
+
function listChanges() {
|
|
6205
|
+
initDatabase();
|
|
6206
|
+
const db = openDatabase();
|
|
6207
|
+
try {
|
|
6208
|
+
return db.prepare("SELECT id, kind, target, status, created_at FROM pending_changes ORDER BY id DESC LIMIT 100").all();
|
|
6209
|
+
} finally {
|
|
6210
|
+
db.close();
|
|
6211
|
+
}
|
|
6212
|
+
}
|
|
6213
|
+
|
|
6214
|
+
function getChange(id) {
|
|
6215
|
+
initDatabase();
|
|
6216
|
+
const db = openDatabase();
|
|
6217
|
+
try {
|
|
6218
|
+
const row = db.prepare("SELECT * FROM pending_changes WHERE id = ?").get(id);
|
|
6219
|
+
if (!row) throw new Error(`Изменение не найдено: ${id}`);
|
|
6220
|
+
return row;
|
|
6221
|
+
} finally {
|
|
6222
|
+
db.close();
|
|
6223
|
+
}
|
|
6224
|
+
}
|
|
6225
|
+
|
|
6226
|
+
function updateChangeStatus(id, status) {
|
|
6227
|
+
initDatabase();
|
|
6228
|
+
const db = openDatabase();
|
|
6229
|
+
try {
|
|
6230
|
+
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);
|
|
6231
|
+
} finally {
|
|
6232
|
+
db.close();
|
|
6233
|
+
}
|
|
6234
|
+
}
|
|
6235
|
+
|
|
6236
|
+
async function applyChange(id) {
|
|
6237
|
+
const change = getChange(id);
|
|
6238
|
+
if (change.status !== "pending") throw new Error(`Изменение уже не pending: ${change.status}`);
|
|
6239
|
+
await filesWrite(change.target, change.after_text);
|
|
6240
|
+
updateChangeStatus(id, "applied");
|
|
6241
|
+
}
|
|
6242
|
+
|
|
6243
|
+
async function importDataFile(target, dataset) {
|
|
6244
|
+
const text = await filesRead(target, { maxBytes: 5_000_000 });
|
|
6245
|
+
const ext = path.extname(target).toLocaleLowerCase("ru-RU");
|
|
6246
|
+
let rows = [];
|
|
6247
|
+
if (ext === ".json") {
|
|
6248
|
+
const parsed = JSON.parse(text);
|
|
6249
|
+
rows = Array.isArray(parsed) ? parsed : normalizeItems(parsed);
|
|
6250
|
+
} else if (ext === ".csv") {
|
|
6251
|
+
rows = parseCsv(text);
|
|
6252
|
+
} else {
|
|
6253
|
+
throw new Error("Поддерживается импорт JSON и CSV.");
|
|
6254
|
+
}
|
|
6255
|
+
saveCustomRecords(dataset, rows);
|
|
6256
|
+
return rows.length;
|
|
6257
|
+
}
|
|
6258
|
+
|
|
6259
|
+
function parseCsv(text) {
|
|
6260
|
+
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
6261
|
+
const headers = splitCsvLine(lines.shift() || "");
|
|
6262
|
+
return lines.map((line) => {
|
|
6263
|
+
const values = splitCsvLine(line);
|
|
6264
|
+
return Object.fromEntries(headers.map((header, index) => [header, values[index] || ""]));
|
|
6265
|
+
});
|
|
6266
|
+
}
|
|
6267
|
+
|
|
6268
|
+
function splitCsvLine(line) {
|
|
6269
|
+
const result = [];
|
|
6270
|
+
let current = "";
|
|
6271
|
+
let quote = false;
|
|
6272
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
6273
|
+
const char = line[index];
|
|
6274
|
+
if (char === '"') quote = !quote;
|
|
6275
|
+
else if (char === "," && !quote) {
|
|
6276
|
+
result.push(current);
|
|
6277
|
+
current = "";
|
|
6278
|
+
} else current += char;
|
|
6279
|
+
}
|
|
6280
|
+
result.push(current);
|
|
6281
|
+
return result.map((value) => value.trim());
|
|
6282
|
+
}
|
|
6283
|
+
|
|
6284
|
+
function saveCustomRecords(dataset, rows) {
|
|
6285
|
+
initDatabase();
|
|
6286
|
+
const db = openDatabase();
|
|
6287
|
+
try {
|
|
6288
|
+
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')");
|
|
6289
|
+
const insertFts = db.prepare("INSERT INTO custom_records_fts(dataset, record_key, searchable_text) VALUES (?, ?, ?)");
|
|
6290
|
+
db.prepare("DELETE FROM custom_records_fts WHERE dataset = ?").run(dataset);
|
|
6291
|
+
rows.forEach((row, index) => {
|
|
6292
|
+
const key = String(row.id || row.inn || index + 1);
|
|
6293
|
+
const json = JSON.stringify(row);
|
|
6294
|
+
const text = json.toLocaleLowerCase("ru-RU");
|
|
6295
|
+
insert.run(dataset, key, json, text);
|
|
6296
|
+
insertFts.run(dataset, key, text);
|
|
6297
|
+
});
|
|
6298
|
+
} finally {
|
|
6299
|
+
db.close();
|
|
6300
|
+
}
|
|
6301
|
+
}
|
|
6302
|
+
|
|
6303
|
+
async function indexFolder(target, options = {}) {
|
|
6304
|
+
const rows = await filesTree(target, { depth: Number(options.depth || 5), limit: Number(options.limit || 1000) });
|
|
6305
|
+
let count = 0;
|
|
6306
|
+
for (const row of rows.filter((item) => item.type === "file" && INDEXABLE_EXTENSIONS.test(item.path))) {
|
|
6307
|
+
try {
|
|
6308
|
+
const text = await filesRead(row.path, { maxBytes: 1_000_000 });
|
|
6309
|
+
saveIndexedDoc(row.path, path.basename(row.path), text);
|
|
6310
|
+
count += 1;
|
|
6311
|
+
} catch {
|
|
6312
|
+
// Skip unreadable files.
|
|
6313
|
+
}
|
|
6314
|
+
}
|
|
6315
|
+
return count;
|
|
6316
|
+
}
|
|
6317
|
+
|
|
6318
|
+
function saveIndexedDoc(file, title, content) {
|
|
6319
|
+
initDatabase();
|
|
6320
|
+
const db = openDatabase();
|
|
6321
|
+
try {
|
|
6322
|
+
const result = db.prepare("INSERT INTO doc_index(file, title, content) VALUES (?, ?, ?)").run(file, title, content);
|
|
6323
|
+
db.prepare("INSERT INTO doc_index_fts(rowid, file, title, content) VALUES (?, ?, ?, ?)").run(Number(result.lastInsertRowid), file, title, content);
|
|
6324
|
+
} finally {
|
|
6325
|
+
db.close();
|
|
6326
|
+
}
|
|
6327
|
+
}
|
|
6328
|
+
|
|
6329
|
+
function getIndexStatus() {
|
|
6330
|
+
initDatabase();
|
|
6331
|
+
const db = openDatabase();
|
|
6332
|
+
try {
|
|
6333
|
+
const docs = db.prepare("SELECT COUNT(*) AS count FROM doc_index").get();
|
|
6334
|
+
return { docs: docs?.count || 0 };
|
|
6335
|
+
} finally {
|
|
6336
|
+
db.close();
|
|
6337
|
+
}
|
|
6338
|
+
}
|
|
6339
|
+
|
|
6340
|
+
function searchDocs(query, limit = 20) {
|
|
6341
|
+
initDatabase();
|
|
6342
|
+
const db = openDatabase();
|
|
6343
|
+
try {
|
|
6344
|
+
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);
|
|
6345
|
+
return rows;
|
|
6346
|
+
} finally {
|
|
6347
|
+
db.close();
|
|
6348
|
+
}
|
|
6349
|
+
}
|
|
6350
|
+
|
|
6351
|
+
function listPlugins() {
|
|
6352
|
+
initDatabase();
|
|
6353
|
+
const db = openDatabase();
|
|
6354
|
+
try {
|
|
6355
|
+
return db.prepare("SELECT name, source, COALESCE(command, '-') AS command FROM plugins ORDER BY name").all();
|
|
6356
|
+
} finally {
|
|
6357
|
+
db.close();
|
|
6358
|
+
}
|
|
6359
|
+
}
|
|
6360
|
+
|
|
6361
|
+
function savePlugin(name, source, command) {
|
|
6362
|
+
initDatabase();
|
|
6363
|
+
const db = openDatabase();
|
|
6364
|
+
try {
|
|
6365
|
+
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);
|
|
6366
|
+
} finally {
|
|
6367
|
+
db.close();
|
|
6368
|
+
}
|
|
6369
|
+
}
|
|
6370
|
+
|
|
6371
|
+
function getPlugin(name) {
|
|
6372
|
+
initDatabase();
|
|
6373
|
+
const db = openDatabase();
|
|
6374
|
+
try {
|
|
6375
|
+
const row = db.prepare("SELECT * FROM plugins WHERE name = ?").get(name);
|
|
6376
|
+
if (!row) throw new Error(`Plugin не найден: ${name}`);
|
|
6377
|
+
return row;
|
|
6378
|
+
} finally {
|
|
6379
|
+
db.close();
|
|
6380
|
+
}
|
|
6381
|
+
}
|
|
6382
|
+
|
|
6383
|
+
function deletePlugin(name) {
|
|
6384
|
+
initDatabase();
|
|
6385
|
+
const db = openDatabase();
|
|
6386
|
+
try {
|
|
6387
|
+
db.prepare("DELETE FROM plugins WHERE name = ?").run(name);
|
|
6388
|
+
} finally {
|
|
6389
|
+
db.close();
|
|
6390
|
+
}
|
|
6391
|
+
}
|
|
6392
|
+
|
|
5787
6393
|
function unifiedPreview(before, after) {
|
|
5788
6394
|
const beforeLines = before.split(/\r?\n/);
|
|
5789
6395
|
const afterLines = after.split(/\r?\n/);
|
|
@@ -5831,6 +6437,9 @@ async function executeRpc(method, options = {}) {
|
|
|
5831
6437
|
if (method === "files.search") {
|
|
5832
6438
|
return filesSearch(options.query || options.search || "", options);
|
|
5833
6439
|
}
|
|
6440
|
+
if (method === "index.search") {
|
|
6441
|
+
return searchDocs(options.query || options.search || "", Number(options.limit || 20));
|
|
6442
|
+
}
|
|
5834
6443
|
throw new Error(`RPC method неизвестен: ${method}. Доступно: status, search, card, quality, sync.`);
|
|
5835
6444
|
}
|
|
5836
6445
|
|
package/wiki/Home.md
CHANGED
|
@@ -31,6 +31,7 @@ iola ask "найди школу 29"
|
|
|
31
31
|
- [Skills и toolsets](Skills-и-toolsets)
|
|
32
32
|
- [Локальные файлы](Локальные-файлы)
|
|
33
33
|
- [Рабочая среда агента](Рабочая-среда-агента)
|
|
34
|
+
- [Расширения и локальные данные](Расширения-и-локальные-данные)
|
|
34
35
|
- [Daemon, RPC и cron](Daemon-RPC-и-cron)
|
|
35
36
|
- [Контекст и память](Контекст-и-память)
|
|
36
37
|
- [Команды](Команды)
|
|
@@ -57,6 +57,12 @@ iola artifacts list
|
|
|
57
57
|
iola snapshot list
|
|
58
58
|
iola trace last
|
|
59
59
|
iola export education-contacts --format xlsx --output contacts.xlsx
|
|
60
|
+
iola changes list
|
|
61
|
+
iola import file data.csv --dataset custom
|
|
62
|
+
iola index folder ./docs
|
|
63
|
+
iola reports list
|
|
64
|
+
iola plugins list
|
|
65
|
+
iola mcp serve
|
|
60
66
|
iola memory suggest
|
|
61
67
|
```
|
|
62
68
|
|
|
@@ -36,6 +36,18 @@ iola files write report.md --text "Текст отчета"
|
|
|
36
36
|
iola files patch README.md --search old --replace new
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
Чтение и индексирование поддерживает:
|
|
40
|
+
|
|
41
|
+
- `.docx`
|
|
42
|
+
- `.xlsx`
|
|
43
|
+
- `.pptx`
|
|
44
|
+
- `.pdf`
|
|
45
|
+
- `.md`
|
|
46
|
+
- `.txt`
|
|
47
|
+
- `.csv`
|
|
48
|
+
- `.json`
|
|
49
|
+
- `.html`
|
|
50
|
+
|
|
39
51
|
AI/tool-agent:
|
|
40
52
|
|
|
41
53
|
```bash
|
|
@@ -43,4 +55,3 @@ iola ask "найди в текущей папке упоминания школ"
|
|
|
43
55
|
```
|
|
44
56
|
|
|
45
57
|
По умолчанию файловый режим `locked`. Запись требует включения `workspace-write` или `full-access`.
|
|
46
|
-
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Расширения и локальные данные
|
|
2
|
+
|
|
3
|
+
Staged changes:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
iola files write report.md --text "..." --stage
|
|
7
|
+
iola files patch README.md --search old --replace new --stage
|
|
8
|
+
iola changes list
|
|
9
|
+
iola changes show 1
|
|
10
|
+
iola changes apply 1
|
|
11
|
+
iola changes discard 1
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Импорт локальных данных:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
iola import file data.csv --dataset custom-schools
|
|
18
|
+
iola import file data.json --dataset custom
|
|
19
|
+
iola import folder ./data
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Индекс документов:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
iola index folder ./docs
|
|
26
|
+
iola index status
|
|
27
|
+
iola index search "школа 29"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Поддерживаемые форматы для чтения и индекса:
|
|
31
|
+
|
|
32
|
+
- `.docx`
|
|
33
|
+
- `.xlsx`
|
|
34
|
+
- `.pptx`
|
|
35
|
+
- `.pdf`
|
|
36
|
+
- `.md`
|
|
37
|
+
- `.txt`
|
|
38
|
+
- `.csv`
|
|
39
|
+
- `.json`
|
|
40
|
+
- `.html`
|
|
41
|
+
|
|
42
|
+
Пакеты отчетов:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
iola reports list
|
|
46
|
+
iola reports run education-passport
|
|
47
|
+
iola reports run data-quality-pack
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Plugins:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
iola plugins list
|
|
54
|
+
iola plugins install quality --command "iola quality"
|
|
55
|
+
iola plugins run quality
|
|
56
|
+
iola plugins remove quality
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Локальный MCP endpoint:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
iola mcp serve
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
По умолчанию MCP запускается на порту `daemon.port + 1`.
|