@iola_adm/iola-cli 0.1.29 → 0.1.31
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 +15 -2
- package/package.json +1 -1
- package/src/cli.js +1013 -25
- package/wiki/Home.md +2 -0
- package/wiki//320/221/321/200/320/260/321/203/320/267/320/265/321/200/320/275/321/213/320/271-/320/260/320/263/320/265/320/275/321/202.md +68 -0
- package/wiki//320/232/320/276/320/274/320/260/320/275/320/264/321/213.md +17 -0
- package/wiki//320/237/320/273/320/260/321/202/321/204/320/276/321/200/320/274/320/260-/320/260/320/263/320/265/320/275/321/202/320/260.md +104 -0
package/src/cli.js
CHANGED
|
@@ -18,12 +18,17 @@ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
|
18
18
|
const LAST_GOOD_CONFIG_FILE = path.join(CONFIG_DIR, "config.last-good.json");
|
|
19
19
|
const SECRETS_FILE = path.join(CONFIG_DIR, "secrets.json");
|
|
20
20
|
const DB_FILE = path.join(CONFIG_DIR, "iola.db");
|
|
21
|
-
const DB_SCHEMA_VERSION =
|
|
21
|
+
const DB_SCHEMA_VERSION = 8;
|
|
22
|
+
const PROJECT_IOLA_DIR = path.join(process.cwd(), ".iola");
|
|
23
|
+
const PROJECT_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "config.json");
|
|
24
|
+
const LOCAL_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "local.json");
|
|
25
|
+
const BROWSER_RUNTIME_DIR = path.join(CONFIG_DIR, "browser-runtime");
|
|
26
|
+
const BROWSER_RUNTIME_PACKAGE = path.join(BROWSER_RUNTIME_DIR, "node_modules", "playwright", "package.json");
|
|
22
27
|
const INDEXABLE_EXTENSIONS = /\.(md|txt|csv|json|html|docx|xlsx|pptx|pdf)$/i;
|
|
23
28
|
const LOCAL_TOOLS = ["search_local", "get_card", "export_data", "run_report", "save_view"];
|
|
24
29
|
const FILE_TOOLS = ["files_tree", "files_read", "files_search", "files_write", "files_patch"];
|
|
25
30
|
const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS];
|
|
26
|
-
const HOOK_EVENTS = ["SessionStart", "BeforeTool", "AfterTool", "AfterSync", "BeforeExport", "SessionEnd"];
|
|
31
|
+
const HOOK_EVENTS = ["SessionStart", "BeforeTool", "AfterTool", "PreToolUse", "PostToolUse", "OnError", "AfterSync", "BeforeExport", "SessionEnd"];
|
|
27
32
|
const DAEMON_PORT = Number(process.env.IOLA_DAEMON_PORT || 18790);
|
|
28
33
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
34
|
const BUILTIN_SKILLS_DIR = path.resolve(__dirname, "..", "skills");
|
|
@@ -82,6 +87,23 @@ const FEATURES = {
|
|
|
82
87
|
"mcp-management": { stage: "stable", defaultEnabled: true, description: "Команды управления MCP-интеграциями." },
|
|
83
88
|
"web-search": { stage: "experimental", defaultEnabled: false, description: "Резерв под web-search режимы AI." },
|
|
84
89
|
};
|
|
90
|
+
const SKILL_BUNDLES = {
|
|
91
|
+
analyst: {
|
|
92
|
+
description: "Аналитик открытых данных: поиск, карточки, отчеты и память.",
|
|
93
|
+
skills: ["open-data", "reports", "local-model"],
|
|
94
|
+
requirements: ["Локальная SQLite-БД", "публичный API"],
|
|
95
|
+
},
|
|
96
|
+
documents: {
|
|
97
|
+
description: "Работа с локальными документами, индексом, архивами и выгрузками.",
|
|
98
|
+
skills: ["open-data", "reports"],
|
|
99
|
+
requirements: ["files mode read-only/workspace-write", "7-Zip для архивов"],
|
|
100
|
+
},
|
|
101
|
+
"local-agent": {
|
|
102
|
+
description: "Локальная модель Ollama с проверочным reasoning и локальными tools.",
|
|
103
|
+
skills: ["local-model", "open-data"],
|
|
104
|
+
requirements: ["Ollama", "локальная модель"],
|
|
105
|
+
},
|
|
106
|
+
};
|
|
85
107
|
const DEFAULT_AI_CONFIG = {
|
|
86
108
|
api: {
|
|
87
109
|
baseUrl: "https://apiiola.yasg.ru/api/v1",
|
|
@@ -239,6 +261,7 @@ const COMMANDS = new Map([
|
|
|
239
261
|
["resume", resumeSession],
|
|
240
262
|
["fork", forkSession],
|
|
241
263
|
["features", handleFeatures],
|
|
264
|
+
["settings", handleSettings],
|
|
242
265
|
["wiki", handleWiki],
|
|
243
266
|
["context", handleContext],
|
|
244
267
|
["skills", handleSkills],
|
|
@@ -250,11 +273,16 @@ const COMMANDS = new Map([
|
|
|
250
273
|
["index", handleIndex],
|
|
251
274
|
["reports", handleReports],
|
|
252
275
|
["plugins", handlePlugins],
|
|
276
|
+
["browser", handleBrowser],
|
|
253
277
|
["workspace", handleWorkspace],
|
|
254
278
|
["tasks", handleTasks],
|
|
255
279
|
["artifacts", handleArtifacts],
|
|
256
280
|
["snapshot", handleSnapshot],
|
|
281
|
+
["sandbox", handleSandbox],
|
|
257
282
|
["trace", handleTrace],
|
|
283
|
+
["trajectory", handleTrajectory],
|
|
284
|
+
["usage", handleUsage],
|
|
285
|
+
["budget", handleBudget],
|
|
258
286
|
["policy", handlePolicy],
|
|
259
287
|
["export", handleExport],
|
|
260
288
|
["cron", handleCron],
|
|
@@ -264,6 +292,8 @@ const COMMANDS = new Map([
|
|
|
264
292
|
["memory", handleMemory],
|
|
265
293
|
["hooks", handleHooks],
|
|
266
294
|
["agents", handleAgents],
|
|
295
|
+
["subagents", handleSubagents],
|
|
296
|
+
["review", handleReview],
|
|
267
297
|
["mcp", handleMcp],
|
|
268
298
|
["cache", handleCache],
|
|
269
299
|
["sync", handleSync],
|
|
@@ -353,12 +383,14 @@ Usage:
|
|
|
353
383
|
iola history [--limit 20]
|
|
354
384
|
iola history clear
|
|
355
385
|
iola sessions [--limit 20]
|
|
386
|
+
iola sessions replay SESSION_ID
|
|
356
387
|
iola resume SESSION_ID [TEXT]
|
|
357
388
|
iola fork SESSION_ID [TEXT]
|
|
358
389
|
iola features list|enable|disable
|
|
390
|
+
iola settings list|get|validate|doctor|init
|
|
359
391
|
iola wiki [open|links]
|
|
360
392
|
iola context list|show|init
|
|
361
|
-
iola skills list|show|paths|enable|disable
|
|
393
|
+
iola skills list|show|paths|enable|disable|bundles|bundle|doctor
|
|
362
394
|
iola tools list|toolsets|enable|disable|profile
|
|
363
395
|
iola files status|mode|approvals|tree|read|search|write|patch
|
|
364
396
|
iola archive doctor|list|test|extract|create|index
|
|
@@ -367,21 +399,28 @@ Usage:
|
|
|
367
399
|
iola index folder|status|search
|
|
368
400
|
iola reports list|run
|
|
369
401
|
iola plugins list|install|run|remove
|
|
402
|
+
iola browser status|install|open|text|html|screenshot|pdf|click|type|eval
|
|
370
403
|
iola workspace init|status|use|list
|
|
371
404
|
iola tasks list|add|done|run
|
|
372
405
|
iola artifacts list|show|open
|
|
373
406
|
iola snapshot create|list|restore
|
|
407
|
+
iola sandbox fork|run|diff|apply
|
|
374
408
|
iola trace last|show
|
|
409
|
+
iola trajectory export|last
|
|
410
|
+
iola usage summary|models|sessions
|
|
411
|
+
iola budget status|set
|
|
375
412
|
iola policy use safe|analyst|developer|full
|
|
376
413
|
iola export REPORT --format docx|xlsx --output FILE
|
|
377
414
|
iola cron list|add|delete|run|tick
|
|
378
415
|
iola daemon start|status
|
|
379
416
|
iola rpc call METHOD [ARGS] [--json]
|
|
380
417
|
iola permissions list|allow|deny
|
|
381
|
-
iola memory show|add|set|clear|export
|
|
382
|
-
iola hooks list|add|delete|run
|
|
418
|
+
iola memory show|add|set|clear|export|curate|duplicates|prune
|
|
419
|
+
iola hooks list|events|add|delete|run|trust|audit
|
|
383
420
|
iola agents list|run
|
|
384
|
-
iola
|
|
421
|
+
iola subagents list|run|parallel|add
|
|
422
|
+
iola review config|data|docs|report
|
|
423
|
+
iola mcp list|status|install|remove|serve [--stdio]
|
|
385
424
|
iola cache status|warm|clear
|
|
386
425
|
iola sync [--dataset schools|kindergartens]
|
|
387
426
|
iola sync status
|
|
@@ -975,6 +1014,14 @@ function showBanner() {
|
|
|
975
1014
|
console.log("открытые данные • MCP • локальный AI");
|
|
976
1015
|
}
|
|
977
1016
|
|
|
1017
|
+
function getPackageVersion() {
|
|
1018
|
+
try {
|
|
1019
|
+
return JSON.parse(readFileSync(path.resolve(__dirname, "..", "package.json"), "utf8")).version;
|
|
1020
|
+
} catch {
|
|
1021
|
+
return "0.0.0";
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
978
1025
|
async function showVersion(args = []) {
|
|
979
1026
|
const options = parseOptions(args);
|
|
980
1027
|
const packageJson = await import("../package.json", { with: { type: "json" } });
|
|
@@ -1512,6 +1559,17 @@ async function handleSessions(args) {
|
|
|
1512
1559
|
return;
|
|
1513
1560
|
}
|
|
1514
1561
|
|
|
1562
|
+
if (action === "replay") {
|
|
1563
|
+
const sessionId = Number(args[1]);
|
|
1564
|
+
if (!sessionId) throw new Error("Пример: iola sessions replay 1");
|
|
1565
|
+
const rows = getSessionMessages(sessionId);
|
|
1566
|
+
for (const row of rows) {
|
|
1567
|
+
console.log(`\n[${row.role}] ${row.created_at}`);
|
|
1568
|
+
console.log(row.content);
|
|
1569
|
+
}
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1515
1573
|
const rows = listSessions(Number(options.limit || 20));
|
|
1516
1574
|
|
|
1517
1575
|
if (options.json) {
|
|
@@ -1587,6 +1645,61 @@ async function handleFeatures(args) {
|
|
|
1587
1645
|
throw new Error("Команды features: list, enable NAME, disable NAME.");
|
|
1588
1646
|
}
|
|
1589
1647
|
|
|
1648
|
+
async function handleSettings(args) {
|
|
1649
|
+
const [action = "list", key] = args;
|
|
1650
|
+
const layers = await loadConfigLayers();
|
|
1651
|
+
const effective = await loadConfig();
|
|
1652
|
+
|
|
1653
|
+
if (action === "list" || action === "ls" || action === "doctor") {
|
|
1654
|
+
const rows = layers.map((layer) => ({
|
|
1655
|
+
scope: layer.scope,
|
|
1656
|
+
file: layer.file,
|
|
1657
|
+
exists: layer.exists ? "yes" : "no",
|
|
1658
|
+
valid: layer.errors.length ? "no" : "yes",
|
|
1659
|
+
errors: layer.errors.join("; ") || "-",
|
|
1660
|
+
}));
|
|
1661
|
+
printTable(rows, [["scope", "Слой"], ["exists", "Есть"], ["valid", "Валиден"], ["file", "Файл"], ["errors", "Ошибки"]]);
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
if (action === "get") {
|
|
1666
|
+
if (!key) {
|
|
1667
|
+
printJson(effective);
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
const value = getConfigValue(effective, key);
|
|
1671
|
+
if (typeof value === "object") printJson(value);
|
|
1672
|
+
else console.log(value ?? "-");
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
if (action === "validate") {
|
|
1677
|
+
const errors = validateConfig(effective);
|
|
1678
|
+
if (errors.length) {
|
|
1679
|
+
printTable(errors.map((error) => ({ error })), [["error", "Ошибка"]]);
|
|
1680
|
+
process.exitCode = 1;
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
console.log("Конфигурация валидна.");
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
if (action === "init") {
|
|
1688
|
+
await mkdir(PROJECT_IOLA_DIR, { recursive: true });
|
|
1689
|
+
if (!existsSync(PROJECT_CONFIG_FILE)) {
|
|
1690
|
+
await writeFile(PROJECT_CONFIG_FILE, `${JSON.stringify({ files: { workspaceRoot: "." } }, null, 2)}\n`, "utf8");
|
|
1691
|
+
}
|
|
1692
|
+
if (!existsSync(LOCAL_CONFIG_FILE)) {
|
|
1693
|
+
await writeFile(LOCAL_CONFIG_FILE, `${JSON.stringify({ local: true }, null, 2)}\n`, "utf8");
|
|
1694
|
+
}
|
|
1695
|
+
console.log(`Создан project config: ${PROJECT_CONFIG_FILE}`);
|
|
1696
|
+
console.log(`Создан local config: ${LOCAL_CONFIG_FILE}`);
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
throw new Error("Команды settings: list, get [KEY], validate, doctor, init.");
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1590
1703
|
async function handleWiki(args) {
|
|
1591
1704
|
const [action = "links"] = args;
|
|
1592
1705
|
const base = "https://github.com/adm-iola/iola-cli/wiki";
|
|
@@ -1599,6 +1712,8 @@ async function handleWiki(args) {
|
|
|
1599
1712
|
["Skills и toolsets", `${base}/Skills-и-toolsets`],
|
|
1600
1713
|
["Локальные файлы", `${base}/Локальные-файлы`],
|
|
1601
1714
|
["Рабочая среда агента", `${base}/Рабочая-среда-агента`],
|
|
1715
|
+
["Платформа агента", `${base}/Платформа-агента`],
|
|
1716
|
+
["Браузерный агент", `${base}/Браузерный-агент`],
|
|
1602
1717
|
["Расширения и локальные данные", `${base}/Расширения-и-локальные-данные`],
|
|
1603
1718
|
["Архивы и мастер настройки", `${base}/Архивы-и-мастер-настройки`],
|
|
1604
1719
|
["Daemon, RPC и cron", `${base}/Daemon-RPC-и-cron`],
|
|
@@ -1701,6 +1816,41 @@ async function handleSkills(args) {
|
|
|
1701
1816
|
return;
|
|
1702
1817
|
}
|
|
1703
1818
|
|
|
1819
|
+
if (action === "bundles") {
|
|
1820
|
+
const enabled = new Set(config.skills?.enabled || []);
|
|
1821
|
+
const rows = Object.entries(SKILL_BUNDLES).map(([bundle, meta]) => ({
|
|
1822
|
+
bundle,
|
|
1823
|
+
enabled: meta.skills.every((skill) => enabled.has(skill)) ? "yes" : "partial/no",
|
|
1824
|
+
skills: meta.skills.join(", "),
|
|
1825
|
+
description: meta.description,
|
|
1826
|
+
}));
|
|
1827
|
+
printTable(rows, [["bundle", "Bundle"], ["enabled", "Вкл"], ["skills", "Skills"], ["description", "Описание"]]);
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
if (action === "bundle") {
|
|
1832
|
+
const [operation, bundleName] = args.slice(1);
|
|
1833
|
+
if (operation !== "enable" || !SKILL_BUNDLES[bundleName]) {
|
|
1834
|
+
throw new Error(`Пример: iola skills bundle enable analyst. Доступно: ${Object.keys(SKILL_BUNDLES).join(", ")}`);
|
|
1835
|
+
}
|
|
1836
|
+
const enabled = new Set(config.skills?.enabled || []);
|
|
1837
|
+
for (const skill of SKILL_BUNDLES[bundleName].skills) enabled.add(skill);
|
|
1838
|
+
await saveConfig({ skills: { ...(config.skills || {}), enabled: [...enabled] } });
|
|
1839
|
+
console.log(`Skill bundle включен: ${bundleName}`);
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
if (action === "doctor") {
|
|
1844
|
+
const skills = listSkills(config);
|
|
1845
|
+
const enabled = new Set(config.skills?.enabled || []);
|
|
1846
|
+
const rows = [
|
|
1847
|
+
...skills.map((skill) => ({ item: skill.name, type: "skill", status: enabled.has(skill.name) ? "enabled" : "available", detail: skill.file })),
|
|
1848
|
+
...Object.entries(SKILL_BUNDLES).map(([bundle, meta]) => ({ item: bundle, type: "bundle", status: meta.skills.every((skill) => enabled.has(skill)) ? "enabled" : "not-complete", detail: meta.requirements.join(", ") })),
|
|
1849
|
+
];
|
|
1850
|
+
printTable(rows, [["type", "Тип"], ["item", "Имя"], ["status", "Статус"], ["detail", "Детали"]]);
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1704
1854
|
if (action === "show") {
|
|
1705
1855
|
const skill = findSkill(name, config);
|
|
1706
1856
|
if (!skill) throw new Error(`Skill не найден: ${name}`);
|
|
@@ -1718,7 +1868,7 @@ async function handleSkills(args) {
|
|
|
1718
1868
|
return;
|
|
1719
1869
|
}
|
|
1720
1870
|
|
|
1721
|
-
throw new Error("Команды skills: list, paths, show NAME, enable NAME, disable NAME.");
|
|
1871
|
+
throw new Error("Команды skills: list, paths, show NAME, enable NAME, disable NAME, bundles, bundle enable NAME, doctor.");
|
|
1722
1872
|
}
|
|
1723
1873
|
|
|
1724
1874
|
async function handleTools(args) {
|
|
@@ -2064,6 +2214,94 @@ async function handlePlugins(args) {
|
|
|
2064
2214
|
throw new Error("Команды plugins: list, install NAME --command CMD, run NAME, remove NAME.");
|
|
2065
2215
|
}
|
|
2066
2216
|
|
|
2217
|
+
async function handleBrowser(args) {
|
|
2218
|
+
const [action = "status", target, ...rest] = args;
|
|
2219
|
+
const options = parseOptions(rest);
|
|
2220
|
+
|
|
2221
|
+
if (action === "status") {
|
|
2222
|
+
printKeyValue(await getBrowserStatus());
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
if (action === "install") {
|
|
2227
|
+
await installBrowserRuntime();
|
|
2228
|
+
printKeyValue(await getBrowserStatus());
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
if (action === "open") {
|
|
2233
|
+
const url = target || options.url;
|
|
2234
|
+
if (!url) throw new Error("Пример: iola browser open https://example.com");
|
|
2235
|
+
if (options.system) {
|
|
2236
|
+
await openUrl(url);
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
await runBrowserAutomation("open", { url, headed: options.headless ? false : true, waitMs: Number(options.wait || 600000) });
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
if (action === "text" || action === "html") {
|
|
2244
|
+
const url = target || options.url;
|
|
2245
|
+
if (!url) throw new Error(`Пример: iola browser ${action} https://example.com`);
|
|
2246
|
+
const result = await runBrowserAutomation(action, browserParams(url, options));
|
|
2247
|
+
if (options.output) {
|
|
2248
|
+
await writeFile(options.output, result, "utf8");
|
|
2249
|
+
console.log(`Файл сохранен: ${options.output}`);
|
|
2250
|
+
} else {
|
|
2251
|
+
console.log(result);
|
|
2252
|
+
}
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
if (action === "screenshot" || action === "pdf") {
|
|
2257
|
+
const url = target || options.url;
|
|
2258
|
+
if (!url) throw new Error(`Пример: iola browser ${action} https://example.com --output page.${action === "pdf" ? "pdf" : "png"}`);
|
|
2259
|
+
const output = options.output || path.join(process.cwd(), action === "pdf" ? "browser-page.pdf" : "browser-page.png");
|
|
2260
|
+
await runBrowserAutomation(action, { ...browserParams(url, options), output: path.resolve(output) });
|
|
2261
|
+
saveArtifact(action === "pdf" ? "browser-pdf" : "browser-screenshot", url, path.resolve(output), { url });
|
|
2262
|
+
console.log(`Файл сохранен: ${output}`);
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
if (action === "click") {
|
|
2267
|
+
const url = target || options.url;
|
|
2268
|
+
if (!url || !options.selector) throw new Error('Пример: iola browser click https://example.com --selector "button" --output after.png');
|
|
2269
|
+
const result = await runBrowserAutomation("click", { ...browserParams(url, options), selector: options.selector, output: options.output ? path.resolve(options.output) : "" });
|
|
2270
|
+
if (result) console.log(result);
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
if (action === "type") {
|
|
2275
|
+
const url = target || options.url;
|
|
2276
|
+
if (!url || !options.selector || options.text === undefined) throw new Error('Пример: iola browser type https://example.com --selector "#q" --text "школа 29"');
|
|
2277
|
+
const result = await runBrowserAutomation("type", { ...browserParams(url, options), selector: options.selector, text: options.text, press: options.press || "", output: options.output ? path.resolve(options.output) : "" });
|
|
2278
|
+
if (result) console.log(result);
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
if (action === "eval") {
|
|
2283
|
+
const url = target || options.url;
|
|
2284
|
+
const script = options.script || rest.join(" ");
|
|
2285
|
+
if (!url || !script) throw new Error('Пример: iola browser eval https://example.com --script "document.title"');
|
|
2286
|
+
const result = await runBrowserAutomation("eval", { ...browserParams(url, options), script });
|
|
2287
|
+
console.log(result);
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
throw new Error("Команды browser: status, install, open URL, text URL, html URL, screenshot URL --output FILE, pdf URL --output FILE, click URL --selector SEL, type URL --selector SEL --text TEXT, eval URL --script JS.");
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
function browserParams(url, options = {}) {
|
|
2295
|
+
return {
|
|
2296
|
+
url,
|
|
2297
|
+
headed: Boolean(options.headed),
|
|
2298
|
+
timeout: Number(options.timeout || 30000),
|
|
2299
|
+
waitMs: Number(options.wait || 0),
|
|
2300
|
+
selector: options.selector || "",
|
|
2301
|
+
viewport: options.viewport || "1366x768",
|
|
2302
|
+
};
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2067
2305
|
async function handleWorkspace(args) {
|
|
2068
2306
|
const [action = "status", nameOrPath] = args;
|
|
2069
2307
|
const config = await loadConfig();
|
|
@@ -2161,6 +2399,38 @@ async function handleSnapshot(args) {
|
|
|
2161
2399
|
throw new Error("Команды snapshot: create, list, restore ID.");
|
|
2162
2400
|
}
|
|
2163
2401
|
|
|
2402
|
+
async function handleSandbox(args) {
|
|
2403
|
+
const [action = "fork", ...rest] = args;
|
|
2404
|
+
if (action === "fork") {
|
|
2405
|
+
const result = await createSandboxCopy(rest[0]);
|
|
2406
|
+
printKeyValue(result);
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
if (action === "run") {
|
|
2410
|
+
const command = rest.join(" ").trim();
|
|
2411
|
+
if (!command) throw new Error('Пример: iola sandbox run "npm test"');
|
|
2412
|
+
const sandbox = await createSandboxCopy();
|
|
2413
|
+
const parts = splitCommandLine(command);
|
|
2414
|
+
console.log(`Sandbox: ${sandbox.path}`);
|
|
2415
|
+
await runCommand(parts[0], parts.slice(1), { inherit: true, cwd: sandbox.path });
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
|
+
if (action === "diff") {
|
|
2419
|
+
const sandboxPath = rest[0];
|
|
2420
|
+
if (!sandboxPath) throw new Error("Пример: iola sandbox diff PATH");
|
|
2421
|
+
await runCommand("git", ["diff", "--no-index", process.cwd(), sandboxPath], { inherit: true }).catch(() => {});
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
if (action === "apply") {
|
|
2425
|
+
const sandboxPath = rest[0];
|
|
2426
|
+
if (!sandboxPath) throw new Error("Пример: iola sandbox apply PATH");
|
|
2427
|
+
await cp(sandboxPath, process.cwd(), { recursive: true, force: true });
|
|
2428
|
+
console.log(`Sandbox применен: ${sandboxPath}`);
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
throw new Error("Команды sandbox: fork [NAME], run COMMAND, diff PATH, apply PATH.");
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2164
2434
|
async function handleTrace(args) {
|
|
2165
2435
|
const [action = "last", id] = args;
|
|
2166
2436
|
if (action === "last") {
|
|
@@ -2174,6 +2444,61 @@ async function handleTrace(args) {
|
|
|
2174
2444
|
throw new Error("Команды trace: last [LIMIT], show RUN_ID.");
|
|
2175
2445
|
}
|
|
2176
2446
|
|
|
2447
|
+
async function handleTrajectory(args) {
|
|
2448
|
+
const [action = "export", ...rest] = args;
|
|
2449
|
+
const options = parseOptions(rest);
|
|
2450
|
+
if (action === "last") {
|
|
2451
|
+
const rows = buildTrajectoryRows(Number(options.limit || rest[0] || 20));
|
|
2452
|
+
if (options.json) printJson(rows);
|
|
2453
|
+
else printTable(rows, [["type", "Тип"], ["id", "ID"], ["created_at", "Дата"], ["summary", "Сводка"]]);
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
if (action === "export") {
|
|
2457
|
+
const format = options.format || "jsonl";
|
|
2458
|
+
const output = options.output || path.join(process.cwd(), `iola-trajectory-${Date.now()}.${format}`);
|
|
2459
|
+
const rows = buildTrajectoryRows(Number(options.limit || 500));
|
|
2460
|
+
const text = format === "json" ? `${JSON.stringify(rows, null, 2)}\n` : rows.map((row) => JSON.stringify(row)).join("\n") + "\n";
|
|
2461
|
+
await writeFile(output, text, "utf8");
|
|
2462
|
+
saveArtifact("trajectory", path.basename(output), output, { format, rows: rows.length });
|
|
2463
|
+
console.log(`Trajectory экспортирована: ${output}`);
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2466
|
+
throw new Error("Команды trajectory: last [--limit N], export [--format jsonl|json] [--output FILE].");
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
async function handleUsage(args) {
|
|
2470
|
+
const [action = "summary"] = args;
|
|
2471
|
+
if (action === "summary") {
|
|
2472
|
+
printKeyValue(getUsageSummary());
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
if (action === "models") {
|
|
2476
|
+
printTable(getUsageByModel(), [["provider", "Провайдер"], ["model", "Модель"], ["requests", "Запросы"], ["tokens", "Токены"], ["cost", "USD"]]);
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
if (action === "sessions") {
|
|
2480
|
+
printTable(getUsageBySession(), [["session_id", "Сессия"], ["requests", "Запросы"], ["tokens", "Токены"], ["cost", "USD"]]);
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
throw new Error("Команды usage: summary, models, sessions.");
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
async function handleBudget(args) {
|
|
2487
|
+
const [action = "status", scope = "daily", amount] = args;
|
|
2488
|
+
if (action === "status") {
|
|
2489
|
+
printTable(listBudgets(), [["scope", "Область"], ["amount_usd", "Лимит USD"], ["spent_usd", "Потрачено"], ["updated_at", "Обновлено"]]);
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2492
|
+
if (action === "set") {
|
|
2493
|
+
const value = Number(amount || args[2]);
|
|
2494
|
+
if (!value || value < 0) throw new Error("Пример: iola budget set daily 5");
|
|
2495
|
+
setBudget(scope, value);
|
|
2496
|
+
console.log(`Budget сохранен: ${scope}=${value} USD`);
|
|
2497
|
+
return;
|
|
2498
|
+
}
|
|
2499
|
+
throw new Error("Команды budget: status, set daily AMOUNT.");
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2177
2502
|
async function handlePolicy(args) {
|
|
2178
2503
|
const [action = "list", name] = args;
|
|
2179
2504
|
const policies = {
|
|
@@ -2445,7 +2770,26 @@ async function handleMemory(args) {
|
|
|
2445
2770
|
return;
|
|
2446
2771
|
}
|
|
2447
2772
|
|
|
2448
|
-
|
|
2773
|
+
if (action === "duplicates" || action === "curate") {
|
|
2774
|
+
const rows = findMemoryDuplicates();
|
|
2775
|
+
if (options.json) printJson(rows);
|
|
2776
|
+
else printTable(rows, [["keeper_id", "Оставить"], ["duplicate_id", "Дубликат"], ["content", "Текст"]]);
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
if (action === "prune") {
|
|
2781
|
+
const rows = findMemoryDuplicates();
|
|
2782
|
+
if (!options.yes) {
|
|
2783
|
+
printTable(rows, [["keeper_id", "Оставить"], ["duplicate_id", "Удалить"], ["content", "Текст"]]);
|
|
2784
|
+
console.log("Для удаления дубликатов запустите: iola memory prune --yes");
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
for (const row of rows) deleteMemory(row.duplicate_id);
|
|
2788
|
+
console.log(`Удалено дубликатов памяти: ${rows.length}`);
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
throw new Error("Команды memory: show, add TEXT, suggest, approve ID, reject ID, delete ID, clear, export [FILE], curate, duplicates, prune --yes.");
|
|
2449
2793
|
}
|
|
2450
2794
|
|
|
2451
2795
|
async function handleHooks(args) {
|
|
@@ -2468,6 +2812,22 @@ async function handleHooks(args) {
|
|
|
2468
2812
|
return;
|
|
2469
2813
|
}
|
|
2470
2814
|
|
|
2815
|
+
if (action === "trust") {
|
|
2816
|
+
await saveConfig({ hooksTrusted: true });
|
|
2817
|
+
console.log("Hooks помечены как доверенные для текущего пользователя.");
|
|
2818
|
+
return;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
if (action === "audit") {
|
|
2822
|
+
const rows = Object.entries(config.hooks || {}).map(([hookEvent, commands]) => ({
|
|
2823
|
+
event: hookEvent,
|
|
2824
|
+
commands: commands.length,
|
|
2825
|
+
trusted: config.hooksTrusted ? "yes" : "no",
|
|
2826
|
+
}));
|
|
2827
|
+
printTable(rows, [["event", "Событие"], ["commands", "Команд"], ["trusted", "Доверено"]]);
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2471
2831
|
if (action === "add") {
|
|
2472
2832
|
if (!HOOK_EVENTS.includes(event) || commandParts.length === 0) {
|
|
2473
2833
|
throw new Error(`Пример: iola hooks add AfterSync "iola quality" Доступно: ${HOOK_EVENTS.join(", ")}`);
|
|
@@ -2499,7 +2859,7 @@ async function handleHooks(args) {
|
|
|
2499
2859
|
return;
|
|
2500
2860
|
}
|
|
2501
2861
|
|
|
2502
|
-
throw new Error("Команды hooks: list, events, add EVENT COMMAND, delete EVENT INDEX, run EVENT.");
|
|
2862
|
+
throw new Error("Команды hooks: list, events, add EVENT COMMAND, delete EVENT INDEX, run EVENT, trust, audit.");
|
|
2503
2863
|
}
|
|
2504
2864
|
|
|
2505
2865
|
async function handleAgents(args) {
|
|
@@ -2548,8 +2908,132 @@ async function handleAgents(args) {
|
|
|
2548
2908
|
throw new Error("Команды agents: list, run NAME TEXT.");
|
|
2549
2909
|
}
|
|
2550
2910
|
|
|
2911
|
+
async function handleSubagents(args) {
|
|
2912
|
+
const [action = "list", name, ...rest] = args;
|
|
2913
|
+
const config = await loadConfig();
|
|
2914
|
+
const custom = config.subagents || {};
|
|
2915
|
+
const agents = { ...AGENTS, ...custom };
|
|
2916
|
+
|
|
2917
|
+
if (action === "list" || action === "ls") {
|
|
2918
|
+
const rows = Object.entries(agents).map(([agent, meta]) => ({
|
|
2919
|
+
agent,
|
|
2920
|
+
profile: meta.profile || "active",
|
|
2921
|
+
tools: meta.tools ? "yes" : "no",
|
|
2922
|
+
source: AGENTS[agent] ? "builtin" : "user",
|
|
2923
|
+
description: meta.description || "-",
|
|
2924
|
+
}));
|
|
2925
|
+
printTable(rows, [["agent", "Subagent"], ["profile", "Профиль"], ["tools", "Tools"], ["source", "Источник"], ["description", "Описание"]]);
|
|
2926
|
+
return;
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
if (action === "add") {
|
|
2930
|
+
const options = parseOptions(rest);
|
|
2931
|
+
if (!name) throw new Error("Пример: iola subagents add culture --profile local --prompt \"...\"");
|
|
2932
|
+
const prompt = options.prompt || options.command || options._.join(" ");
|
|
2933
|
+
const next = {
|
|
2934
|
+
...custom,
|
|
2935
|
+
[name]: {
|
|
2936
|
+
profile: options.profile || null,
|
|
2937
|
+
tools: Boolean(options.tools),
|
|
2938
|
+
prefix: prompt ? `${prompt} ` : "",
|
|
2939
|
+
description: options.description || prompt || "Пользовательский subagent",
|
|
2940
|
+
},
|
|
2941
|
+
};
|
|
2942
|
+
await saveConfig({ subagents: next });
|
|
2943
|
+
console.log(`Subagent добавлен: ${name}`);
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
if (action === "run") {
|
|
2948
|
+
if (!agents[name]) throw new Error(`Subagent неизвестен: ${name}. Доступно: ${Object.keys(agents).join(", ")}`);
|
|
2949
|
+
await runSubagent(name, agents[name], rest);
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
if (action === "parallel") {
|
|
2954
|
+
const names = String(name || "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
2955
|
+
const question = rest.join(" ").trim();
|
|
2956
|
+
if (!names.length || !question) throw new Error('Пример: iola subagents parallel data-analyst,reviewer "проверь школы"');
|
|
2957
|
+
for (const agentName of names) {
|
|
2958
|
+
if (!agents[agentName]) throw new Error(`Subagent неизвестен: ${agentName}`);
|
|
2959
|
+
console.log(`\n## ${agentName}`);
|
|
2960
|
+
await runSubagent(agentName, agents[agentName], [question, "--no-history"]);
|
|
2961
|
+
}
|
|
2962
|
+
return;
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
throw new Error("Команды subagents: list, add NAME --profile PROFILE --prompt TEXT, run NAME TEXT, parallel a,b TEXT.");
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
async function runSubagent(name, agent, rest) {
|
|
2969
|
+
const options = parseOptions(rest);
|
|
2970
|
+
const question = options._.join(" ").trim();
|
|
2971
|
+
if (!question) throw new Error(`Пример: iola subagents run ${name} "найди школы"`);
|
|
2972
|
+
const askArgs = [agent.prefix ? `${agent.prefix}${question}` : question, "--agent", name];
|
|
2973
|
+
if (agent.profile || options.profile) askArgs.push("--profile", options.profile || agent.profile);
|
|
2974
|
+
if (agent.tools || options.tools) askArgs.push("--tools");
|
|
2975
|
+
if (agent.reasoning || options.reasoning) askArgs.push("--reasoning", options.reasoning || agent.reasoning);
|
|
2976
|
+
if (options.files) askArgs.push("--files");
|
|
2977
|
+
if (options.events) askArgs.push("--events");
|
|
2978
|
+
if (options["no-history"]) askArgs.push("--no-history");
|
|
2979
|
+
await aiAsk(askArgs);
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
async function handleReview(args) {
|
|
2983
|
+
const [action = "config", target, ...rest] = args;
|
|
2984
|
+
const options = parseOptions([target, ...rest].filter(Boolean));
|
|
2985
|
+
const actualTarget = options._[0];
|
|
2986
|
+
if (action === "config") {
|
|
2987
|
+
const errors = validateConfig(await loadConfig());
|
|
2988
|
+
const rows = errors.length ? errors.map((error) => ({ level: "error", message: error })) : [{ level: "ok", message: "Конфигурация валидна" }];
|
|
2989
|
+
printTable(rows, [["level", "Уровень"], ["message", "Сообщение"]]);
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
if (action === "data") {
|
|
2993
|
+
await ensureLocalData();
|
|
2994
|
+
const rows = runQuality(actualTarget || "all");
|
|
2995
|
+
if (options.json) printJson(rows);
|
|
2996
|
+
else printTable(rows, [["check", "Проверка"], ["count", "Кол-во"], ["sample", "Пример"]]);
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2999
|
+
if (action === "docs") {
|
|
3000
|
+
const rows = actualTarget ? await reviewDocumentFolder(actualTarget, options) : searchDocs(options.query || "", Number(options.limit || 20));
|
|
3001
|
+
if (options.json) printJson(rows);
|
|
3002
|
+
else printTable(rows, [["file", "Файл"], ["issue", "Замечание"], ["detail", "Детали"]]);
|
|
3003
|
+
return;
|
|
3004
|
+
}
|
|
3005
|
+
if (action === "report") {
|
|
3006
|
+
if (!actualTarget) throw new Error("Пример: iola review report отчет.docx");
|
|
3007
|
+
const text = await extractReadableText(path.resolve(actualTarget));
|
|
3008
|
+
const rows = [
|
|
3009
|
+
{ file: actualTarget, issue: text.trim() ? "ok" : "empty", detail: text.trim() ? "Текст извлечен" : "Не удалось извлечь текст" },
|
|
3010
|
+
{ file: actualTarget, issue: /источник|данн/i.test(text) ? "ok" : "missing-source", detail: "Проверьте указание источника данных" },
|
|
3011
|
+
];
|
|
3012
|
+
printTable(rows, [["file", "Файл"], ["issue", "Замечание"], ["detail", "Детали"]]);
|
|
3013
|
+
return;
|
|
3014
|
+
}
|
|
3015
|
+
throw new Error("Команды review: config, data [scope], docs [PATH], report FILE.");
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
async function reviewDocumentFolder(target, options = {}) {
|
|
3019
|
+
const previous = await loadConfig();
|
|
3020
|
+
const rows = [];
|
|
3021
|
+
try {
|
|
3022
|
+
await saveConfig({ files: { ...(previous.files || {}), workspaceRoot: path.resolve(target), mode: "read-only" } });
|
|
3023
|
+
await setFilesMode("read-only", await loadConfig());
|
|
3024
|
+
const files = await filesTree(".", { depth: Number(options.depth || 5), limit: Number(options.limit || 200) });
|
|
3025
|
+
for (const file of files.filter((item) => item.type === "file" && INDEXABLE_EXTENSIONS.test(item.path))) {
|
|
3026
|
+
rows.push({ file: file.path, issue: "indexable", detail: "Документ можно читать и индексировать" });
|
|
3027
|
+
}
|
|
3028
|
+
} finally {
|
|
3029
|
+
await saveConfig({ files: previous.files, permissions: previous.permissions, toolsets: previous.toolsets }).catch(() => {});
|
|
3030
|
+
}
|
|
3031
|
+
return rows;
|
|
3032
|
+
}
|
|
3033
|
+
|
|
2551
3034
|
async function handleMcp(args) {
|
|
2552
|
-
const [action = "status", target = "codex"] = args;
|
|
3035
|
+
const [action = "status", target = "codex", ...rest] = args;
|
|
3036
|
+
const options = parseOptions([target, ...rest]);
|
|
2553
3037
|
|
|
2554
3038
|
if (action === "status") {
|
|
2555
3039
|
const [health, version] = await Promise.all([
|
|
@@ -2585,6 +3069,10 @@ async function handleMcp(args) {
|
|
|
2585
3069
|
|
|
2586
3070
|
if (action === "serve") {
|
|
2587
3071
|
const config = await loadConfig();
|
|
3072
|
+
if (options.stdio || target === "--stdio" || target === "stdio") {
|
|
3073
|
+
await startMcpStdio();
|
|
3074
|
+
return;
|
|
3075
|
+
}
|
|
2588
3076
|
await startMcpServer(config.daemon?.host || "127.0.0.1", Number(config.daemon?.port || DAEMON_PORT) + 1);
|
|
2589
3077
|
return;
|
|
2590
3078
|
}
|
|
@@ -3508,6 +3996,24 @@ function initDatabase() {
|
|
|
3508
3996
|
command TEXT,
|
|
3509
3997
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
3510
3998
|
);
|
|
3999
|
+
CREATE TABLE IF NOT EXISTS usage_events (
|
|
4000
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
4001
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
4002
|
+
provider TEXT,
|
|
4003
|
+
model TEXT,
|
|
4004
|
+
profile TEXT,
|
|
4005
|
+
input_chars INTEGER NOT NULL DEFAULT 0,
|
|
4006
|
+
output_chars INTEGER NOT NULL DEFAULT 0,
|
|
4007
|
+
estimated_tokens INTEGER NOT NULL DEFAULT 0,
|
|
4008
|
+
estimated_cost_usd REAL NOT NULL DEFAULT 0,
|
|
4009
|
+
session_id INTEGER
|
|
4010
|
+
);
|
|
4011
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_created_at ON usage_events(created_at DESC);
|
|
4012
|
+
CREATE TABLE IF NOT EXISTS budgets (
|
|
4013
|
+
scope TEXT PRIMARY KEY,
|
|
4014
|
+
amount_usd REAL NOT NULL,
|
|
4015
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4016
|
+
);
|
|
3511
4017
|
`);
|
|
3512
4018
|
rebuildFtsIfEmpty(db);
|
|
3513
4019
|
db.prepare(`
|
|
@@ -3564,6 +4070,7 @@ function getDbStatus() {
|
|
|
3564
4070
|
const artifacts = db.prepare("SELECT COUNT(*) AS count FROM artifacts").get();
|
|
3565
4071
|
const docs = db.prepare("SELECT COUNT(*) AS count FROM doc_index").get();
|
|
3566
4072
|
const custom = db.prepare("SELECT COUNT(*) AS count FROM custom_records").get();
|
|
4073
|
+
const usage = db.prepare("SELECT COUNT(*) AS count FROM usage_events").get();
|
|
3567
4074
|
return {
|
|
3568
4075
|
status: "ok",
|
|
3569
4076
|
file: DB_FILE,
|
|
@@ -3579,6 +4086,7 @@ function getDbStatus() {
|
|
|
3579
4086
|
artifacts: artifacts?.count ?? 0,
|
|
3580
4087
|
indexed_docs: docs?.count ?? 0,
|
|
3581
4088
|
custom_records: custom?.count ?? 0,
|
|
4089
|
+
usage_events: usage?.count ?? 0,
|
|
3582
4090
|
};
|
|
3583
4091
|
} finally {
|
|
3584
4092
|
db.close();
|
|
@@ -3749,6 +4257,16 @@ function printSessionMessages(sessionId) {
|
|
|
3749
4257
|
]);
|
|
3750
4258
|
}
|
|
3751
4259
|
|
|
4260
|
+
function getSessionMessages(sessionId) {
|
|
4261
|
+
initDatabase();
|
|
4262
|
+
const db = openDatabase();
|
|
4263
|
+
try {
|
|
4264
|
+
return db.prepare("SELECT id, role, content, created_at FROM session_messages WHERE session_id = ? ORDER BY id ASC").all(sessionId);
|
|
4265
|
+
} finally {
|
|
4266
|
+
db.close();
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
|
|
3752
4270
|
function forkSessionInDb(sessionId) {
|
|
3753
4271
|
initDatabase();
|
|
3754
4272
|
const db = openDatabase();
|
|
@@ -4239,6 +4757,21 @@ function deleteMemory(id) {
|
|
|
4239
4757
|
}
|
|
4240
4758
|
}
|
|
4241
4759
|
|
|
4760
|
+
function findMemoryDuplicates() {
|
|
4761
|
+
const rows = listMemory(1000).reverse();
|
|
4762
|
+
const seen = new Map();
|
|
4763
|
+
const duplicates = [];
|
|
4764
|
+
for (const row of rows) {
|
|
4765
|
+
const normalized = row.content.trim().toLocaleLowerCase("ru-RU").replace(/\s+/g, " ");
|
|
4766
|
+
if (seen.has(normalized)) {
|
|
4767
|
+
duplicates.push({ keeper_id: seen.get(normalized).id, duplicate_id: row.id, content: row.content });
|
|
4768
|
+
} else {
|
|
4769
|
+
seen.set(normalized, row);
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
return duplicates;
|
|
4773
|
+
}
|
|
4774
|
+
|
|
4242
4775
|
function clearMemory() {
|
|
4243
4776
|
initDatabase();
|
|
4244
4777
|
const db = openDatabase();
|
|
@@ -4569,6 +5102,7 @@ async function aiAsk(args, context = {}) {
|
|
|
4569
5102
|
const providerConfig = resolveAiProfile(config, options);
|
|
4570
5103
|
if (providerConfig.provider === "codex") await assertPermission("codex");
|
|
4571
5104
|
if (providerConfig.provider !== "ollama") await assertPermission("externalAi");
|
|
5105
|
+
if (options["stream-json"]) options.events = true;
|
|
4572
5106
|
if (options.tools && providerConfig.provider === "ollama") {
|
|
4573
5107
|
return localToolAsk(question, providerConfig, options);
|
|
4574
5108
|
}
|
|
@@ -4598,6 +5132,13 @@ async function aiAsk(args, context = {}) {
|
|
|
4598
5132
|
recordAskHistory({ question, answer, providerConfig, dataContext, error: "", sessionId });
|
|
4599
5133
|
appendSessionExchange(sessionId, question, answer, dataContext, "");
|
|
4600
5134
|
}
|
|
5135
|
+
recordUsage({
|
|
5136
|
+
providerConfig,
|
|
5137
|
+
question,
|
|
5138
|
+
answer,
|
|
5139
|
+
sessionId,
|
|
5140
|
+
profile: providerConfig.name,
|
|
5141
|
+
});
|
|
4601
5142
|
await maybeSuggestMemory(question, answer, providerConfig);
|
|
4602
5143
|
|
|
4603
5144
|
emitEvent(options, "answer", { length: answer.length, sessionId });
|
|
@@ -4642,6 +5183,7 @@ function resolveAiProfile(config, options = {}) {
|
|
|
4642
5183
|
}
|
|
4643
5184
|
|
|
4644
5185
|
async function localToolAsk(question, providerConfig, options) {
|
|
5186
|
+
if (options["stream-json"]) options.events = true;
|
|
4645
5187
|
await ensureLocalData();
|
|
4646
5188
|
const plan = await buildLocalToolPlan(question, providerConfig, options);
|
|
4647
5189
|
const validated = validateToolPlan(plan, options);
|
|
@@ -4667,6 +5209,7 @@ async function localToolAsk(question, providerConfig, options) {
|
|
|
4667
5209
|
sessionId: null,
|
|
4668
5210
|
});
|
|
4669
5211
|
}
|
|
5212
|
+
recordUsage({ providerConfig, question, answer, sessionId: null, profile: providerConfig.name });
|
|
4670
5213
|
|
|
4671
5214
|
emitEvent(options, "tool_plan", { plan: validated, runId });
|
|
4672
5215
|
saveArtifact("tool-result", question.slice(0, 80), "", { runId, plan: validated, outputs: result.outputs });
|
|
@@ -4772,6 +5315,7 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
4772
5315
|
let status = "ok";
|
|
4773
5316
|
let summary = "";
|
|
4774
5317
|
await assertPermission(step.tool);
|
|
5318
|
+
await runHooks("PreToolUse", { tool: step.tool, args: step.args || {} });
|
|
4775
5319
|
await runHooks("BeforeTool", { tool: step.tool, args: step.args || {} });
|
|
4776
5320
|
try {
|
|
4777
5321
|
if (step.tool === "search_local") {
|
|
@@ -4818,10 +5362,12 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
4818
5362
|
status = "error";
|
|
4819
5363
|
summary = error instanceof Error ? error.message : String(error);
|
|
4820
5364
|
recordToolTrace(options.runId || "manual", step.tool, step.args || {}, status, summary);
|
|
5365
|
+
await runHooks("OnError", { tool: step.tool, args: step.args || {}, error: summary });
|
|
4821
5366
|
throw error;
|
|
4822
5367
|
}
|
|
4823
5368
|
recordToolTrace(options.runId || "manual", step.tool, step.args || {}, status, summary);
|
|
4824
5369
|
await runHooks("AfterTool", { tool: step.tool, rows: current.length });
|
|
5370
|
+
await runHooks("PostToolUse", { tool: step.tool, rows: current.length });
|
|
4825
5371
|
}
|
|
4826
5372
|
return { rows: current, outputs };
|
|
4827
5373
|
}
|
|
@@ -4849,7 +5395,12 @@ async function runHooks(event, payload = {}) {
|
|
|
4849
5395
|
const config = await loadConfig();
|
|
4850
5396
|
const commands = config.hooks?.[event] || [];
|
|
4851
5397
|
for (const command of commands) {
|
|
4852
|
-
const
|
|
5398
|
+
const [maybeFilter, ...rest] = String(command).split(":");
|
|
5399
|
+
const commandText = payload.tool && rest.length > 0 && ALL_LOCAL_TOOLS.includes(maybeFilter.trim())
|
|
5400
|
+
? (maybeFilter.trim() === payload.tool ? rest.join(":").trim() : "")
|
|
5401
|
+
: command;
|
|
5402
|
+
if (!commandText) continue;
|
|
5403
|
+
const parts = splitCommandLine(commandText);
|
|
4853
5404
|
if (parts.length === 0) continue;
|
|
4854
5405
|
await runCommand(parts[0], parts.slice(1), {
|
|
4855
5406
|
inherit: true,
|
|
@@ -5445,12 +5996,12 @@ function parseOptions(args) {
|
|
|
5445
5996
|
|
|
5446
5997
|
for (let index = 0; index < args.length; index += 1) {
|
|
5447
5998
|
const arg = args[index];
|
|
5448
|
-
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") {
|
|
5999
|
+
if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || arg === "--system" || arg === "--headed" || arg === "--headless" || 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") {
|
|
5449
6000
|
result[arg.slice(2)] = true;
|
|
5450
6001
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
5451
6002
|
result.check = true;
|
|
5452
6003
|
result[arg.slice(2)] = true;
|
|
5453
|
-
} 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") {
|
|
6004
|
+
} 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 === "--prompt" || arg === "--description" || 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 === "--selector" || arg === "--url" || arg === "--timeout" || arg === "--wait" || arg === "--viewport" || arg === "--press" || arg === "--script" || arg === "--debug-file") {
|
|
5454
6005
|
result[arg.slice(2)] = args[index + 1];
|
|
5455
6006
|
index += 1;
|
|
5456
6007
|
} else {
|
|
@@ -5827,6 +6378,105 @@ async function installCodexIfMissing() {
|
|
|
5827
6378
|
await runCommand("npm", ["install", "-g", "@openai/codex"], { inherit: true });
|
|
5828
6379
|
}
|
|
5829
6380
|
|
|
6381
|
+
async function getBrowserStatus() {
|
|
6382
|
+
const installed = existsSync(BROWSER_RUNTIME_PACKAGE);
|
|
6383
|
+
let playwright = "не установлен";
|
|
6384
|
+
if (installed) {
|
|
6385
|
+
try {
|
|
6386
|
+
playwright = JSON.parse(await readFile(BROWSER_RUNTIME_PACKAGE, "utf8")).version || "installed";
|
|
6387
|
+
} catch {
|
|
6388
|
+
playwright = "installed";
|
|
6389
|
+
}
|
|
6390
|
+
}
|
|
6391
|
+
return {
|
|
6392
|
+
runtime: BROWSER_RUNTIME_DIR,
|
|
6393
|
+
playwright,
|
|
6394
|
+
installed: installed ? "yes" : "no",
|
|
6395
|
+
install_command: "iola browser install",
|
|
6396
|
+
chromium: installed ? "managed by Playwright" : "not installed",
|
|
6397
|
+
};
|
|
6398
|
+
}
|
|
6399
|
+
|
|
6400
|
+
async function installBrowserRuntime() {
|
|
6401
|
+
await mkdir(BROWSER_RUNTIME_DIR, { recursive: true });
|
|
6402
|
+
const packageFile = path.join(BROWSER_RUNTIME_DIR, "package.json");
|
|
6403
|
+
if (!existsSync(packageFile)) {
|
|
6404
|
+
await writeFile(packageFile, `${JSON.stringify({ private: true, type: "module", dependencies: {} }, null, 2)}\n`, "utf8");
|
|
6405
|
+
}
|
|
6406
|
+
console.log(`Устанавливаю Playwright runtime: ${BROWSER_RUNTIME_DIR}`);
|
|
6407
|
+
await runPackageManager("npm", ["install", "playwright@latest"], { inherit: true, cwd: BROWSER_RUNTIME_DIR });
|
|
6408
|
+
await runPackageManager("npx", ["playwright", "install", "chromium"], { inherit: true, cwd: BROWSER_RUNTIME_DIR });
|
|
6409
|
+
}
|
|
6410
|
+
|
|
6411
|
+
function runPackageManager(command, args, options = {}) {
|
|
6412
|
+
if (process.platform === "win32") {
|
|
6413
|
+
return runCommand(process.env.ComSpec || "cmd.exe", ["/d", "/c", [command, ...args].join(" ")], options);
|
|
6414
|
+
}
|
|
6415
|
+
return runCommand(command, args, options);
|
|
6416
|
+
}
|
|
6417
|
+
|
|
6418
|
+
async function ensureBrowserRuntime() {
|
|
6419
|
+
if (existsSync(BROWSER_RUNTIME_PACKAGE)) return;
|
|
6420
|
+
throw new Error("Browser runtime не установлен. Запустите: iola browser install");
|
|
6421
|
+
}
|
|
6422
|
+
|
|
6423
|
+
async function runBrowserAutomation(action, params) {
|
|
6424
|
+
await ensureBrowserRuntime();
|
|
6425
|
+
const scriptFile = path.join(BROWSER_RUNTIME_DIR, `iola-browser-${Date.now()}-${Math.random().toString(16).slice(2)}.mjs`);
|
|
6426
|
+
await writeFile(scriptFile, browserAutomationScript(action, params), "utf8");
|
|
6427
|
+
try {
|
|
6428
|
+
const { stdout } = await runCommand(process.execPath, [scriptFile], { cwd: BROWSER_RUNTIME_DIR });
|
|
6429
|
+
return stdout.trim();
|
|
6430
|
+
} finally {
|
|
6431
|
+
await rm(scriptFile, { force: true }).catch(() => {});
|
|
6432
|
+
}
|
|
6433
|
+
}
|
|
6434
|
+
|
|
6435
|
+
function browserAutomationScript(action, params) {
|
|
6436
|
+
return `
|
|
6437
|
+
import { chromium } from "playwright";
|
|
6438
|
+
const action = ${JSON.stringify(action)};
|
|
6439
|
+
const params = ${JSON.stringify(params)};
|
|
6440
|
+
const [width, height] = String(params.viewport || "1366x768").split("x").map(Number);
|
|
6441
|
+
const browser = await chromium.launch({ headless: !params.headed });
|
|
6442
|
+
const page = await browser.newPage({ viewport: { width: width || 1366, height: height || 768 } });
|
|
6443
|
+
page.setDefaultTimeout(params.timeout || 30000);
|
|
6444
|
+
try {
|
|
6445
|
+
await page.goto(params.url, { waitUntil: "domcontentloaded", timeout: params.timeout || 30000 });
|
|
6446
|
+
if (params.waitMs) await page.waitForTimeout(params.waitMs);
|
|
6447
|
+
if (action === "open") {
|
|
6448
|
+
if (params.waitMs > 0) await page.waitForTimeout(params.waitMs);
|
|
6449
|
+
else if (!page.context().browser()?.isConnected()) {}
|
|
6450
|
+
} else if (action === "text") {
|
|
6451
|
+
console.log((await page.locator("body").innerText()).trim());
|
|
6452
|
+
} else if (action === "html") {
|
|
6453
|
+
console.log(await page.content());
|
|
6454
|
+
} else if (action === "screenshot") {
|
|
6455
|
+
await page.screenshot({ path: params.output, fullPage: true });
|
|
6456
|
+
} else if (action === "pdf") {
|
|
6457
|
+
await page.pdf({ path: params.output, format: "A4", printBackground: true });
|
|
6458
|
+
} else if (action === "click") {
|
|
6459
|
+
await page.locator(params.selector).first().click();
|
|
6460
|
+
if (params.waitMs) await page.waitForTimeout(params.waitMs);
|
|
6461
|
+
if (params.output) await page.screenshot({ path: params.output, fullPage: true });
|
|
6462
|
+
console.log((await page.locator("body").innerText()).trim().slice(0, 4000));
|
|
6463
|
+
} else if (action === "type") {
|
|
6464
|
+
const locator = page.locator(params.selector).first();
|
|
6465
|
+
await locator.fill(params.text || "");
|
|
6466
|
+
if (params.press) await locator.press(params.press);
|
|
6467
|
+
if (params.waitMs) await page.waitForTimeout(params.waitMs);
|
|
6468
|
+
if (params.output) await page.screenshot({ path: params.output, fullPage: true });
|
|
6469
|
+
console.log((await page.locator("body").innerText()).trim().slice(0, 4000));
|
|
6470
|
+
} else if (action === "eval") {
|
|
6471
|
+
const value = await page.evaluate(new Function("return (" + params.script + ")"));
|
|
6472
|
+
console.log(typeof value === "string" ? value : JSON.stringify(value, null, 2));
|
|
6473
|
+
}
|
|
6474
|
+
} finally {
|
|
6475
|
+
await browser.close();
|
|
6476
|
+
}
|
|
6477
|
+
`;
|
|
6478
|
+
}
|
|
6479
|
+
|
|
5830
6480
|
async function probeEndpoint(url) {
|
|
5831
6481
|
try {
|
|
5832
6482
|
const response = await fetch(url, { headers: { accept: "application/json" } });
|
|
@@ -5840,6 +6490,13 @@ async function startDaemon(host, port) {
|
|
|
5840
6490
|
const server = createServer(async (req, res) => {
|
|
5841
6491
|
try {
|
|
5842
6492
|
const url = new URL(req.url || "/", `http://${host}:${port}`);
|
|
6493
|
+
|
|
6494
|
+
if (req.method === "GET" && url.pathname === "/") {
|
|
6495
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
6496
|
+
res.end(renderDaemonDashboard(host, port));
|
|
6497
|
+
return;
|
|
6498
|
+
}
|
|
6499
|
+
|
|
5843
6500
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
5844
6501
|
|
|
5845
6502
|
if (req.method === "GET" && url.pathname === "/health") {
|
|
@@ -5852,6 +6509,26 @@ async function startDaemon(host, port) {
|
|
|
5852
6509
|
return;
|
|
5853
6510
|
}
|
|
5854
6511
|
|
|
6512
|
+
if (req.method === "GET" && url.pathname === "/api/status") {
|
|
6513
|
+
res.end(JSON.stringify({ db: getDbStatus(), sync: getSyncStatus(), usage: getUsageSummary() }));
|
|
6514
|
+
return;
|
|
6515
|
+
}
|
|
6516
|
+
|
|
6517
|
+
if (req.method === "GET" && url.pathname === "/api/tasks") {
|
|
6518
|
+
res.end(JSON.stringify(listTasks()));
|
|
6519
|
+
return;
|
|
6520
|
+
}
|
|
6521
|
+
|
|
6522
|
+
if (req.method === "GET" && url.pathname === "/api/artifacts") {
|
|
6523
|
+
res.end(JSON.stringify(listArtifacts()));
|
|
6524
|
+
return;
|
|
6525
|
+
}
|
|
6526
|
+
|
|
6527
|
+
if (req.method === "GET" && url.pathname === "/api/trace") {
|
|
6528
|
+
res.end(JSON.stringify(listTrace(50)));
|
|
6529
|
+
return;
|
|
6530
|
+
}
|
|
6531
|
+
|
|
5855
6532
|
if (req.method === "POST" && url.pathname === "/rpc") {
|
|
5856
6533
|
const body = await readRequestBody(req);
|
|
5857
6534
|
const payload = body ? JSON.parse(body) : {};
|
|
@@ -5879,22 +6556,19 @@ async function startDaemon(host, port) {
|
|
|
5879
6556
|
|
|
5880
6557
|
async function startMcpServer(host, port) {
|
|
5881
6558
|
const server = createServer(async (req, res) => {
|
|
6559
|
+
let payload = {};
|
|
5882
6560
|
try {
|
|
5883
6561
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
5884
6562
|
if (req.method !== "POST") {
|
|
5885
|
-
res.end(JSON.stringify({ name: "iola-local-mcp",
|
|
6563
|
+
res.end(JSON.stringify({ name: "iola-local-mcp", protocol: "2024-11-05", tools: mcpTools().map((tool) => tool.name) }));
|
|
5886
6564
|
return;
|
|
5887
6565
|
}
|
|
5888
|
-
|
|
5889
|
-
const
|
|
5890
|
-
const args = payload.params?.arguments || payload.params || {};
|
|
5891
|
-
let result;
|
|
5892
|
-
if (method === "index.search") result = searchDocs(args.query || "", Number(args.limit || 20));
|
|
5893
|
-
else result = await executeRpc(method, { ...args, _: [] });
|
|
6566
|
+
payload = JSON.parse(await readRequestBody(req) || "{}");
|
|
6567
|
+
const result = await handleMcpMessage(payload);
|
|
5894
6568
|
res.end(JSON.stringify({ jsonrpc: "2.0", id: payload.id || null, result }));
|
|
5895
6569
|
} catch (error) {
|
|
5896
6570
|
res.statusCode = 500;
|
|
5897
|
-
res.end(JSON.stringify({ jsonrpc: "2.0", id: null, error: { message: error instanceof Error ? error.message : String(error) } }));
|
|
6571
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", id: payload.id || null, error: { code: -32000, message: error instanceof Error ? error.message : String(error) } }));
|
|
5898
6572
|
}
|
|
5899
6573
|
});
|
|
5900
6574
|
await new Promise((resolve, reject) => {
|
|
@@ -5905,6 +6579,152 @@ async function startMcpServer(host, port) {
|
|
|
5905
6579
|
await new Promise(() => {});
|
|
5906
6580
|
}
|
|
5907
6581
|
|
|
6582
|
+
function renderDaemonDashboard(host, port) {
|
|
6583
|
+
const status = getDbStatus();
|
|
6584
|
+
const sync = getSyncStatus();
|
|
6585
|
+
const usage = getUsageSummary();
|
|
6586
|
+
return `<!doctype html>
|
|
6587
|
+
<html lang="ru">
|
|
6588
|
+
<meta charset="utf-8">
|
|
6589
|
+
<title>iola daemon</title>
|
|
6590
|
+
<style>
|
|
6591
|
+
body{font-family:Segoe UI,Arial,sans-serif;margin:32px;background:#f8fafc;color:#0f172a}
|
|
6592
|
+
h1{margin:0 0 8px;font-size:28px}
|
|
6593
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;margin-top:20px}
|
|
6594
|
+
.card{background:white;border:1px solid #dbe3ef;border-radius:8px;padding:16px}
|
|
6595
|
+
.k{color:#64748b;font-size:12px;text-transform:uppercase}.v{font-size:24px;font-weight:700;margin-top:4px}
|
|
6596
|
+
a{color:#0b62d6}
|
|
6597
|
+
code{background:#eef2f7;padding:2px 5px;border-radius:4px}
|
|
6598
|
+
</style>
|
|
6599
|
+
<h1>iola daemon</h1>
|
|
6600
|
+
<div>Локальная панель CLI-проекта Йошкар-Олы: <code>http://${host}:${port}</code></div>
|
|
6601
|
+
<div class="grid">
|
|
6602
|
+
<div class="card"><div class="k">DB</div><div class="v">${status.status}</div><p>schema ${status.schema}, records ${status.local_records}</p></div>
|
|
6603
|
+
<div class="card"><div class="k">Sync</div><div class="v">${sync.last_status || "none"}</div><p>${sync.last_dataset || "-"} ${sync.last_records || 0}</p></div>
|
|
6604
|
+
<div class="card"><div class="k">Usage</div><div class="v">${usage.requests}</div><p>${usage.estimated_tokens} tokens</p></div>
|
|
6605
|
+
<div class="card"><div class="k">API</div><p><a href="/api/status">/api/status</a></p><p><a href="/api/tasks">/api/tasks</a></p><p><a href="/api/artifacts">/api/artifacts</a></p><p><a href="/api/trace">/api/trace</a></p></div>
|
|
6606
|
+
</div>
|
|
6607
|
+
</html>`;
|
|
6608
|
+
}
|
|
6609
|
+
|
|
6610
|
+
async function startMcpStdio() {
|
|
6611
|
+
const rl = readline.createInterface({ input, terminal: false });
|
|
6612
|
+
for await (const line of rl) {
|
|
6613
|
+
if (!line.trim()) continue;
|
|
6614
|
+
let payload = {};
|
|
6615
|
+
try {
|
|
6616
|
+
payload = JSON.parse(line);
|
|
6617
|
+
const result = await handleMcpMessage(payload);
|
|
6618
|
+
process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id: payload.id || null, result })}\n`);
|
|
6619
|
+
} catch (error) {
|
|
6620
|
+
process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id: payload.id || null, error: { code: -32000, message: error instanceof Error ? error.message : String(error) } })}\n`);
|
|
6621
|
+
}
|
|
6622
|
+
}
|
|
6623
|
+
}
|
|
6624
|
+
|
|
6625
|
+
async function handleMcpMessage(payload) {
|
|
6626
|
+
const method = payload.method;
|
|
6627
|
+
if (method === "initialize") {
|
|
6628
|
+
return {
|
|
6629
|
+
protocolVersion: "2024-11-05",
|
|
6630
|
+
serverInfo: { name: "iola-local-mcp", version: getPackageVersion() },
|
|
6631
|
+
capabilities: { tools: {}, resources: {}, prompts: {} },
|
|
6632
|
+
};
|
|
6633
|
+
}
|
|
6634
|
+
if (method === "tools/list") return { tools: mcpTools() };
|
|
6635
|
+
if (method === "tools/call") {
|
|
6636
|
+
const result = await callMcpTool(payload.params?.name, payload.params?.arguments || {});
|
|
6637
|
+
return { content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }] };
|
|
6638
|
+
}
|
|
6639
|
+
if (method === "resources/list") return { resources: mcpResources() };
|
|
6640
|
+
if (method === "resources/read") {
|
|
6641
|
+
const text = await readMcpResource(payload.params?.uri);
|
|
6642
|
+
return { contents: [{ uri: payload.params?.uri, mimeType: "application/json", text }] };
|
|
6643
|
+
}
|
|
6644
|
+
if (method === "prompts/list") return { prompts: mcpPrompts() };
|
|
6645
|
+
if (method === "prompts/get") return getMcpPrompt(payload.params?.name, payload.params?.arguments || {});
|
|
6646
|
+
if (method === "notifications/initialized") return {};
|
|
6647
|
+
throw new Error(`MCP method неизвестен: ${method}`);
|
|
6648
|
+
}
|
|
6649
|
+
|
|
6650
|
+
function mcpTools() {
|
|
6651
|
+
const schema = (properties = {}) => ({ type: "object", properties, additionalProperties: false });
|
|
6652
|
+
return [
|
|
6653
|
+
{ name: "status", description: "Статус локальной БД, sync и активного AI-профиля.", inputSchema: schema() },
|
|
6654
|
+
{ name: "search", description: "Поиск по локальным открытым данным Йошкар-Олы.", inputSchema: schema({ query: { type: "string" }, dataset: { type: "string" }, limit: { type: "number" } }) },
|
|
6655
|
+
{ name: "card", description: "Карточка объекта по названию или ИНН.", inputSchema: schema({ query: { type: "string" } }) },
|
|
6656
|
+
{ name: "quality", description: "Проверки качества данных.", inputSchema: schema({ scope: { type: "string" } }) },
|
|
6657
|
+
{ name: "sync", description: "Обновление локальной копии слоя.", inputSchema: schema({ dataset: { type: "string" } }) },
|
|
6658
|
+
{ name: "files.tree", description: "Дерево файлов разрешенного workspace.", inputSchema: schema({ path: { type: "string" }, depth: { type: "number" }, limit: { type: "number" } }) },
|
|
6659
|
+
{ name: "files.read", description: "Чтение файла разрешенного workspace.", inputSchema: schema({ path: { type: "string" }, maxBytes: { type: "number" } }) },
|
|
6660
|
+
{ name: "files.search", description: "Поиск текста в файлах workspace.", inputSchema: schema({ query: { type: "string" }, path: { type: "string" }, limit: { type: "number" } }) },
|
|
6661
|
+
{ name: "index.search", description: "Поиск по индексу локальных документов.", inputSchema: schema({ query: { type: "string" }, limit: { type: "number" } }) },
|
|
6662
|
+
{ name: "report", description: "Запуск встроенного отчета.", inputSchema: schema({ name: { type: "string" }, format: { type: "string" }, output: { type: "string" } }) },
|
|
6663
|
+
{ name: "browser.text", description: "Открыть страницу в headless Chromium и вернуть видимый текст.", inputSchema: schema({ url: { type: "string" }, waitMs: { type: "number" } }) },
|
|
6664
|
+
{ name: "browser.screenshot", description: "Сделать скриншот страницы через Chromium.", inputSchema: schema({ url: { type: "string" }, output: { type: "string" }, waitMs: { type: "number" } }) },
|
|
6665
|
+
];
|
|
6666
|
+
}
|
|
6667
|
+
|
|
6668
|
+
function mcpResources() {
|
|
6669
|
+
return [
|
|
6670
|
+
{ uri: "iola://status", name: "Статус CLI", mimeType: "application/json" },
|
|
6671
|
+
{ uri: "iola://sync", name: "Статус синхронизации", mimeType: "application/json" },
|
|
6672
|
+
{ uri: "iola://settings", name: "Эффективные настройки", mimeType: "application/json" },
|
|
6673
|
+
{ uri: "iola://skills", name: "Skills", mimeType: "application/json" },
|
|
6674
|
+
{ uri: "iola://memory", name: "Память агента", mimeType: "application/json" },
|
|
6675
|
+
{ uri: "iola://artifacts", name: "Artifacts", mimeType: "application/json" },
|
|
6676
|
+
];
|
|
6677
|
+
}
|
|
6678
|
+
|
|
6679
|
+
function mcpPrompts() {
|
|
6680
|
+
return [
|
|
6681
|
+
{ name: "data-question", description: "Ответить строго по открытым данным Йошкар-Олы.", arguments: [{ name: "question", required: true }] },
|
|
6682
|
+
{ name: "document-review", description: "Проверить документ на полноту и источники.", arguments: [{ name: "file", required: true }] },
|
|
6683
|
+
{ name: "report-build", description: "Собрать отчет по выбранному слою.", arguments: [{ name: "dataset", required: true }] },
|
|
6684
|
+
];
|
|
6685
|
+
}
|
|
6686
|
+
|
|
6687
|
+
async function callMcpTool(name, args = {}) {
|
|
6688
|
+
if (name === "index.search") return searchDocs(args.query || "", Number(args.limit || 20));
|
|
6689
|
+
if (name === "report") {
|
|
6690
|
+
const output = args.output || `${args.name || "education-contacts"}.${args.format || "xlsx"}`;
|
|
6691
|
+
await handleExport([args.name || "education-contacts", "--format", args.format || "xlsx", "--output", output]);
|
|
6692
|
+
return { output };
|
|
6693
|
+
}
|
|
6694
|
+
if (name === "browser.text") {
|
|
6695
|
+
return runBrowserAutomation("text", { url: args.url, waitMs: Number(args.waitMs || 0), timeout: Number(args.timeout || 30000), viewport: args.viewport || "1366x768" });
|
|
6696
|
+
}
|
|
6697
|
+
if (name === "browser.screenshot") {
|
|
6698
|
+
const output = path.resolve(args.output || "browser-page.png");
|
|
6699
|
+
await runBrowserAutomation("screenshot", { url: args.url, output, waitMs: Number(args.waitMs || 0), timeout: Number(args.timeout || 30000), viewport: args.viewport || "1366x768" });
|
|
6700
|
+
return { output };
|
|
6701
|
+
}
|
|
6702
|
+
return executeRpc(name, { ...args, _: [] });
|
|
6703
|
+
}
|
|
6704
|
+
|
|
6705
|
+
async function readMcpResource(uri) {
|
|
6706
|
+
if (uri === "iola://status") return JSON.stringify({ db: getDbStatus(), sync: getSyncStatus() }, null, 2);
|
|
6707
|
+
if (uri === "iola://sync") return JSON.stringify(getSyncStatus(), null, 2);
|
|
6708
|
+
if (uri === "iola://settings") return JSON.stringify(await loadConfig(), null, 2);
|
|
6709
|
+
if (uri === "iola://skills") return JSON.stringify(listSkills(await loadConfig()), null, 2);
|
|
6710
|
+
if (uri === "iola://memory") return JSON.stringify(listMemory(100), null, 2);
|
|
6711
|
+
if (uri === "iola://artifacts") return JSON.stringify(listArtifacts(), null, 2);
|
|
6712
|
+
throw new Error(`MCP resource неизвестен: ${uri}`);
|
|
6713
|
+
}
|
|
6714
|
+
|
|
6715
|
+
function getMcpPrompt(name, args = {}) {
|
|
6716
|
+
if (name === "data-question") {
|
|
6717
|
+
return { messages: [{ role: "user", content: { type: "text", text: `Ответь по открытым данным городского округа "Город Йошкар-Ола", не выдумывая сведения: ${args.question || ""}` } }] };
|
|
6718
|
+
}
|
|
6719
|
+
if (name === "document-review") {
|
|
6720
|
+
return { messages: [{ role: "user", content: { type: "text", text: `Проверь документ ${args.file || ""}: полнота, источники, противоречия, ошибки оформления.` } }] };
|
|
6721
|
+
}
|
|
6722
|
+
if (name === "report-build") {
|
|
6723
|
+
return { messages: [{ role: "user", content: { type: "text", text: `Собери практичный отчет по слою ${args.dataset || "schools"} с выводами и источником данных.` } }] };
|
|
6724
|
+
}
|
|
6725
|
+
throw new Error(`MCP prompt неизвестен: ${name}`);
|
|
6726
|
+
}
|
|
6727
|
+
|
|
5908
6728
|
function readRequestBody(req) {
|
|
5909
6729
|
return new Promise((resolve, reject) => {
|
|
5910
6730
|
let body = "";
|
|
@@ -6338,6 +7158,101 @@ function recordToolTrace(runId, tool, args, status, summary) {
|
|
|
6338
7158
|
}
|
|
6339
7159
|
}
|
|
6340
7160
|
|
|
7161
|
+
function recordUsage({ providerConfig, question, answer, sessionId, profile }) {
|
|
7162
|
+
try {
|
|
7163
|
+
initDatabase();
|
|
7164
|
+
const inputChars = String(question || "").length;
|
|
7165
|
+
const outputChars = String(answer || "").length;
|
|
7166
|
+
const estimatedTokens = Math.ceil((inputChars + outputChars) / 4);
|
|
7167
|
+
const estimatedCostUsd = estimateCost(providerConfig, estimatedTokens);
|
|
7168
|
+
const db = openDatabase();
|
|
7169
|
+
try {
|
|
7170
|
+
db.prepare(`
|
|
7171
|
+
INSERT INTO usage_events(provider, model, profile, input_chars, output_chars, estimated_tokens, estimated_cost_usd, session_id)
|
|
7172
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
7173
|
+
`).run(
|
|
7174
|
+
providerConfig.provider || "",
|
|
7175
|
+
providerConfig.model || "",
|
|
7176
|
+
profile || providerConfig.name || "",
|
|
7177
|
+
inputChars,
|
|
7178
|
+
outputChars,
|
|
7179
|
+
estimatedTokens,
|
|
7180
|
+
estimatedCostUsd,
|
|
7181
|
+
sessionId || null,
|
|
7182
|
+
);
|
|
7183
|
+
} finally {
|
|
7184
|
+
db.close();
|
|
7185
|
+
}
|
|
7186
|
+
} catch {
|
|
7187
|
+
// Usage accounting must not break the main answer path.
|
|
7188
|
+
}
|
|
7189
|
+
}
|
|
7190
|
+
|
|
7191
|
+
function estimateCost(providerConfig, tokens) {
|
|
7192
|
+
if (!providerConfig || providerConfig.provider === "ollama" || providerConfig.provider === "codex") return 0;
|
|
7193
|
+
const perMillion = providerConfig.provider === "openrouter" ? 0.25 : 0.4;
|
|
7194
|
+
return Math.round((tokens / 1_000_000) * perMillion * 1_000_000) / 1_000_000;
|
|
7195
|
+
}
|
|
7196
|
+
|
|
7197
|
+
function getUsageSummary() {
|
|
7198
|
+
initDatabase();
|
|
7199
|
+
const db = openDatabase();
|
|
7200
|
+
try {
|
|
7201
|
+
const row = db.prepare("SELECT COUNT(*) AS requests, COALESCE(SUM(estimated_tokens),0) AS tokens, COALESCE(SUM(estimated_cost_usd),0) AS cost FROM usage_events").get();
|
|
7202
|
+
return { requests: row.requests || 0, estimated_tokens: row.tokens || 0, estimated_cost_usd: Number(row.cost || 0).toFixed(6) };
|
|
7203
|
+
} finally {
|
|
7204
|
+
db.close();
|
|
7205
|
+
}
|
|
7206
|
+
}
|
|
7207
|
+
|
|
7208
|
+
function getUsageByModel() {
|
|
7209
|
+
initDatabase();
|
|
7210
|
+
const db = openDatabase();
|
|
7211
|
+
try {
|
|
7212
|
+
return db.prepare(`
|
|
7213
|
+
SELECT provider, model, COUNT(*) AS requests, COALESCE(SUM(estimated_tokens),0) AS tokens, printf('%.6f', COALESCE(SUM(estimated_cost_usd),0)) AS cost
|
|
7214
|
+
FROM usage_events GROUP BY provider, model ORDER BY requests DESC
|
|
7215
|
+
`).all();
|
|
7216
|
+
} finally {
|
|
7217
|
+
db.close();
|
|
7218
|
+
}
|
|
7219
|
+
}
|
|
7220
|
+
|
|
7221
|
+
function getUsageBySession() {
|
|
7222
|
+
initDatabase();
|
|
7223
|
+
const db = openDatabase();
|
|
7224
|
+
try {
|
|
7225
|
+
return db.prepare(`
|
|
7226
|
+
SELECT COALESCE(session_id, 0) AS session_id, COUNT(*) AS requests, COALESCE(SUM(estimated_tokens),0) AS tokens, printf('%.6f', COALESCE(SUM(estimated_cost_usd),0)) AS cost
|
|
7227
|
+
FROM usage_events GROUP BY session_id ORDER BY requests DESC LIMIT 100
|
|
7228
|
+
`).all();
|
|
7229
|
+
} finally {
|
|
7230
|
+
db.close();
|
|
7231
|
+
}
|
|
7232
|
+
}
|
|
7233
|
+
|
|
7234
|
+
function listBudgets() {
|
|
7235
|
+
initDatabase();
|
|
7236
|
+
const db = openDatabase();
|
|
7237
|
+
try {
|
|
7238
|
+
const spent = Number(db.prepare("SELECT COALESCE(SUM(estimated_cost_usd),0) AS spent FROM usage_events WHERE created_at >= datetime('now','-1 day')").get()?.spent || 0);
|
|
7239
|
+
return db.prepare("SELECT scope, amount_usd, updated_at FROM budgets ORDER BY scope").all()
|
|
7240
|
+
.map((row) => ({ ...row, spent_usd: row.scope === "daily" ? spent.toFixed(6) : "-" }));
|
|
7241
|
+
} finally {
|
|
7242
|
+
db.close();
|
|
7243
|
+
}
|
|
7244
|
+
}
|
|
7245
|
+
|
|
7246
|
+
function setBudget(scope, amountUsd) {
|
|
7247
|
+
initDatabase();
|
|
7248
|
+
const db = openDatabase();
|
|
7249
|
+
try {
|
|
7250
|
+
db.prepare("INSERT INTO budgets(scope, amount_usd) VALUES (?, ?) ON CONFLICT(scope) DO UPDATE SET amount_usd = excluded.amount_usd, updated_at = datetime('now')").run(scope, amountUsd);
|
|
7251
|
+
} finally {
|
|
7252
|
+
db.close();
|
|
7253
|
+
}
|
|
7254
|
+
}
|
|
7255
|
+
|
|
6341
7256
|
function listTrace(limit = 20) {
|
|
6342
7257
|
initDatabase();
|
|
6343
7258
|
const db = openDatabase();
|
|
@@ -6348,6 +7263,20 @@ function listTrace(limit = 20) {
|
|
|
6348
7263
|
}
|
|
6349
7264
|
}
|
|
6350
7265
|
|
|
7266
|
+
function buildTrajectoryRows(limit = 500) {
|
|
7267
|
+
initDatabase();
|
|
7268
|
+
const db = openDatabase();
|
|
7269
|
+
try {
|
|
7270
|
+
const history = db.prepare("SELECT id, created_at, 'ask' AS type, question AS summary, provider, model FROM ask_history ORDER BY id DESC LIMIT ?").all(limit);
|
|
7271
|
+
const traces = db.prepare("SELECT id, created_at, 'tool' AS type, tool || ': ' || COALESCE(summary,'') AS summary, status, run_id FROM tool_traces ORDER BY id DESC LIMIT ?").all(limit);
|
|
7272
|
+
return [...history, ...traces]
|
|
7273
|
+
.sort((left, right) => String(right.created_at).localeCompare(String(left.created_at)))
|
|
7274
|
+
.slice(0, limit);
|
|
7275
|
+
} finally {
|
|
7276
|
+
db.close();
|
|
7277
|
+
}
|
|
7278
|
+
}
|
|
7279
|
+
|
|
6351
7280
|
function getTraceRun(runId) {
|
|
6352
7281
|
initDatabase();
|
|
6353
7282
|
const db = openDatabase();
|
|
@@ -6378,6 +7307,22 @@ async function createSnapshot() {
|
|
|
6378
7307
|
}
|
|
6379
7308
|
}
|
|
6380
7309
|
|
|
7310
|
+
async function createSandboxCopy(name = "") {
|
|
7311
|
+
const config = await loadConfig();
|
|
7312
|
+
const workspace = resolveWorkspaceRoot(config);
|
|
7313
|
+
const sandboxesDir = path.join(CONFIG_DIR, "sandboxes");
|
|
7314
|
+
await mkdir(sandboxesDir, { recursive: true });
|
|
7315
|
+
const safeName = name ? String(name).replace(/[^a-zA-Z0-9_-]+/g, "-") : `sandbox-${Date.now()}`;
|
|
7316
|
+
const target = path.join(sandboxesDir, safeName);
|
|
7317
|
+
await rm(target, { recursive: true, force: true });
|
|
7318
|
+
await cp(workspace, target, {
|
|
7319
|
+
recursive: true,
|
|
7320
|
+
filter: (source) => !isBlockedPathForConfig(source, config) && !source.includes(`${path.sep}node_modules${path.sep}`) && !source.includes(`${path.sep}.git${path.sep}`),
|
|
7321
|
+
});
|
|
7322
|
+
const id = saveArtifact("sandbox", safeName, target, { workspace });
|
|
7323
|
+
return { id, workspace, path: target };
|
|
7324
|
+
}
|
|
7325
|
+
|
|
6381
7326
|
function listSnapshots() {
|
|
6382
7327
|
initDatabase();
|
|
6383
7328
|
const db = openDatabase();
|
|
@@ -6653,7 +7598,7 @@ async function executeRpc(method, options = {}) {
|
|
|
6653
7598
|
if (method === "index.search") {
|
|
6654
7599
|
return searchDocs(options.query || options.search || "", Number(options.limit || 20));
|
|
6655
7600
|
}
|
|
6656
|
-
throw new Error(`RPC method неизвестен: ${method}. Доступно: status, search, card, quality, sync.`);
|
|
7601
|
+
throw new Error(`RPC method неизвестен: ${method}. Доступно: status, search, card, quality, sync, files.tree, files.read, files.search, index.search.`);
|
|
6657
7602
|
}
|
|
6658
7603
|
|
|
6659
7604
|
async function getLatestNpmVersion(packageName) {
|
|
@@ -6780,11 +7725,39 @@ async function writeConfig(value) {
|
|
|
6780
7725
|
}
|
|
6781
7726
|
|
|
6782
7727
|
async function loadConfig() {
|
|
7728
|
+
let config = DEFAULT_AI_CONFIG;
|
|
7729
|
+
for (const layer of [CONFIG_FILE, PROJECT_CONFIG_FILE, LOCAL_CONFIG_FILE]) {
|
|
7730
|
+
const value = await readConfigLayer(layer);
|
|
7731
|
+
if (value) config = mergeConfig(config, value);
|
|
7732
|
+
}
|
|
7733
|
+
return config;
|
|
7734
|
+
}
|
|
7735
|
+
|
|
7736
|
+
async function loadConfigLayers() {
|
|
7737
|
+
const files = [
|
|
7738
|
+
{ scope: "defaults", file: "builtin", value: DEFAULT_AI_CONFIG, exists: true },
|
|
7739
|
+
{ scope: "user", file: CONFIG_FILE },
|
|
7740
|
+
{ scope: "project", file: PROJECT_CONFIG_FILE },
|
|
7741
|
+
{ scope: "local", file: LOCAL_CONFIG_FILE },
|
|
7742
|
+
];
|
|
7743
|
+
const rows = [];
|
|
7744
|
+
for (const layer of files) {
|
|
7745
|
+
if (layer.scope === "defaults") {
|
|
7746
|
+
rows.push({ ...layer, errors: validateConfig(layer.value) });
|
|
7747
|
+
continue;
|
|
7748
|
+
}
|
|
7749
|
+
const value = await readConfigLayer(layer.file);
|
|
7750
|
+
rows.push({ ...layer, exists: Boolean(value), value, errors: value ? validateConfig(mergeConfig(DEFAULT_AI_CONFIG, value)) : [] });
|
|
7751
|
+
}
|
|
7752
|
+
rows.push({ scope: "runtime", file: "process.env", exists: true, value: { IOLA_API_BASE_URL: process.env.IOLA_API_BASE_URL || "", IOLA_MCP_BASE_URL: process.env.IOLA_MCP_BASE_URL || "" }, errors: [] });
|
|
7753
|
+
return rows;
|
|
7754
|
+
}
|
|
7755
|
+
|
|
7756
|
+
async function readConfigLayer(file) {
|
|
6783
7757
|
try {
|
|
6784
|
-
|
|
6785
|
-
return mergeConfig(DEFAULT_AI_CONFIG, JSON.parse(text));
|
|
7758
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
6786
7759
|
} catch {
|
|
6787
|
-
return
|
|
7760
|
+
return null;
|
|
6788
7761
|
}
|
|
6789
7762
|
}
|
|
6790
7763
|
|
|
@@ -6836,6 +7809,20 @@ function mergeConfig(base, override) {
|
|
|
6836
7809
|
...base.cron,
|
|
6837
7810
|
...(override.cron || {}),
|
|
6838
7811
|
},
|
|
7812
|
+
hooks: {
|
|
7813
|
+
...base.hooks,
|
|
7814
|
+
...(override.hooks || {}),
|
|
7815
|
+
},
|
|
7816
|
+
subagents: {
|
|
7817
|
+
...(base.subagents || {}),
|
|
7818
|
+
...(override.subagents || {}),
|
|
7819
|
+
},
|
|
7820
|
+
workspaces: {
|
|
7821
|
+
...(base.workspaces || {}),
|
|
7822
|
+
...(override.workspaces || {}),
|
|
7823
|
+
},
|
|
7824
|
+
hooksTrusted: override.hooksTrusted ?? base.hooksTrusted,
|
|
7825
|
+
local: override.local ?? base.local,
|
|
6839
7826
|
};
|
|
6840
7827
|
}
|
|
6841
7828
|
|
|
@@ -6996,6 +7983,7 @@ function runCommand(command, args, options = {}) {
|
|
|
6996
7983
|
const child = execFile(command, args, {
|
|
6997
7984
|
windowsHide: true,
|
|
6998
7985
|
maxBuffer: 1024 * 1024 * 5,
|
|
7986
|
+
cwd: options.cwd,
|
|
6999
7987
|
env: {
|
|
7000
7988
|
...process.env,
|
|
7001
7989
|
...(options.env || {}),
|