@iola_adm/iola-cli 0.1.29 → 0.1.30
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 +12 -2
- package/package.json +1 -1
- package/src/cli.js +811 -25
- package/wiki/Home.md +1 -0
- package/wiki//320/232/320/276/320/274/320/260/320/275/320/264/321/213.md +13 -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,15 @@ 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");
|
|
22
25
|
const INDEXABLE_EXTENSIONS = /\.(md|txt|csv|json|html|docx|xlsx|pptx|pdf)$/i;
|
|
23
26
|
const LOCAL_TOOLS = ["search_local", "get_card", "export_data", "run_report", "save_view"];
|
|
24
27
|
const FILE_TOOLS = ["files_tree", "files_read", "files_search", "files_write", "files_patch"];
|
|
25
28
|
const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS];
|
|
26
|
-
const HOOK_EVENTS = ["SessionStart", "BeforeTool", "AfterTool", "AfterSync", "BeforeExport", "SessionEnd"];
|
|
29
|
+
const HOOK_EVENTS = ["SessionStart", "BeforeTool", "AfterTool", "PreToolUse", "PostToolUse", "OnError", "AfterSync", "BeforeExport", "SessionEnd"];
|
|
27
30
|
const DAEMON_PORT = Number(process.env.IOLA_DAEMON_PORT || 18790);
|
|
28
31
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
32
|
const BUILTIN_SKILLS_DIR = path.resolve(__dirname, "..", "skills");
|
|
@@ -82,6 +85,23 @@ const FEATURES = {
|
|
|
82
85
|
"mcp-management": { stage: "stable", defaultEnabled: true, description: "Команды управления MCP-интеграциями." },
|
|
83
86
|
"web-search": { stage: "experimental", defaultEnabled: false, description: "Резерв под web-search режимы AI." },
|
|
84
87
|
};
|
|
88
|
+
const SKILL_BUNDLES = {
|
|
89
|
+
analyst: {
|
|
90
|
+
description: "Аналитик открытых данных: поиск, карточки, отчеты и память.",
|
|
91
|
+
skills: ["open-data", "reports", "local-model"],
|
|
92
|
+
requirements: ["Локальная SQLite-БД", "публичный API"],
|
|
93
|
+
},
|
|
94
|
+
documents: {
|
|
95
|
+
description: "Работа с локальными документами, индексом, архивами и выгрузками.",
|
|
96
|
+
skills: ["open-data", "reports"],
|
|
97
|
+
requirements: ["files mode read-only/workspace-write", "7-Zip для архивов"],
|
|
98
|
+
},
|
|
99
|
+
"local-agent": {
|
|
100
|
+
description: "Локальная модель Ollama с проверочным reasoning и локальными tools.",
|
|
101
|
+
skills: ["local-model", "open-data"],
|
|
102
|
+
requirements: ["Ollama", "локальная модель"],
|
|
103
|
+
},
|
|
104
|
+
};
|
|
85
105
|
const DEFAULT_AI_CONFIG = {
|
|
86
106
|
api: {
|
|
87
107
|
baseUrl: "https://apiiola.yasg.ru/api/v1",
|
|
@@ -239,6 +259,7 @@ const COMMANDS = new Map([
|
|
|
239
259
|
["resume", resumeSession],
|
|
240
260
|
["fork", forkSession],
|
|
241
261
|
["features", handleFeatures],
|
|
262
|
+
["settings", handleSettings],
|
|
242
263
|
["wiki", handleWiki],
|
|
243
264
|
["context", handleContext],
|
|
244
265
|
["skills", handleSkills],
|
|
@@ -254,7 +275,11 @@ const COMMANDS = new Map([
|
|
|
254
275
|
["tasks", handleTasks],
|
|
255
276
|
["artifacts", handleArtifacts],
|
|
256
277
|
["snapshot", handleSnapshot],
|
|
278
|
+
["sandbox", handleSandbox],
|
|
257
279
|
["trace", handleTrace],
|
|
280
|
+
["trajectory", handleTrajectory],
|
|
281
|
+
["usage", handleUsage],
|
|
282
|
+
["budget", handleBudget],
|
|
258
283
|
["policy", handlePolicy],
|
|
259
284
|
["export", handleExport],
|
|
260
285
|
["cron", handleCron],
|
|
@@ -264,6 +289,8 @@ const COMMANDS = new Map([
|
|
|
264
289
|
["memory", handleMemory],
|
|
265
290
|
["hooks", handleHooks],
|
|
266
291
|
["agents", handleAgents],
|
|
292
|
+
["subagents", handleSubagents],
|
|
293
|
+
["review", handleReview],
|
|
267
294
|
["mcp", handleMcp],
|
|
268
295
|
["cache", handleCache],
|
|
269
296
|
["sync", handleSync],
|
|
@@ -353,12 +380,14 @@ Usage:
|
|
|
353
380
|
iola history [--limit 20]
|
|
354
381
|
iola history clear
|
|
355
382
|
iola sessions [--limit 20]
|
|
383
|
+
iola sessions replay SESSION_ID
|
|
356
384
|
iola resume SESSION_ID [TEXT]
|
|
357
385
|
iola fork SESSION_ID [TEXT]
|
|
358
386
|
iola features list|enable|disable
|
|
387
|
+
iola settings list|get|validate|doctor|init
|
|
359
388
|
iola wiki [open|links]
|
|
360
389
|
iola context list|show|init
|
|
361
|
-
iola skills list|show|paths|enable|disable
|
|
390
|
+
iola skills list|show|paths|enable|disable|bundles|bundle|doctor
|
|
362
391
|
iola tools list|toolsets|enable|disable|profile
|
|
363
392
|
iola files status|mode|approvals|tree|read|search|write|patch
|
|
364
393
|
iola archive doctor|list|test|extract|create|index
|
|
@@ -371,17 +400,23 @@ Usage:
|
|
|
371
400
|
iola tasks list|add|done|run
|
|
372
401
|
iola artifacts list|show|open
|
|
373
402
|
iola snapshot create|list|restore
|
|
403
|
+
iola sandbox fork|run|diff|apply
|
|
374
404
|
iola trace last|show
|
|
405
|
+
iola trajectory export|last
|
|
406
|
+
iola usage summary|models|sessions
|
|
407
|
+
iola budget status|set
|
|
375
408
|
iola policy use safe|analyst|developer|full
|
|
376
409
|
iola export REPORT --format docx|xlsx --output FILE
|
|
377
410
|
iola cron list|add|delete|run|tick
|
|
378
411
|
iola daemon start|status
|
|
379
412
|
iola rpc call METHOD [ARGS] [--json]
|
|
380
413
|
iola permissions list|allow|deny
|
|
381
|
-
iola memory show|add|set|clear|export
|
|
382
|
-
iola hooks list|add|delete|run
|
|
414
|
+
iola memory show|add|set|clear|export|curate|duplicates|prune
|
|
415
|
+
iola hooks list|events|add|delete|run|trust|audit
|
|
383
416
|
iola agents list|run
|
|
384
|
-
iola
|
|
417
|
+
iola subagents list|run|parallel|add
|
|
418
|
+
iola review config|data|docs|report
|
|
419
|
+
iola mcp list|status|install|remove|serve [--stdio]
|
|
385
420
|
iola cache status|warm|clear
|
|
386
421
|
iola sync [--dataset schools|kindergartens]
|
|
387
422
|
iola sync status
|
|
@@ -975,6 +1010,14 @@ function showBanner() {
|
|
|
975
1010
|
console.log("открытые данные • MCP • локальный AI");
|
|
976
1011
|
}
|
|
977
1012
|
|
|
1013
|
+
function getPackageVersion() {
|
|
1014
|
+
try {
|
|
1015
|
+
return JSON.parse(readFileSync(path.resolve(__dirname, "..", "package.json"), "utf8")).version;
|
|
1016
|
+
} catch {
|
|
1017
|
+
return "0.0.0";
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
978
1021
|
async function showVersion(args = []) {
|
|
979
1022
|
const options = parseOptions(args);
|
|
980
1023
|
const packageJson = await import("../package.json", { with: { type: "json" } });
|
|
@@ -1512,6 +1555,17 @@ async function handleSessions(args) {
|
|
|
1512
1555
|
return;
|
|
1513
1556
|
}
|
|
1514
1557
|
|
|
1558
|
+
if (action === "replay") {
|
|
1559
|
+
const sessionId = Number(args[1]);
|
|
1560
|
+
if (!sessionId) throw new Error("Пример: iola sessions replay 1");
|
|
1561
|
+
const rows = getSessionMessages(sessionId);
|
|
1562
|
+
for (const row of rows) {
|
|
1563
|
+
console.log(`\n[${row.role}] ${row.created_at}`);
|
|
1564
|
+
console.log(row.content);
|
|
1565
|
+
}
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1515
1569
|
const rows = listSessions(Number(options.limit || 20));
|
|
1516
1570
|
|
|
1517
1571
|
if (options.json) {
|
|
@@ -1587,6 +1641,61 @@ async function handleFeatures(args) {
|
|
|
1587
1641
|
throw new Error("Команды features: list, enable NAME, disable NAME.");
|
|
1588
1642
|
}
|
|
1589
1643
|
|
|
1644
|
+
async function handleSettings(args) {
|
|
1645
|
+
const [action = "list", key] = args;
|
|
1646
|
+
const layers = await loadConfigLayers();
|
|
1647
|
+
const effective = await loadConfig();
|
|
1648
|
+
|
|
1649
|
+
if (action === "list" || action === "ls" || action === "doctor") {
|
|
1650
|
+
const rows = layers.map((layer) => ({
|
|
1651
|
+
scope: layer.scope,
|
|
1652
|
+
file: layer.file,
|
|
1653
|
+
exists: layer.exists ? "yes" : "no",
|
|
1654
|
+
valid: layer.errors.length ? "no" : "yes",
|
|
1655
|
+
errors: layer.errors.join("; ") || "-",
|
|
1656
|
+
}));
|
|
1657
|
+
printTable(rows, [["scope", "Слой"], ["exists", "Есть"], ["valid", "Валиден"], ["file", "Файл"], ["errors", "Ошибки"]]);
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
if (action === "get") {
|
|
1662
|
+
if (!key) {
|
|
1663
|
+
printJson(effective);
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
const value = getConfigValue(effective, key);
|
|
1667
|
+
if (typeof value === "object") printJson(value);
|
|
1668
|
+
else console.log(value ?? "-");
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
if (action === "validate") {
|
|
1673
|
+
const errors = validateConfig(effective);
|
|
1674
|
+
if (errors.length) {
|
|
1675
|
+
printTable(errors.map((error) => ({ error })), [["error", "Ошибка"]]);
|
|
1676
|
+
process.exitCode = 1;
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
console.log("Конфигурация валидна.");
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
if (action === "init") {
|
|
1684
|
+
await mkdir(PROJECT_IOLA_DIR, { recursive: true });
|
|
1685
|
+
if (!existsSync(PROJECT_CONFIG_FILE)) {
|
|
1686
|
+
await writeFile(PROJECT_CONFIG_FILE, `${JSON.stringify({ files: { workspaceRoot: "." } }, null, 2)}\n`, "utf8");
|
|
1687
|
+
}
|
|
1688
|
+
if (!existsSync(LOCAL_CONFIG_FILE)) {
|
|
1689
|
+
await writeFile(LOCAL_CONFIG_FILE, `${JSON.stringify({ local: true }, null, 2)}\n`, "utf8");
|
|
1690
|
+
}
|
|
1691
|
+
console.log(`Создан project config: ${PROJECT_CONFIG_FILE}`);
|
|
1692
|
+
console.log(`Создан local config: ${LOCAL_CONFIG_FILE}`);
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
throw new Error("Команды settings: list, get [KEY], validate, doctor, init.");
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1590
1699
|
async function handleWiki(args) {
|
|
1591
1700
|
const [action = "links"] = args;
|
|
1592
1701
|
const base = "https://github.com/adm-iola/iola-cli/wiki";
|
|
@@ -1599,6 +1708,7 @@ async function handleWiki(args) {
|
|
|
1599
1708
|
["Skills и toolsets", `${base}/Skills-и-toolsets`],
|
|
1600
1709
|
["Локальные файлы", `${base}/Локальные-файлы`],
|
|
1601
1710
|
["Рабочая среда агента", `${base}/Рабочая-среда-агента`],
|
|
1711
|
+
["Платформа агента", `${base}/Платформа-агента`],
|
|
1602
1712
|
["Расширения и локальные данные", `${base}/Расширения-и-локальные-данные`],
|
|
1603
1713
|
["Архивы и мастер настройки", `${base}/Архивы-и-мастер-настройки`],
|
|
1604
1714
|
["Daemon, RPC и cron", `${base}/Daemon-RPC-и-cron`],
|
|
@@ -1701,6 +1811,41 @@ async function handleSkills(args) {
|
|
|
1701
1811
|
return;
|
|
1702
1812
|
}
|
|
1703
1813
|
|
|
1814
|
+
if (action === "bundles") {
|
|
1815
|
+
const enabled = new Set(config.skills?.enabled || []);
|
|
1816
|
+
const rows = Object.entries(SKILL_BUNDLES).map(([bundle, meta]) => ({
|
|
1817
|
+
bundle,
|
|
1818
|
+
enabled: meta.skills.every((skill) => enabled.has(skill)) ? "yes" : "partial/no",
|
|
1819
|
+
skills: meta.skills.join(", "),
|
|
1820
|
+
description: meta.description,
|
|
1821
|
+
}));
|
|
1822
|
+
printTable(rows, [["bundle", "Bundle"], ["enabled", "Вкл"], ["skills", "Skills"], ["description", "Описание"]]);
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
if (action === "bundle") {
|
|
1827
|
+
const [operation, bundleName] = args.slice(1);
|
|
1828
|
+
if (operation !== "enable" || !SKILL_BUNDLES[bundleName]) {
|
|
1829
|
+
throw new Error(`Пример: iola skills bundle enable analyst. Доступно: ${Object.keys(SKILL_BUNDLES).join(", ")}`);
|
|
1830
|
+
}
|
|
1831
|
+
const enabled = new Set(config.skills?.enabled || []);
|
|
1832
|
+
for (const skill of SKILL_BUNDLES[bundleName].skills) enabled.add(skill);
|
|
1833
|
+
await saveConfig({ skills: { ...(config.skills || {}), enabled: [...enabled] } });
|
|
1834
|
+
console.log(`Skill bundle включен: ${bundleName}`);
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
if (action === "doctor") {
|
|
1839
|
+
const skills = listSkills(config);
|
|
1840
|
+
const enabled = new Set(config.skills?.enabled || []);
|
|
1841
|
+
const rows = [
|
|
1842
|
+
...skills.map((skill) => ({ item: skill.name, type: "skill", status: enabled.has(skill.name) ? "enabled" : "available", detail: skill.file })),
|
|
1843
|
+
...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(", ") })),
|
|
1844
|
+
];
|
|
1845
|
+
printTable(rows, [["type", "Тип"], ["item", "Имя"], ["status", "Статус"], ["detail", "Детали"]]);
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1704
1849
|
if (action === "show") {
|
|
1705
1850
|
const skill = findSkill(name, config);
|
|
1706
1851
|
if (!skill) throw new Error(`Skill не найден: ${name}`);
|
|
@@ -1718,7 +1863,7 @@ async function handleSkills(args) {
|
|
|
1718
1863
|
return;
|
|
1719
1864
|
}
|
|
1720
1865
|
|
|
1721
|
-
throw new Error("Команды skills: list, paths, show NAME, enable NAME, disable NAME.");
|
|
1866
|
+
throw new Error("Команды skills: list, paths, show NAME, enable NAME, disable NAME, bundles, bundle enable NAME, doctor.");
|
|
1722
1867
|
}
|
|
1723
1868
|
|
|
1724
1869
|
async function handleTools(args) {
|
|
@@ -2161,6 +2306,38 @@ async function handleSnapshot(args) {
|
|
|
2161
2306
|
throw new Error("Команды snapshot: create, list, restore ID.");
|
|
2162
2307
|
}
|
|
2163
2308
|
|
|
2309
|
+
async function handleSandbox(args) {
|
|
2310
|
+
const [action = "fork", ...rest] = args;
|
|
2311
|
+
if (action === "fork") {
|
|
2312
|
+
const result = await createSandboxCopy(rest[0]);
|
|
2313
|
+
printKeyValue(result);
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
if (action === "run") {
|
|
2317
|
+
const command = rest.join(" ").trim();
|
|
2318
|
+
if (!command) throw new Error('Пример: iola sandbox run "npm test"');
|
|
2319
|
+
const sandbox = await createSandboxCopy();
|
|
2320
|
+
const parts = splitCommandLine(command);
|
|
2321
|
+
console.log(`Sandbox: ${sandbox.path}`);
|
|
2322
|
+
await runCommand(parts[0], parts.slice(1), { inherit: true, cwd: sandbox.path });
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
if (action === "diff") {
|
|
2326
|
+
const sandboxPath = rest[0];
|
|
2327
|
+
if (!sandboxPath) throw new Error("Пример: iola sandbox diff PATH");
|
|
2328
|
+
await runCommand("git", ["diff", "--no-index", process.cwd(), sandboxPath], { inherit: true }).catch(() => {});
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
if (action === "apply") {
|
|
2332
|
+
const sandboxPath = rest[0];
|
|
2333
|
+
if (!sandboxPath) throw new Error("Пример: iola sandbox apply PATH");
|
|
2334
|
+
await cp(sandboxPath, process.cwd(), { recursive: true, force: true });
|
|
2335
|
+
console.log(`Sandbox применен: ${sandboxPath}`);
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
throw new Error("Команды sandbox: fork [NAME], run COMMAND, diff PATH, apply PATH.");
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2164
2341
|
async function handleTrace(args) {
|
|
2165
2342
|
const [action = "last", id] = args;
|
|
2166
2343
|
if (action === "last") {
|
|
@@ -2174,6 +2351,61 @@ async function handleTrace(args) {
|
|
|
2174
2351
|
throw new Error("Команды trace: last [LIMIT], show RUN_ID.");
|
|
2175
2352
|
}
|
|
2176
2353
|
|
|
2354
|
+
async function handleTrajectory(args) {
|
|
2355
|
+
const [action = "export", ...rest] = args;
|
|
2356
|
+
const options = parseOptions(rest);
|
|
2357
|
+
if (action === "last") {
|
|
2358
|
+
const rows = buildTrajectoryRows(Number(options.limit || rest[0] || 20));
|
|
2359
|
+
if (options.json) printJson(rows);
|
|
2360
|
+
else printTable(rows, [["type", "Тип"], ["id", "ID"], ["created_at", "Дата"], ["summary", "Сводка"]]);
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
if (action === "export") {
|
|
2364
|
+
const format = options.format || "jsonl";
|
|
2365
|
+
const output = options.output || path.join(process.cwd(), `iola-trajectory-${Date.now()}.${format}`);
|
|
2366
|
+
const rows = buildTrajectoryRows(Number(options.limit || 500));
|
|
2367
|
+
const text = format === "json" ? `${JSON.stringify(rows, null, 2)}\n` : rows.map((row) => JSON.stringify(row)).join("\n") + "\n";
|
|
2368
|
+
await writeFile(output, text, "utf8");
|
|
2369
|
+
saveArtifact("trajectory", path.basename(output), output, { format, rows: rows.length });
|
|
2370
|
+
console.log(`Trajectory экспортирована: ${output}`);
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
throw new Error("Команды trajectory: last [--limit N], export [--format jsonl|json] [--output FILE].");
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
async function handleUsage(args) {
|
|
2377
|
+
const [action = "summary"] = args;
|
|
2378
|
+
if (action === "summary") {
|
|
2379
|
+
printKeyValue(getUsageSummary());
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
if (action === "models") {
|
|
2383
|
+
printTable(getUsageByModel(), [["provider", "Провайдер"], ["model", "Модель"], ["requests", "Запросы"], ["tokens", "Токены"], ["cost", "USD"]]);
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
if (action === "sessions") {
|
|
2387
|
+
printTable(getUsageBySession(), [["session_id", "Сессия"], ["requests", "Запросы"], ["tokens", "Токены"], ["cost", "USD"]]);
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
throw new Error("Команды usage: summary, models, sessions.");
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
async function handleBudget(args) {
|
|
2394
|
+
const [action = "status", scope = "daily", amount] = args;
|
|
2395
|
+
if (action === "status") {
|
|
2396
|
+
printTable(listBudgets(), [["scope", "Область"], ["amount_usd", "Лимит USD"], ["spent_usd", "Потрачено"], ["updated_at", "Обновлено"]]);
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
if (action === "set") {
|
|
2400
|
+
const value = Number(amount || args[2]);
|
|
2401
|
+
if (!value || value < 0) throw new Error("Пример: iola budget set daily 5");
|
|
2402
|
+
setBudget(scope, value);
|
|
2403
|
+
console.log(`Budget сохранен: ${scope}=${value} USD`);
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
throw new Error("Команды budget: status, set daily AMOUNT.");
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2177
2409
|
async function handlePolicy(args) {
|
|
2178
2410
|
const [action = "list", name] = args;
|
|
2179
2411
|
const policies = {
|
|
@@ -2445,7 +2677,26 @@ async function handleMemory(args) {
|
|
|
2445
2677
|
return;
|
|
2446
2678
|
}
|
|
2447
2679
|
|
|
2448
|
-
|
|
2680
|
+
if (action === "duplicates" || action === "curate") {
|
|
2681
|
+
const rows = findMemoryDuplicates();
|
|
2682
|
+
if (options.json) printJson(rows);
|
|
2683
|
+
else printTable(rows, [["keeper_id", "Оставить"], ["duplicate_id", "Дубликат"], ["content", "Текст"]]);
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
if (action === "prune") {
|
|
2688
|
+
const rows = findMemoryDuplicates();
|
|
2689
|
+
if (!options.yes) {
|
|
2690
|
+
printTable(rows, [["keeper_id", "Оставить"], ["duplicate_id", "Удалить"], ["content", "Текст"]]);
|
|
2691
|
+
console.log("Для удаления дубликатов запустите: iola memory prune --yes");
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
for (const row of rows) deleteMemory(row.duplicate_id);
|
|
2695
|
+
console.log(`Удалено дубликатов памяти: ${rows.length}`);
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
throw new Error("Команды memory: show, add TEXT, suggest, approve ID, reject ID, delete ID, clear, export [FILE], curate, duplicates, prune --yes.");
|
|
2449
2700
|
}
|
|
2450
2701
|
|
|
2451
2702
|
async function handleHooks(args) {
|
|
@@ -2468,6 +2719,22 @@ async function handleHooks(args) {
|
|
|
2468
2719
|
return;
|
|
2469
2720
|
}
|
|
2470
2721
|
|
|
2722
|
+
if (action === "trust") {
|
|
2723
|
+
await saveConfig({ hooksTrusted: true });
|
|
2724
|
+
console.log("Hooks помечены как доверенные для текущего пользователя.");
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
if (action === "audit") {
|
|
2729
|
+
const rows = Object.entries(config.hooks || {}).map(([hookEvent, commands]) => ({
|
|
2730
|
+
event: hookEvent,
|
|
2731
|
+
commands: commands.length,
|
|
2732
|
+
trusted: config.hooksTrusted ? "yes" : "no",
|
|
2733
|
+
}));
|
|
2734
|
+
printTable(rows, [["event", "Событие"], ["commands", "Команд"], ["trusted", "Доверено"]]);
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2471
2738
|
if (action === "add") {
|
|
2472
2739
|
if (!HOOK_EVENTS.includes(event) || commandParts.length === 0) {
|
|
2473
2740
|
throw new Error(`Пример: iola hooks add AfterSync "iola quality" Доступно: ${HOOK_EVENTS.join(", ")}`);
|
|
@@ -2499,7 +2766,7 @@ async function handleHooks(args) {
|
|
|
2499
2766
|
return;
|
|
2500
2767
|
}
|
|
2501
2768
|
|
|
2502
|
-
throw new Error("Команды hooks: list, events, add EVENT COMMAND, delete EVENT INDEX, run EVENT.");
|
|
2769
|
+
throw new Error("Команды hooks: list, events, add EVENT COMMAND, delete EVENT INDEX, run EVENT, trust, audit.");
|
|
2503
2770
|
}
|
|
2504
2771
|
|
|
2505
2772
|
async function handleAgents(args) {
|
|
@@ -2548,8 +2815,132 @@ async function handleAgents(args) {
|
|
|
2548
2815
|
throw new Error("Команды agents: list, run NAME TEXT.");
|
|
2549
2816
|
}
|
|
2550
2817
|
|
|
2818
|
+
async function handleSubagents(args) {
|
|
2819
|
+
const [action = "list", name, ...rest] = args;
|
|
2820
|
+
const config = await loadConfig();
|
|
2821
|
+
const custom = config.subagents || {};
|
|
2822
|
+
const agents = { ...AGENTS, ...custom };
|
|
2823
|
+
|
|
2824
|
+
if (action === "list" || action === "ls") {
|
|
2825
|
+
const rows = Object.entries(agents).map(([agent, meta]) => ({
|
|
2826
|
+
agent,
|
|
2827
|
+
profile: meta.profile || "active",
|
|
2828
|
+
tools: meta.tools ? "yes" : "no",
|
|
2829
|
+
source: AGENTS[agent] ? "builtin" : "user",
|
|
2830
|
+
description: meta.description || "-",
|
|
2831
|
+
}));
|
|
2832
|
+
printTable(rows, [["agent", "Subagent"], ["profile", "Профиль"], ["tools", "Tools"], ["source", "Источник"], ["description", "Описание"]]);
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
if (action === "add") {
|
|
2837
|
+
const options = parseOptions(rest);
|
|
2838
|
+
if (!name) throw new Error("Пример: iola subagents add culture --profile local --prompt \"...\"");
|
|
2839
|
+
const prompt = options.prompt || options.command || options._.join(" ");
|
|
2840
|
+
const next = {
|
|
2841
|
+
...custom,
|
|
2842
|
+
[name]: {
|
|
2843
|
+
profile: options.profile || null,
|
|
2844
|
+
tools: Boolean(options.tools),
|
|
2845
|
+
prefix: prompt ? `${prompt} ` : "",
|
|
2846
|
+
description: options.description || prompt || "Пользовательский subagent",
|
|
2847
|
+
},
|
|
2848
|
+
};
|
|
2849
|
+
await saveConfig({ subagents: next });
|
|
2850
|
+
console.log(`Subagent добавлен: ${name}`);
|
|
2851
|
+
return;
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
if (action === "run") {
|
|
2855
|
+
if (!agents[name]) throw new Error(`Subagent неизвестен: ${name}. Доступно: ${Object.keys(agents).join(", ")}`);
|
|
2856
|
+
await runSubagent(name, agents[name], rest);
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
if (action === "parallel") {
|
|
2861
|
+
const names = String(name || "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
2862
|
+
const question = rest.join(" ").trim();
|
|
2863
|
+
if (!names.length || !question) throw new Error('Пример: iola subagents parallel data-analyst,reviewer "проверь школы"');
|
|
2864
|
+
for (const agentName of names) {
|
|
2865
|
+
if (!agents[agentName]) throw new Error(`Subagent неизвестен: ${agentName}`);
|
|
2866
|
+
console.log(`\n## ${agentName}`);
|
|
2867
|
+
await runSubagent(agentName, agents[agentName], [question, "--no-history"]);
|
|
2868
|
+
}
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
throw new Error("Команды subagents: list, add NAME --profile PROFILE --prompt TEXT, run NAME TEXT, parallel a,b TEXT.");
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
async function runSubagent(name, agent, rest) {
|
|
2876
|
+
const options = parseOptions(rest);
|
|
2877
|
+
const question = options._.join(" ").trim();
|
|
2878
|
+
if (!question) throw new Error(`Пример: iola subagents run ${name} "найди школы"`);
|
|
2879
|
+
const askArgs = [agent.prefix ? `${agent.prefix}${question}` : question, "--agent", name];
|
|
2880
|
+
if (agent.profile || options.profile) askArgs.push("--profile", options.profile || agent.profile);
|
|
2881
|
+
if (agent.tools || options.tools) askArgs.push("--tools");
|
|
2882
|
+
if (agent.reasoning || options.reasoning) askArgs.push("--reasoning", options.reasoning || agent.reasoning);
|
|
2883
|
+
if (options.files) askArgs.push("--files");
|
|
2884
|
+
if (options.events) askArgs.push("--events");
|
|
2885
|
+
if (options["no-history"]) askArgs.push("--no-history");
|
|
2886
|
+
await aiAsk(askArgs);
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
async function handleReview(args) {
|
|
2890
|
+
const [action = "config", target, ...rest] = args;
|
|
2891
|
+
const options = parseOptions([target, ...rest].filter(Boolean));
|
|
2892
|
+
const actualTarget = options._[0];
|
|
2893
|
+
if (action === "config") {
|
|
2894
|
+
const errors = validateConfig(await loadConfig());
|
|
2895
|
+
const rows = errors.length ? errors.map((error) => ({ level: "error", message: error })) : [{ level: "ok", message: "Конфигурация валидна" }];
|
|
2896
|
+
printTable(rows, [["level", "Уровень"], ["message", "Сообщение"]]);
|
|
2897
|
+
return;
|
|
2898
|
+
}
|
|
2899
|
+
if (action === "data") {
|
|
2900
|
+
await ensureLocalData();
|
|
2901
|
+
const rows = runQuality(actualTarget || "all");
|
|
2902
|
+
if (options.json) printJson(rows);
|
|
2903
|
+
else printTable(rows, [["check", "Проверка"], ["count", "Кол-во"], ["sample", "Пример"]]);
|
|
2904
|
+
return;
|
|
2905
|
+
}
|
|
2906
|
+
if (action === "docs") {
|
|
2907
|
+
const rows = actualTarget ? await reviewDocumentFolder(actualTarget, options) : searchDocs(options.query || "", Number(options.limit || 20));
|
|
2908
|
+
if (options.json) printJson(rows);
|
|
2909
|
+
else printTable(rows, [["file", "Файл"], ["issue", "Замечание"], ["detail", "Детали"]]);
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
if (action === "report") {
|
|
2913
|
+
if (!actualTarget) throw new Error("Пример: iola review report отчет.docx");
|
|
2914
|
+
const text = await extractReadableText(path.resolve(actualTarget));
|
|
2915
|
+
const rows = [
|
|
2916
|
+
{ file: actualTarget, issue: text.trim() ? "ok" : "empty", detail: text.trim() ? "Текст извлечен" : "Не удалось извлечь текст" },
|
|
2917
|
+
{ file: actualTarget, issue: /источник|данн/i.test(text) ? "ok" : "missing-source", detail: "Проверьте указание источника данных" },
|
|
2918
|
+
];
|
|
2919
|
+
printTable(rows, [["file", "Файл"], ["issue", "Замечание"], ["detail", "Детали"]]);
|
|
2920
|
+
return;
|
|
2921
|
+
}
|
|
2922
|
+
throw new Error("Команды review: config, data [scope], docs [PATH], report FILE.");
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
async function reviewDocumentFolder(target, options = {}) {
|
|
2926
|
+
const previous = await loadConfig();
|
|
2927
|
+
const rows = [];
|
|
2928
|
+
try {
|
|
2929
|
+
await saveConfig({ files: { ...(previous.files || {}), workspaceRoot: path.resolve(target), mode: "read-only" } });
|
|
2930
|
+
await setFilesMode("read-only", await loadConfig());
|
|
2931
|
+
const files = await filesTree(".", { depth: Number(options.depth || 5), limit: Number(options.limit || 200) });
|
|
2932
|
+
for (const file of files.filter((item) => item.type === "file" && INDEXABLE_EXTENSIONS.test(item.path))) {
|
|
2933
|
+
rows.push({ file: file.path, issue: "indexable", detail: "Документ можно читать и индексировать" });
|
|
2934
|
+
}
|
|
2935
|
+
} finally {
|
|
2936
|
+
await saveConfig({ files: previous.files, permissions: previous.permissions, toolsets: previous.toolsets }).catch(() => {});
|
|
2937
|
+
}
|
|
2938
|
+
return rows;
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2551
2941
|
async function handleMcp(args) {
|
|
2552
|
-
const [action = "status", target = "codex"] = args;
|
|
2942
|
+
const [action = "status", target = "codex", ...rest] = args;
|
|
2943
|
+
const options = parseOptions([target, ...rest]);
|
|
2553
2944
|
|
|
2554
2945
|
if (action === "status") {
|
|
2555
2946
|
const [health, version] = await Promise.all([
|
|
@@ -2585,6 +2976,10 @@ async function handleMcp(args) {
|
|
|
2585
2976
|
|
|
2586
2977
|
if (action === "serve") {
|
|
2587
2978
|
const config = await loadConfig();
|
|
2979
|
+
if (options.stdio || target === "--stdio" || target === "stdio") {
|
|
2980
|
+
await startMcpStdio();
|
|
2981
|
+
return;
|
|
2982
|
+
}
|
|
2588
2983
|
await startMcpServer(config.daemon?.host || "127.0.0.1", Number(config.daemon?.port || DAEMON_PORT) + 1);
|
|
2589
2984
|
return;
|
|
2590
2985
|
}
|
|
@@ -3508,6 +3903,24 @@ function initDatabase() {
|
|
|
3508
3903
|
command TEXT,
|
|
3509
3904
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
3510
3905
|
);
|
|
3906
|
+
CREATE TABLE IF NOT EXISTS usage_events (
|
|
3907
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3908
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
3909
|
+
provider TEXT,
|
|
3910
|
+
model TEXT,
|
|
3911
|
+
profile TEXT,
|
|
3912
|
+
input_chars INTEGER NOT NULL DEFAULT 0,
|
|
3913
|
+
output_chars INTEGER NOT NULL DEFAULT 0,
|
|
3914
|
+
estimated_tokens INTEGER NOT NULL DEFAULT 0,
|
|
3915
|
+
estimated_cost_usd REAL NOT NULL DEFAULT 0,
|
|
3916
|
+
session_id INTEGER
|
|
3917
|
+
);
|
|
3918
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_created_at ON usage_events(created_at DESC);
|
|
3919
|
+
CREATE TABLE IF NOT EXISTS budgets (
|
|
3920
|
+
scope TEXT PRIMARY KEY,
|
|
3921
|
+
amount_usd REAL NOT NULL,
|
|
3922
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
3923
|
+
);
|
|
3511
3924
|
`);
|
|
3512
3925
|
rebuildFtsIfEmpty(db);
|
|
3513
3926
|
db.prepare(`
|
|
@@ -3564,6 +3977,7 @@ function getDbStatus() {
|
|
|
3564
3977
|
const artifacts = db.prepare("SELECT COUNT(*) AS count FROM artifacts").get();
|
|
3565
3978
|
const docs = db.prepare("SELECT COUNT(*) AS count FROM doc_index").get();
|
|
3566
3979
|
const custom = db.prepare("SELECT COUNT(*) AS count FROM custom_records").get();
|
|
3980
|
+
const usage = db.prepare("SELECT COUNT(*) AS count FROM usage_events").get();
|
|
3567
3981
|
return {
|
|
3568
3982
|
status: "ok",
|
|
3569
3983
|
file: DB_FILE,
|
|
@@ -3579,6 +3993,7 @@ function getDbStatus() {
|
|
|
3579
3993
|
artifacts: artifacts?.count ?? 0,
|
|
3580
3994
|
indexed_docs: docs?.count ?? 0,
|
|
3581
3995
|
custom_records: custom?.count ?? 0,
|
|
3996
|
+
usage_events: usage?.count ?? 0,
|
|
3582
3997
|
};
|
|
3583
3998
|
} finally {
|
|
3584
3999
|
db.close();
|
|
@@ -3749,6 +4164,16 @@ function printSessionMessages(sessionId) {
|
|
|
3749
4164
|
]);
|
|
3750
4165
|
}
|
|
3751
4166
|
|
|
4167
|
+
function getSessionMessages(sessionId) {
|
|
4168
|
+
initDatabase();
|
|
4169
|
+
const db = openDatabase();
|
|
4170
|
+
try {
|
|
4171
|
+
return db.prepare("SELECT id, role, content, created_at FROM session_messages WHERE session_id = ? ORDER BY id ASC").all(sessionId);
|
|
4172
|
+
} finally {
|
|
4173
|
+
db.close();
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4176
|
+
|
|
3752
4177
|
function forkSessionInDb(sessionId) {
|
|
3753
4178
|
initDatabase();
|
|
3754
4179
|
const db = openDatabase();
|
|
@@ -4239,6 +4664,21 @@ function deleteMemory(id) {
|
|
|
4239
4664
|
}
|
|
4240
4665
|
}
|
|
4241
4666
|
|
|
4667
|
+
function findMemoryDuplicates() {
|
|
4668
|
+
const rows = listMemory(1000).reverse();
|
|
4669
|
+
const seen = new Map();
|
|
4670
|
+
const duplicates = [];
|
|
4671
|
+
for (const row of rows) {
|
|
4672
|
+
const normalized = row.content.trim().toLocaleLowerCase("ru-RU").replace(/\s+/g, " ");
|
|
4673
|
+
if (seen.has(normalized)) {
|
|
4674
|
+
duplicates.push({ keeper_id: seen.get(normalized).id, duplicate_id: row.id, content: row.content });
|
|
4675
|
+
} else {
|
|
4676
|
+
seen.set(normalized, row);
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
return duplicates;
|
|
4680
|
+
}
|
|
4681
|
+
|
|
4242
4682
|
function clearMemory() {
|
|
4243
4683
|
initDatabase();
|
|
4244
4684
|
const db = openDatabase();
|
|
@@ -4569,6 +5009,7 @@ async function aiAsk(args, context = {}) {
|
|
|
4569
5009
|
const providerConfig = resolveAiProfile(config, options);
|
|
4570
5010
|
if (providerConfig.provider === "codex") await assertPermission("codex");
|
|
4571
5011
|
if (providerConfig.provider !== "ollama") await assertPermission("externalAi");
|
|
5012
|
+
if (options["stream-json"]) options.events = true;
|
|
4572
5013
|
if (options.tools && providerConfig.provider === "ollama") {
|
|
4573
5014
|
return localToolAsk(question, providerConfig, options);
|
|
4574
5015
|
}
|
|
@@ -4598,6 +5039,13 @@ async function aiAsk(args, context = {}) {
|
|
|
4598
5039
|
recordAskHistory({ question, answer, providerConfig, dataContext, error: "", sessionId });
|
|
4599
5040
|
appendSessionExchange(sessionId, question, answer, dataContext, "");
|
|
4600
5041
|
}
|
|
5042
|
+
recordUsage({
|
|
5043
|
+
providerConfig,
|
|
5044
|
+
question,
|
|
5045
|
+
answer,
|
|
5046
|
+
sessionId,
|
|
5047
|
+
profile: providerConfig.name,
|
|
5048
|
+
});
|
|
4601
5049
|
await maybeSuggestMemory(question, answer, providerConfig);
|
|
4602
5050
|
|
|
4603
5051
|
emitEvent(options, "answer", { length: answer.length, sessionId });
|
|
@@ -4642,6 +5090,7 @@ function resolveAiProfile(config, options = {}) {
|
|
|
4642
5090
|
}
|
|
4643
5091
|
|
|
4644
5092
|
async function localToolAsk(question, providerConfig, options) {
|
|
5093
|
+
if (options["stream-json"]) options.events = true;
|
|
4645
5094
|
await ensureLocalData();
|
|
4646
5095
|
const plan = await buildLocalToolPlan(question, providerConfig, options);
|
|
4647
5096
|
const validated = validateToolPlan(plan, options);
|
|
@@ -4667,6 +5116,7 @@ async function localToolAsk(question, providerConfig, options) {
|
|
|
4667
5116
|
sessionId: null,
|
|
4668
5117
|
});
|
|
4669
5118
|
}
|
|
5119
|
+
recordUsage({ providerConfig, question, answer, sessionId: null, profile: providerConfig.name });
|
|
4670
5120
|
|
|
4671
5121
|
emitEvent(options, "tool_plan", { plan: validated, runId });
|
|
4672
5122
|
saveArtifact("tool-result", question.slice(0, 80), "", { runId, plan: validated, outputs: result.outputs });
|
|
@@ -4772,6 +5222,7 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
4772
5222
|
let status = "ok";
|
|
4773
5223
|
let summary = "";
|
|
4774
5224
|
await assertPermission(step.tool);
|
|
5225
|
+
await runHooks("PreToolUse", { tool: step.tool, args: step.args || {} });
|
|
4775
5226
|
await runHooks("BeforeTool", { tool: step.tool, args: step.args || {} });
|
|
4776
5227
|
try {
|
|
4777
5228
|
if (step.tool === "search_local") {
|
|
@@ -4818,10 +5269,12 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
4818
5269
|
status = "error";
|
|
4819
5270
|
summary = error instanceof Error ? error.message : String(error);
|
|
4820
5271
|
recordToolTrace(options.runId || "manual", step.tool, step.args || {}, status, summary);
|
|
5272
|
+
await runHooks("OnError", { tool: step.tool, args: step.args || {}, error: summary });
|
|
4821
5273
|
throw error;
|
|
4822
5274
|
}
|
|
4823
5275
|
recordToolTrace(options.runId || "manual", step.tool, step.args || {}, status, summary);
|
|
4824
5276
|
await runHooks("AfterTool", { tool: step.tool, rows: current.length });
|
|
5277
|
+
await runHooks("PostToolUse", { tool: step.tool, rows: current.length });
|
|
4825
5278
|
}
|
|
4826
5279
|
return { rows: current, outputs };
|
|
4827
5280
|
}
|
|
@@ -4849,7 +5302,12 @@ async function runHooks(event, payload = {}) {
|
|
|
4849
5302
|
const config = await loadConfig();
|
|
4850
5303
|
const commands = config.hooks?.[event] || [];
|
|
4851
5304
|
for (const command of commands) {
|
|
4852
|
-
const
|
|
5305
|
+
const [maybeFilter, ...rest] = String(command).split(":");
|
|
5306
|
+
const commandText = payload.tool && rest.length > 0 && ALL_LOCAL_TOOLS.includes(maybeFilter.trim())
|
|
5307
|
+
? (maybeFilter.trim() === payload.tool ? rest.join(":").trim() : "")
|
|
5308
|
+
: command;
|
|
5309
|
+
if (!commandText) continue;
|
|
5310
|
+
const parts = splitCommandLine(commandText);
|
|
4853
5311
|
if (parts.length === 0) continue;
|
|
4854
5312
|
await runCommand(parts[0], parts.slice(1), {
|
|
4855
5313
|
inherit: true,
|
|
@@ -5445,12 +5903,12 @@ function parseOptions(args) {
|
|
|
5445
5903
|
|
|
5446
5904
|
for (let index = 0; index < args.length; index += 1) {
|
|
5447
5905
|
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") {
|
|
5906
|
+
if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || 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
5907
|
result[arg.slice(2)] = true;
|
|
5450
5908
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
5451
5909
|
result.check = true;
|
|
5452
5910
|
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") {
|
|
5911
|
+
} 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 === "--debug-file") {
|
|
5454
5912
|
result[arg.slice(2)] = args[index + 1];
|
|
5455
5913
|
index += 1;
|
|
5456
5914
|
} else {
|
|
@@ -5840,6 +6298,13 @@ async function startDaemon(host, port) {
|
|
|
5840
6298
|
const server = createServer(async (req, res) => {
|
|
5841
6299
|
try {
|
|
5842
6300
|
const url = new URL(req.url || "/", `http://${host}:${port}`);
|
|
6301
|
+
|
|
6302
|
+
if (req.method === "GET" && url.pathname === "/") {
|
|
6303
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
6304
|
+
res.end(renderDaemonDashboard(host, port));
|
|
6305
|
+
return;
|
|
6306
|
+
}
|
|
6307
|
+
|
|
5843
6308
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
5844
6309
|
|
|
5845
6310
|
if (req.method === "GET" && url.pathname === "/health") {
|
|
@@ -5852,6 +6317,26 @@ async function startDaemon(host, port) {
|
|
|
5852
6317
|
return;
|
|
5853
6318
|
}
|
|
5854
6319
|
|
|
6320
|
+
if (req.method === "GET" && url.pathname === "/api/status") {
|
|
6321
|
+
res.end(JSON.stringify({ db: getDbStatus(), sync: getSyncStatus(), usage: getUsageSummary() }));
|
|
6322
|
+
return;
|
|
6323
|
+
}
|
|
6324
|
+
|
|
6325
|
+
if (req.method === "GET" && url.pathname === "/api/tasks") {
|
|
6326
|
+
res.end(JSON.stringify(listTasks()));
|
|
6327
|
+
return;
|
|
6328
|
+
}
|
|
6329
|
+
|
|
6330
|
+
if (req.method === "GET" && url.pathname === "/api/artifacts") {
|
|
6331
|
+
res.end(JSON.stringify(listArtifacts()));
|
|
6332
|
+
return;
|
|
6333
|
+
}
|
|
6334
|
+
|
|
6335
|
+
if (req.method === "GET" && url.pathname === "/api/trace") {
|
|
6336
|
+
res.end(JSON.stringify(listTrace(50)));
|
|
6337
|
+
return;
|
|
6338
|
+
}
|
|
6339
|
+
|
|
5855
6340
|
if (req.method === "POST" && url.pathname === "/rpc") {
|
|
5856
6341
|
const body = await readRequestBody(req);
|
|
5857
6342
|
const payload = body ? JSON.parse(body) : {};
|
|
@@ -5879,22 +6364,19 @@ async function startDaemon(host, port) {
|
|
|
5879
6364
|
|
|
5880
6365
|
async function startMcpServer(host, port) {
|
|
5881
6366
|
const server = createServer(async (req, res) => {
|
|
6367
|
+
let payload = {};
|
|
5882
6368
|
try {
|
|
5883
6369
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
5884
6370
|
if (req.method !== "POST") {
|
|
5885
|
-
res.end(JSON.stringify({ name: "iola-local-mcp",
|
|
6371
|
+
res.end(JSON.stringify({ name: "iola-local-mcp", protocol: "2024-11-05", tools: mcpTools().map((tool) => tool.name) }));
|
|
5886
6372
|
return;
|
|
5887
6373
|
}
|
|
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, _: [] });
|
|
6374
|
+
payload = JSON.parse(await readRequestBody(req) || "{}");
|
|
6375
|
+
const result = await handleMcpMessage(payload);
|
|
5894
6376
|
res.end(JSON.stringify({ jsonrpc: "2.0", id: payload.id || null, result }));
|
|
5895
6377
|
} catch (error) {
|
|
5896
6378
|
res.statusCode = 500;
|
|
5897
|
-
res.end(JSON.stringify({ jsonrpc: "2.0", id: null, error: { message: error instanceof Error ? error.message : String(error) } }));
|
|
6379
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", id: payload.id || null, error: { code: -32000, message: error instanceof Error ? error.message : String(error) } }));
|
|
5898
6380
|
}
|
|
5899
6381
|
});
|
|
5900
6382
|
await new Promise((resolve, reject) => {
|
|
@@ -5905,6 +6387,142 @@ async function startMcpServer(host, port) {
|
|
|
5905
6387
|
await new Promise(() => {});
|
|
5906
6388
|
}
|
|
5907
6389
|
|
|
6390
|
+
function renderDaemonDashboard(host, port) {
|
|
6391
|
+
const status = getDbStatus();
|
|
6392
|
+
const sync = getSyncStatus();
|
|
6393
|
+
const usage = getUsageSummary();
|
|
6394
|
+
return `<!doctype html>
|
|
6395
|
+
<html lang="ru">
|
|
6396
|
+
<meta charset="utf-8">
|
|
6397
|
+
<title>iola daemon</title>
|
|
6398
|
+
<style>
|
|
6399
|
+
body{font-family:Segoe UI,Arial,sans-serif;margin:32px;background:#f8fafc;color:#0f172a}
|
|
6400
|
+
h1{margin:0 0 8px;font-size:28px}
|
|
6401
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;margin-top:20px}
|
|
6402
|
+
.card{background:white;border:1px solid #dbe3ef;border-radius:8px;padding:16px}
|
|
6403
|
+
.k{color:#64748b;font-size:12px;text-transform:uppercase}.v{font-size:24px;font-weight:700;margin-top:4px}
|
|
6404
|
+
a{color:#0b62d6}
|
|
6405
|
+
code{background:#eef2f7;padding:2px 5px;border-radius:4px}
|
|
6406
|
+
</style>
|
|
6407
|
+
<h1>iola daemon</h1>
|
|
6408
|
+
<div>Локальная панель CLI-проекта Йошкар-Олы: <code>http://${host}:${port}</code></div>
|
|
6409
|
+
<div class="grid">
|
|
6410
|
+
<div class="card"><div class="k">DB</div><div class="v">${status.status}</div><p>schema ${status.schema}, records ${status.local_records}</p></div>
|
|
6411
|
+
<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>
|
|
6412
|
+
<div class="card"><div class="k">Usage</div><div class="v">${usage.requests}</div><p>${usage.estimated_tokens} tokens</p></div>
|
|
6413
|
+
<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>
|
|
6414
|
+
</div>
|
|
6415
|
+
</html>`;
|
|
6416
|
+
}
|
|
6417
|
+
|
|
6418
|
+
async function startMcpStdio() {
|
|
6419
|
+
const rl = readline.createInterface({ input, terminal: false });
|
|
6420
|
+
for await (const line of rl) {
|
|
6421
|
+
if (!line.trim()) continue;
|
|
6422
|
+
let payload = {};
|
|
6423
|
+
try {
|
|
6424
|
+
payload = JSON.parse(line);
|
|
6425
|
+
const result = await handleMcpMessage(payload);
|
|
6426
|
+
process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id: payload.id || null, result })}\n`);
|
|
6427
|
+
} catch (error) {
|
|
6428
|
+
process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id: payload.id || null, error: { code: -32000, message: error instanceof Error ? error.message : String(error) } })}\n`);
|
|
6429
|
+
}
|
|
6430
|
+
}
|
|
6431
|
+
}
|
|
6432
|
+
|
|
6433
|
+
async function handleMcpMessage(payload) {
|
|
6434
|
+
const method = payload.method;
|
|
6435
|
+
if (method === "initialize") {
|
|
6436
|
+
return {
|
|
6437
|
+
protocolVersion: "2024-11-05",
|
|
6438
|
+
serverInfo: { name: "iola-local-mcp", version: getPackageVersion() },
|
|
6439
|
+
capabilities: { tools: {}, resources: {}, prompts: {} },
|
|
6440
|
+
};
|
|
6441
|
+
}
|
|
6442
|
+
if (method === "tools/list") return { tools: mcpTools() };
|
|
6443
|
+
if (method === "tools/call") {
|
|
6444
|
+
const result = await callMcpTool(payload.params?.name, payload.params?.arguments || {});
|
|
6445
|
+
return { content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }] };
|
|
6446
|
+
}
|
|
6447
|
+
if (method === "resources/list") return { resources: mcpResources() };
|
|
6448
|
+
if (method === "resources/read") {
|
|
6449
|
+
const text = await readMcpResource(payload.params?.uri);
|
|
6450
|
+
return { contents: [{ uri: payload.params?.uri, mimeType: "application/json", text }] };
|
|
6451
|
+
}
|
|
6452
|
+
if (method === "prompts/list") return { prompts: mcpPrompts() };
|
|
6453
|
+
if (method === "prompts/get") return getMcpPrompt(payload.params?.name, payload.params?.arguments || {});
|
|
6454
|
+
if (method === "notifications/initialized") return {};
|
|
6455
|
+
throw new Error(`MCP method неизвестен: ${method}`);
|
|
6456
|
+
}
|
|
6457
|
+
|
|
6458
|
+
function mcpTools() {
|
|
6459
|
+
const schema = (properties = {}) => ({ type: "object", properties, additionalProperties: false });
|
|
6460
|
+
return [
|
|
6461
|
+
{ name: "status", description: "Статус локальной БД, sync и активного AI-профиля.", inputSchema: schema() },
|
|
6462
|
+
{ name: "search", description: "Поиск по локальным открытым данным Йошкар-Олы.", inputSchema: schema({ query: { type: "string" }, dataset: { type: "string" }, limit: { type: "number" } }) },
|
|
6463
|
+
{ name: "card", description: "Карточка объекта по названию или ИНН.", inputSchema: schema({ query: { type: "string" } }) },
|
|
6464
|
+
{ name: "quality", description: "Проверки качества данных.", inputSchema: schema({ scope: { type: "string" } }) },
|
|
6465
|
+
{ name: "sync", description: "Обновление локальной копии слоя.", inputSchema: schema({ dataset: { type: "string" } }) },
|
|
6466
|
+
{ name: "files.tree", description: "Дерево файлов разрешенного workspace.", inputSchema: schema({ path: { type: "string" }, depth: { type: "number" }, limit: { type: "number" } }) },
|
|
6467
|
+
{ name: "files.read", description: "Чтение файла разрешенного workspace.", inputSchema: schema({ path: { type: "string" }, maxBytes: { type: "number" } }) },
|
|
6468
|
+
{ name: "files.search", description: "Поиск текста в файлах workspace.", inputSchema: schema({ query: { type: "string" }, path: { type: "string" }, limit: { type: "number" } }) },
|
|
6469
|
+
{ name: "index.search", description: "Поиск по индексу локальных документов.", inputSchema: schema({ query: { type: "string" }, limit: { type: "number" } }) },
|
|
6470
|
+
{ name: "report", description: "Запуск встроенного отчета.", inputSchema: schema({ name: { type: "string" }, format: { type: "string" }, output: { type: "string" } }) },
|
|
6471
|
+
];
|
|
6472
|
+
}
|
|
6473
|
+
|
|
6474
|
+
function mcpResources() {
|
|
6475
|
+
return [
|
|
6476
|
+
{ uri: "iola://status", name: "Статус CLI", mimeType: "application/json" },
|
|
6477
|
+
{ uri: "iola://sync", name: "Статус синхронизации", mimeType: "application/json" },
|
|
6478
|
+
{ uri: "iola://settings", name: "Эффективные настройки", mimeType: "application/json" },
|
|
6479
|
+
{ uri: "iola://skills", name: "Skills", mimeType: "application/json" },
|
|
6480
|
+
{ uri: "iola://memory", name: "Память агента", mimeType: "application/json" },
|
|
6481
|
+
{ uri: "iola://artifacts", name: "Artifacts", mimeType: "application/json" },
|
|
6482
|
+
];
|
|
6483
|
+
}
|
|
6484
|
+
|
|
6485
|
+
function mcpPrompts() {
|
|
6486
|
+
return [
|
|
6487
|
+
{ name: "data-question", description: "Ответить строго по открытым данным Йошкар-Олы.", arguments: [{ name: "question", required: true }] },
|
|
6488
|
+
{ name: "document-review", description: "Проверить документ на полноту и источники.", arguments: [{ name: "file", required: true }] },
|
|
6489
|
+
{ name: "report-build", description: "Собрать отчет по выбранному слою.", arguments: [{ name: "dataset", required: true }] },
|
|
6490
|
+
];
|
|
6491
|
+
}
|
|
6492
|
+
|
|
6493
|
+
async function callMcpTool(name, args = {}) {
|
|
6494
|
+
if (name === "index.search") return searchDocs(args.query || "", Number(args.limit || 20));
|
|
6495
|
+
if (name === "report") {
|
|
6496
|
+
const output = args.output || `${args.name || "education-contacts"}.${args.format || "xlsx"}`;
|
|
6497
|
+
await handleExport([args.name || "education-contacts", "--format", args.format || "xlsx", "--output", output]);
|
|
6498
|
+
return { output };
|
|
6499
|
+
}
|
|
6500
|
+
return executeRpc(name, { ...args, _: [] });
|
|
6501
|
+
}
|
|
6502
|
+
|
|
6503
|
+
async function readMcpResource(uri) {
|
|
6504
|
+
if (uri === "iola://status") return JSON.stringify({ db: getDbStatus(), sync: getSyncStatus() }, null, 2);
|
|
6505
|
+
if (uri === "iola://sync") return JSON.stringify(getSyncStatus(), null, 2);
|
|
6506
|
+
if (uri === "iola://settings") return JSON.stringify(await loadConfig(), null, 2);
|
|
6507
|
+
if (uri === "iola://skills") return JSON.stringify(listSkills(await loadConfig()), null, 2);
|
|
6508
|
+
if (uri === "iola://memory") return JSON.stringify(listMemory(100), null, 2);
|
|
6509
|
+
if (uri === "iola://artifacts") return JSON.stringify(listArtifacts(), null, 2);
|
|
6510
|
+
throw new Error(`MCP resource неизвестен: ${uri}`);
|
|
6511
|
+
}
|
|
6512
|
+
|
|
6513
|
+
function getMcpPrompt(name, args = {}) {
|
|
6514
|
+
if (name === "data-question") {
|
|
6515
|
+
return { messages: [{ role: "user", content: { type: "text", text: `Ответь по открытым данным городского округа "Город Йошкар-Ола", не выдумывая сведения: ${args.question || ""}` } }] };
|
|
6516
|
+
}
|
|
6517
|
+
if (name === "document-review") {
|
|
6518
|
+
return { messages: [{ role: "user", content: { type: "text", text: `Проверь документ ${args.file || ""}: полнота, источники, противоречия, ошибки оформления.` } }] };
|
|
6519
|
+
}
|
|
6520
|
+
if (name === "report-build") {
|
|
6521
|
+
return { messages: [{ role: "user", content: { type: "text", text: `Собери практичный отчет по слою ${args.dataset || "schools"} с выводами и источником данных.` } }] };
|
|
6522
|
+
}
|
|
6523
|
+
throw new Error(`MCP prompt неизвестен: ${name}`);
|
|
6524
|
+
}
|
|
6525
|
+
|
|
5908
6526
|
function readRequestBody(req) {
|
|
5909
6527
|
return new Promise((resolve, reject) => {
|
|
5910
6528
|
let body = "";
|
|
@@ -6338,6 +6956,101 @@ function recordToolTrace(runId, tool, args, status, summary) {
|
|
|
6338
6956
|
}
|
|
6339
6957
|
}
|
|
6340
6958
|
|
|
6959
|
+
function recordUsage({ providerConfig, question, answer, sessionId, profile }) {
|
|
6960
|
+
try {
|
|
6961
|
+
initDatabase();
|
|
6962
|
+
const inputChars = String(question || "").length;
|
|
6963
|
+
const outputChars = String(answer || "").length;
|
|
6964
|
+
const estimatedTokens = Math.ceil((inputChars + outputChars) / 4);
|
|
6965
|
+
const estimatedCostUsd = estimateCost(providerConfig, estimatedTokens);
|
|
6966
|
+
const db = openDatabase();
|
|
6967
|
+
try {
|
|
6968
|
+
db.prepare(`
|
|
6969
|
+
INSERT INTO usage_events(provider, model, profile, input_chars, output_chars, estimated_tokens, estimated_cost_usd, session_id)
|
|
6970
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
6971
|
+
`).run(
|
|
6972
|
+
providerConfig.provider || "",
|
|
6973
|
+
providerConfig.model || "",
|
|
6974
|
+
profile || providerConfig.name || "",
|
|
6975
|
+
inputChars,
|
|
6976
|
+
outputChars,
|
|
6977
|
+
estimatedTokens,
|
|
6978
|
+
estimatedCostUsd,
|
|
6979
|
+
sessionId || null,
|
|
6980
|
+
);
|
|
6981
|
+
} finally {
|
|
6982
|
+
db.close();
|
|
6983
|
+
}
|
|
6984
|
+
} catch {
|
|
6985
|
+
// Usage accounting must not break the main answer path.
|
|
6986
|
+
}
|
|
6987
|
+
}
|
|
6988
|
+
|
|
6989
|
+
function estimateCost(providerConfig, tokens) {
|
|
6990
|
+
if (!providerConfig || providerConfig.provider === "ollama" || providerConfig.provider === "codex") return 0;
|
|
6991
|
+
const perMillion = providerConfig.provider === "openrouter" ? 0.25 : 0.4;
|
|
6992
|
+
return Math.round((tokens / 1_000_000) * perMillion * 1_000_000) / 1_000_000;
|
|
6993
|
+
}
|
|
6994
|
+
|
|
6995
|
+
function getUsageSummary() {
|
|
6996
|
+
initDatabase();
|
|
6997
|
+
const db = openDatabase();
|
|
6998
|
+
try {
|
|
6999
|
+
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();
|
|
7000
|
+
return { requests: row.requests || 0, estimated_tokens: row.tokens || 0, estimated_cost_usd: Number(row.cost || 0).toFixed(6) };
|
|
7001
|
+
} finally {
|
|
7002
|
+
db.close();
|
|
7003
|
+
}
|
|
7004
|
+
}
|
|
7005
|
+
|
|
7006
|
+
function getUsageByModel() {
|
|
7007
|
+
initDatabase();
|
|
7008
|
+
const db = openDatabase();
|
|
7009
|
+
try {
|
|
7010
|
+
return db.prepare(`
|
|
7011
|
+
SELECT provider, model, COUNT(*) AS requests, COALESCE(SUM(estimated_tokens),0) AS tokens, printf('%.6f', COALESCE(SUM(estimated_cost_usd),0)) AS cost
|
|
7012
|
+
FROM usage_events GROUP BY provider, model ORDER BY requests DESC
|
|
7013
|
+
`).all();
|
|
7014
|
+
} finally {
|
|
7015
|
+
db.close();
|
|
7016
|
+
}
|
|
7017
|
+
}
|
|
7018
|
+
|
|
7019
|
+
function getUsageBySession() {
|
|
7020
|
+
initDatabase();
|
|
7021
|
+
const db = openDatabase();
|
|
7022
|
+
try {
|
|
7023
|
+
return db.prepare(`
|
|
7024
|
+
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
|
|
7025
|
+
FROM usage_events GROUP BY session_id ORDER BY requests DESC LIMIT 100
|
|
7026
|
+
`).all();
|
|
7027
|
+
} finally {
|
|
7028
|
+
db.close();
|
|
7029
|
+
}
|
|
7030
|
+
}
|
|
7031
|
+
|
|
7032
|
+
function listBudgets() {
|
|
7033
|
+
initDatabase();
|
|
7034
|
+
const db = openDatabase();
|
|
7035
|
+
try {
|
|
7036
|
+
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);
|
|
7037
|
+
return db.prepare("SELECT scope, amount_usd, updated_at FROM budgets ORDER BY scope").all()
|
|
7038
|
+
.map((row) => ({ ...row, spent_usd: row.scope === "daily" ? spent.toFixed(6) : "-" }));
|
|
7039
|
+
} finally {
|
|
7040
|
+
db.close();
|
|
7041
|
+
}
|
|
7042
|
+
}
|
|
7043
|
+
|
|
7044
|
+
function setBudget(scope, amountUsd) {
|
|
7045
|
+
initDatabase();
|
|
7046
|
+
const db = openDatabase();
|
|
7047
|
+
try {
|
|
7048
|
+
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);
|
|
7049
|
+
} finally {
|
|
7050
|
+
db.close();
|
|
7051
|
+
}
|
|
7052
|
+
}
|
|
7053
|
+
|
|
6341
7054
|
function listTrace(limit = 20) {
|
|
6342
7055
|
initDatabase();
|
|
6343
7056
|
const db = openDatabase();
|
|
@@ -6348,6 +7061,20 @@ function listTrace(limit = 20) {
|
|
|
6348
7061
|
}
|
|
6349
7062
|
}
|
|
6350
7063
|
|
|
7064
|
+
function buildTrajectoryRows(limit = 500) {
|
|
7065
|
+
initDatabase();
|
|
7066
|
+
const db = openDatabase();
|
|
7067
|
+
try {
|
|
7068
|
+
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);
|
|
7069
|
+
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);
|
|
7070
|
+
return [...history, ...traces]
|
|
7071
|
+
.sort((left, right) => String(right.created_at).localeCompare(String(left.created_at)))
|
|
7072
|
+
.slice(0, limit);
|
|
7073
|
+
} finally {
|
|
7074
|
+
db.close();
|
|
7075
|
+
}
|
|
7076
|
+
}
|
|
7077
|
+
|
|
6351
7078
|
function getTraceRun(runId) {
|
|
6352
7079
|
initDatabase();
|
|
6353
7080
|
const db = openDatabase();
|
|
@@ -6378,6 +7105,22 @@ async function createSnapshot() {
|
|
|
6378
7105
|
}
|
|
6379
7106
|
}
|
|
6380
7107
|
|
|
7108
|
+
async function createSandboxCopy(name = "") {
|
|
7109
|
+
const config = await loadConfig();
|
|
7110
|
+
const workspace = resolveWorkspaceRoot(config);
|
|
7111
|
+
const sandboxesDir = path.join(CONFIG_DIR, "sandboxes");
|
|
7112
|
+
await mkdir(sandboxesDir, { recursive: true });
|
|
7113
|
+
const safeName = name ? String(name).replace(/[^a-zA-Z0-9_-]+/g, "-") : `sandbox-${Date.now()}`;
|
|
7114
|
+
const target = path.join(sandboxesDir, safeName);
|
|
7115
|
+
await rm(target, { recursive: true, force: true });
|
|
7116
|
+
await cp(workspace, target, {
|
|
7117
|
+
recursive: true,
|
|
7118
|
+
filter: (source) => !isBlockedPathForConfig(source, config) && !source.includes(`${path.sep}node_modules${path.sep}`) && !source.includes(`${path.sep}.git${path.sep}`),
|
|
7119
|
+
});
|
|
7120
|
+
const id = saveArtifact("sandbox", safeName, target, { workspace });
|
|
7121
|
+
return { id, workspace, path: target };
|
|
7122
|
+
}
|
|
7123
|
+
|
|
6381
7124
|
function listSnapshots() {
|
|
6382
7125
|
initDatabase();
|
|
6383
7126
|
const db = openDatabase();
|
|
@@ -6653,7 +7396,7 @@ async function executeRpc(method, options = {}) {
|
|
|
6653
7396
|
if (method === "index.search") {
|
|
6654
7397
|
return searchDocs(options.query || options.search || "", Number(options.limit || 20));
|
|
6655
7398
|
}
|
|
6656
|
-
throw new Error(`RPC method неизвестен: ${method}. Доступно: status, search, card, quality, sync.`);
|
|
7399
|
+
throw new Error(`RPC method неизвестен: ${method}. Доступно: status, search, card, quality, sync, files.tree, files.read, files.search, index.search.`);
|
|
6657
7400
|
}
|
|
6658
7401
|
|
|
6659
7402
|
async function getLatestNpmVersion(packageName) {
|
|
@@ -6780,11 +7523,39 @@ async function writeConfig(value) {
|
|
|
6780
7523
|
}
|
|
6781
7524
|
|
|
6782
7525
|
async function loadConfig() {
|
|
7526
|
+
let config = DEFAULT_AI_CONFIG;
|
|
7527
|
+
for (const layer of [CONFIG_FILE, PROJECT_CONFIG_FILE, LOCAL_CONFIG_FILE]) {
|
|
7528
|
+
const value = await readConfigLayer(layer);
|
|
7529
|
+
if (value) config = mergeConfig(config, value);
|
|
7530
|
+
}
|
|
7531
|
+
return config;
|
|
7532
|
+
}
|
|
7533
|
+
|
|
7534
|
+
async function loadConfigLayers() {
|
|
7535
|
+
const files = [
|
|
7536
|
+
{ scope: "defaults", file: "builtin", value: DEFAULT_AI_CONFIG, exists: true },
|
|
7537
|
+
{ scope: "user", file: CONFIG_FILE },
|
|
7538
|
+
{ scope: "project", file: PROJECT_CONFIG_FILE },
|
|
7539
|
+
{ scope: "local", file: LOCAL_CONFIG_FILE },
|
|
7540
|
+
];
|
|
7541
|
+
const rows = [];
|
|
7542
|
+
for (const layer of files) {
|
|
7543
|
+
if (layer.scope === "defaults") {
|
|
7544
|
+
rows.push({ ...layer, errors: validateConfig(layer.value) });
|
|
7545
|
+
continue;
|
|
7546
|
+
}
|
|
7547
|
+
const value = await readConfigLayer(layer.file);
|
|
7548
|
+
rows.push({ ...layer, exists: Boolean(value), value, errors: value ? validateConfig(mergeConfig(DEFAULT_AI_CONFIG, value)) : [] });
|
|
7549
|
+
}
|
|
7550
|
+
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: [] });
|
|
7551
|
+
return rows;
|
|
7552
|
+
}
|
|
7553
|
+
|
|
7554
|
+
async function readConfigLayer(file) {
|
|
6783
7555
|
try {
|
|
6784
|
-
|
|
6785
|
-
return mergeConfig(DEFAULT_AI_CONFIG, JSON.parse(text));
|
|
7556
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
6786
7557
|
} catch {
|
|
6787
|
-
return
|
|
7558
|
+
return null;
|
|
6788
7559
|
}
|
|
6789
7560
|
}
|
|
6790
7561
|
|
|
@@ -6836,6 +7607,20 @@ function mergeConfig(base, override) {
|
|
|
6836
7607
|
...base.cron,
|
|
6837
7608
|
...(override.cron || {}),
|
|
6838
7609
|
},
|
|
7610
|
+
hooks: {
|
|
7611
|
+
...base.hooks,
|
|
7612
|
+
...(override.hooks || {}),
|
|
7613
|
+
},
|
|
7614
|
+
subagents: {
|
|
7615
|
+
...(base.subagents || {}),
|
|
7616
|
+
...(override.subagents || {}),
|
|
7617
|
+
},
|
|
7618
|
+
workspaces: {
|
|
7619
|
+
...(base.workspaces || {}),
|
|
7620
|
+
...(override.workspaces || {}),
|
|
7621
|
+
},
|
|
7622
|
+
hooksTrusted: override.hooksTrusted ?? base.hooksTrusted,
|
|
7623
|
+
local: override.local ?? base.local,
|
|
6839
7624
|
};
|
|
6840
7625
|
}
|
|
6841
7626
|
|
|
@@ -6996,6 +7781,7 @@ function runCommand(command, args, options = {}) {
|
|
|
6996
7781
|
const child = execFile(command, args, {
|
|
6997
7782
|
windowsHide: true,
|
|
6998
7783
|
maxBuffer: 1024 * 1024 * 5,
|
|
7784
|
+
cwd: options.cwd,
|
|
6999
7785
|
env: {
|
|
7000
7786
|
...process.env,
|
|
7001
7787
|
...(options.env || {}),
|