@iola_adm/iola-cli 0.1.13 → 0.1.14

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.
Files changed (3) hide show
  1. package/README.md +45 -0
  2. package/package.json +1 -1
  3. package/src/cli.js +444 -14
package/README.md CHANGED
@@ -91,12 +91,22 @@ iola db status
91
91
  iola db init
92
92
  iola history --limit 20
93
93
  iola history clear
94
+ iola sessions --limit 20
95
+ iola resume 1 "продолжи"
96
+ iola fork 1 "новый вопрос"
97
+ iola features list
98
+ iola features enable api-cache
99
+ iola mcp status
100
+ iola mcp list
101
+ iola mcp install codex
94
102
  iola config get
95
103
  iola config set api.baseUrl https://apiiola.yasg.ru/api/v1
96
104
  iola config reset
97
105
  iola update
98
106
  iola version --check
99
107
  iola ask "Найди школу 29"
108
+ iola ask "Найди школу 29" --profile codex --events --output answer.txt
109
+ iola ask "Найди школу 29" --schema json --no-history
100
110
  iola data schools --limit 10
101
111
  iola data kindergartens --search "29"
102
112
  iola data schools --where address=Петрова --columns name,address,phone
@@ -144,6 +154,10 @@ iola agent
144
154
  /health
145
155
  /doctor
146
156
  /db status
157
+ /sessions
158
+ /resume 1
159
+ /features list
160
+ /mcp status
147
161
  /config get
148
162
  /layers
149
163
  /data schools --limit 10
@@ -300,11 +314,42 @@ iola db reset
300
314
  iola history --limit 20
301
315
  iola history --json
302
316
  iola history clear
