@iola_adm/iola-cli 0.1.27 → 0.1.29
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 +5 -0
- package/package.json +1 -1
- package/src/cli.js +366 -7
- package/wiki/Home.md +1 -0
- package/wiki//320/220/321/200/321/205/320/270/320/262/321/213-/320/270-/320/274/320/260/321/201/321/202/320/265/321/200-/320/275/320/260/321/201/321/202/321/200/320/276/320/271/320/272/320/270.md +42 -0
- package/wiki//320/232/320/276/320/274/320/260/320/275/320/264/321/213.md +4 -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 +12 -1
package/README.md
CHANGED
|
@@ -59,6 +59,7 @@ iola tasks list
|
|
|
59
59
|
iola artifacts list
|
|
60
60
|
iola trace last
|
|
61
61
|
iola changes list
|
|
62
|
+
iola archive doctor
|
|
62
63
|
iola index status
|
|
63
64
|
iola reports list
|
|
64
65
|
iola plugins list
|
|
@@ -92,6 +93,7 @@ iola version --check
|
|
|
92
93
|
- [Локальные файлы](https://github.com/adm-iola/iola-cli/wiki/Локальные-файлы)
|
|
93
94
|
- [Рабочая среда агента](https://github.com/adm-iola/iola-cli/wiki/Рабочая-среда-агента)
|
|
94
95
|
- [Расширения и локальные данные](https://github.com/adm-iola/iola-cli/wiki/Расширения-и-локальные-данные)
|
|
96
|
+
- [Архивы и мастер настройки](https://github.com/adm-iola/iola-cli/wiki/Архивы-и-мастер-настройки)
|
|
95
97
|
- [Daemon, RPC и cron](https://github.com/adm-iola/iola-cli/wiki/Daemon-RPC-и-cron)
|
|
96
98
|
- [Контекст и память](https://github.com/adm-iola/iola-cli/wiki/Контекст-и-память)
|
|
97
99
|
- [Команды](https://github.com/adm-iola/iola-cli/wiki/Команды)
|
|
@@ -108,6 +110,9 @@ iola version --check
|
|
|
108
110
|
- планы выполнения, traces, tasks, artifacts, snapshots и policy-профили;
|
|
109
111
|
- экспорт отчетов в Excel/Word-совместимые файлы;
|
|
110
112
|
- staged changes, импорт локальных CSV/JSON, индекс локальных документов, report packs, plugins и локальный MCP endpoint;
|
|
113
|
+
- чтение и индексирование `.docx`, `.xlsx`, `.pptx`, `.pdf`, `.md`, `.txt`, `.csv`, `.json`, `.html`;
|
|
114
|
+
- работа с архивами через 7-Zip: `.zip`, `.7z`, `.rar`, `.tar`, `.gz`, `.tgz`, `.bz2`, `.xz` и другие;
|
|
115
|
+
- расширенный `iola onboard` с установкой 7-Zip, Ollama, Codex CLI и настройкой выбранных компонентов.
|
|
111
116
|
- cron-задачи, локальный daemon и RPC для автоматизаций;
|
|
112
117
|
- контекстные файлы `IOLA.md` и `.iola/context.md`;
|
|
113
118
|
- интеграция с публичным 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,7 @@ const COMMANDS = new Map([
|
|
|
242
244
|
["skills", handleSkills],
|
|
243
245
|
["tools", handleTools],
|
|
244
246
|
["files", handleFiles],
|
|
247
|
+
["archive", handleArchive],
|
|
245
248
|
["changes", handleChanges],
|
|
246
249
|
["import", handleImport],
|
|
247
250
|
["index", handleIndex],
|
|
@@ -358,6 +361,7 @@ Usage:
|
|
|
358
361
|
iola skills list|show|paths|enable|disable
|
|
359
362
|
iola tools list|toolsets|enable|disable|profile
|
|
360
363
|
iola files status|mode|approvals|tree|read|search|write|patch
|
|
364
|
+
iola archive doctor|list|test|extract|create|index
|
|
361
365
|
iola changes list|show|apply|discard
|
|
362
366
|
iola import file|folder
|
|
363
367
|
iola index folder|status|search
|
|
@@ -616,6 +620,11 @@ async function handleAgentLine(line, state) {
|
|
|
616
620
|
return false;
|
|
617
621
|
}
|
|
618
622
|
|
|
623
|
+
if (command === "archive") {
|
|
624
|
+
await handleArchive(args);
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
|
|
619
628
|
if (command === "changes") {
|
|
620
629
|
await handleChanges(args);
|
|
621
630
|
return false;
|
|
@@ -789,6 +798,7 @@ async function handleAgentLine(line, state) {
|
|
|
789
798
|
context: ["context", args],
|
|
790
799
|
skills: ["skills", args],
|
|
791
800
|
files: ["files", args],
|
|
801
|
+
archive: ["archive", args],
|
|
792
802
|
changes: ["changes", args],
|
|
793
803
|
index: ["index", args],
|
|
794
804
|
reports: ["reports", args],
|
|
@@ -848,6 +858,7 @@ function printAgentHelp() {
|
|
|
848
858
|
/permissions
|
|
849
859
|
/tools
|
|
850
860
|
/files status
|
|
861
|
+
/archive doctor
|
|
851
862
|
/changes list
|
|
852
863
|
/index status
|
|
853
864
|
/reports list
|
|
@@ -1589,6 +1600,7 @@ async function handleWiki(args) {
|
|
|
1589
1600
|
["Локальные файлы", `${base}/Локальные-файлы`],
|
|
1590
1601
|
["Рабочая среда агента", `${base}/Рабочая-среда-агента`],
|
|
1591
1602
|
["Расширения и локальные данные", `${base}/Расширения-и-локальные-данные`],
|
|
1603
|
+
["Архивы и мастер настройки", `${base}/Архивы-и-мастер-настройки`],
|
|
1592
1604
|
["Daemon, RPC и cron", `${base}/Daemon-RPC-и-cron`],
|
|
1593
1605
|
["Контекст и память", `${base}/Контекст-и-память`],
|
|
1594
1606
|
["Команды", `${base}/Команды`],
|
|
@@ -1843,6 +1855,88 @@ async function handleFiles(args) {
|
|
|
1843
1855
|
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.");
|
|
1844
1856
|
}
|
|
1845
1857
|
|
|
1858
|
+
async function handleArchive(args) {
|
|
1859
|
+
const [action = "doctor", target, ...rest] = args;
|
|
1860
|
+
const options = parseOptions(rest);
|
|
1861
|
+
if (action === "doctor") {
|
|
1862
|
+
const sevenZip = await ensureArchiveTool({ install: true });
|
|
1863
|
+
printKeyValue({ sevenZip, status: "ok", formats: "zip, 7z, rar, tar, gz, tgz, bz2, xz и др." });
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
if (action === "list") {
|
|
1867
|
+
if (!target) throw new Error("Пример: iola archive list docs.zip");
|
|
1868
|
+
const rows = await archiveList(target);
|
|
1869
|
+
printTable(rows, [["date", "Дата"], ["size", "Размер"], ["name", "Файл"]]);
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
if (action === "test") {
|
|
1873
|
+
if (!target) throw new Error("Пример: iola archive test docs.zip");
|
|
1874
|
+
await archiveRun(["t", target]);
|
|
1875
|
+
console.log("Архив проверен.");
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
if (action === "extract") {
|
|
1879
|
+
if (!target) throw new Error("Пример: iola archive extract docs.zip --output ./out");
|
|
1880
|
+
const outputDir = options.output || path.join(process.cwd(), path.basename(target, path.extname(target)));
|
|
1881
|
+
await archiveRun(["x", target, `-o${outputDir}`, "-y"]);
|
|
1882
|
+
console.log(`Архив распакован: ${outputDir}`);
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
if (action === "create") {
|
|
1886
|
+
const outputFile = target;
|
|
1887
|
+
const inputPath = rest[0] || options.path || ".";
|
|
1888
|
+
if (!outputFile) throw new Error("Пример: iola archive create docs.zip ./docs");
|
|
1889
|
+
await archiveRun(["a", outputFile, inputPath]);
|
|
1890
|
+
console.log(`Архив создан: ${outputFile}`);
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
if (action === "index") {
|
|
1894
|
+
if (!target) throw new Error("Пример: iola archive index docs.zip");
|
|
1895
|
+
const tempDir = path.join(os.tmpdir(), `iola-archive-${Date.now()}`);
|
|
1896
|
+
const previous = await loadConfig();
|
|
1897
|
+
await mkdir(tempDir, { recursive: true });
|
|
1898
|
+
try {
|
|
1899
|
+
await archiveRun(["x", target, `-o${tempDir}`, "-y"]);
|
|
1900
|
+
await saveConfig({ files: { ...(previous.files || {}), workspaceRoot: tempDir, mode: "read-only" } });
|
|
1901
|
+
await setFilesMode("read-only", await loadConfig());
|
|
1902
|
+
const count = await indexFolder(".", { depth: options.depth || 8, limit: options.limit || 2000 });
|
|
1903
|
+
console.log(`Проиндексировано файлов из архива: ${count}`);
|
|
1904
|
+
} finally {
|
|
1905
|
+
await saveConfig({ files: previous.files, permissions: previous.permissions, toolsets: previous.toolsets }).catch(() => {});
|
|
1906
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1907
|
+
}
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
throw new Error("Команды archive: doctor, list FILE, test FILE, extract FILE --output DIR, create OUT INPUT, index FILE.");
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
async function archiveRun(args) {
|
|
1914
|
+
const command = await ensureArchiveTool({ install: true });
|
|
1915
|
+
return runCommand(command, args, { inherit: true });
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
async function archiveList(target) {
|
|
1919
|
+
const command = await ensureArchiveTool({ install: true });
|
|
1920
|
+
const { stdout } = await runCommand(command, ["l", "-slt", target]);
|
|
1921
|
+
const rows = [];
|
|
1922
|
+
let current = {};
|
|
1923
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
1924
|
+
if (!line.trim()) {
|
|
1925
|
+
if (current.Path && current.Path !== target) rows.push({
|
|
1926
|
+
date: current.Modified || current.Created || "-",
|
|
1927
|
+
size: current.Size || "-",
|
|
1928
|
+
name: current.Path,
|
|
1929
|
+
});
|
|
1930
|
+
current = {};
|
|
1931
|
+
continue;
|
|
1932
|
+
}
|
|
1933
|
+
const [key, ...parts] = line.split(" = ");
|
|
1934
|
+
if (key && parts.length) current[key.trim()] = parts.join(" = ").trim();
|
|
1935
|
+
}
|
|
1936
|
+
if (current.Path && current.Path !== target) rows.push({ date: current.Modified || current.Created || "-", size: current.Size || "-", name: current.Path });
|
|
1937
|
+
return rows;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1846
1940
|
async function handleChanges(args) {
|
|
1847
1941
|
const [action = "list", id] = args;
|
|
1848
1942
|
if (action === "list" || action === "ls") {
|
|
@@ -1903,13 +1997,18 @@ async function handleIndex(args) {
|
|
|
1903
1997
|
console.log(`Проиндексировано документов: ${count}`);
|
|
1904
1998
|
return;
|
|
1905
1999
|
}
|
|
2000
|
+
if (action === "archive") {
|
|
2001
|
+
if (!target) throw new Error("Пример: iola index archive docs.zip");
|
|
2002
|
+
await handleArchive(["index", target, ...rest]);
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
1906
2005
|
if (action === "search") {
|
|
1907
2006
|
const query = [target, ...rest].filter(Boolean).join(" ");
|
|
1908
2007
|
if (!query) throw new Error('Пример: iola index search "школа 29"');
|
|
1909
2008
|
printTable(searchDocs(query, Number(options.limit || 20)), [["file", "Файл"], ["title", "Название"], ["snippet", "Фрагмент"]]);
|
|
1910
2009
|
return;
|
|
1911
2010
|
}
|
|
1912
|
-
throw new Error("Команды index: status, folder PATH, search TEXT.");
|
|
2011
|
+
throw new Error("Команды index: status, folder PATH, archive FILE, search TEXT.");
|
|
1913
2012
|
}
|
|
1914
2013
|
|
|
1915
2014
|
async function handleReports(args) {
|
|
@@ -5279,12 +5378,68 @@ async function onboard(args = []) {
|
|
|
5279
5378
|
initDatabase();
|
|
5280
5379
|
await handleConfig(["validate"]);
|
|
5281
5380
|
await doctor(["--summary"]);
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5381
|
+
await ensureArchiveTool({ install: true });
|
|
5382
|
+
|
|
5383
|
+
const components = options.yes ? ["workspace", "policy", "ollama", "openai", "openrouter", "codex", "codex-mcp", "index"] : await chooseOnboardComponents();
|
|
5384
|
+
if (components.includes("workspace")) await handleWorkspace(["init"]);
|
|
5385
|
+
if (components.includes("policy")) await handlePolicy(["use", "analyst"]);
|
|
5386
|
+
if (components.includes("ollama")) {
|
|
5387
|
+
await installOllamaIfMissing();
|
|
5388
|
+
await setupOllama(["--yes"]);
|
|
5389
|
+
}
|
|
5390
|
+
if (components.includes("openai")) {
|
|
5391
|
+
await aiSetup(["openai"]);
|
|
5392
|
+
if (process.stdin.isTTY) await setAiKey("openai");
|
|
5393
|
+
}
|
|
5394
|
+
if (components.includes("openrouter")) {
|
|
5395
|
+
await aiSetup(["openrouter"]);
|
|
5396
|
+
if (process.stdin.isTTY) await setAiKey("openrouter");
|
|
5397
|
+
}
|
|
5398
|
+
if (components.includes("codex")) {
|
|
5399
|
+
await installCodexIfMissing();
|
|
5400
|
+
await aiSetup(["codex"]);
|
|
5401
|
+
}
|
|
5402
|
+
if (components.includes("codex-mcp")) await setupClient(["codex"]);
|
|
5403
|
+
if (components.includes("index")) {
|
|
5404
|
+
await setFilesMode("read-only", await loadConfig());
|
|
5405
|
+
console.log("Индекс документов можно запустить командой: iola index folder ./docs");
|
|
5406
|
+
}
|
|
5285
5407
|
console.log("Onboard завершен.");
|
|
5286
5408
|
}
|
|
5287
5409
|
|
|
5410
|
+
async function chooseOnboardComponents() {
|
|
5411
|
+
if (!process.stdin.isTTY) return ["workspace", "policy"];
|
|
5412
|
+
console.log("");
|
|
5413
|
+
console.log("Выберите компоненты через запятую:");
|
|
5414
|
+
console.log("1. workspace и контекст");
|
|
5415
|
+
console.log("2. policy analyst");
|
|
5416
|
+
console.log("3. Ollama + локальная модель");
|
|
5417
|
+
console.log("4. OpenAI API");
|
|
5418
|
+
console.log("5. OpenRouter API");
|
|
5419
|
+
console.log("6. Codex CLI");
|
|
5420
|
+
console.log("7. MCP для Codex");
|
|
5421
|
+
console.log("8. Индекс локальных документов");
|
|
5422
|
+
console.log("");
|
|
5423
|
+
const rl = readline.createInterface({ input, output });
|
|
5424
|
+
try {
|
|
5425
|
+
const answer = (await rl.question("Компоненты [1,2,8]: ")).trim() || "1,2,8";
|
|
5426
|
+
const selected = new Set(answer.split(/[,\s]+/).filter(Boolean));
|
|
5427
|
+
const map = {
|
|
5428
|
+
1: "workspace",
|
|
5429
|
+
2: "policy",
|
|
5430
|
+
3: "ollama",
|
|
5431
|
+
4: "openai",
|
|
5432
|
+
5: "openrouter",
|
|
5433
|
+
6: "codex",
|
|
5434
|
+
7: "codex-mcp",
|
|
5435
|
+
8: "index",
|
|
5436
|
+
};
|
|
5437
|
+
return [...selected].map((item) => map[item] || item).filter(Boolean);
|
|
5438
|
+
} finally {
|
|
5439
|
+
rl.close();
|
|
5440
|
+
}
|
|
5441
|
+
}
|
|
5442
|
+
|
|
5288
5443
|
function parseOptions(args) {
|
|
5289
5444
|
const result = { _: [] };
|
|
5290
5445
|
|
|
@@ -5612,6 +5767,66 @@ async function getCommandVersion(command, args) {
|
|
|
5612
5767
|
}
|
|
5613
5768
|
}
|
|
5614
5769
|
|
|
5770
|
+
async function findCommand(candidates, versionArgs = ["--version"]) {
|
|
5771
|
+
for (const command of candidates) {
|
|
5772
|
+
const version = await getCommandVersion(command, versionArgs);
|
|
5773
|
+
if (version !== "не найден") return { command, version };
|
|
5774
|
+
}
|
|
5775
|
+
return null;
|
|
5776
|
+
}
|
|
5777
|
+
|
|
5778
|
+
async function ensureArchiveTool(options = {}) {
|
|
5779
|
+
const found = await findCommand(["7z", "7zz", "7za"], ["--help"]);
|
|
5780
|
+
if (found) return found.command;
|
|
5781
|
+
if (options.install === false) throw new Error("7-Zip не найден.");
|
|
5782
|
+
await installSevenZip();
|
|
5783
|
+
const installed = await findCommand(["7z", "7zz", "7za"], ["--help"]);
|
|
5784
|
+
if (!installed) throw new Error("7-Zip не найден после установки. Перезапустите терминал и проверьте: 7z");
|
|
5785
|
+
return installed.command;
|
|
5786
|
+
}
|
|
5787
|
+
|
|
5788
|
+
async function installSevenZip() {
|
|
5789
|
+
console.log("7-Zip не найден. Устанавливаю архиватор для работы со всеми типами архивов.");
|
|
5790
|
+
if (process.platform === "win32") {
|
|
5791
|
+
await runCommand("winget", ["install", "7zip.7zip", "--accept-package-agreements", "--accept-source-agreements"], { inherit: true });
|
|
5792
|
+
return;
|
|
5793
|
+
}
|
|
5794
|
+
if (process.platform === "darwin") {
|
|
5795
|
+
try {
|
|
5796
|
+
await runCommand("brew", ["install", "sevenzip"], { inherit: true });
|
|
5797
|
+
} catch {
|
|
5798
|
+
await runCommand("brew", ["install", "p7zip"], { inherit: true });
|
|
5799
|
+
}
|
|
5800
|
+
return;
|
|
5801
|
+
}
|
|
5802
|
+
try {
|
|
5803
|
+
await runCommand("sh", ["-c", "sudo apt-get update && sudo apt-get install -y p7zip-full p7zip-rar"], { inherit: true });
|
|
5804
|
+
} catch {
|
|
5805
|
+
await runCommand("sh", ["-c", "sudo apt-get update && sudo apt-get install -y 7zip"], { inherit: true });
|
|
5806
|
+
}
|
|
5807
|
+
}
|
|
5808
|
+
|
|
5809
|
+
async function installOllamaIfMissing() {
|
|
5810
|
+
if (await getOllamaVersion()) return;
|
|
5811
|
+
console.log("Ollama не найден. Устанавливаю Ollama.");
|
|
5812
|
+
if (process.platform === "win32") {
|
|
5813
|
+
await runCommand("winget", ["install", "Ollama.Ollama", "--accept-package-agreements", "--accept-source-agreements"], { inherit: true });
|
|
5814
|
+
return;
|
|
5815
|
+
}
|
|
5816
|
+
if (process.platform === "darwin") {
|
|
5817
|
+
await runCommand("brew", ["install", "--cask", "ollama"], { inherit: true });
|
|
5818
|
+
return;
|
|
5819
|
+
}
|
|
5820
|
+
await runCommand("sh", ["-c", "curl -fsSL https://ollama.com/install.sh | sh"], { inherit: true });
|
|
5821
|
+
}
|
|
5822
|
+
|
|
5823
|
+
async function installCodexIfMissing() {
|
|
5824
|
+
const version = await getCommandVersion("codex", ["--version"]);
|
|
5825
|
+
if (version !== "не найден") return;
|
|
5826
|
+
console.log("Codex CLI не найден. Устанавливаю через npm.");
|
|
5827
|
+
await runCommand("npm", ["install", "-g", "@openai/codex"], { inherit: true });
|
|
5828
|
+
}
|
|
5829
|
+
|
|
5615
5830
|
async function probeEndpoint(url) {
|
|
5616
5831
|
try {
|
|
5617
5832
|
const response = await fetch(url, { headers: { accept: "application/json" } });
|
|
@@ -5833,7 +6048,7 @@ async function filesRead(target, options = {}) {
|
|
|
5833
6048
|
if (!info.isFile()) throw new Error(`Это не файл: ${target}`);
|
|
5834
6049
|
const maxBytes = Number(options.maxBytes || config.files?.maxReadBytes || 200000);
|
|
5835
6050
|
if (info.size > maxBytes) throw new Error(`Файл слишком большой: ${info.size} байт. Лимит: ${maxBytes}`);
|
|
5836
|
-
return
|
|
6051
|
+
return extractReadableText(resolved);
|
|
5837
6052
|
}
|
|
5838
6053
|
|
|
5839
6054
|
async function filesSearch(query, options = {}) {
|
|
@@ -5882,6 +6097,150 @@ async function filesPatch(target, search, replace) {
|
|
|
5882
6097
|
return { path: relative, replacements };
|
|
5883
6098
|
}
|
|
5884
6099
|
|
|
6100
|
+
async function extractReadableText(file) {
|
|
6101
|
+
const ext = path.extname(file).toLocaleLowerCase("ru-RU");
|
|
6102
|
+
if (ext === ".docx") return extractDocxText(await readFile(file));
|
|
6103
|
+
if (ext === ".xlsx") return extractXlsxText(await readFile(file));
|
|
6104
|
+
if (ext === ".pptx") return extractPptxText(await readFile(file));
|
|
6105
|
+
if (ext === ".pdf") return extractPdfText(await readFile(file));
|
|
6106
|
+
return readFile(file, "utf8");
|
|
6107
|
+
}
|
|
6108
|
+
|
|
6109
|
+
function extractDocxText(buffer) {
|
|
6110
|
+
const entries = readZipEntries(buffer);
|
|
6111
|
+
const documentXml = entries.get("word/document.xml") || "";
|
|
6112
|
+
const footnotes = [...entries.entries()].filter(([name]) => name.startsWith("word/") && /footnotes|endnotes|comments/.test(name)).map(([, text]) => text).join("\n");
|
|
6113
|
+
return xmlToText(`${documentXml}\n${footnotes}`);
|
|
6114
|
+
}
|
|
6115
|
+
|
|
6116
|
+
function extractXlsxText(buffer) {
|
|
6117
|
+
const entries = readZipEntries(buffer);
|
|
6118
|
+
const sharedStrings = parseSharedStrings(entries.get("xl/sharedStrings.xml") || "");
|
|
6119
|
+
const chunks = [];
|
|
6120
|
+
for (const [name, xml] of entries.entries()) {
|
|
6121
|
+
if (!/^xl\/worksheets\/sheet\d+\.xml$/i.test(name)) continue;
|
|
6122
|
+
chunks.push(name);
|
|
6123
|
+
const resolved = xml.replace(/<c[^>]*t="s"[^>]*>[\s\S]*?<v>(\d+)<\/v>[\s\S]*?<\/c>/g, (_, index) => ` ${sharedStrings[Number(index)] || ""} `);
|
|
6124
|
+
chunks.push(xmlToText(resolved));
|
|
6125
|
+
}
|
|
6126
|
+
return normalizeExtractedText(chunks.join("\n"));
|
|
6127
|
+
}
|
|
6128
|
+
|
|
6129
|
+
function extractPptxText(buffer) {
|
|
6130
|
+
const entries = readZipEntries(buffer);
|
|
6131
|
+
const slides = [...entries.entries()]
|
|
6132
|
+
.filter(([name]) => /^ppt\/slides\/slide\d+\.xml$/i.test(name))
|
|
6133
|
+
.sort(([left], [right]) => left.localeCompare(right, undefined, { numeric: true }));
|
|
6134
|
+
return normalizeExtractedText(slides.map(([name, xml]) => `${name}\n${xmlToText(xml)}`).join("\n\n"));
|
|
6135
|
+
}
|
|
6136
|
+
|
|
6137
|
+
function extractPdfText(buffer) {
|
|
6138
|
+
const latin = buffer.toString("latin1");
|
|
6139
|
+
const chunks = [];
|
|
6140
|
+
const streamPattern = /<<(?:.|\r|\n)*?>>\s*stream\r?\n([\s\S]*?)\r?\nendstream/g;
|
|
6141
|
+
let match;
|
|
6142
|
+
while ((match = streamPattern.exec(latin))) {
|
|
6143
|
+
const dictionary = latin.slice(Math.max(0, match.index - 500), match.index + 500);
|
|
6144
|
+
let data = Buffer.from(match[1], "latin1");
|
|
6145
|
+
if (/FlateDecode/.test(dictionary)) {
|
|
6146
|
+
try {
|
|
6147
|
+
data = inflateSync(data);
|
|
6148
|
+
} catch {
|
|
6149
|
+
try {
|
|
6150
|
+
data = inflateRawSync(data);
|
|
6151
|
+
} catch {
|
|
6152
|
+
// Leave compressed stream unreadable.
|
|
6153
|
+
}
|
|
6154
|
+
}
|
|
6155
|
+
}
|
|
6156
|
+
chunks.push(extractPdfStrings(data.toString("latin1")));
|
|
6157
|
+
}
|
|
6158
|
+
chunks.push(extractPdfStrings(latin));
|
|
6159
|
+
return normalizeExtractedText(chunks.join("\n"));
|
|
6160
|
+
}
|
|
6161
|
+
|
|
6162
|
+
function extractPdfStrings(text) {
|
|
6163
|
+
const strings = [];
|
|
6164
|
+
for (const match of text.matchAll(/\(([^()\\]*(?:\\.[^()\\]*)*)\)\s*T[jJ]?/g)) {
|
|
6165
|
+
strings.push(unescapePdfString(match[1]));
|
|
6166
|
+
}
|
|
6167
|
+
for (const match of text.matchAll(/\[([\s\S]*?)\]\s*TJ/g)) {
|
|
6168
|
+
for (const item of match[1].matchAll(/\(([^()\\]*(?:\\.[^()\\]*)*)\)/g)) {
|
|
6169
|
+
strings.push(unescapePdfString(item[1]));
|
|
6170
|
+
}
|
|
6171
|
+
}
|
|
6172
|
+
return strings.join(" ");
|
|
6173
|
+
}
|
|
6174
|
+
|
|
6175
|
+
function unescapePdfString(value) {
|
|
6176
|
+
const unescaped = value
|
|
6177
|
+
.replace(/\\n/g, "\n")
|
|
6178
|
+
.replace(/\\r/g, "\r")
|
|
6179
|
+
.replace(/\\t/g, "\t")
|
|
6180
|
+
.replace(/\\([()\\])/g, "$1")
|
|
6181
|
+
.replace(/\\(\d{3})/g, (_, octal) => String.fromCharCode(parseInt(octal, 8)));
|
|
6182
|
+
return decodePossiblyUtf8(unescaped);
|
|
6183
|
+
}
|
|
6184
|
+
|
|
6185
|
+
function decodePossiblyUtf8(value) {
|
|
6186
|
+
const decoded = Buffer.from(value, "latin1").toString("utf8");
|
|
6187
|
+
return decoded.includes("\uFFFD") ? value : decoded;
|
|
6188
|
+
}
|
|
6189
|
+
|
|
6190
|
+
function readZipEntries(buffer) {
|
|
6191
|
+
const entries = new Map();
|
|
6192
|
+
let offset = 0;
|
|
6193
|
+
while (offset < buffer.length - 30) {
|
|
6194
|
+
const signature = buffer.readUInt32LE(offset);
|
|
6195
|
+
if (signature !== 0x04034b50) {
|
|
6196
|
+
offset += 1;
|
|
6197
|
+
continue;
|
|
6198
|
+
}
|
|
6199
|
+
const method = buffer.readUInt16LE(offset + 8);
|
|
6200
|
+
const compressedSize = buffer.readUInt32LE(offset + 18);
|
|
6201
|
+
const fileNameLength = buffer.readUInt16LE(offset + 26);
|
|
6202
|
+
const extraLength = buffer.readUInt16LE(offset + 28);
|
|
6203
|
+
const nameStart = offset + 30;
|
|
6204
|
+
const name = buffer.subarray(nameStart, nameStart + fileNameLength).toString("utf8");
|
|
6205
|
+
const dataStart = nameStart + fileNameLength + extraLength;
|
|
6206
|
+
const dataEnd = dataStart + compressedSize;
|
|
6207
|
+
const compressed = buffer.subarray(dataStart, dataEnd);
|
|
6208
|
+
try {
|
|
6209
|
+
const data = method === 8 ? inflateRawSync(compressed) : compressed;
|
|
6210
|
+
entries.set(name.replace(/\\/g, "/"), data.toString("utf8"));
|
|
6211
|
+
} catch {
|
|
6212
|
+
// Skip unreadable ZIP entry.
|
|
6213
|
+
}
|
|
6214
|
+
offset = dataEnd;
|
|
6215
|
+
}
|
|
6216
|
+
return entries;
|
|
6217
|
+
}
|
|
6218
|
+
|
|
6219
|
+
function parseSharedStrings(xml) {
|
|
6220
|
+
return [...xml.matchAll(/<si[\s\S]*?<\/si>/g)].map((match) => xmlToText(match[0]));
|
|
6221
|
+
}
|
|
6222
|
+
|
|
6223
|
+
function xmlToText(xml) {
|
|
6224
|
+
return normalizeExtractedText(String(xml)
|
|
6225
|
+
.replace(/<w:tab\/>/g, "\t")
|
|
6226
|
+
.replace(/<w:br\/>|<a:br\/>|<\/w:p>|<\/a:p>|<\/row>/g, "\n")
|
|
6227
|
+
.replace(/<[^>]+>/g, " ")
|
|
6228
|
+
.replace(/"/g, "\"")
|
|
6229
|
+
.replace(/'/g, "'")
|
|
6230
|
+
.replace(/</g, "<")
|
|
6231
|
+
.replace(/>/g, ">")
|
|
6232
|
+
.replace(/&/g, "&"));
|
|
6233
|
+
}
|
|
6234
|
+
|
|
6235
|
+
function normalizeExtractedText(text) {
|
|
6236
|
+
return String(text)
|
|
6237
|
+
.replace(/\u0000/g, "")
|
|
6238
|
+
.replace(/[ \t]+/g, " ")
|
|
6239
|
+
.replace(/\s*\n\s*/g, "\n")
|
|
6240
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
6241
|
+
.trim();
|
|
6242
|
+
}
|
|
6243
|
+
|
|
5885
6244
|
async function maybeConfirmFileOperation(operation, target, preview) {
|
|
5886
6245
|
const config = await loadConfig();
|
|
5887
6246
|
const approvals = config.files?.approvals || "on-write";
|
|
@@ -6157,7 +6516,7 @@ function saveCustomRecords(dataset, rows) {
|
|
|
6157
6516
|
async function indexFolder(target, options = {}) {
|
|
6158
6517
|
const rows = await filesTree(target, { depth: Number(options.depth || 5), limit: Number(options.limit || 1000) });
|
|
6159
6518
|
let count = 0;
|
|
6160
|
-
for (const row of rows.filter((item) => item.type === "file" &&
|
|
6519
|
+
for (const row of rows.filter((item) => item.type === "file" && INDEXABLE_EXTENSIONS.test(item.path))) {
|
|
6161
6520
|
try {
|
|
6162
6521
|
const text = await filesRead(row.path, { maxBytes: 1_000_000 });
|
|
6163
6522
|
saveIndexedDoc(row.path, path.basename(row.path), text);
|
package/wiki/Home.md
CHANGED
|
@@ -32,6 +32,7 @@ iola ask "найди школу 29"
|
|
|
32
32
|
- [Локальные файлы](Локальные-файлы)
|
|
33
33
|
- [Рабочая среда агента](Рабочая-среда-агента)
|
|
34
34
|
- [Расширения и локальные данные](Расширения-и-локальные-данные)
|
|
35
|
+
- [Архивы и мастер настройки](Архивы-и-мастер-настройки)
|
|
35
36
|
- [Daemon, RPC и cron](Daemon-RPC-и-cron)
|
|
36
37
|
- [Контекст и память](Контекст-и-память)
|
|
37
38
|
- [Команды](Команды)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Архивы и мастер настройки
|
|
2
|
+
|
|
3
|
+
`iola-cli` использует 7-Zip как штатный архиватор. Если 7-Zip не найден, `iola archive doctor` и `iola onboard` устанавливают его автоматически.
|
|
4
|
+
|
|
5
|
+
Поддерживаемые форматы зависят от 7-Zip:
|
|
6
|
+
|
|
7
|
+
- `.zip`
|
|
8
|
+
- `.7z`
|
|
9
|
+
- `.rar`
|
|
10
|
+
- `.tar`
|
|
11
|
+
- `.gz`
|
|
12
|
+
- `.tgz`
|
|
13
|
+
- `.bz2`
|
|
14
|
+
- `.xz`
|
|
15
|
+
- другие форматы, которые поддерживает установленный 7-Zip
|
|
16
|
+
|
|
17
|
+
Команды:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
iola archive doctor
|
|
21
|
+
iola archive list docs.zip
|
|
22
|
+
iola archive test docs.zip
|
|
23
|
+
iola archive extract docs.zip --output ./out
|
|
24
|
+
iola archive create docs.zip ./docs
|
|
25
|
+
iola archive index docs.zip
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Индекс архива:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
iola index archive docs.zip
|
|
32
|
+
iola index search "контракт"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Мастер настройки:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
iola onboard
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Мастер проверяет Node/npm, SQLite, API, 7-Zip и предлагает подключить workspace, policy, Ollama, OpenAI, OpenRouter, Codex CLI, MCP и индекс локальных документов.
|
|
42
|
+
|
|
@@ -58,6 +58,10 @@ iola snapshot list
|
|
|
58
58
|
iola trace last
|
|
59
59
|
iola export education-contacts --format xlsx --output contacts.xlsx
|
|
60
60
|
iola changes list
|
|
61
|
+
iola archive doctor
|
|
62
|
+
iola archive list docs.zip
|
|
63
|
+
iola archive extract docs.zip --output ./out
|
|
64
|
+
iola index archive docs.zip
|
|
61
65
|
iola import file data.csv --dataset custom
|
|
62
66
|
iola index folder ./docs
|
|
63
67
|
iola reports list
|
|
@@ -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
|
-
|
|
@@ -27,6 +27,18 @@ iola index status
|
|
|
27
27
|
iola index search "школа 29"
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
Поддерживаемые форматы для чтения и индекса:
|
|
31
|
+
|
|
32
|
+
- `.docx`
|
|
33
|
+
- `.xlsx`
|
|
34
|
+
- `.pptx`
|
|
35
|
+
- `.pdf`
|
|
36
|
+
- `.md`
|
|
37
|
+
- `.txt`
|
|
38
|
+
- `.csv`
|
|
39
|
+
- `.json`
|
|
40
|
+
- `.html`
|
|
41
|
+
|
|
30
42
|
Пакеты отчетов:
|
|
31
43
|
|
|
32
44
|
```bash
|
|
@@ -51,4 +63,3 @@ iola mcp serve
|
|
|
51
63
|
```
|
|
52
64
|
|
|
53
65
|
По умолчанию MCP запускается на порту `daemon.port + 1`.
|
|
54
|
-
|