@iola_adm/iola-cli 0.1.15 → 0.1.17

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
@@ -103,7 +103,13 @@ iola cache status
103
103
  iola cache warm
104
104
  iola cache clear
105
105
  iola sync
106
+ iola sync status
107
+ iola diff schools
106
108
  iola search "Петрова" --local
109
+ iola search "Петрова" --local --fts
110
+ iola card "школа 29"
111
+ iola quality
112
+ iola quality missing-phones
107
113
  iola data schools --where address=Петрова --save schools-petrova
108
114
  iola views
109
115
  iola view schools-petrova --format csv --output schools-petrova.csv
@@ -121,6 +127,7 @@ iola version --check
121
127
  iola ask "Найди школу 29"
122
128
  iola ask "Найди школу 29" --profile codex --events --output answer.txt
123
129
  iola ask "Найди школу 29" --schema json --no-history
130
+ iola ask "выгрузи школы на Петрова в csv" --profile local --tools --reasoning verify
124
131
  iola data schools --format csv --output schools.csv
125
132
  iola data schools --limit 10
126
133
  iola data kindergartens --search "29"
@@ -381,7 +388,10 @@ iola data schools --cache
381
388
 
382
389
  ```bash
383
390
  iola sync
391
+ iola sync status
392
+ iola diff
384
393
  iola search "Петрова" --local
394
+ iola search "школа Петрова" --local --fts
385
395
  iola data schools --local --search "лицей"
386
396
  ```
387
397
 
@@ -409,6 +419,55 @@ iola petrova
409
419
  iola run "выгрузи школы на Петрова в csv"
410
420
  ```
411
421
 
422
+ ## Локальный tool-agent для слабых моделей
423
+
424
+ Для локального профиля Ollama доступен режим `--tools`. Он сделан специально
425
+ для маленьких моделей, которые хуже отвечают свободным текстом, но могут быть
426
+ полезны как планировщик действий.
427
+
428
+ В этом режиме CLI не доверяет модели напрямую. Модель предлагает JSON-план,
429
+ CLI валидирует список разрешенных tools и сам выполняет действия через
430
+ проверенные локальные функции:
431
+
432
+ - `search_local`
433
+ - `get_card`
434
+ - `export_data`
435
+ - `run_report`
436
+ - `save_view`
437
+
438
+ Пример:
439
+
440
+ ```bash
441
+ iola ask "выгрузи школы на Петрова в csv" --profile local --tools
442
+ iola ask "найди детсады без телефона" --profile local --tools --reasoning verify
443
+ ```
444
+
445
+ Режимы:
446
+
447
+ ```bash
448
+ --reasoning fast # один план
449
+ --reasoning verify # план с валидацией результата
450
+ --reasoning vote # несколько вариантов, выбирается валидный
451
+ ```
452
+
453
+ OpenAI, OpenRouter и Codex работают как раньше. `--tools` применяется только
454
+ к локальному Ollama-профилю, чтобы не менять поведение внешних провайдеров.
455
+
456
+ Карточки, качество данных и изменения:
457
+
458
+ ```bash
459
+ iola card schools 1215067180
460
+ iola card "школа 29"
461
+ iola quality
462
+ iola quality schools
463
+ iola quality missing-phones
464
+ iola quality invalid-emails
465
+ iola quality duplicate-inn
466
+ iola sync status
467
+ iola diff
468
+ iola diff schools
469
+ ```
470
+
412
471
  ## Переменные окружения
413
472
 
414
473
  ```bash
