@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "CLI и AI-агент для работы с открытыми данными городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
package/src/cli.js CHANGED
@@ -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 = 6;
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
- 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([]);
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 readFile(resolved, "utf8");
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(/&quot;/g, "\"")
6229
+ .replace(/&apos;/g, "'")
6230
+ .replace(/&lt;/g, "<")
6231
+ .replace(/&gt;/g, ">")
6232
+ .replace(/&amp;/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" && /\.(md|txt|csv|json|html)$/i.test(item.path))) {
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
-