@iola_adm/iola-cli 0.1.28 → 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 +15 -1
- package/package.json +1 -1
- package/src/cli.js +1028 -29
- package/wiki/Home.md +2 -0
- package/wiki//320/220/321/200/321/205/320/270/320/262/321/213-/320/270-/320/274/320/260/321/201/321/202/320/265/321/200-/320/275/320/260/321/201/321/202/321/200/320/276/320/271/320/272/320/270.md +42 -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,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,11 +259,13 @@ 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],
|
|
245
266
|
["tools", handleTools],
|
|
246
267
|
["files", handleFiles],
|
|
268
|
+
["archive", handleArchive],
|
|
247
269
|
["changes", handleChanges],
|
|
248
270
|
["import", handleImport],
|
|
249
271
|
["index", handleIndex],
|
|
@@ -253,7 +275,11 @@ const COMMANDS = new Map([
|
|
|
253
275
|
["tasks", handleTasks],
|
|
254
276
|
["artifacts", handleArtifacts],
|
|
255
277
|
["snapshot", handleSnapshot],
|
|
278
|
+
["sandbox", handleSandbox],
|
|
256
279
|
["trace", handleTrace],
|
|
280
|
+
["trajectory", handleTrajectory],
|
|
281
|
+
["usage", handleUsage],
|
|
282
|
+
["budget", handleBudget],
|
|
257
283
|
["policy", handlePolicy],
|
|
258
284
|
["export", handleExport],
|
|
259
285
|
["cron", handleCron],
|
|
@@ -263,6 +289,8 @@ const COMMANDS = new Map([
|
|
|
263
289
|
["memory", handleMemory],
|
|
264
290
|
["hooks", handleHooks],
|
|
265
291
|
["agents", handleAgents],
|
|
292
|
+
["subagents", handleSubagents],
|
|
293
|
+
["review", handleReview],
|
|
266
294
|
["mcp", handleMcp],
|
|
267
295
|
["cache", handleCache],
|
|
268
296
|
["sync", handleSync],
|
|
@@ -352,14 +380,17 @@ Usage:
|
|
|
352
380
|
iola history [--limit 20]
|
|
353
381
|
iola history clear
|
|
354
382
|
iola sessions [--limit 20]
|
|
383
|
+
iola sessions replay SESSION_ID
|
|
355
384
|
iola resume SESSION_ID [TEXT]
|
|
356
385
|
iola fork SESSION_ID [TEXT]
|
|
357
386
|
iola features list|enable|disable
|
|
387
|
+
iola settings list|get|validate|doctor|init
|
|
358
388
|
iola wiki [open|links]
|
|
359
389
|
iola context list|show|init
|
|
360
|
-
iola skills list|show|paths|enable|disable
|
|
390
|
+
iola skills list|show|paths|enable|disable|bundles|bundle|doctor
|
|
361
391
|
iola tools list|toolsets|enable|disable|profile
|
|
362
392
|
iola files status|mode|approvals|tree|read|search|write|patch
|
|
393
|
+
iola archive doctor|list|test|extract|create|index
|
|
363
394
|
iola changes list|show|apply|discard
|
|
364
395
|
iola import file|folder
|
|
365
396
|
iola index folder|status|search
|
|
@@ -369,17 +400,23 @@ Usage:
|
|
|
369
400
|
iola tasks list|add|done|run
|
|
370
401
|
iola artifacts list|show|open
|
|
371
402
|
iola snapshot create|list|restore
|
|
403
|
+
iola sandbox fork|run|diff|apply
|
|
372
404
|
iola trace last|show
|
|
405
|
+
iola trajectory export|last
|
|
406
|
+
iola usage summary|models|sessions
|
|
407
|
+
iola budget status|set
|
|
373
408
|
iola policy use safe|analyst|developer|full
|
|
374
409
|
iola export REPORT --format docx|xlsx --output FILE
|
|
375
410
|
iola cron list|add|delete|run|tick
|
|
376
411
|
iola daemon start|status
|
|
377
412
|
iola rpc call METHOD [ARGS] [--json]
|
|
378
413
|
iola permissions list|allow|deny
|
|
379
|
-
iola memory show|add|set|clear|export
|
|
380
|
-
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
|
|
381
416
|
iola agents list|run
|
|
382
|
-
iola
|
|
417
|
+
iola subagents list|run|parallel|add
|
|
418
|
+
iola review config|data|docs|report
|
|
419
|
+
iola mcp list|status|install|remove|serve [--stdio]
|
|
383
420
|
iola cache status|warm|clear
|
|
384
421
|
iola sync [--dataset schools|kindergartens]
|
|
385
422
|
iola sync status
|
|
@@ -618,6 +655,11 @@ async function handleAgentLine(line, state) {
|
|
|
618
655
|
return false;
|
|
619
656
|
}
|
|
620
657
|
|
|
658
|
+
if (command === "archive") {
|
|
659
|
+
await handleArchive(args);
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
|
|
621
663
|
if (command === "changes") {
|
|
622
664
|
await handleChanges(args);
|
|
623
665
|
return false;
|
|
@@ -791,6 +833,7 @@ async function handleAgentLine(line, state) {
|
|
|
791
833
|
context: ["context", args],
|
|
792
834
|
skills: ["skills", args],
|
|
793
835
|
files: ["files", args],
|
|
836
|
+
archive: ["archive", args],
|
|
794
837
|
changes: ["changes", args],
|
|
795
838
|
index: ["index", args],
|
|
796
839
|
reports: ["reports", args],
|
|
@@ -850,6 +893,7 @@ function printAgentHelp() {
|
|
|
850
893
|
/permissions
|
|
851
894
|
/tools
|
|
852
895
|
/files status
|
|
896
|
+
/archive doctor
|
|
853
897
|
/changes list
|
|
854
898
|
/index status
|
|
855
899
|
/reports list
|
|
@@ -966,6 +1010,14 @@ function showBanner() {
|
|
|
966
1010
|
console.log("открытые данные • MCP • локальный AI");
|
|
967
1011
|
}
|
|
968
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
|
+
|
|
969
1021
|
async function showVersion(args = []) {
|
|
970
1022
|
const options = parseOptions(args);
|
|
971
1023
|
const packageJson = await import("../package.json", { with: { type: "json" } });
|
|
@@ -1503,6 +1555,17 @@ async function handleSessions(args) {
|
|
|
1503
1555
|
return;
|
|
1504
1556
|
}
|
|
1505
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
|
+
|
|
1506
1569
|
const rows = listSessions(Number(options.limit || 20));
|
|
1507
1570
|
|
|
1508
1571
|
if (options.json) {
|
|
@@ -1578,6 +1641,61 @@ async function handleFeatures(args) {
|
|
|
1578
1641
|
throw new Error("Команды features: list, enable NAME, disable NAME.");
|
|
1579
1642
|
}
|
|
1580
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
|
+
|
|
1581
1699
|
async function handleWiki(args) {
|
|
1582
1700
|
const [action = "links"] = args;
|
|
1583
1701
|
const base = "https://github.com/adm-iola/iola-cli/wiki";
|
|
@@ -1590,7 +1708,9 @@ async function handleWiki(args) {
|
|
|
1590
1708
|
["Skills и toolsets", `${base}/Skills-и-toolsets`],
|
|
1591
1709
|
["Локальные файлы", `${base}/Локальные-файлы`],
|
|
1592
1710
|
["Рабочая среда агента", `${base}/Рабочая-среда-агента`],
|
|
1711
|
+
["Платформа агента", `${base}/Платформа-агента`],
|
|
1593
1712
|
["Расширения и локальные данные", `${base}/Расширения-и-локальные-данные`],
|
|
1713
|
+
["Архивы и мастер настройки", `${base}/Архивы-и-мастер-настройки`],
|
|
1594
1714
|
["Daemon, RPC и cron", `${base}/Daemon-RPC-и-cron`],
|
|
1595
1715
|
["Контекст и память", `${base}/Контекст-и-память`],
|
|
1596
1716
|
["Команды", `${base}/Команды`],
|
|
@@ -1691,6 +1811,41 @@ async function handleSkills(args) {
|
|
|
1691
1811
|
return;
|
|
1692
1812
|
}
|
|
1693
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
|
+
|
|
1694
1849
|
if (action === "show") {
|
|
1695
1850
|
const skill = findSkill(name, config);
|
|
1696
1851
|
if (!skill) throw new Error(`Skill не найден: ${name}`);
|
|
@@ -1708,7 +1863,7 @@ async function handleSkills(args) {
|
|
|
1708
1863
|
return;
|
|
1709
1864
|
}
|
|
1710
1865
|
|
|
1711
|
-
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.");
|
|
1712
1867
|
}
|
|
1713
1868
|
|
|
1714
1869
|
async function handleTools(args) {
|
|
@@ -1845,6 +2000,88 @@ async function handleFiles(args) {
|
|
|
1845
2000
|
throw new Error("Команды files: status, mode MODE, approvals POLICY, tree [PATH], read FILE, search TEXT, write FILE --text TEXT, patch FILE --search OLD --replace NEW.");
|
|
1846
2001
|
}
|
|
1847
2002
|
|
|
2003
|
+
async function handleArchive(args) {
|
|
2004
|
+
const [action = "doctor", target, ...rest] = args;
|
|
2005
|
+
const options = parseOptions(rest);
|
|
2006
|
+
if (action === "doctor") {
|
|
2007
|
+
const sevenZip = await ensureArchiveTool({ install: true });
|
|
2008
|
+
printKeyValue({ sevenZip, status: "ok", formats: "zip, 7z, rar, tar, gz, tgz, bz2, xz и др." });
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
if (action === "list") {
|
|
2012
|
+
if (!target) throw new Error("Пример: iola archive list docs.zip");
|
|
2013
|
+
const rows = await archiveList(target);
|
|
2014
|
+
printTable(rows, [["date", "Дата"], ["size", "Размер"], ["name", "Файл"]]);
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
if (action === "test") {
|
|
2018
|
+
if (!target) throw new Error("Пример: iola archive test docs.zip");
|
|
2019
|
+
await archiveRun(["t", target]);
|
|
2020
|
+
console.log("Архив проверен.");
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
if (action === "extract") {
|
|
2024
|
+
if (!target) throw new Error("Пример: iola archive extract docs.zip --output ./out");
|
|
2025
|
+
const outputDir = options.output || path.join(process.cwd(), path.basename(target, path.extname(target)));
|
|
2026
|
+
await archiveRun(["x", target, `-o${outputDir}`, "-y"]);
|
|
2027
|
+
console.log(`Архив распакован: ${outputDir}`);
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
if (action === "create") {
|
|
2031
|
+
const outputFile = target;
|
|
2032
|
+
const inputPath = rest[0] || options.path || ".";
|
|
2033
|
+
if (!outputFile) throw new Error("Пример: iola archive create docs.zip ./docs");
|
|
2034
|
+
await archiveRun(["a", outputFile, inputPath]);
|
|
2035
|
+
console.log(`Архив создан: ${outputFile}`);
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
if (action === "index") {
|
|
2039
|
+
if (!target) throw new Error("Пример: iola archive index docs.zip");
|
|
2040
|
+
const tempDir = path.join(os.tmpdir(), `iola-archive-${Date.now()}`);
|
|
2041
|
+
const previous = await loadConfig();
|
|
2042
|
+
await mkdir(tempDir, { recursive: true });
|
|
2043
|
+
try {
|
|
2044
|
+
await archiveRun(["x", target, `-o${tempDir}`, "-y"]);
|
|
2045
|
+
await saveConfig({ files: { ...(previous.files || {}), workspaceRoot: tempDir, mode: "read-only" } });
|
|
2046
|
+
await setFilesMode("read-only", await loadConfig());
|
|
2047
|
+
const count = await indexFolder(".", { depth: options.depth || 8, limit: options.limit || 2000 });
|
|
2048
|
+
console.log(`Проиндексировано файлов из архива: ${count}`);
|
|
2049
|
+
} finally {
|
|
2050
|
+
await saveConfig({ files: previous.files, permissions: previous.permissions, toolsets: previous.toolsets }).catch(() => {});
|
|
2051
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
2052
|
+
}
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
throw new Error("Команды archive: doctor, list FILE, test FILE, extract FILE --output DIR, create OUT INPUT, index FILE.");
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
async function archiveRun(args) {
|
|
2059
|
+
const command = await ensureArchiveTool({ install: true });
|
|
2060
|
+
return runCommand(command, args, { inherit: true });
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
async function archiveList(target) {
|
|
2064
|
+
const command = await ensureArchiveTool({ install: true });
|
|
2065
|
+
const { stdout } = await runCommand(command, ["l", "-slt", target]);
|
|
2066
|
+
const rows = [];
|
|
2067
|
+
let current = {};
|
|
2068
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
2069
|
+
if (!line.trim()) {
|
|
2070
|
+
if (current.Path && current.Path !== target) rows.push({
|
|
2071
|
+
date: current.Modified || current.Created || "-",
|
|
2072
|
+
size: current.Size || "-",
|
|
2073
|
+
name: current.Path,
|
|
2074
|
+
});
|
|
2075
|
+
current = {};
|
|
2076
|
+
continue;
|
|
2077
|
+
}
|
|
2078
|
+
const [key, ...parts] = line.split(" = ");
|
|
2079
|
+
if (key && parts.length) current[key.trim()] = parts.join(" = ").trim();
|
|
2080
|
+
}
|
|
2081
|
+
if (current.Path && current.Path !== target) rows.push({ date: current.Modified || current.Created || "-", size: current.Size || "-", name: current.Path });
|
|
2082
|
+
return rows;
|
|
2083
|
+
}
|
|
2084
|
+
|
|
1848
2085
|
async function handleChanges(args) {
|
|
1849
2086
|
const [action = "list", id] = args;
|
|
1850
2087
|
if (action === "list" || action === "ls") {
|
|
@@ -1905,13 +2142,18 @@ async function handleIndex(args) {
|
|
|
1905
2142
|
console.log(`Проиндексировано документов: ${count}`);
|
|
1906
2143
|
return;
|
|
1907
2144
|
}
|
|
2145
|
+
if (action === "archive") {
|
|
2146
|
+
if (!target) throw new Error("Пример: iola index archive docs.zip");
|
|
2147
|
+
await handleArchive(["index", target, ...rest]);
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
1908
2150
|
if (action === "search") {
|
|
1909
2151
|
const query = [target, ...rest].filter(Boolean).join(" ");
|
|
1910
2152
|
if (!query) throw new Error('Пример: iola index search "школа 29"');
|
|
1911
2153
|
printTable(searchDocs(query, Number(options.limit || 20)), [["file", "Файл"], ["title", "Название"], ["snippet", "Фрагмент"]]);
|
|
1912
2154
|
return;
|
|
1913
2155
|
}
|
|
1914
|
-
throw new Error("Команды index: status, folder PATH, search TEXT.");
|
|
2156
|
+
throw new Error("Команды index: status, folder PATH, archive FILE, search TEXT.");
|
|
1915
2157
|
}
|
|
1916
2158
|
|
|
1917
2159
|
async function handleReports(args) {
|
|
@@ -2064,6 +2306,38 @@ async function handleSnapshot(args) {
|
|
|
2064
2306
|
throw new Error("Команды snapshot: create, list, restore ID.");
|
|
2065
2307
|
}
|
|
2066
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
|
+
|
|
2067
2341
|
async function handleTrace(args) {
|
|
2068
2342
|
const [action = "last", id] = args;
|
|
2069
2343
|
if (action === "last") {
|
|
@@ -2077,6 +2351,61 @@ async function handleTrace(args) {
|
|
|
2077
2351
|
throw new Error("Команды trace: last [LIMIT], show RUN_ID.");
|
|
2078
2352
|
}
|
|
2079
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
|
+
|
|
2080
2409
|
async function handlePolicy(args) {
|
|
2081
2410
|
const [action = "list", name] = args;
|
|
2082
2411
|
const policies = {
|
|
@@ -2348,7 +2677,26 @@ async function handleMemory(args) {
|
|
|
2348
2677
|
return;
|
|
2349
2678
|
}
|
|
2350
2679
|
|
|
2351
|
-
|
|
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.");
|
|
2352
2700
|
}
|
|
2353
2701
|
|
|
2354
2702
|
async function handleHooks(args) {
|
|
@@ -2371,6 +2719,22 @@ async function handleHooks(args) {
|
|
|
2371
2719
|
return;
|
|
2372
2720
|
}
|
|
2373
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
|
+
|
|
2374
2738
|
if (action === "add") {
|
|
2375
2739
|
if (!HOOK_EVENTS.includes(event) || commandParts.length === 0) {
|
|
2376
2740
|
throw new Error(`Пример: iola hooks add AfterSync "iola quality" Доступно: ${HOOK_EVENTS.join(", ")}`);
|
|
@@ -2402,7 +2766,7 @@ async function handleHooks(args) {
|
|
|
2402
2766
|
return;
|
|
2403
2767
|
}
|
|
2404
2768
|
|
|
2405
|
-
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.");
|
|
2406
2770
|
}
|
|
2407
2771
|
|
|
2408
2772
|
async function handleAgents(args) {
|
|
@@ -2451,8 +2815,132 @@ async function handleAgents(args) {
|
|
|
2451
2815
|
throw new Error("Команды agents: list, run NAME TEXT.");
|
|
2452
2816
|
}
|
|
2453
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
|
+
|
|
2454
2941
|
async function handleMcp(args) {
|
|
2455
|
-
const [action = "status", target = "codex"] = args;
|
|
2942
|
+
const [action = "status", target = "codex", ...rest] = args;
|
|
2943
|
+
const options = parseOptions([target, ...rest]);
|
|
2456
2944
|
|
|
2457
2945
|
if (action === "status") {
|
|
2458
2946
|
const [health, version] = await Promise.all([
|
|
@@ -2488,6 +2976,10 @@ async function handleMcp(args) {
|
|
|
2488
2976
|
|
|
2489
2977
|
if (action === "serve") {
|
|
2490
2978
|
const config = await loadConfig();
|
|
2979
|
+
if (options.stdio || target === "--stdio" || target === "stdio") {
|
|
2980
|
+
await startMcpStdio();
|
|
2981
|
+
return;
|
|
2982
|
+
}
|
|
2491
2983
|
await startMcpServer(config.daemon?.host || "127.0.0.1", Number(config.daemon?.port || DAEMON_PORT) + 1);
|
|
2492
2984
|
return;
|
|
2493
2985
|
}
|
|
@@ -3411,6 +3903,24 @@ function initDatabase() {
|
|
|
3411
3903
|
command TEXT,
|
|
3412
3904
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
3413
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
|
+
);
|
|
3414
3924
|
`);
|
|
3415
3925
|
rebuildFtsIfEmpty(db);
|
|
3416
3926
|
db.prepare(`
|
|
@@ -3467,6 +3977,7 @@ function getDbStatus() {
|
|
|
3467
3977
|
const artifacts = db.prepare("SELECT COUNT(*) AS count FROM artifacts").get();
|
|
3468
3978
|
const docs = db.prepare("SELECT COUNT(*) AS count FROM doc_index").get();
|
|
3469
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();
|
|
3470
3981
|
return {
|
|
3471
3982
|
status: "ok",
|
|
3472
3983
|
file: DB_FILE,
|
|
@@ -3482,6 +3993,7 @@ function getDbStatus() {
|
|
|
3482
3993
|
artifacts: artifacts?.count ?? 0,
|
|
3483
3994
|
indexed_docs: docs?.count ?? 0,
|
|
3484
3995
|
custom_records: custom?.count ?? 0,
|
|
3996
|
+
usage_events: usage?.count ?? 0,
|
|
3485
3997
|
};
|
|
3486
3998
|
} finally {
|
|
3487
3999
|
db.close();
|
|
@@ -3652,6 +4164,16 @@ function printSessionMessages(sessionId) {
|
|
|
3652
4164
|
]);
|
|
3653
4165
|
}
|
|
3654
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
|
+
|
|
3655
4177
|
function forkSessionInDb(sessionId) {
|
|
3656
4178
|
initDatabase();
|
|
3657
4179
|
const db = openDatabase();
|
|
@@ -4142,6 +4664,21 @@ function deleteMemory(id) {
|
|
|
4142
4664
|
}
|
|
4143
4665
|
}
|
|
4144
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
|
+
|
|
4145
4682
|
function clearMemory() {
|
|
4146
4683
|
initDatabase();
|
|
4147
4684
|
const db = openDatabase();
|
|
@@ -4472,6 +5009,7 @@ async function aiAsk(args, context = {}) {
|
|
|
4472
5009
|
const providerConfig = resolveAiProfile(config, options);
|
|
4473
5010
|
if (providerConfig.provider === "codex") await assertPermission("codex");
|
|
4474
5011
|
if (providerConfig.provider !== "ollama") await assertPermission("externalAi");
|
|
5012
|
+
if (options["stream-json"]) options.events = true;
|
|
4475
5013
|
if (options.tools && providerConfig.provider === "ollama") {
|
|
4476
5014
|
return localToolAsk(question, providerConfig, options);
|
|
4477
5015
|
}
|
|
@@ -4501,6 +5039,13 @@ async function aiAsk(args, context = {}) {
|
|
|
4501
5039
|
recordAskHistory({ question, answer, providerConfig, dataContext, error: "", sessionId });
|
|
4502
5040
|
appendSessionExchange(sessionId, question, answer, dataContext, "");
|
|
4503
5041
|
}
|
|
5042
|
+
recordUsage({
|
|
5043
|
+
providerConfig,
|
|
5044
|
+
question,
|
|
5045
|
+
answer,
|
|
5046
|
+
sessionId,
|
|
5047
|
+
profile: providerConfig.name,
|
|
5048
|
+
});
|
|
4504
5049
|
await maybeSuggestMemory(question, answer, providerConfig);
|
|
4505
5050
|
|
|
4506
5051
|
emitEvent(options, "answer", { length: answer.length, sessionId });
|
|
@@ -4545,6 +5090,7 @@ function resolveAiProfile(config, options = {}) {
|
|
|
4545
5090
|
}
|
|
4546
5091
|
|
|
4547
5092
|
async function localToolAsk(question, providerConfig, options) {
|
|
5093
|
+
if (options["stream-json"]) options.events = true;
|
|
4548
5094
|
await ensureLocalData();
|
|
4549
5095
|
const plan = await buildLocalToolPlan(question, providerConfig, options);
|
|
4550
5096
|
const validated = validateToolPlan(plan, options);
|
|
@@ -4570,6 +5116,7 @@ async function localToolAsk(question, providerConfig, options) {
|
|
|
4570
5116
|
sessionId: null,
|
|
4571
5117
|
});
|
|
4572
5118
|
}
|
|
5119
|
+
recordUsage({ providerConfig, question, answer, sessionId: null, profile: providerConfig.name });
|
|
4573
5120
|
|
|
4574
5121
|
emitEvent(options, "tool_plan", { plan: validated, runId });
|
|
4575
5122
|
saveArtifact("tool-result", question.slice(0, 80), "", { runId, plan: validated, outputs: result.outputs });
|
|
@@ -4675,6 +5222,7 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
4675
5222
|
let status = "ok";
|
|
4676
5223
|
let summary = "";
|
|
4677
5224
|
await assertPermission(step.tool);
|
|
5225
|
+
await runHooks("PreToolUse", { tool: step.tool, args: step.args || {} });
|
|
4678
5226
|
await runHooks("BeforeTool", { tool: step.tool, args: step.args || {} });
|
|
4679
5227
|
try {
|
|
4680
5228
|
if (step.tool === "search_local") {
|
|
@@ -4721,10 +5269,12 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
4721
5269
|
status = "error";
|
|
4722
5270
|
summary = error instanceof Error ? error.message : String(error);
|
|
4723
5271
|
recordToolTrace(options.runId || "manual", step.tool, step.args || {}, status, summary);
|
|
5272
|
+
await runHooks("OnError", { tool: step.tool, args: step.args || {}, error: summary });
|
|
4724
5273
|
throw error;
|
|
4725
5274
|
}
|
|
4726
5275
|
recordToolTrace(options.runId || "manual", step.tool, step.args || {}, status, summary);
|
|
4727
5276
|
await runHooks("AfterTool", { tool: step.tool, rows: current.length });
|
|
5277
|
+
await runHooks("PostToolUse", { tool: step.tool, rows: current.length });
|
|
4728
5278
|
}
|
|
4729
5279
|
return { rows: current, outputs };
|
|
4730
5280
|
}
|
|
@@ -4752,7 +5302,12 @@ async function runHooks(event, payload = {}) {
|
|
|
4752
5302
|
const config = await loadConfig();
|
|
4753
5303
|
const commands = config.hooks?.[event] || [];
|
|
4754
5304
|
for (const command of commands) {
|
|
4755
|
-
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);
|
|
4756
5311
|
if (parts.length === 0) continue;
|
|
4757
5312
|
await runCommand(parts[0], parts.slice(1), {
|
|
4758
5313
|
inherit: true,
|
|
@@ -5281,23 +5836,79 @@ async function onboard(args = []) {
|
|
|
5281
5836
|
initDatabase();
|
|
5282
5837
|
await handleConfig(["validate"]);
|
|
5283
5838
|
await doctor(["--summary"]);
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5839
|
+
await ensureArchiveTool({ install: true });
|
|
5840
|
+
|
|
5841
|
+
const components = options.yes ? ["workspace", "policy", "ollama", "openai", "openrouter", "codex", "codex-mcp", "index"] : await chooseOnboardComponents();
|
|
5842
|
+
if (components.includes("workspace")) await handleWorkspace(["init"]);
|
|
5843
|
+
if (components.includes("policy")) await handlePolicy(["use", "analyst"]);
|
|
5844
|
+
if (components.includes("ollama")) {
|
|
5845
|
+
await installOllamaIfMissing();
|
|
5846
|
+
await setupOllama(["--yes"]);
|
|
5847
|
+
}
|
|
5848
|
+
if (components.includes("openai")) {
|
|
5849
|
+
await aiSetup(["openai"]);
|
|
5850
|
+
if (process.stdin.isTTY) await setAiKey("openai");
|
|
5851
|
+
}
|
|
5852
|
+
if (components.includes("openrouter")) {
|
|
5853
|
+
await aiSetup(["openrouter"]);
|
|
5854
|
+
if (process.stdin.isTTY) await setAiKey("openrouter");
|
|
5855
|
+
}
|
|
5856
|
+
if (components.includes("codex")) {
|
|
5857
|
+
await installCodexIfMissing();
|
|
5858
|
+
await aiSetup(["codex"]);
|
|
5859
|
+
}
|
|
5860
|
+
if (components.includes("codex-mcp")) await setupClient(["codex"]);
|
|
5861
|
+
if (components.includes("index")) {
|
|
5862
|
+
await setFilesMode("read-only", await loadConfig());
|
|
5863
|
+
console.log("Индекс документов можно запустить командой: iola index folder ./docs");
|
|
5864
|
+
}
|
|
5287
5865
|
console.log("Onboard завершен.");
|
|
5288
5866
|
}
|
|
5289
5867
|
|
|
5868
|
+
async function chooseOnboardComponents() {
|
|
5869
|
+
if (!process.stdin.isTTY) return ["workspace", "policy"];
|
|
5870
|
+
console.log("");
|
|
5871
|
+
console.log("Выберите компоненты через запятую:");
|
|
5872
|
+
console.log("1. workspace и контекст");
|
|
5873
|
+
console.log("2. policy analyst");
|
|
5874
|
+
console.log("3. Ollama + локальная модель");
|
|
5875
|
+
console.log("4. OpenAI API");
|
|
5876
|
+
console.log("5. OpenRouter API");
|
|
5877
|
+
console.log("6. Codex CLI");
|
|
5878
|
+
console.log("7. MCP для Codex");
|
|
5879
|
+
console.log("8. Индекс локальных документов");
|
|
5880
|
+
console.log("");
|
|
5881
|
+
const rl = readline.createInterface({ input, output });
|
|
5882
|
+
try {
|
|
5883
|
+
const answer = (await rl.question("Компоненты [1,2,8]: ")).trim() || "1,2,8";
|
|
5884
|
+
const selected = new Set(answer.split(/[,\s]+/).filter(Boolean));
|
|
5885
|
+
const map = {
|
|
5886
|
+
1: "workspace",
|
|
5887
|
+
2: "policy",
|
|
5888
|
+
3: "ollama",
|
|
5889
|
+
4: "openai",
|
|
5890
|
+
5: "openrouter",
|
|
5891
|
+
6: "codex",
|
|
5892
|
+
7: "codex-mcp",
|
|
5893
|
+
8: "index",
|
|
5894
|
+
};
|
|
5895
|
+
return [...selected].map((item) => map[item] || item).filter(Boolean);
|
|
5896
|
+
} finally {
|
|
5897
|
+
rl.close();
|
|
5898
|
+
}
|
|
5899
|
+
}
|
|
5900
|
+
|
|
5290
5901
|
function parseOptions(args) {
|
|
5291
5902
|
const result = { _: [] };
|
|
5292
5903
|
|
|
5293
5904
|
for (let index = 0; index < args.length; index += 1) {
|
|
5294
5905
|
const arg = args[index];
|
|
5295
|
-
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") {
|
|
5296
5907
|
result[arg.slice(2)] = true;
|
|
5297
5908
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
5298
5909
|
result.check = true;
|
|
5299
5910
|
result[arg.slice(2)] = true;
|
|
5300
|
-
} 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") {
|
|
5301
5912
|
result[arg.slice(2)] = args[index + 1];
|
|
5302
5913
|
index += 1;
|
|
5303
5914
|
} else {
|
|
@@ -5614,6 +6225,66 @@ async function getCommandVersion(command, args) {
|
|
|
5614
6225
|
}
|
|
5615
6226
|
}
|
|
5616
6227
|
|
|
6228
|
+
async function findCommand(candidates, versionArgs = ["--version"]) {
|
|
6229
|
+
for (const command of candidates) {
|
|
6230
|
+
const version = await getCommandVersion(command, versionArgs);
|
|
6231
|
+
if (version !== "не найден") return { command, version };
|
|
6232
|
+
}
|
|
6233
|
+
return null;
|
|
6234
|
+
}
|
|
6235
|
+
|
|
6236
|
+
async function ensureArchiveTool(options = {}) {
|
|
6237
|
+
const found = await findCommand(["7z", "7zz", "7za"], ["--help"]);
|
|
6238
|
+
if (found) return found.command;
|
|
6239
|
+
if (options.install === false) throw new Error("7-Zip не найден.");
|
|
6240
|
+
await installSevenZip();
|
|
6241
|
+
const installed = await findCommand(["7z", "7zz", "7za"], ["--help"]);
|
|
6242
|
+
if (!installed) throw new Error("7-Zip не найден после установки. Перезапустите терминал и проверьте: 7z");
|
|
6243
|
+
return installed.command;
|
|
6244
|
+
}
|
|
6245
|
+
|
|
6246
|
+
async function installSevenZip() {
|
|
6247
|
+
console.log("7-Zip не найден. Устанавливаю архиватор для работы со всеми типами архивов.");
|
|
6248
|
+
if (process.platform === "win32") {
|
|
6249
|
+
await runCommand("winget", ["install", "7zip.7zip", "--accept-package-agreements", "--accept-source-agreements"], { inherit: true });
|
|
6250
|
+
return;
|
|
6251
|
+
}
|
|
6252
|
+
if (process.platform === "darwin") {
|
|
6253
|
+
try {
|
|
6254
|
+
await runCommand("brew", ["install", "sevenzip"], { inherit: true });
|
|
6255
|
+
} catch {
|
|
6256
|
+
await runCommand("brew", ["install", "p7zip"], { inherit: true });
|
|
6257
|
+
}
|
|
6258
|
+
return;
|
|
6259
|
+
}
|
|
6260
|
+
try {
|
|
6261
|
+
await runCommand("sh", ["-c", "sudo apt-get update && sudo apt-get install -y p7zip-full p7zip-rar"], { inherit: true });
|
|
6262
|
+
} catch {
|
|
6263
|
+
await runCommand("sh", ["-c", "sudo apt-get update && sudo apt-get install -y 7zip"], { inherit: true });
|
|
6264
|
+
}
|
|
6265
|
+
}
|
|
6266
|
+
|
|
6267
|
+
async function installOllamaIfMissing() {
|
|
6268
|
+
if (await getOllamaVersion()) return;
|
|
6269
|
+
console.log("Ollama не найден. Устанавливаю Ollama.");
|
|
6270
|
+
if (process.platform === "win32") {
|
|
6271
|
+
await runCommand("winget", ["install", "Ollama.Ollama", "--accept-package-agreements", "--accept-source-agreements"], { inherit: true });
|
|
6272
|
+
return;
|
|
6273
|
+
}
|
|
6274
|
+
if (process.platform === "darwin") {
|
|
6275
|
+
await runCommand("brew", ["install", "--cask", "ollama"], { inherit: true });
|
|
6276
|
+
return;
|
|
6277
|
+
}
|
|
6278
|
+
await runCommand("sh", ["-c", "curl -fsSL https://ollama.com/install.sh | sh"], { inherit: true });
|
|
6279
|
+
}
|
|
6280
|
+
|
|
6281
|
+
async function installCodexIfMissing() {
|
|
6282
|
+
const version = await getCommandVersion("codex", ["--version"]);
|
|
6283
|
+
if (version !== "не найден") return;
|
|
6284
|
+
console.log("Codex CLI не найден. Устанавливаю через npm.");
|
|
6285
|
+
await runCommand("npm", ["install", "-g", "@openai/codex"], { inherit: true });
|
|
6286
|
+
}
|
|
6287
|
+
|
|
5617
6288
|
async function probeEndpoint(url) {
|
|
5618
6289
|
try {
|
|
5619
6290
|
const response = await fetch(url, { headers: { accept: "application/json" } });
|
|
@@ -5627,6 +6298,13 @@ async function startDaemon(host, port) {
|
|
|
5627
6298
|
const server = createServer(async (req, res) => {
|
|
5628
6299
|
try {
|
|
5629
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
|
+
|
|
5630
6308
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
5631
6309
|
|
|
5632
6310
|
if (req.method === "GET" && url.pathname === "/health") {
|
|
@@ -5639,6 +6317,26 @@ async function startDaemon(host, port) {
|
|
|
5639
6317
|
return;
|
|
5640
6318
|
}
|
|
5641
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
|
+
|
|
5642
6340
|
if (req.method === "POST" && url.pathname === "/rpc") {
|
|
5643
6341
|
const body = await readRequestBody(req);
|
|
5644
6342
|
const payload = body ? JSON.parse(body) : {};
|
|
@@ -5666,22 +6364,19 @@ async function startDaemon(host, port) {
|
|
|
5666
6364
|
|
|
5667
6365
|
async function startMcpServer(host, port) {
|
|
5668
6366
|
const server = createServer(async (req, res) => {
|
|
6367
|
+
let payload = {};
|
|
5669
6368
|
try {
|
|
5670
6369
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
5671
6370
|
if (req.method !== "POST") {
|
|
5672
|
-
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) }));
|
|
5673
6372
|
return;
|
|
5674
6373
|
}
|
|
5675
|
-
|
|
5676
|
-
const
|
|
5677
|
-
const args = payload.params?.arguments || payload.params || {};
|
|
5678
|
-
let result;
|
|
5679
|
-
if (method === "index.search") result = searchDocs(args.query || "", Number(args.limit || 20));
|
|
5680
|
-
else result = await executeRpc(method, { ...args, _: [] });
|
|
6374
|
+
payload = JSON.parse(await readRequestBody(req) || "{}");
|
|
6375
|
+
const result = await handleMcpMessage(payload);
|
|
5681
6376
|
res.end(JSON.stringify({ jsonrpc: "2.0", id: payload.id || null, result }));
|
|
5682
6377
|
} catch (error) {
|
|
5683
6378
|
res.statusCode = 500;
|
|
5684
|
-
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) } }));
|
|
5685
6380
|
}
|
|
5686
6381
|
});
|
|
5687
6382
|
await new Promise((resolve, reject) => {
|
|
@@ -5692,6 +6387,142 @@ async function startMcpServer(host, port) {
|
|
|
5692
6387
|
await new Promise(() => {});
|
|
5693
6388
|
}
|
|
5694
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
|
+
|
|
5695
6526
|
function readRequestBody(req) {
|
|
5696
6527
|
return new Promise((resolve, reject) => {
|
|
5697
6528
|
let body = "";
|
|
@@ -6125,6 +6956,101 @@ function recordToolTrace(runId, tool, args, status, summary) {
|
|
|
6125
6956
|
}
|
|
6126
6957
|
}
|
|
6127
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
|
+
|
|
6128
7054
|
function listTrace(limit = 20) {
|
|
6129
7055
|
initDatabase();
|
|
6130
7056
|
const db = openDatabase();
|
|
@@ -6135,6 +7061,20 @@ function listTrace(limit = 20) {
|
|
|
6135
7061
|
}
|
|
6136
7062
|
}
|
|
6137
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
|
+
|
|
6138
7078
|
function getTraceRun(runId) {
|
|
6139
7079
|
initDatabase();
|
|
6140
7080
|
const db = openDatabase();
|
|
@@ -6165,6 +7105,22 @@ async function createSnapshot() {
|
|
|
6165
7105
|
}
|
|
6166
7106
|
}
|
|
6167
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
|
+
|
|
6168
7124
|
function listSnapshots() {
|
|
6169
7125
|
initDatabase();
|
|
6170
7126
|
const db = openDatabase();
|
|
@@ -6440,7 +7396,7 @@ async function executeRpc(method, options = {}) {
|
|
|
6440
7396
|
if (method === "index.search") {
|
|
6441
7397
|
return searchDocs(options.query || options.search || "", Number(options.limit || 20));
|
|
6442
7398
|
}
|
|
6443
|
-
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.`);
|
|
6444
7400
|
}
|
|
6445
7401
|
|
|
6446
7402
|
async function getLatestNpmVersion(packageName) {
|
|
@@ -6567,11 +7523,39 @@ async function writeConfig(value) {
|
|
|
6567
7523
|
}
|
|
6568
7524
|
|
|
6569
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) {
|
|
6570
7555
|
try {
|
|
6571
|
-
|
|
6572
|
-
return mergeConfig(DEFAULT_AI_CONFIG, JSON.parse(text));
|
|
7556
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
6573
7557
|
} catch {
|
|
6574
|
-
return
|
|
7558
|
+
return null;
|
|
6575
7559
|
}
|
|
6576
7560
|
}
|
|
6577
7561
|
|
|
@@ -6623,6 +7607,20 @@ function mergeConfig(base, override) {
|
|
|
6623
7607
|
...base.cron,
|
|
6624
7608
|
...(override.cron || {}),
|
|
6625
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,
|
|
6626
7624
|
};
|
|
6627
7625
|
}
|
|
6628
7626
|
|
|
@@ -6783,6 +7781,7 @@ function runCommand(command, args, options = {}) {
|
|
|
6783
7781
|
const child = execFile(command, args, {
|
|
6784
7782
|
windowsHide: true,
|
|
6785
7783
|
maxBuffer: 1024 * 1024 * 5,
|
|
7784
|
+
cwd: options.cwd,
|
|
6786
7785
|
env: {
|
|
6787
7786
|
...process.env,
|
|
6788
7787
|
...(options.env || {}),
|