@iola_adm/iola-cli 0.1.26 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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,7 @@ 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;
105
111
  - cron-задачи, локальный daemon и RPC для автоматизаций;
106
112
  - контекстные файлы `IOLA.md` и `.iola/context.md`;
107
113
  - интеграция с публичным MCP-сервером Йошкар-Олы.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
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
@@ -17,7 +17,7 @@ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
17
17
  const LAST_GOOD_CONFIG_FILE = path.join(CONFIG_DIR, "config.last-good.json");
18
18
  const SECRETS_FILE = path.join(CONFIG_DIR, "secrets.json");
19
19
  const DB_FILE = path.join(CONFIG_DIR, "iola.db");
20
- const DB_SCHEMA_VERSION = 5;
20
+ const DB_SCHEMA_VERSION = 6;
21
21
  const LOCAL_TOOLS = ["search_local", "get_card", "export_data", "run_report", "save_view"];
22
22
  const FILE_TOOLS = ["files_tree", "files_read", "files_search", "files_write", "files_patch"];
23
23
  const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS];
@@ -242,6 +242,11 @@ const COMMANDS = new Map([
242
242
  ["skills", handleSkills],
243
243
  ["tools", handleTools],
244
244
  ["files", handleFiles],
245
+ ["changes", handleChanges],
246
+ ["import", handleImport],
247
+ ["index", handleIndex],
248
+ ["reports", handleReports],
249
+ ["plugins", handlePlugins],
245
250
  ["workspace", handleWorkspace],
246
251
  ["tasks", handleTasks],
247
252
  ["artifacts", handleArtifacts],
@@ -284,6 +289,7 @@ const COMMANDS = new Map([
284
289
  ["search", searchAll],
285
290
  ["mcp-info", showMcpInfo],
286
291
  ["setup", setupClient],
292
+ ["onboard", onboard],
287
293
  ]);
288
294
 
289
295
  export async function main(argv) {
@@ -352,6 +358,11 @@ Usage:
352
358
  iola skills list|show|paths|enable|disable
353
359
  iola tools list|toolsets|enable|disable|profile
354
360
  iola files status|mode|approvals|tree|read|search|write|patch
361
+ iola changes list|show|apply|discard
362
+ iola import file|folder
363
+ iola index folder|status|search
364
+ iola reports list|run
365
+ iola plugins list|install|run|remove
355
366
  iola workspace init|status|use|list
356
367
  iola tasks list|add|done|run
357
368
  iola artifacts list|show|open
@@ -366,7 +377,7 @@ Usage:
366
377
  iola memory show|add|set|clear|export
367
378
  iola hooks list|add|delete|run
368
379
  iola agents list|run
369
- iola mcp list|status|install|remove
380
+ iola mcp list|status|install|remove|serve
370
381
  iola cache status|warm|clear
371
382
  iola sync [--dataset schools|kindergartens]
372
383
  iola sync status
@@ -413,6 +424,7 @@ Usage:
413
424
  iola search TEXT [--limit 5] [--format table|json|csv]
414
425
  iola mcp-info [--json]
415
426
  iola setup codex
427
+ iola onboard
416
428
  iola version
417
429
 
418
430
  Environment:
@@ -604,6 +616,26 @@ async function handleAgentLine(line, state) {
604
616
  return false;
605
617
  }
606
618
 
619
+ if (command === "changes") {
620
+ await handleChanges(args);
621
+ return false;
622
+ }
623
+
624
+ if (command === "index") {
625
+ await handleIndex(args);
626
+ return false;
627
+ }
628
+
629
+ if (command === "reports") {
630
+ await handleReports(args);
631
+ return false;
632
+ }
633
+
634
+ if (command === "plugins") {
635
+ await handlePlugins(args);
636
+ return false;
637
+ }
638
+
607
639
  if (command === "workspace") {
608
640
  await handleWorkspace(args);
609
641
  return false;
@@ -757,6 +789,10 @@ async function handleAgentLine(line, state) {
757
789
  context: ["context", args],
758
790
  skills: ["skills", args],
759
791
  files: ["files", args],
792
+ changes: ["changes", args],
793
+ index: ["index", args],
794
+ reports: ["reports", args],
795
+ plugins: ["plugins", args],
760
796
  workspace: ["workspace", args],
761
797
  tasks: ["tasks", args],
762
798
  todos: ["tasks", args],
@@ -812,6 +848,10 @@ function printAgentHelp() {
812
848
  /permissions
813
849
  /tools
814
850
  /files status
851
+ /changes list
852
+ /index status
853
+ /reports list
854
+ /plugins list
815
855
  /workspace status
816
856
  /tasks list
817
857
  /artifacts list
@@ -1548,6 +1588,7 @@ async function handleWiki(args) {
1548
1588
  ["Skills и toolsets", `${base}/Skills-и-toolsets`],
1549
1589
  ["Локальные файлы", `${base}/Локальные-файлы`],
1550
1590
  ["Рабочая среда агента", `${base}/Рабочая-среда-агента`],
1591
+ ["Расширения и локальные данные", `${base}/Расширения-и-локальные-данные`],
1551
1592
  ["Daemon, RPC и cron", `${base}/Daemon-RPC-и-cron`],
1552
1593
  ["Контекст и память", `${base}/Контекст-и-память`],
1553
1594
  ["Команды", `${base}/Команды`],
@@ -1774,22 +1815,156 @@ async function handleFiles(args) {
1774
1815
  if (!target) throw new Error('Пример: iola files write report.md --text "..."');
1775
1816
  const text = options.text ?? rest.join(" ");
1776
1817
  if (!text) throw new Error('Для записи нужен --text "..." или текст после пути.');
1777
- await filesWrite(target, text, { append: Boolean(options.append) });
1778
- console.log(`Файл записан: ${target}`);
1818
+ if (options.stage) {
1819
+ const id = await stageFileChange("write", target, text);
1820
+ console.log(`Изменение подготовлено: ${id}`);
1821
+ } else {
1822
+ await filesWrite(target, text, { append: Boolean(options.append) });
1823
+ console.log(`Файл записан: ${target}`);
1824
+ }
1779
1825
  return;
1780
1826
  }
1781
1827
 
1782
1828
  if (action === "patch") {
1783
1829
  if (!target) throw new Error('Пример: iola files patch README.md --search old --replace new');
1784
1830
  if (!options.search || options.replace === undefined) throw new Error("Для patch нужны --search и --replace.");
1785
- const result = await filesPatch(target, options.search, options.replace);
1786
- printKeyValue(result);
1831
+ if (options.stage) {
1832
+ const current = await filesRead(target);
1833
+ const next = current.split(options.search).join(options.replace);
1834
+ const id = await stageFileChange("patch", target, next, current);
1835
+ console.log(`Изменение подготовлено: ${id}`);
1836
+ } else {
1837
+ const result = await filesPatch(target, options.search, options.replace);
1838
+ printKeyValue(result);
1839
+ }
1787
1840
  return;
1788
1841
  }
1789
1842
 
1790
1843
  throw new Error("Команды files: status, mode MODE, approvals POLICY, tree [PATH], read FILE, search TEXT, write FILE --text TEXT, patch FILE --search OLD --replace NEW.");
1791
1844
  }
1792
1845
 
1846
+ async function handleChanges(args) {
1847
+ const [action = "list", id] = args;
1848
+ if (action === "list" || action === "ls") {
1849
+ printTable(listChanges(), [["id", "ID"], ["kind", "Тип"], ["target", "Файл"], ["status", "Статус"], ["created_at", "Дата"]]);
1850
+ return;
1851
+ }
1852
+ if (action === "show") {
1853
+ const change = getChange(Number(id));
1854
+ console.log(unifiedPreview(change.before_text || "", change.after_text || ""));
1855
+ return;
1856
+ }
1857
+ if (action === "apply") {
1858
+ await applyChange(Number(id));
1859
+ console.log(`Изменение применено: ${id}`);
1860
+ return;
1861
+ }
1862
+ if (action === "discard") {
1863
+ updateChangeStatus(Number(id), "discarded");
1864
+ console.log(`Изменение отклонено: ${id}`);
1865
+ return;
1866
+ }
1867
+ throw new Error("Команды changes: list, show ID, apply ID, discard ID.");
1868
+ }
1869
+
1870
+ async function handleImport(args) {
1871
+ const [action, target, ...rest] = args;
1872
+ const options = parseOptions(rest);
1873
+ if (action === "file") {
1874
+ if (!target) throw new Error("Пример: iola import file data.csv --dataset custom");
1875
+ const dataset = options.dataset || path.basename(target, path.extname(target));
1876
+ const count = await importDataFile(target, dataset);
1877
+ console.log(`Импортировано записей: ${count}, dataset=${dataset}`);
1878
+ return;
1879
+ }
1880
+ if (action === "folder") {
1881
+ if (!target) throw new Error("Пример: iola import folder ./data");
1882
+ const rows = await filesTree(target, { depth: 1, limit: 200 });
1883
+ let total = 0;
1884
+ for (const row of rows.filter((item) => item.type === "file" && /\.(json|csv)$/i.test(item.path))) {
1885
+ total += await importDataFile(row.path, options.dataset || path.basename(row.path, path.extname(row.path)));
1886
+ }
1887
+ console.log(`Импортировано записей: ${total}`);
1888
+ return;
1889
+ }
1890
+ throw new Error("Команды import: file PATH --dataset NAME, folder PATH.");
1891
+ }
1892
+
1893
+ async function handleIndex(args) {
1894
+ const [action = "status", target, ...rest] = args;
1895
+ const options = parseOptions(rest);
1896
+ if (action === "status") {
1897
+ printKeyValue(getIndexStatus());
1898
+ return;
1899
+ }
1900
+ if (action === "folder") {
1901
+ if (!target) throw new Error("Пример: iola index folder ./docs");
1902
+ const count = await indexFolder(target, options);
1903
+ console.log(`Проиндексировано документов: ${count}`);
1904
+ return;
1905
+ }
1906
+ if (action === "search") {
1907
+ const query = [target, ...rest].filter(Boolean).join(" ");
1908
+ if (!query) throw new Error('Пример: iola index search "школа 29"');
1909
+ printTable(searchDocs(query, Number(options.limit || 20)), [["file", "Файл"], ["title", "Название"], ["snippet", "Фрагмент"]]);
1910
+ return;
1911
+ }
1912
+ throw new Error("Команды index: status, folder PATH, search TEXT.");
1913
+ }
1914
+
1915
+ async function handleReports(args) {
1916
+ const [action = "list", name, ...rest] = args;
1917
+ const packs = {
1918
+ "education-passport": ["education-contacts", "licenses"],
1919
+ "data-quality-pack": ["schools-summary", "missing-phones"],
1920
+ };
1921
+ if (action === "list") {
1922
+ printTable(Object.entries(packs).map(([pack, reports]) => ({ pack, reports: reports.join(", ") })), [["pack", "Пакет"], ["reports", "Отчеты"]]);
1923
+ return;
1924
+ }
1925
+ if (action === "run") {
1926
+ if (!packs[name]) throw new Error(`Пакет неизвестен: ${Object.keys(packs).join(", ")}`);
1927
+ const options = parseOptions(rest);
1928
+ const dir = options.output || path.join(process.cwd(), `iola-report-${name}-${Date.now()}`);
1929
+ await mkdir(dir, { recursive: true });
1930
+ for (const report of packs[name]) {
1931
+ await handleExport([report, "--format", "xlsx", "--output", path.join(dir, `${report}.xlsx`)]);
1932
+ await handleExport([report, "--format", "docx", "--output", path.join(dir, `${report}.docx`)]);
1933
+ }
1934
+ saveArtifact("report-pack", name, dir, { reports: packs[name] });
1935
+ console.log(`Пакет отчетов создан: ${dir}`);
1936
+ return;
1937
+ }
1938
+ throw new Error("Команды reports: list, run NAME [--output DIR].");
1939
+ }
1940
+
1941
+ async function handlePlugins(args) {
1942
+ const [action = "list", name, ...rest] = args;
1943
+ if (action === "list" || action === "ls") {
1944
+ printTable(listPlugins(), [["name", "Plugin"], ["source", "Источник"], ["command", "Команда"]]);
1945
+ return;
1946
+ }
1947
+ if (action === "install") {
1948
+ const options = parseOptions(rest);
1949
+ if (!name) throw new Error("Пример: iola plugins install my-plugin --command \"iola quality\"");
1950
+ savePlugin(name, options.source || name, options.command || "");
1951
+ console.log(`Plugin установлен: ${name}`);
1952
+ return;
1953
+ }
1954
+ if (action === "run") {
1955
+ const plugin = getPlugin(name);
1956
+ if (!plugin.command) throw new Error("У plugin нет command.");
1957
+ await main(splitCommandLine(plugin.command));
1958
+ return;
1959
+ }
1960
+ if (action === "remove" || action === "delete") {
1961
+ deletePlugin(name);
1962
+ console.log(`Plugin удален: ${name}`);
1963
+ return;
1964
+ }
1965
+ throw new Error("Команды plugins: list, install NAME --command CMD, run NAME, remove NAME.");
1966
+ }
1967
+
1793
1968
  async function handleWorkspace(args) {
1794
1969
  const [action = "status", nameOrPath] = args;
1795
1970
  const config = await loadConfig();
@@ -2309,7 +2484,13 @@ async function handleMcp(args) {
2309
2484
  return;
2310
2485
  }
2311
2486
 
2312
- throw new Error("Команды mcp: status, list, install codex, remove codex.");
2487
+ if (action === "serve") {
2488
+ const config = await loadConfig();
2489
+ await startMcpServer(config.daemon?.host || "127.0.0.1", Number(config.daemon?.port || DAEMON_PORT) + 1);
2490
+ return;
2491
+ }
2492
+
2493
+ throw new Error("Команды mcp: status, list, install codex, remove codex, serve.");
2313
2494
  }
2314
2495
 
2315
2496
  async function handleCache(args) {
@@ -3195,6 +3376,39 @@ function initDatabase() {
3195
3376
  path TEXT NOT NULL,
3196
3377
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
3197
3378
  );
3379
+ CREATE TABLE IF NOT EXISTS pending_changes (
3380
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3381
+ kind TEXT NOT NULL,
3382
+ target TEXT NOT NULL,
3383
+ before_text TEXT,
3384
+ after_text TEXT NOT NULL,
3385
+ status TEXT NOT NULL DEFAULT 'pending',
3386
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
3387
+ applied_at TEXT
3388
+ );
3389
+ CREATE TABLE IF NOT EXISTS custom_records (
3390
+ dataset TEXT NOT NULL,
3391
+ record_key TEXT NOT NULL,
3392
+ record_json TEXT NOT NULL,
3393
+ searchable_text TEXT NOT NULL,
3394
+ imported_at TEXT NOT NULL DEFAULT (datetime('now')),
3395
+ PRIMARY KEY(dataset, record_key)
3396
+ );
3397
+ CREATE VIRTUAL TABLE IF NOT EXISTS custom_records_fts USING fts5(dataset, record_key, searchable_text);
3398
+ CREATE TABLE IF NOT EXISTS doc_index (
3399
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3400
+ file TEXT NOT NULL,
3401
+ title TEXT,
3402
+ content TEXT NOT NULL,
3403
+ indexed_at TEXT NOT NULL DEFAULT (datetime('now'))
3404
+ );
3405
+ CREATE VIRTUAL TABLE IF NOT EXISTS doc_index_fts USING fts5(file, title, content);
3406
+ CREATE TABLE IF NOT EXISTS plugins (
3407
+ name TEXT PRIMARY KEY,
3408
+ source TEXT NOT NULL,
3409
+ command TEXT,
3410
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
3411
+ );
3198
3412
  `);
3199
3413
  rebuildFtsIfEmpty(db);
3200
3414
  db.prepare(`
@@ -3249,6 +3463,8 @@ function getDbStatus() {
3249
3463
  const cron = db.prepare("SELECT COUNT(*) AS count FROM cron_jobs").get();
3250
3464
  const tasks = db.prepare("SELECT COUNT(*) AS count FROM tasks WHERE status != 'done'").get();
3251
3465
  const artifacts = db.prepare("SELECT COUNT(*) AS count FROM artifacts").get();
3466
+ const docs = db.prepare("SELECT COUNT(*) AS count FROM doc_index").get();
3467
+ const custom = db.prepare("SELECT COUNT(*) AS count FROM custom_records").get();
3252
3468
  return {
3253
3469
  status: "ok",
3254
3470
  file: DB_FILE,
@@ -3262,6 +3478,8 @@ function getDbStatus() {
3262
3478
  cron_jobs: cron?.count ?? 0,
3263
3479
  open_tasks: tasks?.count ?? 0,
3264
3480
  artifacts: artifacts?.count ?? 0,
3481
+ indexed_docs: docs?.count ?? 0,
3482
+ custom_records: custom?.count ?? 0,
3265
3483
  };
3266
3484
  } finally {
3267
3485
  db.close();
@@ -5055,17 +5273,29 @@ async function setupClient(args) {
5055
5273
  console.log("Codex MCP и skill установлены.");
5056
5274
  }
5057
5275
 
5276
+ async function onboard(args = []) {
5277
+ const options = parseOptions(args);
5278
+ showBanner();
5279
+ initDatabase();
5280
+ await handleConfig(["validate"]);
5281
+ await doctor(["--summary"]);
5282
+ if (options.yes || await confirm("Инициализировать workspace? [Y/n] ")) await handleWorkspace(["init"]);
5283
+ if (options.yes || await confirm("Применить policy analyst? [Y/n] ")) await handlePolicy(["use", "analyst"]);
5284
+ if (options.yes || await confirm("Настроить AI сейчас? [y/N] ")) await aiSetup([]);
5285
+ console.log("Onboard завершен.");
5286
+ }
5287
+
5058
5288
  function parseOptions(args) {
5059
5289
  const result = { _: [] };
5060
5290
 
5061
5291
  for (let index = 0; index < args.length; index += 1) {
5062
5292
  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") {
5293
+ if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--append") {
5064
5294
  result[arg.slice(2)] = true;
5065
5295
  } else if (arg === "--check" || arg === "--upgrade-node") {
5066
5296
  result.check = true;
5067
5297
  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") {
5298
+ } else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--base-url" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--debug-file") {
5069
5299
  result[arg.slice(2)] = args[index + 1];
5070
5300
  index += 1;
5071
5301
  } else {
@@ -5432,6 +5662,34 @@ async function startDaemon(host, port) {
5432
5662
  await new Promise(() => {});
5433
5663
  }
5434
5664
 
5665
+ async function startMcpServer(host, port) {
5666
+ const server = createServer(async (req, res) => {
5667
+ try {
5668
+ res.setHeader("content-type", "application/json; charset=utf-8");
5669
+ if (req.method !== "POST") {
5670
+ res.end(JSON.stringify({ name: "iola-local-mcp", tools: ["status", "search", "card", "quality", "files.search", "index.search"] }));
5671
+ return;
5672
+ }
5673
+ const payload = JSON.parse(await readRequestBody(req) || "{}");
5674
+ const method = payload.method === "tools/call" ? payload.params?.name : payload.method;
5675
+ const args = payload.params?.arguments || payload.params || {};
5676
+ let result;
5677
+ if (method === "index.search") result = searchDocs(args.query || "", Number(args.limit || 20));
5678
+ else result = await executeRpc(method, { ...args, _: [] });
5679
+ res.end(JSON.stringify({ jsonrpc: "2.0", id: payload.id || null, result }));
5680
+ } catch (error) {
5681
+ res.statusCode = 500;
5682
+ res.end(JSON.stringify({ jsonrpc: "2.0", id: null, error: { message: error instanceof Error ? error.message : String(error) } }));
5683
+ }
5684
+ });
5685
+ await new Promise((resolve, reject) => {
5686
+ server.once("error", reject);
5687
+ server.listen(port, host, resolve);
5688
+ });
5689
+ console.log(`iola local MCP запущен: http://${host}:${port}`);
5690
+ await new Promise(() => {});
5691
+ }
5692
+
5435
5693
  function readRequestBody(req) {
5436
5694
  return new Promise((resolve, reject) => {
5437
5695
  let body = "";
@@ -5784,6 +6042,208 @@ async function restoreSnapshot(id) {
5784
6042
  await cp(row.path, row.workspace, { recursive: true, force: true });
5785
6043
  }
5786
6044
 
6045
+ async function stageFileChange(kind, target, afterText, beforeText = null) {
6046
+ const { resolved, relative } = await resolveFileTarget(target, kind === "patch" ? "edit" : "write");
6047
+ const before = beforeText ?? (existsSync(resolved) ? await readFile(resolved, "utf8").catch(() => "") : "");
6048
+ initDatabase();
6049
+ const db = openDatabase();
6050
+ try {
6051
+ const result = db.prepare("INSERT INTO pending_changes(kind, target, before_text, after_text) VALUES (?, ?, ?, ?)").run(kind, relative, before, afterText);
6052
+ return Number(result.lastInsertRowid);
6053
+ } finally {
6054
+ db.close();
6055
+ }
6056
+ }
6057
+
6058
+ function listChanges() {
6059
+ initDatabase();
6060
+ const db = openDatabase();
6061
+ try {
6062
+ return db.prepare("SELECT id, kind, target, status, created_at FROM pending_changes ORDER BY id DESC LIMIT 100").all();
6063
+ } finally {
6064
+ db.close();
6065
+ }
6066
+ }
6067
+
6068
+ function getChange(id) {
6069
+ initDatabase();
6070
+ const db = openDatabase();
6071
+ try {
6072
+ const row = db.prepare("SELECT * FROM pending_changes WHERE id = ?").get(id);
6073
+ if (!row) throw new Error(`Изменение не найдено: ${id}`);
6074
+ return row;
6075
+ } finally {
6076
+ db.close();
6077
+ }
6078
+ }
6079
+
6080
+ function updateChangeStatus(id, status) {
6081
+ initDatabase();
6082
+ const db = openDatabase();
6083
+ try {
6084
+ db.prepare("UPDATE pending_changes SET status = ?, applied_at = CASE WHEN ? = 'applied' THEN datetime('now') ELSE applied_at END WHERE id = ?").run(status, status, id);
6085
+ } finally {
6086
+ db.close();
6087
+ }
6088
+ }
6089
+
6090
+ async function applyChange(id) {
6091
+ const change = getChange(id);
6092
+ if (change.status !== "pending") throw new Error(`Изменение уже не pending: ${change.status}`);
6093
+ await filesWrite(change.target, change.after_text);
6094
+ updateChangeStatus(id, "applied");
6095
+ }
6096
+
6097
+ async function importDataFile(target, dataset) {
6098
+ const text = await filesRead(target, { maxBytes: 5_000_000 });
6099
+ const ext = path.extname(target).toLocaleLowerCase("ru-RU");
6100
+ let rows = [];
6101
+ if (ext === ".json") {
6102
+ const parsed = JSON.parse(text);
6103
+ rows = Array.isArray(parsed) ? parsed : normalizeItems(parsed);
6104
+ } else if (ext === ".csv") {
6105
+ rows = parseCsv(text);
6106
+ } else {
6107
+ throw new Error("Поддерживается импорт JSON и CSV.");
6108
+ }
6109
+ saveCustomRecords(dataset, rows);
6110
+ return rows.length;
6111
+ }
6112
+
6113
+ function parseCsv(text) {
6114
+ const lines = text.split(/\r?\n/).filter(Boolean);
6115
+ const headers = splitCsvLine(lines.shift() || "");
6116
+ return lines.map((line) => {
6117
+ const values = splitCsvLine(line);
6118
+ return Object.fromEntries(headers.map((header, index) => [header, values[index] || ""]));
6119
+ });
6120
+ }
6121
+
6122
+ function splitCsvLine(line) {
6123
+ const result = [];
6124
+ let current = "";
6125
+ let quote = false;
6126
+ for (let index = 0; index < line.length; index += 1) {
6127
+ const char = line[index];
6128
+ if (char === '"') quote = !quote;
6129
+ else if (char === "," && !quote) {
6130
+ result.push(current);
6131
+ current = "";
6132
+ } else current += char;
6133
+ }
6134
+ result.push(current);
6135
+ return result.map((value) => value.trim());
6136
+ }
6137
+
6138
+ function saveCustomRecords(dataset, rows) {
6139
+ initDatabase();
6140
+ const db = openDatabase();
6141
+ try {
6142
+ const insert = db.prepare("INSERT INTO custom_records(dataset, record_key, record_json, searchable_text) VALUES (?, ?, ?, ?) ON CONFLICT(dataset, record_key) DO UPDATE SET record_json = excluded.record_json, searchable_text = excluded.searchable_text, imported_at = datetime('now')");
6143
+ const insertFts = db.prepare("INSERT INTO custom_records_fts(dataset, record_key, searchable_text) VALUES (?, ?, ?)");
6144
+ db.prepare("DELETE FROM custom_records_fts WHERE dataset = ?").run(dataset);
6145
+ rows.forEach((row, index) => {
6146
+ const key = String(row.id || row.inn || index + 1);
6147
+ const json = JSON.stringify(row);
6148
+ const text = json.toLocaleLowerCase("ru-RU");
6149
+ insert.run(dataset, key, json, text);
6150
+ insertFts.run(dataset, key, text);
6151
+ });
6152
+ } finally {
6153
+ db.close();
6154
+ }
6155
+ }
6156
+
6157
+ async function indexFolder(target, options = {}) {
6158
+ const rows = await filesTree(target, { depth: Number(options.depth || 5), limit: Number(options.limit || 1000) });
6159
+ let count = 0;
6160
+ for (const row of rows.filter((item) => item.type === "file" && /\.(md|txt|csv|json|html)$/i.test(item.path))) {
6161
+ try {
6162
+ const text = await filesRead(row.path, { maxBytes: 1_000_000 });
6163
+ saveIndexedDoc(row.path, path.basename(row.path), text);
6164
+ count += 1;
6165
+ } catch {
6166
+ // Skip unreadable files.
6167
+ }
6168
+ }
6169
+ return count;
6170
+ }
6171
+
6172
+ function saveIndexedDoc(file, title, content) {
6173
+ initDatabase();
6174
+ const db = openDatabase();
6175
+ try {
6176
+ const result = db.prepare("INSERT INTO doc_index(file, title, content) VALUES (?, ?, ?)").run(file, title, content);
6177
+ db.prepare("INSERT INTO doc_index_fts(rowid, file, title, content) VALUES (?, ?, ?, ?)").run(Number(result.lastInsertRowid), file, title, content);
6178
+ } finally {
6179
+ db.close();
6180
+ }
6181
+ }
6182
+
6183
+ function getIndexStatus() {
6184
+ initDatabase();
6185
+ const db = openDatabase();
6186
+ try {
6187
+ const docs = db.prepare("SELECT COUNT(*) AS count FROM doc_index").get();
6188
+ return { docs: docs?.count || 0 };
6189
+ } finally {
6190
+ db.close();
6191
+ }
6192
+ }
6193
+
6194
+ function searchDocs(query, limit = 20) {
6195
+ initDatabase();
6196
+ const db = openDatabase();
6197
+ try {
6198
+ const rows = db.prepare("SELECT file, title, snippet(doc_index_fts, 2, '[', ']', '...', 16) AS snippet FROM doc_index_fts WHERE doc_index_fts MATCH ? LIMIT ?").all(toFtsQuery(query), limit);
6199
+ return rows;
6200
+ } finally {
6201
+ db.close();
6202
+ }
6203
+ }
6204
+
6205
+ function listPlugins() {
6206
+ initDatabase();
6207
+ const db = openDatabase();
6208
+ try {
6209
+ return db.prepare("SELECT name, source, COALESCE(command, '-') AS command FROM plugins ORDER BY name").all();
6210
+ } finally {
6211
+ db.close();
6212
+ }
6213
+ }
6214
+
6215
+ function savePlugin(name, source, command) {
6216
+ initDatabase();
6217
+ const db = openDatabase();
6218
+ try {
6219
+ db.prepare("INSERT INTO plugins(name, source, command) VALUES (?, ?, ?) ON CONFLICT(name) DO UPDATE SET source = excluded.source, command = excluded.command").run(name, source, command);
6220
+ } finally {
6221
+ db.close();
6222
+ }
6223
+ }
6224
+
6225
+ function getPlugin(name) {
6226
+ initDatabase();
6227
+ const db = openDatabase();
6228
+ try {
6229
+ const row = db.prepare("SELECT * FROM plugins WHERE name = ?").get(name);
6230
+ if (!row) throw new Error(`Plugin не найден: ${name}`);
6231
+ return row;
6232
+ } finally {
6233
+ db.close();
6234
+ }
6235
+ }
6236
+
6237
+ function deletePlugin(name) {
6238
+ initDatabase();
6239
+ const db = openDatabase();
6240
+ try {
6241
+ db.prepare("DELETE FROM plugins WHERE name = ?").run(name);
6242
+ } finally {
6243
+ db.close();
6244
+ }
6245
+ }
6246
+
5787
6247
  function unifiedPreview(before, after) {
5788
6248
  const beforeLines = before.split(/\r?\n/);
5789
6249
  const afterLines = after.split(/\r?\n/);
@@ -5831,6 +6291,9 @@ async function executeRpc(method, options = {}) {
5831
6291
  if (method === "files.search") {
5832
6292
  return filesSearch(options.query || options.search || "", options);
5833
6293
  }
6294
+ if (method === "index.search") {
6295
+ return searchDocs(options.query || options.search || "", Number(options.limit || 20));
6296
+ }
5834
6297
  throw new Error(`RPC method неизвестен: ${method}. Доступно: status, search, card, quality, sync.`);
5835
6298
  }
5836
6299
 
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
 
@@ -0,0 +1,54 @@
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
+ ```bash
33
+ iola reports list
34
+ iola reports run education-passport
35
+ iola reports run data-quality-pack
36
+ ```
37
+
38
+ Plugins:
39
+
40
+ ```bash
41
+ iola plugins list
42
+ iola plugins install quality --command "iola quality"
43
+ iola plugins run quality
44
+ iola plugins remove quality
45
+ ```
46
+
47
+ Локальный MCP endpoint:
48
+
49
+ ```bash
50
+ iola mcp serve
51
+ ```
52
+
53
+ По умолчанию MCP запускается на порту `daemon.port + 1`.
54
+