317
+ iola sessions --limit 20
318
+ iola sessions clear
319
+ iola resume 1 "продолжи"
320
+ iola fork 1 "новая ветка разговора"
303
321
  ```
304
322
 
305
323
  Ключи OpenAI/OpenRouter в SQLite не сохраняются. Они остаются в локальном
306
324
  `secrets.json` или в переменных окружения.
307
325
 
326
+ ## Feature flags, MCP и машинный вывод
327
+
328
+ Экспериментальные и системные возможности можно включать отдельно:
329
+
330
+ ```bash
331
+ iola features list
332
+ iola features enable api-cache
333
+ iola features disable sqlite-history
334
+ ```
335
+
336
+ MCP-интеграции:
337
+
338
+ ```bash
339
+ iola mcp status
340
+ iola mcp list
341
+ iola mcp install codex
342
+ iola mcp remove codex
343
+ ```
344
+
345
+ Для автоматизации доступны события, JSON-ответ и запись результата в файл:
346
+
347
+ ```bash
348
+ iola ask "Найди школу 29" --events
349
+ iola ask "Найди школу 29" --schema json
350
+ iola ask "Найди школу 29" --output answer.txt
351
+ ```
352
+
308
353
  ## Переменные окружения
309
354
 
310
355
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
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
@@ -14,7 +14,15 @@ const CONFIG_DIR = path.join(os.homedir(), ".iola");
14
14
  const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
15
15
  const SECRETS_FILE = path.join(CONFIG_DIR, "secrets.json");
16
16
  const DB_FILE = path.join(CONFIG_DIR, "iola.db");
17
- const DB_SCHEMA_VERSION = 1;
17
+ const DB_SCHEMA_VERSION = 2;
18
+ const FEATURES = {
19
+ "sqlite-history": { stage: "stable", defaultEnabled: true, description: "Запись истории AI-запросов в SQLite." },
20
+ sessions: { stage: "stable", defaultEnabled: true, description: "Сессии, resume и fork для AI-диалогов." },
21
+ "api-cache": { stage: "experimental", defaultEnabled: false, description: "Локальный кеш API-ответов." },
22
+ events: { stage: "experimental", defaultEnabled: true, description: "JSONL-события выполнения ask." },
23
+ "mcp-management": { stage: "stable", defaultEnabled: true, description: "Команды управления MCP-интеграциями." },
24
+ "web-search": { stage: "experimental", defaultEnabled: false, description: "Резерв под web-search режимы AI." },
25
+ };
18
26
  const DEFAULT_AI_CONFIG = {
19
27
  api: {
20
28
  baseUrl: "https://apiiola.yasg.ru/api/v1",
@@ -82,6 +90,11 @@ const COMMANDS = new Map([
82
90
  ["doctor", doctor],
83
91
  ["db", handleDb],
84
92
  ["history", handleHistory],
93
+ ["sessions", handleSessions],
94
+ ["resume", resumeSession],
95
+ ["fork", forkSession],
96
+ ["features", handleFeatures],
97
+ ["mcp", handleMcp],
85
98
  ["config", handleConfig],
86
99
  ["banner", showBanner],
87
100
  ["agent", startAgent],
@@ -129,12 +142,17 @@ Usage:
129
142
  iola db init
130
143
  iola history [--limit 20]
131
144
  iola history clear
145
+ iola sessions [--limit 20]
146
+ iola resume SESSION_ID [TEXT]
147
+ iola fork SESSION_ID [TEXT]
148
+ iola features list|enable|disable
149
+ iola mcp list|status|install|remove
132
150
  iola config get
133
151
  iola config set api.baseUrl URL
134
152
  iola config set api.mcpBaseUrl URL
135
153
  iola config reset
136
154
  iola update
137
- iola ask TEXT
155
+ iola ask TEXT [--profile NAME] [--model MODEL] [--output FILE] [--schema json|table] [--events] [--no-history]
138
156
  iola data LAYER [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
139
157
  iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
140
158
  iola ai context TEXT [--json]
@@ -248,6 +266,31 @@ async function handleAgentLine(line, state) {
248
266
  return false;
249
267
  }
250
268
 
269
+ if (command === "sessions") {
270
+ await handleSessions(args);
271
+ return false;
272
+ }
273
+
274
+ if (command === "resume") {
275
+ await resumeSession(args);
276
+ return false;
277
+ }
278
+
279
+ if (command === "fork") {
280
+ await forkSession(args);
281
+ return false;
282
+ }
283
+
284
+ if (command === "features") {
285
+ await handleFeatures(args);
286
+ return false;
287
+ }
288
+
289
+ if (command === "mcp") {
290
+ await handleMcp(args);
291
+ return false;
292
+ }
293
+
251
294
  if (command === "config") {
252
295
  await handleConfig(args.length > 0 ? args : ["get"]);
253
296
  return false;
@@ -328,6 +371,11 @@ async function handleAgentLine(line, state) {
328
371
  doctor: ["doctor", args],
329
372
  db: ["db", args],
330
373
  history: ["history", args],
374
+ sessions: ["sessions", args],
375
+ resume: ["resume", args],
376
+ fork: ["fork", args],
377
+ features: ["features", args],
378
+ mcp: ["mcp", args],
331
379
  config: ["config", args],
332
380
  layers: ["layers", args],
333
381
  data: ["data", args],
@@ -355,6 +403,10 @@ function printAgentHelp() {
355
403
  /health
356
404
  /doctor
357
405
  /db status
406
+ /sessions
407
+ /resume SESSION_ID
408
+ /features list
409
+ /mcp status
358
410
  /config get
359
411
  /config set api.baseUrl URL
360
412
  /layers
@@ -520,6 +572,20 @@ async function doctor(args = []) {
520
572
  return;
521
573
  }
522
574
 
575
+ if (options.summary) {
576
+ printTable([
577
+ { group: "cli", status: report.cli.nodeStatus === "ok" && report.cli.update !== "available" ? "ok" : "check" },
578
+ { group: "sqlite", status: report.db.status },
579
+ { group: "api", status: report.api.health },
580
+ { group: "ai", status: report.ai.provider },
581
+ { group: "ollama", status: report.ai.ollama },
582
+ ], [
583
+ ["group", "Группа"],
584
+ ["status", "Статус"],
585
+ ]);
586
+ return;
587
+ }
588
+
523
589
  console.log("CLI");
524
590
  printKeyValue(report.cli);
525
591
  console.log("");
@@ -533,6 +599,11 @@ async function doctor(args = []) {
533
599
  printKeyValue(report.ai);
534
600
  console.log("");
535
601
  printDiagnostics(diagnostics, recommendOllamaModel(diagnostics));
602
+ if (options.all) {
603
+ console.log("");
604
+ console.log("Фичи");
605
+ await handleFeatures(["list"]);
606
+ }
536
607
  }
537
608
 
538
609
  function getUpdateStatus(current, latest) {
@@ -853,6 +924,128 @@ async function handleHistory(args) {
853
924
  ]);
854
925
  }
855
926
 
927
+ async function handleSessions(args) {
928
+ const [action] = args;
929
+ if (action === "clear") {
930
+ clearSessions();
931
+ console.log("Сессии очищены.");
932
+ return;
933
+ }
934
+
935
+ const options = parseOptions(args);
936
+ const rows = listSessions(Number(options.limit || 20));
937
+
938
+ if (options.json) {
939
+ printJson(rows);
940
+ return;
941
+ }
942
+
943
+ printTable(rows, [
944
+ ["id", "ID"],
945
+ ["updated_at", "Обновлена"],
946
+ ["profile", "Профиль"],
947
+ ["provider", "Провайдер"],
948
+ ["model", "Модель"],
949
+ ["messages", "Сообщ."],
950
+ ["title", "Название"],
951
+ ]);
952
+ }
953
+
954
+ async function resumeSession(args) {
955
+ const [sessionId, ...questionParts] = args;
956
+ if (!sessionId) {
957
+ throw new Error("SESSION_ID обязателен. Пример: iola resume 1 \"продолжи\"");
958
+ }
959
+
960
+ const question = questionParts.join(" ").trim();
961
+ if (!question) {
962
+ printSessionMessages(Number(sessionId));
963
+ return;
964
+ }
965
+
966
+ const session = getSession(Number(sessionId));
967
+ await aiAsk([question, "--session", sessionId, "--profile", session.profile || "local"]);
968
+ }
969
+
970
+ async function forkSession(args) {
971
+ const [sessionId, ...questionParts] = args;
972
+ if (!sessionId) {
973
+ throw new Error("SESSION_ID обязателен. Пример: iola fork 1 \"новый вопрос\"");
974
+ }
975
+
976
+ const forkedId = forkSessionInDb(Number(sessionId));
977
+ console.log(`Создана новая сессия: ${forkedId}`);
978
+ const question = questionParts.join(" ").trim();
979
+ if (question) {
980
+ const session = getSession(forkedId);
981
+ await aiAsk([question, "--session", String(forkedId), "--profile", session.profile || "local"]);
982
+ }
983
+ }
984
+
985
+ async function handleFeatures(args) {
986
+ const [action = "list", name] = args;
987
+
988
+ if (action === "list" || action === "ls") {
989
+ const rows = listFeatures();
990
+ printTable(rows, [
991
+ ["name", "Фича"],
992
+ ["enabled", "Вкл"],
993
+ ["stage", "Стадия"],
994
+ ["description", "Описание"],
995
+ ]);
996
+ return;
997
+ }
998
+
999
+ if (action === "enable" || action === "disable") {
1000
+ if (!name || !FEATURES[name]) {
1001
+ throw new Error(`Неизвестная фича. Доступно: ${Object.keys(FEATURES).join(", ")}`);
1002
+ }
1003
+ setFeatureEnabled(name, action === "enable");
1004
+ console.log(`${name}: ${action === "enable" ? "enabled" : "disabled"}`);
1005
+ return;
1006
+ }
1007
+
1008
+ throw new Error("Команды features: list, enable NAME, disable NAME.");
1009
+ }
1010
+
1011
+ async function handleMcp(args) {
1012
+ const [action = "status", target = "codex"] = args;
1013
+
1014
+ if (action === "status") {
1015
+ const [health, version] = await Promise.all([
1016
+ fetchJson(`${await getMcpBaseUrl()}/mcp-health`),
1017
+ fetchJson(`${await getMcpBaseUrl()}/mcp-version`),
1018
+ ]);
1019
+ printKeyValue({
1020
+ endpoint: `${await getMcpBaseUrl()}/mcp`,
1021
+ status: health.status,
1022
+ server_version: version.server_version,
1023
+ layers: version.data_layers?.map((layer) => layer.id).join(", "),
1024
+ });
1025
+ return;
1026
+ }
1027
+
1028
+ if (action === "list") {
1029
+ await runCommand("codex", ["mcp", "list"], { inherit: true });
1030
+ return;
1031
+ }
1032
+
1033
+ if (action === "install" || action === "add") {
1034
+ await setupClient([target]);
1035
+ return;
1036
+ }
1037
+
1038
+ if (action === "remove" || action === "delete") {
1039
+ if (target !== "codex") {
1040
+ throw new Error("Пока доступно удаление только Codex MCP.");
1041
+ }
1042
+ await runCommand("codex", ["mcp", "remove", "yoshkarOlaPublicData"], { inherit: true });
1043
+ return;
1044
+ }
1045
+
1046
+ throw new Error("Команды mcp: status, list, install codex, remove codex.");
1047
+ }
1048
+
856
1049
  async function aiDoctor(args) {
857
1050
  const options = parseOptions(args);
858
1051
  const diagnostics = await getLocalDiagnostics();
@@ -1367,6 +1560,32 @@ function initDatabase() {
1367
1560
  error TEXT
1368
1561
  );
1369
1562
  CREATE INDEX IF NOT EXISTS idx_ask_history_created_at ON ask_history(created_at DESC);
1563
+ CREATE TABLE IF NOT EXISTS sessions (
1564
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1565
+ parent_id INTEGER,
1566
+ title TEXT NOT NULL,
1567
+ profile TEXT,
1568
+ provider TEXT,
1569
+ model TEXT,
1570
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1571
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1572
+ );
1573
+ CREATE INDEX IF NOT EXISTS idx_sessions_updated_at ON sessions(updated_at DESC);
1574
+ CREATE TABLE IF NOT EXISTS session_messages (
1575
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1576
+ session_id INTEGER NOT NULL,
1577
+ role TEXT NOT NULL,
1578
+ content TEXT NOT NULL,
1579
+ context_json TEXT,
1580
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1581
+ FOREIGN KEY(session_id) REFERENCES sessions(id) ON DELETE CASCADE
1582
+ );
1583
+ CREATE INDEX IF NOT EXISTS idx_session_messages_session_id ON session_messages(session_id, id);
1584
+ CREATE TABLE IF NOT EXISTS feature_flags (
1585
+ name TEXT PRIMARY KEY,
1586
+ enabled INTEGER NOT NULL,
1587
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1588
+ );
1370
1589
  CREATE TABLE IF NOT EXISTS api_cache (
1371
1590
  key TEXT PRIMARY KEY,
1372
1591
  url TEXT NOT NULL,
@@ -1406,11 +1625,13 @@ function getDbStatus() {
1406
1625
  try {
1407
1626
  const schema = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
1408
1627
  const history = db.prepare("SELECT COUNT(*) AS count FROM ask_history").get();
1628
+ const sessions = db.prepare("SELECT COUNT(*) AS count FROM sessions").get();
1409
1629
  return {
1410
1630
  status: "ok",
1411
1631
  file: DB_FILE,
1412
1632
  schema: schema?.value || "-",
1413
1633
  history: history?.count ?? 0,
1634
+ sessions: sessions?.count ?? 0,
1414
1635
  };
1415
1636
  } finally {
1416
1637
  db.close();
@@ -1426,7 +1647,7 @@ function getDbStatus() {
1426
1647
  }
1427
1648
  }
1428
1649
 
1429
- function recordAskHistory({ question, answer, providerConfig, dataContext, error }) {
1650
+ function recordAskHistory({ question, answer, providerConfig, dataContext, error, sessionId }) {
1430
1651
  try {
1431
1652
  initDatabase();
1432
1653
  const db = openDatabase();
@@ -1476,6 +1697,171 @@ function clearHistory() {
1476
1697
  }
1477
1698
  }
1478
1699
 
1700
+ function clearSessions() {
1701
+ initDatabase();
1702
+ const db = openDatabase();
1703
+ try {
1704
+ db.exec("DELETE FROM session_messages; DELETE FROM sessions;");
1705
+ } finally {
1706
+ db.close();
1707
+ }
1708
+ }
1709
+
1710
+ function createSession(providerConfig, title, parentId = null) {
1711
+ initDatabase();
1712
+ const db = openDatabase();
1713
+ try {
1714
+ const result = db.prepare(`
1715
+ INSERT INTO sessions(parent_id, title, profile, provider, model)
1716
+ VALUES (?, ?, ?, ?, ?)
1717
+ `).run(parentId, title.slice(0, 120), providerConfig.name || "", providerConfig.provider || "", providerConfig.model || "");
1718
+ return Number(result.lastInsertRowid);
1719
+ } finally {
1720
+ db.close();
1721
+ }
1722
+ }
1723
+
1724
+ function ensureSessionForAsk(options, providerConfig, question) {
1725
+ if (options.session) {
1726
+ return Number(options.session);
1727
+ }
1728
+ return createSession(providerConfig, question);
1729
+ }
1730
+
1731
+ function appendSessionExchange(sessionId, question, answer, dataContext, error) {
1732
+ if (!sessionId) {
1733
+ return;
1734
+ }
1735
+ const db = openDatabase();
1736
+ try {
1737
+ db.prepare("INSERT INTO session_messages(session_id, role, content, context_json) VALUES (?, 'user', ?, ?)")
1738
+ .run(sessionId, question, JSON.stringify(dataContext));
1739
+ db.prepare("INSERT INTO session_messages(session_id, role, content, context_json) VALUES (?, 'assistant', ?, ?)")
1740
+ .run(sessionId, error || answer || "", JSON.stringify({ error: error || "" }));
1741
+ db.prepare("UPDATE sessions SET updated_at = datetime('now') WHERE id = ?").run(sessionId);
1742
+ } finally {
1743
+ db.close();
1744
+ }
1745
+ }
1746
+
1747
+ function listSessions(limit) {
1748
+ initDatabase();
1749
+ const db = openDatabase();
1750
+ try {
1751
+ return db.prepare(`
1752
+ SELECT s.id, s.updated_at, s.profile, s.provider, s.model, s.title, COUNT(m.id) AS messages
1753
+ FROM sessions s
1754
+ LEFT JOIN session_messages m ON m.session_id = s.id
1755
+ GROUP BY s.id
1756
+ ORDER BY s.updated_at DESC, s.id DESC
1757
+ LIMIT ?
1758
+ `).all(limit);
1759
+ } finally {
1760
+ db.close();
1761
+ }
1762
+ }
1763
+
1764
+ function getSessionAiHistory(sessionId) {
1765
+ initDatabase();
1766
+ const db = openDatabase();
1767
+ try {
1768
+ return db.prepare("SELECT role, content FROM session_messages WHERE session_id = ? ORDER BY id ASC")
1769
+ .all(sessionId)
1770
+ .map((row) => ({ role: row.role, content: row.content }));
1771
+ } finally {
1772
+ db.close();
1773
+ }
1774
+ }
1775
+
1776
+ function getSession(sessionId) {
1777
+ initDatabase();
1778
+ const db = openDatabase();
1779
+ try {
1780
+ const session = db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
1781
+ if (!session) {
1782
+ throw new Error(`Сессия не найдена: ${sessionId}`);
1783
+ }
1784
+ return session;
1785
+ } finally {
1786
+ db.close();
1787
+ }
1788
+ }
1789
+
1790
+ function printSessionMessages(sessionId) {
1791
+ const rows = getSessionAiHistory(sessionId).map((row, index) => ({ id: index + 1, ...row }));
1792
+ printTable(rows, [
1793
+ ["id", "#"],
1794
+ ["role", "Роль"],
1795
+ ["content", "Текст"],
1796
+ ]);
1797
+ }
1798
+
1799
+ function forkSessionInDb(sessionId) {
1800
+ initDatabase();
1801
+ const db = openDatabase();
1802
+ try {
1803
+ const session = db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
1804
+ if (!session) {
1805
+ throw new Error(`Сессия не найдена: ${sessionId}`);
1806
+ }
1807
+ const result = db.prepare(`
1808
+ INSERT INTO sessions(parent_id, title, profile, provider, model)
1809
+ VALUES (?, ?, ?, ?, ?)
1810
+ `).run(sessionId, `Fork: ${session.title}`, session.profile, session.provider, session.model);
1811
+ const newId = Number(result.lastInsertRowid);
1812
+ const messages = db.prepare("SELECT role, content, context_json FROM session_messages WHERE session_id = ? ORDER BY id ASC").all(sessionId);
1813
+ const insert = db.prepare("INSERT INTO session_messages(session_id, role, content, context_json) VALUES (?, ?, ?, ?)");
1814
+ for (const message of messages) {
1815
+ insert.run(newId, message.role, message.content, message.context_json);
1816
+ }
1817
+ return newId;
1818
+ } finally {
1819
+ db.close();
1820
+ }
1821
+ }
1822
+
1823
+ function listFeatures() {
1824
+ initDatabase();
1825
+ const db = openDatabase();
1826
+ try {
1827
+ return Object.entries(FEATURES).map(([name, meta]) => {
1828
+ const row = db.prepare("SELECT enabled FROM feature_flags WHERE name = ?").get(name);
1829
+ const enabled = row ? Boolean(row.enabled) : meta.defaultEnabled;
1830
+ return { name, enabled: enabled ? "yes" : "no", stage: meta.stage, description: meta.description };
1831
+ });
1832
+ } finally {
1833
+ db.close();
1834
+ }
1835
+ }
1836
+
1837
+ function isFeatureEnabled(name) {
1838
+ const meta = FEATURES[name];
1839
+ if (!meta) {
1840
+ return false;
1841
+ }
1842
+ initDatabase();
1843
+ const db = openDatabase();
1844
+ try {
1845
+ const row = db.prepare("SELECT enabled FROM feature_flags WHERE name = ?").get(name);
1846
+ return row ? Boolean(row.enabled) : meta.defaultEnabled;
1847
+ } finally {
1848
+ db.close();
1849
+ }
1850
+ }
1851
+
1852
+ function setFeatureEnabled(name, enabled) {
1853
+ initDatabase();
1854
+ const db = openDatabase();
1855
+ try {
1856
+ db.prepare(`
1857
+ INSERT INTO feature_flags(name, enabled, updated_at) VALUES (?, ?, datetime('now'))
1858
+ ON CONFLICT(name) DO UPDATE SET enabled = excluded.enabled, updated_at = excluded.updated_at
1859
+ `).run(name, enabled ? 1 : 0);
1860
+ } finally {
1861
+ db.close();
1862
+ }
1863
+ }
1864
+
1479
1865
  function assertKeyProvider(provider) {
1480
1866
  if (provider !== "openai" && provider !== "openrouter") {
1481
1867
  throw new Error("Провайдер должен быть openai или openrouter.");
@@ -1565,20 +1951,43 @@ async function aiAsk(args, context = {}) {
1565
1951
 
1566
1952
  const config = await loadConfig();
1567
1953
  const providerConfig = resolveAiProfile(config, options);
1954
+ applyRuntimeConfig(providerConfig, options.config);
1568
1955
  const dataContext = await buildDataContext(question);
1569
- const messages = buildAiMessages(question, dataContext, context.history || []);
1956
+ emitEvent(options, "context_loaded", { schools: dataContext.schools.length, kindergartens: dataContext.kindergartens.length });
1957
+ const historyEnabled = !options["no-history"] && isFeatureEnabled("sqlite-history");
1958
+ const sessionId = historyEnabled && isFeatureEnabled("sessions") ? ensureSessionForAsk(options, providerConfig, question) : null;
1959
+ const history = context.history || (sessionId ? getSessionAiHistory(sessionId) : []);
1960
+ const messages = buildAiMessages(question, dataContext, history, options);
1570
1961
  let answer = "";
1571
1962
  let errorMessage = "";
1572
1963
 
1573
1964
  try {
1965
+ emitEvent(options, "provider_selected", { profile: providerConfig.name, provider: providerConfig.provider, model: providerConfig.model });
1574
1966
  answer = await callAiProvider(providerConfig, messages);
1575
1967
  } catch (error) {
1576
1968
  errorMessage = error instanceof Error ? error.message : String(error);
1577
- recordAskHistory({ question, answer: "", providerConfig, dataContext, error: errorMessage });
1969
+ if (historyEnabled) {
1970
+ recordAskHistory({ question, answer: "", providerConfig, dataContext, error: errorMessage, sessionId });
1971
+ appendSessionExchange(sessionId, question, "", dataContext, errorMessage);
1972
+ }
1578
1973
  throw error;
1579
1974
  }
1580
1975
 
1581
- recordAskHistory({ question, answer, providerConfig, dataContext, error: "" });
1976
+ if (historyEnabled) {
1977
+ recordAskHistory({ question, answer, providerConfig, dataContext, error: "", sessionId });
1978
+ appendSessionExchange(sessionId, question, answer, dataContext, "");
1979
+ }
1980
+
1981
+ emitEvent(options, "answer", { length: answer.length, sessionId });
1982
+
1983
+ if (options.output) {
1984
+ await writeFile(options.output, answer, "utf8");
1985
+ }
1986
+
1987
+ if (options.format === "json" || options.schema === "json") {
1988
+ printJson({ answer, profile: providerConfig.name, provider: providerConfig.provider, model: providerConfig.model, sessionId, context: dataContext });
1989
+ return answer;
1990
+ }
1582
1991
 
1583
1992
  console.log(answer);
1584
1993
  return answer;
@@ -1601,9 +2010,28 @@ function resolveAiProfile(config, options = {}) {
1601
2010
  provider,
1602
2011
  model: options.model || activeProfile.model || config.ai.model,
1603
2012
  baseUrl: options["base-url"] || activeProfile.baseUrl || config.ai.baseUrl,
2013
+ temperature: options.temperature || activeProfile.temperature,
1604
2014
  };
1605
2015
  }
1606
2016
 
2017
+ function applyRuntimeConfig(target, value) {
2018
+ if (!value) {
2019
+ return;
2020
+ }
2021
+ const [key, ...parts] = String(value).split("=");
2022
+ if (!key || parts.length === 0) {
2023
+ throw new Error("Флаг --config должен быть в формате key=value.");
2024
+ }
2025
+ setConfigValue(target, key, parts.join("="));
2026
+ }
2027
+
2028
+ function emitEvent(options, type, data) {
2029
+ if (!options.events) {
2030
+ return;
2031
+ }
2032
+ printJson({ type, at: new Date().toISOString(), ...data });
2033
+ }
2034
+
1607
2035
  async function buildDataContext(question) {
1608
2036
  const apiBaseUrl = await getApiBaseUrl();
1609
2037
  const mcpBaseUrl = await getMcpBaseUrl();
@@ -1726,7 +2154,7 @@ function scoreItem(item, terms, patterns, layer) {
1726
2154
  return score;
1727
2155
  }
1728
2156
 
1729
- function buildAiMessages(question, dataContext, history) {
2157
+ function buildAiMessages(question, dataContext, history, options = {}) {
1730
2158
  const sourceLines = buildSourceLines(dataContext);
1731
2159
  const system = [
1732
2160
  "Ты терминальный AI-ассистент CLI-проекта Йошкар-Олы.",
@@ -1735,8 +2163,10 @@ function buildAiMessages(question, dataContext, history) {
1735
2163
  "Если в контексте нет нужных сведений, прямо напиши, что данных недостаточно.",
1736
2164
  "Не выдумывай адреса, телефоны, лицензии и руководителей.",
1737
2165
  "Если отвечаешь по конкретным организациям, укажи источник в конце: слой, название и ИНН.",
2166
+ options.schema === "json" ? "Верни валидный JSON без markdown-обертки." : "",
2167
+ options.schema === "table" ? "Если уместно, верни ответ в виде markdown-таблицы." : "",
1738
2168
  "Отвечай кратко и по делу.",
1739
- ].join(" ");
2169
+ ].filter(Boolean).join(" ");
1740
2170
  const contextText = JSON.stringify(dataContext, null, 2);
1741
2171
  const recentHistory = history.slice(-6);
1742
2172
 
@@ -1862,7 +2292,7 @@ async function callOpenAiCompatible(config, messages, apiKey, providerName) {
1862
2292
  body: JSON.stringify({
1863
2293
  model: config.model,
1864
2294
  messages,
1865
- temperature: 0.2,
2295
+ temperature: Number(config.temperature ?? 0.2),
1866
2296
  }),
1867
2297
  });
1868
2298
 
@@ -2052,9 +2482,9 @@ async function setupClient(args) {
2052
2482
  throw new Error('Only "iola setup codex" is available in this first release.');
2053
2483
  }
2054
2484
 
2055
- console.log("Run:");
2056
- console.log(" codex mcp add yoshkarOlaPublicData --url https://apiiola.yasg.ru/mcp");
2057
- console.log(" npx -y @iola_adm/yoshkar-ola-public-mcp install-skill codex");
2485
+ await runCommand("codex", ["mcp", "add", "yoshkarOlaPublicData", "--url", `${await getMcpBaseUrl()}/mcp`], { inherit: true });
2486
+ await runCommand("npx", ["-y", "@iola_adm/yoshkar-ola-public-mcp", "install-skill", "codex"], { inherit: true });
2487
+ console.log("Codex MCP и skill установлены.");
2058
2488
  }
2059
2489
 
2060
2490
  function parseOptions(args) {
@@ -2062,12 +2492,12 @@ function parseOptions(args) {
2062
2492
 
2063
2493
  for (let index = 0; index < args.length; index += 1) {
2064
2494
  const arg = args[index];
2065
- if (arg === "--json" || arg === "--yes" || arg === "--silent") {
2495
+ if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--no-history" || arg === "--summary" || arg === "--all") {
2066
2496
  result[arg.slice(2)] = true;
2067
2497
  } else if (arg === "--check" || arg === "--upgrade-node") {
2068
2498
  result.check = true;
2069
2499
  result[arg.slice(2)] = true;
2070
- } 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") {
2500
+ } 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") {
2071
2501
  result[arg.slice(2)] = args[index + 1];
2072
2502
  index += 1;
2073
2503
  } else {