package/bin/iola.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
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
@@ -97,8 +97,11 @@ const COMMANDS = new Map([
97
97
  ["mcp", handleMcp],
98
98
  ["cache", handleCache],
99
99
  ["sync", handleSync],
100
+ ["diff", handleDiff],
100
101
  ["views", handleViews],
101
102
  ["view", handleView],
103
+ ["card", handleCard],
104
+ ["quality", handleQuality],
102
105
  ["report", handleReport],
103
106
  ["privacy", handlePrivacy],
104
107
  ["backup", handleBackup],
@@ -163,6 +166,11 @@ Usage:
163
166
  iola mcp list|status|install|remove
164
167
  iola cache status|warm|clear
165
168
  iola sync [--dataset schools|kindergartens]
169
+ iola sync status
170
+ iola diff [schools|kindergartens]
171
+ iola card schools 1215067180
172
+ iola card "школа 29"
173
+ iola quality [schools|kindergartens|missing-phones|invalid-emails|duplicate-inn]
166
174
  iola views
167
175
  iola view NAME [--format table|json|csv] [--output FILE]
168
176
  iola report schools-summary|education-contacts|missing-phones|licenses
@@ -175,7 +183,7 @@ Usage:
175
183
  iola config set api.mcpBaseUrl URL
176
184
  iola config reset
177
185
  iola update
178
- iola ask TEXT [--profile NAME] [--model MODEL] [--output FILE] [--schema json|table] [--events] [--no-history]
186
+ iola ask TEXT [--profile NAME] [--model MODEL] [--tools] [--reasoning fast|verify|vote] [--output FILE] [--schema json|table] [--events] [--no-history]
179
187
  iola data LAYER [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
180
188
  iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
181
189
  iola ai context TEXT [--json]
@@ -324,7 +332,7 @@ async function handleAgentLine(line, state) {
324
332
  return false;
325
333
  }
326
334
 
327
- if (command === "views" || command === "view" || command === "report" || command === "privacy" || command === "backup" || command === "alias" || command === "run") {
335
+ if (command === "diff" || command === "card" || command === "quality" || command === "views" || command === "view" || command === "report" || command === "privacy" || command === "backup" || command === "alias" || command === "run") {
328
336
  await COMMANDS.get(command)(args);
329
337
  return false;
330
338
  }
@@ -416,6 +424,7 @@ async function handleAgentLine(line, state) {
416
424
  mcp: ["mcp", args],
417
425
  cache: ["cache", args],
418
426
  sync: ["sync", args],
427
+ diff: ["diff", args],
419
428
  config: ["config", args],
420
429
  layers: ["layers", args],
421
430
  data: ["data", args],
@@ -449,6 +458,9 @@ function printAgentHelp() {
449
458
  /mcp status
450
459
  /cache status
451
460
  /sync
461
+ /diff
462
+ /card школа 29
463
+ /quality
452
464
  /views
453
465
  /config get
454
466
  /config set api.baseUrl URL
@@ -1109,6 +1121,16 @@ async function handleCache(args) {
1109
1121
  }
1110
1122
 
1111
1123
  async function handleSync(args) {
1124
+ const [action] = args;
1125
+ if (action === "status") {
1126
+ printTable(getSyncStatus(), [
1127
+ ["dataset", "Слой"],
1128
+ ["records", "Записей"],
1129
+ ["last_sync", "Последний sync"],
1130
+ ["status", "Статус"],
1131
+ ]);
1132
+ return;
1133
+ }
1112
1134
  const options = parseOptions(args);
1113
1135
  const datasets = options.dataset ? [options.dataset] : Object.keys(DATASETS);
1114
1136
  const rows = [];
@@ -1123,6 +1145,44 @@ async function handleSync(args) {
1123
1145
  ]);
1124
1146
  }
1125
1147
 
1148
+ async function handleDiff(args) {
1149
+ const [dataset] = args;
1150
+ const rows = listSyncChanges(dataset);
1151
+ printTable(rows, [
1152
+ ["created_at", "Дата"],
1153
+ ["dataset", "Слой"],
1154
+ ["change_type", "Тип"],
1155
+ ["record_key", "Ключ"],
1156
+ ["summary", "Сводка"],
1157
+ ]);
1158
+ }
1159
+
1160
+ async function handleCard(args) {
1161
+ await ensureLocalData();
1162
+ const options = parseOptions(args);
1163
+ const query = args.join(" ").trim();
1164
+ if (!query) throw new Error('Пример: iola card "школа 29"');
1165
+ const item = findCard(query);
1166
+ if (!item) throw new Error(`Объект не найден: ${query}`);
1167
+ if (options.json) {
1168
+ printJson(item);
1169
+ return;
1170
+ }
1171
+ printKeyValue(item);
1172
+ }
1173
+
1174
+ async function handleQuality(args) {
1175
+ const [scope = "all"] = args;
1176
+ await ensureLocalData();
1177
+ const rows = runQuality(scope);
1178
+ printTable(rows, [
1179
+ ["check", "Проверка"],
1180
+ ["dataset", "Слой"],
1181
+ ["count", "Кол-во"],
1182
+ ["sample", "Пример"],
1183
+ ]);
1184
+ }
1185
+
1126
1186
  async function handleViews(args) {
1127
1187
  const [action, name] = args;
1128
1188
  if (action === "delete" || action === "remove") {
@@ -1803,6 +1863,16 @@ function initDatabase() {
1803
1863
  message TEXT,
1804
1864
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
1805
1865
  );
1866
+ CREATE TABLE IF NOT EXISTS sync_changes (
1867
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1868
+ dataset TEXT NOT NULL,
1869
+ record_key TEXT NOT NULL,
1870
+ change_type TEXT NOT NULL,
1871
+ old_json TEXT,
1872
+ new_json TEXT,
1873
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1874
+ );
1875
+ CREATE INDEX IF NOT EXISTS idx_sync_changes_dataset_created_at ON sync_changes(dataset, created_at DESC);
1806
1876
  CREATE TABLE IF NOT EXISTS aliases (
1807
1877
  name TEXT PRIMARY KEY,
1808
1878
  command TEXT NOT NULL,
@@ -2174,17 +2244,29 @@ function saveLocalRecords(dataset, items) {
2174
2244
  initDatabase();
2175
2245
  const db = openDatabase();
2176
2246
  try {
2247
+ const oldRows = db.prepare("SELECT record_key, record_json FROM local_records WHERE dataset = ?").all(dataset);
2248
+ const oldMap = new Map(oldRows.map((row) => [row.record_key, row.record_json]));
2249
+ const newKeys = new Set();
2177
2250
  db.prepare("DELETE FROM local_records WHERE dataset = ?").run(dataset);
2178
2251
  db.prepare("DELETE FROM local_records_fts WHERE dataset = ?").run(dataset);
2179
2252
  const insert = db.prepare("INSERT INTO local_records(dataset, record_key, record_json, searchable_text, synced_at) VALUES (?, ?, ?, ?, datetime('now'))");
2180
2253
  const insertFts = db.prepare("INSERT INTO local_records_fts(dataset, record_key, searchable_text) VALUES (?, ?, ?)");
2254
+ const insertChange = db.prepare("INSERT INTO sync_changes(dataset, record_key, change_type, old_json, new_json) VALUES (?, ?, ?, ?, ?)");
2181
2255
  for (const item of items) {
2182
2256
  const summary = selectPublicSummary(item);
2183
2257
  const key = String(summary.inn || item.id || `${dataset}-${Math.random()}`);
2258
+ newKeys.add(key);
2259
+ const newJson = JSON.stringify(item);
2260
+ const oldJson = oldMap.get(key);
2261
+ if (!oldJson) insertChange.run(dataset, key, "added", null, newJson);
2262
+ else if (oldJson !== newJson) insertChange.run(dataset, key, "changed", oldJson, newJson);
2184
2263
  const text = JSON.stringify(summary).toLocaleLowerCase("ru-RU");
2185
- insert.run(dataset, key, JSON.stringify(item), text);
2264
+ insert.run(dataset, key, newJson, text);
2186
2265
  insertFts.run(dataset, key, text);
2187
2266
  }
2267
+ for (const [key, oldJson] of oldMap.entries()) {
2268
+ if (!newKeys.has(key)) insertChange.run(dataset, key, "removed", oldJson, null);
2269
+ }
2188
2270
  } finally {
2189
2271
  db.close();
2190
2272
  }
@@ -2213,6 +2295,14 @@ function searchLocalRecords(query, options = {}) {
2213
2295
  const dataset = options.dataset || "all";
2214
2296
  const limit = Number(options.limit || 20);
2215
2297
  try {
2298
+ if (options.fts && query) {
2299
+ const ftsQuery = query.split(/\s+/).filter(Boolean).map((term) => `"${term.replace(/"/g, "")}"`).join(" ");
2300
+ const params = dataset === "all" ? [ftsQuery, limit] : [ftsQuery, dataset, limit];
2301
+ const sql = dataset === "all"
2302
+ ? "SELECT r.record_json FROM local_records_fts f JOIN local_records r ON r.dataset=f.dataset AND r.record_key=f.record_key WHERE local_records_fts MATCH ? LIMIT ?"
2303
+ : "SELECT r.record_json FROM local_records_fts f JOIN local_records r ON r.dataset=f.dataset AND r.record_key=f.record_key WHERE local_records_fts MATCH ? AND f.dataset = ? LIMIT ?";
2304
+ return db.prepare(sql).all(...params).map((row) => selectPublicSummary(JSON.parse(row.record_json)));
2305
+ }
2216
2306
  const params = [];
2217
2307
  let sql = "SELECT dataset, record_json FROM local_records";
2218
2308
  const where = [];
@@ -2233,6 +2323,76 @@ function searchLocalRecords(query, options = {}) {
2233
2323
  }
2234
2324
  }
2235
2325
 
2326
+ function getSyncStatus() {
2327
+ initDatabase();
2328
+ const db = openDatabase();
2329
+ try {
2330
+ return Object.keys(DATASETS).map((dataset) => {
2331
+ const records = db.prepare("SELECT COUNT(*) AS count FROM local_records WHERE dataset = ?").get(dataset);
2332
+ const run = db.prepare("SELECT status, created_at FROM sync_runs WHERE dataset = ? ORDER BY id DESC LIMIT 1").get(dataset);
2333
+ return { dataset, records: records?.count || 0, last_sync: run?.created_at || "-", status: run?.status || "never" };
2334
+ });
2335
+ } finally {
2336
+ db.close();
2337
+ }
2338
+ }
2339
+
2340
+ function listSyncChanges(dataset) {
2341
+ initDatabase();
2342
+ const db = openDatabase();
2343
+ try {
2344
+ const rows = dataset
2345
+ ? db.prepare("SELECT * FROM sync_changes WHERE dataset = ? ORDER BY id DESC LIMIT 50").all(dataset)
2346
+ : db.prepare("SELECT * FROM sync_changes ORDER BY id DESC LIMIT 50").all();
2347
+ return rows.map((row) => ({
2348
+ ...row,
2349
+ summary: summarizeChange(row),
2350
+ }));
2351
+ } finally {
2352
+ db.close();
2353
+ }
2354
+ }
2355
+
2356
+ function summarizeChange(row) {
2357
+ const payload = row.new_json || row.old_json;
2358
+ if (!payload) return "-";
2359
+ try {
2360
+ const item = selectPublicSummary(JSON.parse(payload));
2361
+ return item.name || item.inn || "-";
2362
+ } catch {
2363
+ return "-";
2364
+ }
2365
+ }
2366
+
2367
+ function findCard(query) {
2368
+ const normalized = query.toLocaleLowerCase("ru-RU");
2369
+ const dataset = normalized.includes("сад") ? "kindergartens" : normalized.includes("школ") || normalized.includes("лицей") ? "schools" : "all";
2370
+ const inn = normalized.match(/\b\d{10,12}\b/)?.[0];
2371
+ const number = normalized.match(/\b\d{1,3}\b/)?.[0];
2372
+ const rows = searchLocalRecords(inn || number || query, { dataset, limit: 20, fts: false });
2373
+ if (inn) return rows.find((row) => String(row.inn) === inn) || null;
2374
+ if (number) return rows.find((row) => String(row.name || "").includes(`№ ${number}`) || String(row.name || "").includes(`№${number}`)) || rows[0] || null;
2375
+ return rows[0] || null;
2376
+ }
2377
+
2378
+ function runQuality(scope) {
2379
+ const datasets = ["schools", "kindergartens"];
2380
+ const rows = [];
2381
+ for (const dataset of datasets) {
2382
+ if (scope !== "all" && scope !== dataset && !scope.includes("-")) continue;
2383
+ const records = searchLocalRecords("", { dataset, limit: 1000 });
2384
+ const missingPhones = records.filter((item) => !item.phone || item.phone === "-");
2385
+ const invalidEmails = records.filter((item) => item.email && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(item.email));
2386
+ const innCounts = new Map();
2387
+ for (const item of records) innCounts.set(item.inn, (innCounts.get(item.inn) || 0) + 1);
2388
+ const duplicateInn = records.filter((item) => item.inn && innCounts.get(item.inn) > 1);
2389
+ if (scope === "all" || scope === dataset || scope === "missing-phones") rows.push({ check: "missing-phones", dataset, count: missingPhones.length, sample: missingPhones[0]?.name || "-" });
2390
+ if (scope === "all" || scope === dataset || scope === "invalid-emails") rows.push({ check: "invalid-emails", dataset, count: invalidEmails.length, sample: invalidEmails[0]?.email || "-" });
2391
+ if (scope === "all" || scope === dataset || scope === "duplicate-inn") rows.push({ check: "duplicate-inn", dataset, count: duplicateInn.length, sample: duplicateInn[0]?.inn || "-" });
2392
+ }
2393
+ return rows;
2394
+ }
2395
+
2236
2396
  function saveView(name, dataset, args) {
2237
2397
  initDatabase();
2238
2398
  const db = openDatabase();
@@ -2466,6 +2626,9 @@ async function aiAsk(args, context = {}) {
2466
2626
 
2467
2627
  const config = await loadConfig();
2468
2628
  const providerConfig = resolveAiProfile(config, options);
2629
+ if (options.tools && providerConfig.provider === "ollama") {
2630
+ return localToolAsk(question, providerConfig, options);
2631
+ }
2469
2632
  applyRuntimeConfig(providerConfig, options.config);
2470
2633
  const dataContext = await buildDataContext(question);
2471
2634
  emitEvent(options, "context_loaded", { schools: dataContext.schools.length, kindergartens: dataContext.kindergartens.length });
@@ -2529,6 +2692,132 @@ function resolveAiProfile(config, options = {}) {
2529
2692
  };
2530
2693
  }
2531
2694
 
2695
+ async function localToolAsk(question, providerConfig, options) {
2696
+ await ensureLocalData();
2697
+ const plan = await buildLocalToolPlan(question, providerConfig, options);
2698
+ const validated = validateToolPlan(plan);
2699
+ const result = await executeToolPlan(validated);
2700
+ const answer = formatToolResult(result, options);
2701
+
2702
+ if (!options["no-history"] && isFeatureEnabled("sqlite-history")) {
2703
+ recordAskHistory({
2704
+ question,
2705
+ answer,
2706
+ providerConfig,
2707
+ dataContext: { tool_plan: validated, tool_result: result },
2708
+ error: "",
2709
+ sessionId: null,
2710
+ });
2711
+ }
2712
+
2713
+ emitEvent(options, "tool_plan", { plan: validated });
2714
+ if (options.output) await writeFile(options.output, answer, "utf8");
2715
+ if (options.format === "json" || options.schema === "json") {
2716
+ printJson({ answer, plan: validated, result });
2717
+ } else {
2718
+ console.log(answer);
2719
+ }
2720
+ return answer;
2721
+ }
2722
+
2723
+ async function buildLocalToolPlan(question, providerConfig, options) {
2724
+ const mode = options.reasoning || "verify";
2725
+ const prompt = [
2726
+ "Ты планировщик CLI iola. Верни только JSON.",
2727
+ "Доступные tools: search_local, get_card, export_data, run_report, save_view.",
2728
+ "Схема: {\"steps\":[{\"tool\":\"search_local\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
2729
+ "Для выгрузки CSV добавь export_data с format=csv и output, если пользователь назвал файл.",
2730
+ `Вопрос: ${question}`,
2731
+ ].join("\n");
2732
+
2733
+ try {
2734
+ const raw = await callOllama(providerConfig, [{ role: "user", content: prompt }]);
2735
+ const parsed = parseJsonObject(raw);
2736
+ if (mode === "vote") {
2737
+ return chooseBestPlan([parsed, inferToolPlan(question)]);
2738
+ }
2739
+ return parsed;
2740
+ } catch {
2741
+ return inferToolPlan(question);
2742
+ }
2743
+ }
2744
+
2745
+ function parseJsonObject(text) {
2746
+ const match = String(text).match(/\{[\s\S]*\}/);
2747
+ if (!match) throw new Error("JSON-план не найден.");
2748
+ return JSON.parse(match[0]);
2749
+ }
2750
+
2751
+ function inferToolPlan(question) {
2752
+ const normalized = question.toLocaleLowerCase("ru-RU");
2753
+ const dataset = normalized.includes("сад") ? "kindergartens" : normalized.includes("школ") || normalized.includes("лицей") ? "schools" : "all";
2754
+ const steps = [];
2755
+ if (normalized.includes("без телефона")) {
2756
+ steps.push({ tool: "run_report", args: { name: "missing-phones" } });
2757
+ } else {
2758
+ const query = normalized.match(/петрова|школ[а-яё ]*\d+|сад[а-яё ]*\d+|лицей[а-яё ]*\d+/iu)?.[0] || question;
2759
+ steps.push({ tool: "search_local", args: { dataset, query, limit: 20 } });
2760
+ }
2761
+ if (normalized.includes("csv") || normalized.includes("выгруз")) {
2762
+ steps.push({ tool: "export_data", args: { format: "csv", output: normalized.match(/([a-z0-9_-]+\.csv)/i)?.[1] || "iola-export.csv" } });
2763
+ }
2764
+ return { steps };
2765
+ }
2766
+
2767
+ function chooseBestPlan(plans) {
2768
+ return plans.find((plan) => {
2769
+ try {
2770
+ validateToolPlan(plan);
2771
+ return true;
2772
+ } catch {
2773
+ return false;
2774
+ }
2775
+ }) || plans.at(-1);
2776
+ }
2777
+
2778
+ function validateToolPlan(plan) {
2779
+ const allowed = new Set(["search_local", "get_card", "export_data", "run_report", "save_view"]);
2780
+ if (!plan || !Array.isArray(plan.steps)) throw new Error("Некорректный tool-plan.");
2781
+ for (const step of plan.steps) {
2782
+ if (!allowed.has(step.tool)) throw new Error(`Недопустимый tool: ${step.tool}`);
2783
+ }
2784
+ return plan;
2785
+ }
2786
+
2787
+ async function executeToolPlan(plan) {
2788
+ let current = [];
2789
+ const outputs = [];
2790
+ for (const step of plan.steps) {
2791
+ if (step.tool === "search_local") {
2792
+ current = searchLocalRecords(step.args?.query || "", { dataset: step.args?.dataset || "all", limit: step.args?.limit || 20, fts: true });
2793
+ outputs.push({ tool: step.tool, rows: current.length });
2794
+ } else if (step.tool === "get_card") {
2795
+ const card = findCard(step.args?.query || "");
2796
+ current = card ? [card] : [];
2797
+ outputs.push({ tool: step.tool, rows: current.length });
2798
+ } else if (step.tool === "run_report") {
2799
+ current = runQuality(step.args?.name || "all");
2800
+ outputs.push({ tool: step.tool, rows: current.length });
2801
+ } else if (step.tool === "save_view") {
2802
+ saveView(step.args?.name, step.args?.dataset || "all", step.args?.args || []);
2803
+ outputs.push({ tool: step.tool, saved: step.args?.name });
2804
+ } else if (step.tool === "export_data") {
2805
+ const text = step.args?.format === "json" ? JSON.stringify(current, null, 2) : toCsv(current);
2806
+ await writeFile(step.args?.output || "iola-export.csv", text, "utf8");
2807
+ outputs.push({ tool: step.tool, output: step.args?.output || "iola-export.csv", rows: current.length });
2808
+ }
2809
+ }
2810
+ return { rows: current, outputs };
2811
+ }
2812
+
2813
+ function formatToolResult(result, options) {
2814
+ if (options.schema === "json") return JSON.stringify(result, null, 2);
2815
+ const exported = result.outputs.find((item) => item.output);
2816
+ if (exported) return `Готово. Файл сохранен: ${exported.output}. Записей: ${exported.rows}`;
2817
+ if (!result.rows.length) return "Данных не найдено.";
2818
+ return result.rows.slice(0, 10).map((row) => `${row.name || row.check}: ${row.address || row.count || ""}`).join("\n");
2819
+ }
2820
+
2532
2821
  function applyRuntimeConfig(target, value) {
2533
2822
  if (!value) {
2534
2823
  return;
@@ -2912,7 +3201,7 @@ async function listDataset(dataset, args) {
2912
3201
  params.set("offset", options.offset || "0");
2913
3202
 
2914
3203
  const data = options.local
2915
- ? searchLocalRecords(options.search || "", { dataset, limit: Number(options.limit || 20) })
3204
+ ? searchLocalRecords(options.search || options._.join(" ") || "", { dataset, limit: Number(options.limit || 20), fts: options.fts })
2916
3205
  : normalizeItems(await fetchJsonMaybeCached(`${await getApiBaseUrl()}/${DATASETS[dataset].endpoint}?${params}`, options));
2917
3206
  const items = data;
2918
3207
  const filtered = applyDatasetFilters(items, options);
@@ -2968,8 +3257,8 @@ async function searchAll(args) {
2968
3257
  const limit = Number(options.limit || 5);
2969
3258
  const [schools, kindergartens] = options.local
2970
3259
  ? [
2971
- searchLocalRecords(query, { dataset: "schools", limit }),
2972
- searchLocalRecords(query, { dataset: "kindergartens", limit }),
3260
+ searchLocalRecords(query, { dataset: "schools", limit, fts: options.fts }),
3261
+ searchLocalRecords(query, { dataset: "kindergartens", limit, fts: options.fts }),
2973
3262
  ]
2974
3263
  : await Promise.all([
2975
3264
  fetchJsonMaybeCached(`${await getApiBaseUrl()}/schools?limit=100&offset=0`, options),
@@ -3018,12 +3307,12 @@ function parseOptions(args) {
3018
3307
 
3019
3308
  for (let index = 0; index < args.length; index += 1) {
3020
3309
  const arg = args[index];
3021
- if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--local" || arg === "--cache") {
3310
+ if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--fts") {
3022
3311
  result[arg.slice(2)] = true;
3023
3312
  } else if (arg === "--check" || arg === "--upgrade-node") {
3024
3313
  result.check = true;
3025
3314
  result[arg.slice(2)] = true;
3026
- } else if (arg === "--limit" || arg === "--offset" || arg === "--search" || 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") {
3315
+ } else if (arg === "--limit" || arg === "--offset" || arg === "--search" || 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") {
3027
3316
  result[arg.slice(2)] = args[index + 1];
3028
3317
  index += 1;
3029
3318
  } else {