@naparnik/mcp 0.10.50 → 0.10.53

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.
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomUUID, randomBytes } from 'crypto';
2
- import { existsSync, readFileSync, closeSync, mkdirSync, openSync, rmSync, renameSync, readdirSync, statSync, writeFileSync, symlinkSync, chmodSync, lstatSync, realpathSync, mkdtempSync, linkSync, readlinkSync, appendFileSync, fstatSync, readSync, fsyncSync } from 'fs';
2
+ import { existsSync, readFileSync, closeSync, mkdirSync, openSync, rmSync, renameSync, readdirSync, statSync, writeFileSync, symlinkSync, chmodSync, lstatSync, realpathSync, mkdtempSync, linkSync, readlinkSync, appendFileSync, fsyncSync, fstatSync, readSync } from 'fs';
3
3
  import { win32, join, dirname, sep, resolve, basename, relative, isAbsolute, extname } from 'path';
4
4
  import { gunzipSync } from 'zlib';
5
5
  import { execFile, spawn, execFileSync } from 'child_process';
@@ -232,6 +232,9 @@ function toolMixed(blocks, structured) {
232
232
  return { type: "success", content: blocks, ...structured !== void 0 ? { structured } : {} };
233
233
  }
234
234
  function toolError(error40, code) {
235
+ if (code !== void 0) {
236
+ return { type: "error", error: error40, code };
237
+ }
235
238
  return { type: "error", error: error40 };
236
239
  }
237
240
 
@@ -291,6 +294,27 @@ var McpServer = class {
291
294
  void this.sendNotification("notifications/tools/list_changed");
292
295
  }
293
296
  }
297
+ /**
298
+ * Заменить весь набор инструментов разом — так новое тело оболочки встаёт
299
+ * без перезапуска (`body-swap.ts`).
300
+ *
301
+ * ⚠ УВЕДОМЛЕНИЕ ХОСТУ — ТОЛЬКО КОГДА ОБЪЯВЛЕНИЕ ИЗМЕНИЛОСЬ. Новое тело чаще
302
+ * всего несёт те же имена и схемы; `notifications/tools/list_changed` на
303
+ * каждую замену заставлял бы хост перечитывать список, а модель — заново
304
+ * смотреть на те же двери. Сравниваем то, что уезжает в `tools/list`.
305
+ *
306
+ * @returns изменилось ли объявление
307
+ */
308
+ replaceTools(tools) {
309
+ const before = JSON.stringify(this.listMcpTools());
310
+ this.tools.clear();
311
+ for (const tool of tools) this.tools.set(tool.name, tool);
312
+ const changed = JSON.stringify(this.listMcpTools()) !== before;
313
+ if (changed && this.clientInitialized && this.transport !== null) {
314
+ void this.sendNotification("notifications/tools/list_changed");
315
+ }
316
+ return changed;
317
+ }
294
318
  /**
295
319
  * Удаляет инструмент из MCP-экспорта.
296
320
  * @returns true если инструмент был найден и удалён
@@ -730,7 +754,7 @@ function parsePositiveInt(raw, fallback) {
730
754
  }
731
755
 
732
756
  // src/version.ts
733
- var VERSION = "0.10.50";
757
+ var VERSION = "0.10.53";
734
758
  var USER_AGENT = `naparnik-mcp/${VERSION}`;
735
759
  var MIN_ENGINE = "2026.09.24-478";
736
760
  var LAUNCHER_PROTOCOL = 1;
@@ -1621,7 +1645,9 @@ var Engine = class {
1621
1645
  env;
1622
1646
  awake;
1623
1647
  passportCache;
1624
- versionAtStart;
1648
+ pinned;
1649
+ /** Фоновые работы этого процесса, которые ещё идут: номер → версия, на которой заведены. */
1650
+ live = /* @__PURE__ */ new Map();
1625
1651
  pythonChecked;
1626
1652
  // ─── где что лежит ──────────────────────────────────────────────────────
1627
1653
  /**
@@ -1659,26 +1685,46 @@ var Engine = class {
1659
1685
  return join(this.home(), "engine");
1660
1686
  }
1661
1687
  /**
1662
- * Какая версия движка объявлена рабочей.
1688
+ * Какая версия движка рабочая ДЛЯ СЛЕДУЮЩЕЙ ЗАДАЧИ.
1663
1689
  *
1664
1690
  * Обычный ТЕКСТОВЫЙ файл, а не симлинк: создание симлинка на Windows требует
1665
1691
  * режима разработчика или прав администратора, а строка в файле переносима и
1666
1692
  * переименовывается атомарно.
1667
1693
  *
1668
- * ⚠ ЗАПОМИНАЕТСЯ НА ВСЮ ЖИЗНЬ ПРОЦЕССА, И ЭТО НЕСУЩЕЕ. Так работает обещание
1669
- * «подмена начинки — только на старте»: обновление меняет файл `current`, а
1670
- * запущенный процесс продолжает считать на той версии, с которой начал.
1671
- * Читай мы файл каждый раз — задача, заведённая старой версией, дочитывалась
1672
- * бы новой. У движка кэш по отпечаткам, и в отпечаток входит код шага: смесь
1673
- * двух версий внутри одной задачи не упала бы, а тихо смешала результаты.
1694
+ * ⚠ ВЕРСИЯ ЧИТАЕТСЯ НА СТАРТЕ КАЖДОЙ ЗАДАЧИ, А НЕ ОДИН РАЗ НА ПРОЦЕСС (решение
1695
+ * владельца 2026-09-25: «зачем перезапускать Claude, если код просто
1696
+ * скачался»). Движок — отдельный питон на каждую команду, и новой версии не
1697
+ * нужен новый процесс оболочки. До 2026-09-25 версия запоминалась на всю жизнь
1698
+ * процесса, и каждое обновление кончалось «перезапусти Claude».
1674
1699
  *
1675
- * `freshVersion()` даёт незакэшированное значение — она нужна ровно двум местам:
1676
- * самому обновлению и рассказу человеку о том, что новая версия уже готова и
1677
- * подхватится после перезапуска.
1700
+ * ⚠ НО ИДУЩАЯ ЗАДАЧА ДОИГРЫВАЕТ НА СВОЕЙ ВЕРСИИ, И ЭТО НЕСУЩЕЕ. У движка кэш
1701
+ * по отпечаткам, и в отпечаток входит код шага: смесь двух версий внутри
1702
+ * одной задачи не упала бы, а тихо смешала результаты. Пока жива фоновая
1703
+ * работа, заведённая этим процессом (`background`), её опрос (`wait`) и её
1704
+ * отказ зовут ту же версию; сменилась — подхватывается после неё.
1705
+ *
1706
+ * `freshVersion()` даёт то, что лежит в файле прямо сейчас, мимо задачи: оно
1707
+ * нужно самому обновлению и новостям о версиях.
1678
1708
  */
1679
1709
  currentVersion() {
1680
- if (this.versionAtStart === void 0) this.versionAtStart = this.freshVersion();
1681
- return this.versionAtStart;
1710
+ const fresh = this.freshVersion();
1711
+ if (this.pinned === void 0 || fresh !== this.pinned && this.live.size === 0) {
1712
+ if (this.pinned !== void 0 && fresh !== this.pinned) {
1713
+ this.passportCache = void 0;
1714
+ this.pythonChecked = void 0;
1715
+ }
1716
+ this.pinned = fresh;
1717
+ }
1718
+ return this.pinned;
1719
+ }
1720
+ /**
1721
+ * Идёт ли фоновая работа, заведённая этим процессом.
1722
+ *
1723
+ * Нужен двоим: версии движка (сменить её под живой задачей нельзя) и замене
1724
+ * тела оболочки (`body-swap.ts`): тело меняется, только когда ничего не идёт.
1725
+ */
1726
+ busy() {
1727
+ return this.live.size > 0;
1682
1728
  }
1683
1729
  /** Что записано в файле `current` прямо сейчас, мимо памяти процесса. */
1684
1730
  freshVersion() {
@@ -1742,20 +1788,6 @@ var Engine = class {
1742
1788
  \u0418\u0441\u043A\u0430\u043B: ${join(this.versionsDir(), "<\u0432\u0435\u0440\u0441\u0438\u044F>", "engine", "console.py")}
1743
1789
  \u0427\u0442\u043E \u0434\u0435\u043B\u0430\u0442\u044C: \u043F\u043E\u0437\u043E\u0432\u0438 setup \u2014 \u043E\u043D \u0441\u043A\u0430\u0447\u0430\u0435\u0442 \u043D\u0430\u0447\u0438\u043D\u043A\u0443 \u043C\u043E\u043D\u0442\u0430\u0436\u0430 (\u044D\u0442\u043E \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u0439 \u0430\u0440\u0442\u0435\u0444\u0430\u043A\u0442: \u043F\u0438\u0442\u043E\u043D \u0438 \u043F\u0430\u043A \u0432\u0435\u0441\u044F\u0442 \u0434\u0435\u0441\u044F\u0442\u043A\u0438 \u043C\u0435\u0433\u0430\u0431\u0430\u0439\u0442, \u0432 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0435 \u043E\u043D\u0438 \u043D\u0435 \u043A\u043B\u0430\u0434\u0443\u0442\u0441\u044F). \u0421\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0441\u043E\u0433\u043B\u0430\u0441\u0438\u044F \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430. \u0415\u0441\u043B\u0438 \u0434\u0432\u0438\u0436\u043E\u043A \u0443 \u043D\u0435\u0433\u043E \u0443\u0436\u0435 \u0435\u0441\u0442\u044C \u0441\u0432\u043E\u0435\u0439 \u043F\u0430\u043F\u043A\u043E\u0439 \u2014 \u043F\u0443\u0442\u044C \u043A \u043D\u0435\u0439 \u0443\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u0442\u0441\u044F \u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u043E\u0439 NAPARNIK_ENGINE \u0432 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430\u0445 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F, \u043F\u043E\u0441\u043B\u0435 \u0447\u0435\u0433\u043E \u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u0443 \u043D\u0430\u0434\u043E \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C.`;
1744
1790
  }
1745
- /**
1746
- * Забыть, где движок, — после того как мы его только что поставили.
1747
- *
1748
- * ⚠ БЕЗ ЭТОГО ПЕРВАЯ УСТАНОВКА НЕ ВИДНА САМОЙ СЕБЕ. `currentVersion`
1749
- * запоминается на всю жизнь процесса нарочно (подмена начинки — только на
1750
- * старте), и сразу после доставки следующий же вызов получил бы
1751
- * закэшированное «версии нет» про версию, которую мы сами и разложили.
1752
- * Здесь это не нарушает обещания: смешивать две версии внутри задачи нечем,
1753
- * до этого момента версии не было вовсе.
1754
- */
1755
- forgetPlace() {
1756
- this.versionAtStart = void 0;
1757
- this.passportCache = void 0;
1758
- }
1759
1791
  /** Забыть ответ на вопрос «есть ли питон» — после того как мы его принесли. */
1760
1792
  forgetPython() {
1761
1793
  this.pythonChecked = void 0;
@@ -1861,7 +1893,7 @@ var Engine = class {
1861
1893
  }
1862
1894
  } catch {
1863
1895
  }
1864
- this.pythonChecked = "\u0421\u0432\u043E\u0435\u0433\u043E \u043F\u0438\u0442\u043E\u043D\u0430 \u043C\u043E\u043D\u0442\u0430\u0436\u0430 \u043D\u0430 \u044D\u0442\u043E\u043C \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440\u0435 \u0435\u0449\u0451 \u043D\u0435\u0442 \u2014 \u043E\u043D \u0441\u0442\u0430\u0432\u0438\u0442\u0441\u044F \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u0434\u0432\u0438\u0436\u043A\u043E\u043C, \u043E\u0434\u043D\u043E\u0439 \u043F\u0430\u043F\u043A\u043E\u0439, \u0432 \u0441\u0438\u0441\u0442\u0435\u043C\u0443 \u043D\u0438\u0447\u0435\u0433\u043E \u043D\u0435 \u0441\u0442\u0430\u0432\u0438\u0442\u0441\u044F.\n\u0427\u0442\u043E \u0434\u0435\u043B\u0430\u0442\u044C: \u043F\u043E\u0437\u043E\u0432\u0438 setup \u0441 what=all \u2014 \u043F\u043E\u0441\u0442\u0430\u0432\u043B\u044E \u0434\u0432\u0438\u0436\u043E\u043A \u0438 \u0432\u0441\u0451, \u0447\u0442\u043E \u0435\u043C\u0443 \u043D\u0443\u0436\u043D\u043E.";
1896
+ this.pythonChecked = "\u0421\u0432\u043E\u0435\u0433\u043E \u043F\u0438\u0442\u043E\u043D\u0430 \u043C\u043E\u043D\u0442\u0430\u0436\u0430 \u043D\u0430 \u044D\u0442\u043E\u043C \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440\u0435 \u0435\u0449\u0451 \u043D\u0435\u0442 \u2014 \u043E\u043D \u0441\u0442\u0430\u0432\u0438\u0442\u0441\u044F \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u0434\u0432\u0438\u0436\u043A\u043E\u043C, \u043E\u0434\u043D\u043E\u0439 \u043F\u0430\u043F\u043A\u043E\u0439, \u0432 \u0441\u0438\u0441\u0442\u0435\u043C\u0443 \u043D\u0438\u0447\u0435\u0433\u043E \u043D\u0435 \u0441\u0442\u0430\u0432\u0438\u0442\u0441\u044F.\n\u0427\u0442\u043E \u0434\u0435\u043B\u0430\u0442\u044C: \u043F\u043E\u0437\u043E\u0432\u0438 install \u0441 what=all \u2014 \u043F\u043E\u0441\u0442\u0430\u0432\u043B\u044E \u0434\u0432\u0438\u0436\u043E\u043A \u0438 \u0432\u0441\u0451, \u0447\u0442\u043E \u0435\u043C\u0443 \u043D\u0443\u0436\u043D\u043E.";
1865
1897
  return this.pythonChecked;
1866
1898
  }
1867
1899
  // ─── запуск ─────────────────────────────────────────────────────────────
@@ -1912,9 +1944,10 @@ var Engine = class {
1912
1944
  async runWith(executable, place, args, {
1913
1945
  maxBuffer = 8 << 20,
1914
1946
  timeout = 3e5,
1915
- key
1947
+ key,
1948
+ onLine
1916
1949
  } = {}) {
1917
- return execFileP(executable, [place.consolePy, ...args], {
1950
+ const options = {
1918
1951
  // ⚠ ПАПКА ПАКА МОЖЕТ НЕ СУЩЕСТВОВАТЬ, И ЭТО НЕ ГИПОТЕЗА. Так выглядит
1919
1952
  // машина, где движок разложен, а пак ещё нет — например, сразу после
1920
1953
  // обновления начинки урезанным архивом. `execFile` с несуществующим `cwd`
@@ -1933,6 +1966,20 @@ var Engine = class {
1933
1966
  // обновление получает 403 и говорит человеку «каталог ещё не выложен».
1934
1967
  // Поэтому не «вернуть ключ в среду», а передать его ровно на этот запуск.
1935
1968
  env: montageEnv(this.pythonEnv(), key)
1969
+ };
1970
+ if (onLine === void 0) return execFileP(executable, [place.consolePy, ...args], options);
1971
+ return new Promise((done, fail) => {
1972
+ const child = execFile(executable, [place.consolePy, ...args], options, (err, out, errOut) => {
1973
+ if (err !== null) fail(Object.assign(err, { stdout: out, stderr: errOut }));
1974
+ else done({ stdout: out, stderr: errOut });
1975
+ });
1976
+ let rest = "";
1977
+ child.stdout?.on("data", (chunk) => {
1978
+ rest += String(chunk);
1979
+ const lines = rest.split(/\r?\n|\r/);
1980
+ rest = lines.pop() ?? "";
1981
+ for (const line of lines) if (line.trim() !== "") onLine(line.trim());
1982
+ });
1936
1983
  });
1937
1984
  }
1938
1985
  /**
@@ -2053,6 +2100,15 @@ var Engine = class {
2053
2100
  }
2054
2101
  }
2055
2102
  if (child.pid !== void 0) this.awake.follow(child.pid);
2103
+ const pid = child.pid;
2104
+ if (pid !== void 0) {
2105
+ this.live.set(pid, this.pinned ?? null);
2106
+ const done = () => {
2107
+ this.live.delete(pid);
2108
+ };
2109
+ child.once("close", done);
2110
+ child.once("error", done);
2111
+ }
2056
2112
  this.watch(child, opts.place, id, opts.advice, log.file);
2057
2113
  return { id };
2058
2114
  }
@@ -2175,7 +2231,7 @@ function lastLine(output) {
2175
2231
  function lastLines(text2, howMany) {
2176
2232
  return text2.trim().split("\n").slice(-8).join("\n");
2177
2233
  }
2178
- var NOT_JSON = "\u0414\u0432\u0438\u0436\u043E\u043A \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043D\u0435 \u0442\u0435\u043C, \u0447\u0435\u0433\u043E \u0436\u0434\u0430\u043B\u0438. \u041E\u0431\u043D\u043E\u0432\u0438 \u0434\u0432\u0438\u0436\u043E\u043A \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u043E\u043C setup \u0441 what=engine \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438.";
2234
+ var NOT_JSON = "\u0414\u0432\u0438\u0436\u043E\u043A \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043D\u0435 \u0442\u0435\u043C, \u0447\u0435\u0433\u043E \u0436\u0434\u0430\u043B\u0438. \u041E\u0431\u043D\u043E\u0432\u0438 \u043C\u043E\u043D\u0442\u0430\u0436 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u043E\u043C install \u0441 what=all \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438.";
2179
2235
 
2180
2236
  // node_modules/zod/v4/classic/external.js
2181
2237
  var external_exports = {};
@@ -13425,6 +13481,27 @@ function withArgs(tool) {
13425
13481
  }
13426
13482
  };
13427
13483
  }
13484
+ var scope = new AsyncLocalStorage();
13485
+ function noteInstallFault(stage, trouble) {
13486
+ scope.getStore()?.push({ stage, trouble });
13487
+ }
13488
+ async function collectInstallFaults(work) {
13489
+ const faults = [];
13490
+ const value = await scope.run(faults, work);
13491
+ return { value, faults };
13492
+ }
13493
+ var JOBS_KEPT = 10;
13494
+ var byJob = /* @__PURE__ */ new Map();
13495
+ function collectJobInstallFaults(id, work) {
13496
+ const faults = [];
13497
+ byJob.delete(id);
13498
+ byJob.set(id, faults);
13499
+ while (byJob.size > JOBS_KEPT) byJob.delete(byJob.keys().next().value);
13500
+ return scope.run(faults, work);
13501
+ }
13502
+ function jobInstallFaults(id) {
13503
+ return byJob.get(id) ?? [];
13504
+ }
13428
13505
  var KEEP_VERSIONS = 2;
13429
13506
  var BROKEN_PREFIX = "broken-";
13430
13507
  var WHY_FILE = "why.txt";
@@ -14096,29 +14173,6 @@ async function askPointer(opts) {
14096
14173
  clearTimeout(timer);
14097
14174
  }
14098
14175
  }
14099
- var scope = new AsyncLocalStorage();
14100
- function noteInstallFault(stage, trouble) {
14101
- scope.getStore()?.push({ stage, trouble });
14102
- }
14103
- async function collectInstallFaults(work) {
14104
- const faults = [];
14105
- const value = await scope.run(faults, work);
14106
- return { value, faults };
14107
- }
14108
- var JOBS_KEPT = 10;
14109
- var byJob = /* @__PURE__ */ new Map();
14110
- function collectJobInstallFaults(id, work) {
14111
- const faults = [];
14112
- byJob.delete(id);
14113
- byJob.set(id, faults);
14114
- while (byJob.size > JOBS_KEPT) byJob.delete(byJob.keys().next().value);
14115
- return scope.run(faults, work);
14116
- }
14117
- function jobInstallFaults(id) {
14118
- return byJob.get(id) ?? [];
14119
- }
14120
-
14121
- // src/update.ts
14122
14176
  var MANIFEST_PATH = "/download/engine/manifest.json";
14123
14177
  function manifestUrlFor(baseUrl) {
14124
14178
  return pointerUrl(baseUrl, MANIFEST_PATH);
@@ -14266,7 +14320,7 @@ ${delivery.text}` };
14266
14320
  }
14267
14321
  }
14268
14322
  hooks.downloaded?.();
14269
- if (candidateDeps(engine)) return activatePreparedCandidateLocked(engine, manifest, trial);
14323
+ if (candidateDeps(engine)) return activatePreparedCandidateLocked(engine, manifest, trial, "now", hooks.line);
14270
14324
  const smoke = await dryRun(engine, dest, manifest.version, trial);
14271
14325
  if (smoke !== null) {
14272
14326
  const where = keepBroken(dir, manifest.version, smoke);
@@ -14295,7 +14349,7 @@ ${smoke}`
14295
14349
  return {
14296
14350
  ok: true,
14297
14351
  text: `\u041D\u0430\u0447\u0438\u043D\u043A\u0430 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0430: ${before ?? "\u043D\u0435\u0442"} \u2192 ${manifest.version}.
14298
- \u0422\u0435\u043A\u0443\u0449\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0442\u0441\u044F \u043D\u0430 \u043F\u0440\u0435\u0436\u043D\u0435\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u2014 \u043D\u043E\u0432\u0430\u044F \u043F\u043E\u0434\u0445\u0432\u0430\u0442\u0438\u0442\u0441\u044F \u043F\u043E\u0441\u043B\u0435 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430 \u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u044B, \u0438 \u044D\u0442\u043E \u043D\u0430\u0440\u043E\u0447\u043D\u043E: \u0443 \u0434\u0432\u0438\u0436\u043A\u0430 \u043A\u044D\u0448 \u043F\u043E \u043E\u0442\u043F\u0435\u0447\u0430\u0442\u043A\u0430\u043C, \u0438 \u0441\u043C\u0435\u0448\u0438\u0432\u0430\u0442\u044C \u0434\u0432\u0435 \u0432\u0435\u0440\u0441\u0438\u0438 \u0432\u043D\u0443\u0442\u0440\u0438 \u043E\u0434\u043D\u043E\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u043D\u0435\u043B\u044C\u0437\u044F.
14352
+ \u041D\u043E\u0432\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0441\u043E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438, \u0431\u0435\u0437 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430; \u0438\u0434\u0443\u0449\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430 \u0434\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0435\u0442 \u043D\u0430 \u043F\u0440\u0435\u0436\u043D\u0435\u0439 \u2014 \u0443 \u0434\u0432\u0438\u0436\u043A\u0430 \u043A\u044D\u0448 \u043F\u043E \u043E\u0442\u043F\u0435\u0447\u0430\u0442\u043A\u0430\u043C, \u0438 \u0441\u043C\u0435\u0448\u0438\u0432\u0430\u0442\u044C \u0432\u0435\u0440\u0441\u0438\u0438 \u0432\u043D\u0443\u0442\u0440\u0438 \u0437\u0430\u0434\u0430\u0447\u0438 \u043D\u0435\u043B\u044C\u0437\u044F.
14299
14353
  ` + (wiped.length > 0 ? `\u0423\u0431\u0440\u0430\u043B \u0441\u0442\u0430\u0440\u044B\u0435 \u0432\u0435\u0440\u0441\u0438\u0438: ${wiped.join(", ")}.
14300
14354
  ` : "") + tidyFailed + `\u041E\u0442\u043A\u0430\u0442\u0438\u0442\u044C\u0441\u044F \u043D\u0430\u0437\u0430\u0434: setup \u0441 rollback=engine.`
14301
14355
  };
@@ -14441,6 +14495,12 @@ function pollutedPayload(dest, candidate) {
14441
14495
  function samePreparedCandidate(actual, expected) {
14442
14496
  return actual !== null && actual.version === expected.version && actual.sha256 === expected.sha256 && actual.state === "prepared" && actual.baseCurrent === expected.baseCurrent && actual.environment === expected.environment && actual.mineDigest === expected.mineDigest && actual.payload !== void 0 && expected.payload !== void 0 && samePayloadProof(actual.payload, expected.payload);
14443
14497
  }
14498
+ var STEP_ANSWER = /^(Шаг «|Дальше:|Спроси у человека:|Нужно скачать )/;
14499
+ function progressOnly(say) {
14500
+ return (line) => {
14501
+ if (!STEP_ANSWER.test(line)) say(line);
14502
+ };
14503
+ }
14444
14504
  async function runCandidateWith(engine, dir, version2, executable, place, args, opts = {}, requireVenv = true) {
14445
14505
  const venvTrouble = candidateVenvTrouble(dir, version2, place, requireVenv);
14446
14506
  if (venvTrouble !== null) throw new Error(venvTrouble);
@@ -14488,7 +14548,7 @@ async function compatibilityProof(engine, dir, version2, python, place) {
14488
14548
  return { ok: false, text: `\u0421\u043E\u0432\u043C\u0435\u0441\u0442\u0438\u043C\u043E\u0441\u0442\u044C \u043A\u0430\u043D\u0434\u0438\u0434\u0430\u0442\u0430 \u043D\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u0435\u043D\u0430 (unknown): ${describeFailure(trouble)}` };
14489
14549
  }
14490
14550
  }
14491
- async function activatePreparedCandidateLocked(engine, manifest, trial, extras = "now") {
14551
+ async function activatePreparedCandidateLocked(engine, manifest, trial, extras = "now", say) {
14492
14552
  const dir = engine.versionsDir();
14493
14553
  const candidate = readCandidate(dir);
14494
14554
  if (candidate?.version !== manifest.version || candidate.sha256 !== manifest.sha256) {
@@ -14536,7 +14596,7 @@ async function activatePreparedCandidateLocked(engine, manifest, trial, extras =
14536
14596
  "--consent",
14537
14597
  ...extras === "later" ? requiredOnly(dir, candidate.version) : []
14538
14598
  ],
14539
- { timeout: 60 * 6e4 },
14599
+ { timeout: 60 * 6e4, ...say === void 0 ? {} : { onLine: progressOnly(say) } },
14540
14600
  false
14541
14601
  );
14542
14602
  const installedVenvTrouble = candidateVenvTrouble(dir, candidate.version, place, true);
@@ -14727,7 +14787,7 @@ async function installVoiceCleaner(engine, dir, version2, python, place) {
14727
14787
  );
14728
14788
  return "";
14729
14789
  } catch (trouble) {
14730
- return `\u0428\u0443\u043C\u043E\u0434\u0430\u0432 \u0433\u043E\u043B\u043E\u0441\u0430 \u043D\u0435 \u0432\u0441\u0442\u0430\u043B: ${voiceTrouble(trouble)}. \u0414\u0432\u0438\u0436\u043E\u043A \u0432\u043A\u043B\u044E\u0447\u0451\u043D \u0438 \u043C\u043E\u043D\u0442\u0438\u0440\u0443\u0435\u0442; \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0448\u0443\u043C\u043E\u0434\u0430\u0432 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E \u2014 setup \u0441 what=voice.`;
14790
+ return `\u0428\u0443\u043C\u043E\u0434\u0430\u0432 \u0433\u043E\u043B\u043E\u0441\u0430 \u043D\u0435 \u0432\u0441\u0442\u0430\u043B: ${voiceTrouble(trouble)}. \u0414\u0432\u0438\u0436\u043E\u043A \u0432\u043A\u043B\u044E\u0447\u0451\u043D \u0438 \u043C\u043E\u043D\u0442\u0438\u0440\u0443\u0435\u0442; \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0448\u0443\u043C\u043E\u0434\u0430\u0432 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E \u2014 install \u0441 what=voice.`;
14731
14791
  }
14732
14792
  }
14733
14793
  function voiceTrouble(trouble) {
@@ -14782,7 +14842,7 @@ function rollback(engine) {
14782
14842
  return {
14783
14843
  ok: true,
14784
14844
  text: `\u041E\u0442\u043A\u0430\u0442\u0438\u043B \u043D\u0430\u0447\u0438\u043D\u043A\u0443: ${before ?? "\u043D\u0435\u0442"} \u2192 ${back}.
14785
- \u041F\u043E\u0434\u0445\u0432\u0430\u0442\u0438\u0442\u0441\u044F \u043F\u043E\u0441\u043B\u0435 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430 \u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u044B.`
14845
+ \u0420\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0441\u043E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438, \u0431\u0435\u0437 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430.`
14786
14846
  };
14787
14847
  });
14788
14848
  } catch (trouble) {
@@ -15235,7 +15295,7 @@ ${smoke}`
15235
15295
  return {
15236
15296
  ok: true,
15237
15297
  text: `\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0435 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u043E: ${before} \u2192 ${manifest.version}.
15238
- \u0417\u0430\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043F\u043E\u0441\u043B\u0435 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430 \u2014 \u0437\u0430\u043A\u0440\u043E\u0439\u0442\u0435 \u0438 \u043E\u0442\u043A\u0440\u043E\u0439\u0442\u0435 \u041A\u043B\u043E\u0434\u0430 \u0438\u043B\u0438 Codex. \u0414\u043E \u044D\u0442\u043E\u0433\u043E \u0432\u0441\u0451 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0442 \u0440\u0430\u0431\u043E\u0442\u0430\u0442\u044C \u043D\u0430 \u043F\u0440\u0435\u0436\u043D\u0435\u0439 \u0432\u0435\u0440\u0441\u0438\u0438, \u0438 \u044D\u0442\u043E \u043D\u0430\u0440\u043E\u0447\u043D\u043E: \u0441\u0435\u0441\u0441\u0438\u044F \u0443\u0436\u0435 \u043F\u043E\u0434\u043D\u044F\u0442\u0430 \u0441\u0442\u0430\u0440\u044B\u043C \u043A\u043E\u0434\u043E\u043C, \u043C\u0435\u043D\u044F\u0442\u044C \u0435\u0433\u043E \u043F\u043E\u0434 \u0441\u043E\u0431\u043E\u0439 \u043D\u0430 \u0445\u043E\u0434\u0443 \u043D\u0435\u043B\u044C\u0437\u044F.
15298
+ \u0412\u0441\u0442\u0430\u043D\u0435\u0442 \u0441\u0430\u043C\u043E, \u043A\u0430\u043A \u0442\u043E\u043B\u044C\u043A\u043E \u043D\u0435 \u0438\u0434\u0451\u0442 \u043D\u0438 \u043E\u0434\u043D\u043E\u0439 \u0437\u0430\u0434\u0430\u0447\u0438, \u2014 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0442\u044C \u041A\u043B\u043E\u0434\u0430 \u0438\u043B\u0438 Codex \u043D\u0435 \u043D\u0443\u0436\u043D\u043E.
15239
15299
  ` + (wiped.length > 0 ? `\u0423\u0431\u0440\u0430\u043B \u0441\u0442\u0430\u0440\u044B\u0435 \u0432\u0435\u0440\u0441\u0438\u0438: ${wiped.join(", ")}.
15240
15300
  ` : "") + tidyFailed + "\u041E\u0442\u043A\u0430\u0442\u0438\u0442\u044C\u0441\u044F \u043D\u0430\u0437\u0430\u0434: setup \u0441 rollback=shell."
15241
15301
  };
@@ -15296,7 +15356,7 @@ function rollbackShell(home) {
15296
15356
  return {
15297
15357
  ok: true,
15298
15358
  text: `\u041E\u0442\u043A\u0430\u0442\u0438\u043B \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0435: ${before} \u2192 ${back}.
15299
- \u041F\u043E\u0434\u0445\u0432\u0430\u0442\u0438\u0442\u0441\u044F \u043F\u043E\u0441\u043B\u0435 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430 \u041A\u043B\u043E\u0434\u0430 \u0438\u043B\u0438 Codex.`
15359
+ \u0412\u0441\u0442\u0430\u043D\u0435\u0442 \u0441\u0430\u043C\u043E, \u043A\u0430\u043A \u0442\u043E\u043B\u044C\u043A\u043E \u043D\u0435 \u0438\u0434\u0451\u0442 \u043D\u0438 \u043E\u0434\u043D\u043E\u0439 \u0437\u0430\u0434\u0430\u0447\u0438.`
15300
15360
  };
15301
15361
  });
15302
15362
  } catch (trouble) {
@@ -15419,7 +15479,7 @@ async function installPayload(engine, opts, hooks = {}) {
15419
15479
  text: `\u0414\u0432\u0438\u0436\u043A\u0430 \u043C\u043E\u043D\u0442\u0430\u0436\u0430 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435\u0442 \u2014 \u043D\u0430\u0447\u0438\u043D\u043A\u0443 \u043D\u0430\u0434\u043E \u0441\u043A\u0430\u0447\u0430\u0442\u044C (${mb} \u041C\u0411, \u0432\u0435\u0440\u0441\u0438\u044F ${manifest.version}).
15420
15480
  \u041B\u044F\u0436\u0435\u0442 \u043E\u0434\u043D\u043E\u0439 \u043F\u0430\u043F\u043A\u043E\u0439 \u0432 ${engine.versionsDir()}; \u0432 \u0441\u0438\u0441\u0442\u0435\u043C\u0443 \u043D\u0438\u0447\u0435\u0433\u043E \u043D\u0435 \u0441\u0442\u0430\u0432\u0438\u0442\u0441\u044F.
15421
15481
  ${VOICE_CLEANER_NOTE}
15422
- \u0421\u043F\u0440\u043E\u0441\u0438 \u0443 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0438 \u043F\u043E\u0437\u043E\u0432\u0438 setup \u0435\u0449\u0451 \u0440\u0430\u0437 \u0441 consent=true.`
15482
+ \u0421\u043F\u0440\u043E\u0441\u0438 \u0443 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0438 \u043F\u043E\u0437\u043E\u0432\u0438 install \u0441 what=all.`
15423
15483
  };
15424
15484
  }
15425
15485
  const dest = join(versionsDir, manifest.version);
@@ -15596,190 +15656,58 @@ function installLadder(step) {
15596
15656
  "\u0418\u0434\u0451\u0442 \u043C\u0438\u043D\u0443\u0442\u0430\u043C\u0438. \u041E\u0431\u043E\u0440\u0432\u0451\u0442\u0441\u044F \u2014 \u043F\u043E\u0432\u0442\u043E\u0440\u0438 \u0442\u043E\u0442 \u0436\u0435 \u0448\u0430\u0433: \u0443\u0436\u0435 \u0441\u043A\u0430\u0447\u0430\u043D\u043D\u043E\u0435 \u043D\u0435 \u043F\u0440\u043E\u043F\u0430\u0434\u0451\u0442."
15597
15657
  ];
15598
15658
  }
15599
- var CALLS_FILE = "calls.jsonl";
15600
- var CALLS_PREVIOUS = "calls.1.jsonl";
15601
- var CALLS_CAP_BYTES = 1e6;
15602
- function withCallLog(home, now = () => Date.now()) {
15603
- return (tool) => {
15604
- const original = tool.execute.bind(tool);
15605
- return {
15606
- ...tool,
15607
- async execute(input, ...rest) {
15608
- const started = now();
15609
- const result = await original(input, ...rest);
15610
- write(home, {
15611
- ts: (/* @__PURE__ */ new Date()).toISOString(),
15612
- tool: tool.name,
15613
- ok: result.type === "success",
15614
- ms: now() - started
15615
- });
15616
- return result;
15617
- }
15618
- };
15619
- };
15620
- }
15621
- function write(home, line) {
15622
- try {
15623
- mkdirSync(home, { recursive: true });
15624
- const file2 = join(home, CALLS_FILE);
15625
- rotate(file2, join(home, CALLS_PREVIOUS));
15626
- appendFileSync(file2, `${JSON.stringify(line)}
15627
- `, { encoding: "utf8", mode: 384 });
15628
- } catch {
15629
- }
15630
- }
15631
- function rotate(file2, previous) {
15632
- try {
15633
- if (statSync(file2).size >= CALLS_CAP_BYTES) renameSync(file2, previous);
15634
- } catch {
15635
- }
15636
- }
15637
15659
 
15638
- // src/contracts/crash-report.ts
15639
- var CRASH_REPORT_PATH = "/crash-report";
15640
- var CRASH_CONTRACT = "naparnik-crash/1";
15641
- var CRASH_LIMITS = {
15642
- bodyBytes: 65536,
15643
- errorType: 120,
15644
- message: 500,
15645
- trace: 8e3,
15646
- stage: 120,
15647
- osPlatform: 32,
15648
- osRelease: 64,
15649
- osArch: 16,
15650
- hostName: 64,
15651
- hostVersion: 32,
15652
- shellVersion: 32,
15653
- engineVersion: 64,
15654
- pythonVersion: 16,
15655
- ffmpegVersion: 64,
15656
- recentCalls: 10,
15657
- repeats: 1e5
15658
- };
15659
- var CRASH_TOOL_NAME = /^[a-z_]{1,64}$/;
15660
- var CRASH_FINGERPRINT = /^[0-9a-f]{16}$/;
15661
- var text = (max) => external_exports.string().max(max);
15662
- var CrashReportRequest = external_exports.object({
15663
- contract: external_exports.literal(CRASH_CONTRACT),
15664
- occurred_at: external_exports.string().datetime({ offset: true }),
15665
- where: external_exports.enum(["tool", "job", "process"]),
15666
- tool: external_exports.string().regex(CRASH_TOOL_NAME).nullable(),
15667
- stage: text(CRASH_LIMITS.stage).nullable(),
15668
- error_type: external_exports.string().min(1).max(CRASH_LIMITS.errorType),
15669
- message: text(CRASH_LIMITS.message),
15670
- trace: text(CRASH_LIMITS.trace),
15671
- fingerprint: external_exports.string().regex(CRASH_FINGERPRINT),
15672
- repeats: external_exports.number().int().min(0).max(CRASH_LIMITS.repeats),
15673
- os: external_exports.object({
15674
- platform: text(CRASH_LIMITS.osPlatform),
15675
- release: text(CRASH_LIMITS.osRelease),
15676
- arch: text(CRASH_LIMITS.osArch)
15677
- }).strict(),
15678
- host: external_exports.object({
15679
- name: text(CRASH_LIMITS.hostName),
15680
- version: text(CRASH_LIMITS.hostVersion)
15681
- }).strict().nullable(),
15682
- versions: external_exports.object({
15683
- shell: text(CRASH_LIMITS.shellVersion),
15684
- engine: text(CRASH_LIMITS.engineVersion).nullable(),
15685
- python: text(CRASH_LIMITS.pythonVersion).nullable(),
15686
- ffmpeg: text(CRASH_LIMITS.ffmpegVersion).nullable()
15687
- }).strict(),
15688
- recent_calls: external_exports.array(external_exports.string().regex(CRASH_TOOL_NAME)).max(CRASH_LIMITS.recentCalls)
15689
- }).strict();
15690
- var SECRET_MIN = 4;
15691
- var HUMAN_FILE = /[^\s"'«»“”/\\<>()]{0,255}\.(?:mp4|mov|m4v|mkv|avi|webm|mts|wav|mp3|m4a|aac|flac|ogg|jpg|jpeg|png|heic|gif|srt|vtt)\b/gi;
15692
- var PATH = /(?:<naparnik>|<shell>|~|[A-Za-z]:)?(?:[\\/][^\s"'«»“”<>|,;()[\]{}\\/]+)+[\\/]?/g;
15693
- var QUOTED = /(["'])([^"'\n]*)\1|«([^»\n]*)»|“([^”\n]*)”/g;
15694
- var KEY = /npk_[A-Za-z0-9_-]{6,}/g;
15695
- function scrub(text2, context) {
15696
- let out = text2.replace(KEY, "<key>");
15697
- for (const secret of secretForms(context.secrets)) {
15698
- out = replaceAll(out, secret, "<video>");
15699
- }
15700
- out = replaceAll(out, context.shellDir, "<shell>");
15701
- out = replaceAll(out, context.naparnikHome, "<naparnik>");
15702
- out = replaceAll(out, context.home, "~");
15703
- out = out.replace(/(?:\/Users|\/home|[A-Za-z]:\\Users|[A-Za-z]:\/Users)[\\/][^\s"'«»“”\\/]+/g, "~");
15704
- out = out.replace(QUOTED, (whole, _q, plain, fir, curly) => {
15705
- const inside2 = plain ?? fir ?? curly ?? "";
15706
- if (!looksHuman(inside2)) return whole;
15707
- const [open, close] = plain !== void 0 ? [_q, _q] : fir !== void 0 ? ["\xAB", "\xBB"] : ["\u201C", "\u201D"];
15708
- return `${open}${looksLikePath(inside2) ? pathOrMark(inside2) : "<text>"}${close}`;
15709
- });
15710
- out = out.replace(PATH, (found) => pathOrMark(found));
15711
- out = out.replace(HUMAN_FILE, "<file>");
15712
- out = swallowSpacedTails(out);
15713
- if (context.user.length >= 3) out = replaceAll(out, context.user, "<user>");
15714
- return out;
15715
- }
15716
- function swallowSpacedTails(text2) {
15717
- return text2.replace(/<(?:path|file)>[^\n:<>"'«»“”]{0,512}<(?:path|file)>/g, "<path>").replace(/(<(?:path|file)>)([^\n:<>"'«»“”]{0,512})/g, (whole, mark, tail2) => /[^\x00-\x7F]/.test(tail2) ? mark : whole);
15718
- }
15719
- function secretForms(secrets) {
15720
- const forms = /* @__PURE__ */ new Set();
15721
- for (const raw of secrets) {
15722
- const value = raw.trim();
15723
- if (value.length < SECRET_MIN) continue;
15724
- forms.add(value);
15725
- if (!looksLikePath(value)) continue;
15726
- if (isAbsolute(value)) forms.add(resolve(value));
15727
- const name = basename(value);
15728
- const stem = name.slice(0, name.length - extname(name).length);
15729
- for (const one of [name, stem, `${stem}.montage`]) {
15730
- if (one.length >= SECRET_MIN) forms.add(one);
15731
- }
15732
- }
15733
- return [...forms].sort((a, b) => b.length - a.length);
15734
- }
15735
- function pathOrMark(found) {
15736
- const flat = found.replace(/\\/g, "/");
15737
- const ours = /^(?:<naparnik>|~\/\.naparnik)\/(?:engine|shell|bin)(?:\/|$)/.test(flat) || flat.startsWith("<shell>") || /\/(?:site-packages|dist-packages|node_modules)\//.test(flat) || /\/lib\/python\d/i.test(flat) || /\/Python\d+\/Lib\//i.test(flat);
15738
- return ours ? found : "<path>";
15739
- }
15740
- var HUMAN_FILE_ONE = new RegExp(HUMAN_FILE.source, "i");
15741
- function looksLikePath(value) {
15742
- return /[\\/]/.test(value) || HUMAN_FILE_ONE.test(value);
15743
- }
15744
- function looksHuman(inside2) {
15745
- return /[^\x00-\x7F]/.test(inside2) || /\s/.test(inside2) || looksLikePath(inside2);
15660
+ // src/words.ts
15661
+ function human(text2) {
15662
+ return text2;
15746
15663
  }
15747
- function replaceAll(text2, what, by) {
15748
- if (what === "") return text2;
15749
- return text2.split(what).join(by);
15664
+ function forModel(text2) {
15665
+ return text2;
15750
15666
  }
15667
+ var NO_HUMAN_TEXT = human("");
15751
15668
 
15752
15669
  // src/job-report.ts
15753
15670
  var JOB_CONTRACT = "naparnik-job/1";
15754
- function empty() {
15671
+ function blankJob() {
15672
+ const none = NO_HUMAN_TEXT;
15755
15673
  return {
15756
15674
  contract: JOB_CONTRACT,
15757
15675
  id: "",
15758
- what: "",
15676
+ what: none,
15759
15677
  status: "running",
15760
15678
  sec: 0,
15761
15679
  at: 0,
15762
15680
  total: 0,
15763
- now: "",
15764
- left: "",
15765
- behind: "",
15766
- ahead: "",
15767
- message: "",
15681
+ now: none,
15682
+ left: none,
15683
+ behind: none,
15684
+ ahead: none,
15685
+ message: none,
15768
15686
  got: 0,
15769
15687
  of: 0,
15770
15688
  percent: 0,
15771
- eta: "",
15689
+ eta: none,
15772
15690
  file: "",
15773
- outcome: "",
15774
- say: "",
15775
- trouble: "",
15776
- advice: "",
15691
+ outcome: forModel(""),
15692
+ say: none,
15693
+ trouble: none,
15694
+ advice: none,
15777
15695
  complaints: [],
15778
15696
  plan: []
15779
15697
  };
15780
15698
  }
15699
+ function downloadOf(message) {
15700
+ const download = DOWNLOAD.exec(message);
15701
+ if (download === null) return null;
15702
+ return {
15703
+ got: Number(download[1]),
15704
+ of: Number(download[2] ?? 0),
15705
+ percent: Math.min(100, Number(download[3] ?? 0)),
15706
+ eta: human(download[4] ?? "")
15707
+ };
15708
+ }
15781
15709
  function startedJob(o) {
15782
- return { ...empty(), id: o.id, what: o.what, ahead: o.ahead, message: o.message };
15710
+ return { ...blankJob(), id: o.id, what: o.what, ahead: o.ahead, message: o.message };
15783
15711
  }
15784
15712
  var INSTALL_TITLES = {
15785
15713
  all: "\u0423\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u044E \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438 \u0434\u043B\u044F \u043C\u043E\u043D\u0442\u0430\u0436\u0430",
@@ -15793,7 +15721,7 @@ var INSTALL_TITLES = {
15793
15721
  voice: "\u0423\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u044E \u0447\u0438\u0441\u0442\u043A\u0443 \u0433\u043E\u043B\u043E\u0441\u0430"
15794
15722
  };
15795
15723
  function installTitle(step) {
15796
- return INSTALL_TITLES[step] ?? "\u0423\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u044E \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438 \u0434\u043B\u044F \u043C\u043E\u043D\u0442\u0430\u0436\u0430";
15724
+ return human(INSTALL_TITLES[step] ?? "\u0423\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u044E \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438 \u0434\u043B\u044F \u043C\u043E\u043D\u0442\u0430\u0436\u0430");
15797
15725
  }
15798
15726
  var INSTALL_HEAD = /^установка: (\S+)$/;
15799
15727
  var HEAD = /^Задание (\S+): ?(.*)$/;
@@ -15819,9 +15747,9 @@ function readJob(text2) {
15819
15747
  const lines = text2.split("\n").map((row) => row.trimEnd());
15820
15748
  const head2 = HEAD.exec(lines[0]?.trim() ?? "");
15821
15749
  if (head2 === null) return null;
15822
- const job = empty();
15750
+ const job = blankJob();
15823
15751
  job.id = head2[1] ?? "";
15824
- job.what = (head2[2] ?? "").trim();
15752
+ job.what = human((head2[2] ?? "").trim());
15825
15753
  const install2 = INSTALL_HEAD.exec(job.what);
15826
15754
  if (install2 !== null) job.what = installTitle(install2[1] ?? "");
15827
15755
  let tail2 = null;
@@ -15831,7 +15759,7 @@ function readJob(text2) {
15831
15759
  if (STYLE.test(line)) continue;
15832
15760
  const complaint = COMPLAINT.exec(line);
15833
15761
  if (complaint !== null) {
15834
- job.complaints.push(complaint[1] ?? "");
15762
+ job.complaints.push(human(complaint[1] ?? ""));
15835
15763
  tail2 = null;
15836
15764
  continue;
15837
15765
  }
@@ -15841,13 +15769,13 @@ function readJob(text2) {
15841
15769
  }
15842
15770
  const planRow = PLAN_ROW.exec(line);
15843
15771
  if (planRow !== null) {
15844
- job.plan.push(planRow[1] ?? "");
15772
+ job.plan.push(forModel(planRow[1] ?? ""));
15845
15773
  tail2 = null;
15846
15774
  continue;
15847
15775
  }
15848
15776
  const behind = BEHIND.exec(line);
15849
15777
  if (behind !== null) {
15850
- job.behind = behind[1] ?? "";
15778
+ job.behind = human(behind[1] ?? "");
15851
15779
  continue;
15852
15780
  }
15853
15781
  const now = NOW.exec(line);
@@ -15857,16 +15785,16 @@ function readJob(text2) {
15857
15785
  const title = now[3] ?? "";
15858
15786
  const left = LEFT_TAIL.exec(title);
15859
15787
  if (left !== null) {
15860
- job.now = title.slice(0, left.index).trim();
15861
- job.left = (left[1] ?? "").trim();
15788
+ job.now = human(title.slice(0, left.index).trim());
15789
+ job.left = human((left[1] ?? "").trim());
15862
15790
  } else {
15863
- job.now = title.trim();
15791
+ job.now = human(title.trim());
15864
15792
  }
15865
15793
  continue;
15866
15794
  }
15867
15795
  const ahead = AHEAD.exec(line);
15868
15796
  if (ahead !== null) {
15869
- job.ahead = ahead[1] ?? "";
15797
+ job.ahead = human(ahead[1] ?? "");
15870
15798
  continue;
15871
15799
  }
15872
15800
  const done = DONE.exec(line);
@@ -15886,31 +15814,31 @@ function readJob(text2) {
15886
15814
  if (failed !== null) {
15887
15815
  job.status = "error";
15888
15816
  job.sec = seconds(failed[1]);
15889
- job.trouble = withoutPaths(failed[2] ?? "");
15817
+ job.trouble = human(withoutPaths(failed[2] ?? ""));
15890
15818
  tail2 = null;
15891
15819
  continue;
15892
15820
  }
15893
15821
  if (ABORTED.test(line)) {
15894
15822
  job.status = "aborted";
15895
- job.trouble = line;
15823
+ job.trouble = human(line);
15896
15824
  job.sec = seconds(ABORTED_SEC.exec(line)?.[1]);
15897
15825
  tail2 = null;
15898
15826
  continue;
15899
15827
  }
15900
15828
  const advice = ADVICE.exec(line);
15901
15829
  if (advice !== null) {
15902
- job.advice = advice[1] ?? "";
15830
+ job.advice = human(advice[1] ?? "");
15903
15831
  tail2 = null;
15904
15832
  continue;
15905
15833
  }
15906
15834
  const say = SAY.exec(line);
15907
15835
  if (say !== null) {
15908
- job.say = say[1] ?? "";
15836
+ job.say = human(say[1] ?? "");
15909
15837
  continue;
15910
15838
  }
15911
15839
  if (WAITING.test(line)) {
15912
15840
  job.status = "waiting";
15913
- job.message = line;
15841
+ job.message = human(line);
15914
15842
  continue;
15915
15843
  }
15916
15844
  const running2 = RUNNING.exec(line);
@@ -15921,18 +15849,13 @@ function readJob(text2) {
15921
15849
  }
15922
15850
  if (tail2 !== null) {
15923
15851
  tail2.push(line);
15924
- job.outcome = tail2.join("\n");
15852
+ job.outcome = forModel(tail2.join("\n"));
15925
15853
  continue;
15926
15854
  }
15927
- if (job.status === "running" || job.status === "waiting") job.message = line;
15928
- }
15929
- const download = job.status === "running" ? DOWNLOAD.exec(job.message) : null;
15930
- if (download !== null) {
15931
- job.got = Number(download[1]);
15932
- job.of = Number(download[2] ?? 0);
15933
- job.percent = Math.min(100, Number(download[3] ?? 0));
15934
- job.eta = download[4] ?? "";
15855
+ if (job.status === "running" || job.status === "waiting") job.message = human(line);
15935
15856
  }
15857
+ const download = job.status === "running" ? downloadOf(job.message) : null;
15858
+ if (download !== null) Object.assign(job, download);
15936
15859
  return job;
15937
15860
  }
15938
15861
  function withoutPaths(text2) {
@@ -15944,781 +15867,643 @@ function seconds(text2) {
15944
15867
  return Number.isFinite(value) ? value : 0;
15945
15868
  }
15946
15869
 
15947
- // src/crash-report.ts
15948
- var RESEND_AFTER_MS = 30 * 6e4;
15949
- var QUEUE_CAP = 20;
15950
- var FLUSH_PER_PASS = 5;
15951
- var FLUSH_EVERY_MS = 6e4;
15952
- var SEND_TIMEOUT_MS = 3e3;
15953
- var JOB_LOG_TAIL = 64 * 1024;
15954
- var PYTHON_TRACEBACK = "Traceback (most recent call last):";
15955
- var SUPPORT_TOPIC = "https://t.me/c/3763431875/10";
15956
- var WORDS_GRACE_MS = 1e3;
15957
- var CrashReporter = class {
15958
- #deps;
15959
- #now;
15960
- #inflight = /* @__PURE__ */ new Set();
15961
- #lastFlush = Number.NEGATIVE_INFINITY;
15962
- #host = null;
15963
- /**
15964
- * Строки из аргументов недавних вызовов — только в памяти процесса.
15965
- *
15966
- * ⚠ ЗАЧЕМ. Задание упало — отчёт о нём приходит на `wait`, а в аргументах
15967
- * `wait` лишь номер задания; путь к ролику был в аргументах `render`,
15968
- * который задание запустил. Без этой памяти путь с пробелами из трейса ffmpeg
15969
- * вычищался бы только общими правилами, а они его режут на куски (ревью
15970
- * безопасности 2026-09-24). На диск не пишется: это ровно то, что мы
15971
- * обещали не хранить.
15972
- */
15973
- #recentSecrets = [];
15974
- #ffmpeg = null;
15975
- constructor(deps) {
15976
- this.#deps = deps;
15977
- this.#now = deps.now ?? (() => Date.now());
15978
- }
15979
- get canSend() {
15980
- return this.#deps.canSend;
15981
- }
15982
- get home() {
15983
- return this.#deps.home;
15984
- }
15985
- /** Запомнить программу-хост: при падении всего процесса контекста вызова нет. */
15986
- /** Запомнить строки аргументов вызова — чтобы вычеркнуть их из будущих отчётов. */
15987
- noteSecrets(secrets) {
15988
- const fresh = secrets.filter((one) => one.length >= 4);
15989
- if (fresh.length === 0) return;
15990
- this.#recentSecrets = [...this.#recentSecrets.filter((one) => !fresh.includes(one)), ...fresh].slice(-50);
15870
+ // src/tool-kit.ts
15871
+ function manifestUrl(engine, baseUrl) {
15872
+ return engine.variable("NAPARNIK_UPDATE_MANIFEST") ?? manifestUrlFor(baseUrl);
15873
+ }
15874
+ function aboutDefaults(map2) {
15875
+ if (map2 === void 0 || Object.keys(map2).length === 0) return "\u0441\u043C\u043E\u0442\u0440\u0438 setup";
15876
+ return Object.entries(map2).map(([engine, model]) => `${model} (${engine})`).join(", ");
15877
+ }
15878
+ function aboutModels(passport) {
15879
+ if (passport === null) return "\u0421\u043F\u0438\u0441\u043E\u043A \u043C\u043E\u0434\u0435\u043B\u0435\u0439 \u0441\u043C\u043E\u0442\u0440\u0438 \u0432 setup";
15880
+ return `\u041A\u0430\u043A\u0438\u0435 \u0435\u0441\u0442\u044C: ${passport["models"].join(", ")}; \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u2014 \u0441\u0432\u043E\u0451 \u043D\u0430 \u0434\u0432\u0438\u0436\u043E\u043A: ${aboutDefaults(passport["default_model"])}. \u041D\u0435 \u043D\u0430\u0437\u044B\u0432\u0430\u0439 \u043C\u043E\u0434\u0435\u043B\u044C \u0431\u0435\u0437 \u043D\u0443\u0436\u0434\u044B: \u043F\u0430\u043A \u0441\u0430\u043C \u0432\u043E\u0437\u044C\u043C\u0451\u0442 \u0442\u0443, \u0447\u0442\u043E \u0431\u044B\u0441\u0442\u0440\u0435\u0435 \u043D\u0430 \u044D\u0442\u043E\u043C \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440\u0435`;
15881
+ }
15882
+ function fromList(list, description) {
15883
+ return list !== void 0 && list.length > 0 ? external_exports.enum([...list]).describe(description) : external_exports.string().describe(description);
15884
+ }
15885
+ function asToolResult(outcome) {
15886
+ return outcome.ok ? toolSuccess(outcome.text) : toolError(outcome.text);
15887
+ }
15888
+ function asJobResult(outcome) {
15889
+ if (!outcome.ok) return toolError(outcome.text);
15890
+ const job = readJob(outcome.text);
15891
+ return job === null ? toolSuccess(outcome.text) : toolSuccess(outcome.text, job);
15892
+ }
15893
+ var CAPABILITIES_CONTRACT = "naparnik-capabilities/1";
15894
+ var HEAVY_WORK = {
15895
+ model: "\u0440\u0430\u0441\u043F\u043E\u0437\u043D\u0430\u0432\u0430\u043D\u0438\u0435 \u0440\u0435\u0447\u0438",
15896
+ cutout: "\u0432\u044B\u0440\u0435\u0437 \u0433\u0435\u0440\u043E\u044F",
15897
+ voice: "\u0447\u0438\u0441\u0442\u043A\u0430 \u0433\u043E\u043B\u043E\u0441\u0430"
15898
+ };
15899
+ var KNOB_KIND = {
15900
+ file: "\u0444\u0430\u0439\u043B",
15901
+ string: "\u0441\u0442\u0440\u043E\u043A\u0430",
15902
+ number: "\u0447\u0438\u0441\u043B\u043E",
15903
+ "yes-no": "\u0434\u0430/\u043D\u0435\u0442"
15904
+ };
15905
+ var FROM_MINE = "mine";
15906
+ function catalogInWords(data) {
15907
+ const lines = [];
15908
+ const version2 = data.engine?.version;
15909
+ lines.push(`\u0427\u0442\u043E \u0443\u043C\u0435\u0435\u0442 \u043C\u043E\u043D\u0442\u0430\u0436 \u043D\u0430 \u044D\u0442\u043E\u043C \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440\u0435${version2 !== void 0 ? `. \u0414\u0432\u0438\u0436\u043E\u043A ${version2}` : ""}.`);
15910
+ const steps = data.steps ?? [];
15911
+ const ready = steps.filter((s) => s.ready !== false);
15912
+ const later = steps.filter((s) => s.ready === false);
15913
+ if (ready.length > 0) {
15914
+ lines.push(`\u0423\u0436\u0435 \u0441\u0442\u043E\u0438\u0442: ${ready.map((s) => `${s.title} (${s.name})`).join(", ")}.`);
15991
15915
  }
15992
- noteHost(host) {
15993
- if (host !== null) this.#host = host;
15916
+ if (later.length > 0) {
15917
+ const said = later.map((s) => {
15918
+ const work = (s.needs ?? []).map((n) => HEAVY_WORK[n] ?? n).join(" \u0438 ");
15919
+ const step = (s.needs ?? [])[0];
15920
+ return `${s.title} (${s.name}) \u2014 ${work}${step !== void 0 ? ` (install what="${step}")` : ""}`;
15921
+ }).join("; ");
15922
+ lines.push(
15923
+ `\u0414\u043E\u043A\u0430\u0447\u0430\u0435\u0442\u0441\u044F \u043F\u0435\u0440\u0435\u0434 \u043F\u0435\u0440\u0432\u044B\u043C \u043C\u043E\u043D\u0442\u0430\u0436\u043E\u043C, \u0441\u043F\u0440\u043E\u0441\u0438 \u0441\u043E\u0433\u043B\u0430\u0441\u0438\u0435 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430: ${said}.`
15924
+ );
15994
15925
  }
15995
- /**
15996
- * Отправить отчёт. Ответ инструмента ждёт итог через {@link wordsFor}, а
15997
- * всё прочее — нет; для тестов и смоука — `settled()`.
15998
- */
15999
- submit(crash) {
16000
- const delivery = this.#deliver(crash).catch(() => "none");
16001
- this.#track(delivery.then(() => void 0));
16002
- return delivery;
15926
+ const recipes = data.recipes ?? [];
15927
+ if (recipes.length > 0) {
15928
+ lines.push(`\u0413\u043E\u0442\u043E\u0432\u044B\u0435 \u043D\u0430\u0431\u043E\u0440\u044B \u0448\u0430\u0433\u043E\u0432: ${recipes.map((r) => `${r.title} (${r.name})`).join(", ")}.`);
16003
15929
  }
16004
- /**
16005
- * Итог отправки для слов человеку — не дольше таймаута отправки с запасом.
16006
- * Не успела — отчёт ещё в пути и либо уйдёт, либо ляжет в очередь: «уйдёт»
16007
- * правда в обоих случаях.
16008
- */
16009
- async outcome(delivery) {
16010
- let timer;
16011
- const late = new Promise((done) => {
16012
- timer = setTimeout(() => done("queued"), (this.#deps.timeoutMs ?? SEND_TIMEOUT_MS) + WORDS_GRACE_MS);
16013
- timer.unref?.();
16014
- });
16015
- try {
16016
- return await Promise.race([delivery, late]);
16017
- } finally {
16018
- clearTimeout(timer);
16019
- }
15930
+ const inForce = data.recipe;
15931
+ if (inForce?.name !== void 0) {
15932
+ lines.push(
15933
+ inForce.origin === FROM_MINE ? `\u0420\u0435\u0446\u0435\u043F\u0442 \u0432 \u0434\u0435\u043B\u0435: \u0441\u0432\u043E\u0439 \xAB${inForce.name}\xBB \u2014 ${inForce.path ?? ""}.` : `\u0420\u0435\u0446\u0435\u043F\u0442 \u0432 \u0434\u0435\u043B\u0435: \u043D\u0430\u0448 \xAB${inForce.name}\xBB. \u0421\u0432\u043E\u0439 \u0437\u0430\u0432\u043E\u0434\u0438\u0442\u0441\u044F \u0447\u0435\u0440\u0435\u0437 plan_edit \u0441\u043E step.`
15934
+ );
16020
15935
  }
16021
- /**
16022
- * Процесс падает: отчёт кладётся на диск СИНХРОННО и уходит на следующем
16023
- * запуске. Сеть здесь не трогаем — процессу осталось жить миллисекунды, и
16024
- * недоставленный наполовину запрос хуже файла, который точно дойдёт.
16025
- */
16026
- persist(crash) {
16027
- try {
16028
- const report = this.#build(crash);
16029
- if (report !== null) this.#enqueue(report);
16030
- } catch {
15936
+ const questions = data.questions ?? [];
15937
+ if (questions.length > 0) {
15938
+ const sections = [...new Map(
15939
+ questions.filter((q) => q.section).map((q) => [q.section ?? "", q.group ?? ""])
15940
+ )].map(([id, tab]) => `${tab} (${id})`);
15941
+ lines.push(
15942
+ "\u0421\u043F\u0440\u043E\u0441\u0438\u0442\u044C \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0430\u043C\u0438 \u2014 choose_variant(questions=[\u2026]) \u0438\u043C\u0435\u043D\u0430\u043C\u0438 \u0432\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0438\u043B\u0438 \u0440\u0430\u0437\u0434\u0435\u043B\u043E\u0432, \u0438 \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u0440\u043E \u0442\u043E, \u0447\u0442\u043E \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u0432\u044B\u0431\u0440\u0430\u043B \u0438\u043B\u0438 \u043F\u043E\u043F\u0440\u043E\u0441\u0438\u043B." + (sections.length > 0 ? ` \u0420\u0430\u0437\u0434\u0435\u043B\u044B: ${sections.join(", ")}.` : "")
15943
+ );
15944
+ for (const q of questions) {
15945
+ const tab = q.group ? `[${q.group}] ` : "";
15946
+ if (q.kind === "range") {
15947
+ const dials = (q.sliders ?? []).map((s) => `${s.title} (${s.name})`).join(", ");
15948
+ lines.push(`\u2014 ${tab}${q.title} (${q.name}), \u043F\u043E\u043B\u0437\u0443\u043D\u043A\u0430\u043C\u0438: ${dials}.`);
15949
+ continue;
15950
+ }
15951
+ const plain = q.kind === "plain" ? ", \u0431\u0435\u0437 \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0438" : "";
15952
+ const named = q.variants ?? [];
15953
+ const variants = named.length > 0 ? named.map((v) => `${v.title} (${v.name})`).join(", ") : "\u0432\u0430\u0440\u0438\u0430\u043D\u0442\u044B \u0441\u0447\u0438\u0442\u0430\u044E\u0442\u0441\u044F \u043F\u043E \u043A\u0430\u0434\u0440\u0443 \u0440\u043E\u043B\u0438\u043A\u0430";
15954
+ const total = typeof q.total === "number" ? q.total : named.length;
15955
+ const rest = total > named.length ? ` \u0438 \u0435\u0449\u0451 ${total - named.length}` : "";
15956
+ lines.push(`\u2014 ${tab}${q.title} (${q.name})${plain}: ${variants}${rest}.`);
16031
15957
  }
16032
15958
  }
16033
- /** Досылка очереди — не чаще раза в минуту. */
16034
- flushSoon() {
16035
- const now = this.#now();
16036
- if (now - this.#lastFlush < FLUSH_EVERY_MS) return;
16037
- this.#lastFlush = now;
16038
- this.#track(this.#flush());
15959
+ const knobs = data.knobs ?? [];
15960
+ if (knobs.length > 0) {
15961
+ const said = knobs.map((k) => `${k.name} (${k.kind !== void 0 ? KNOB_KIND[k.kind] ?? k.kind : "\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435"})`).join(", ");
15962
+ lines.push(`\u0420\u0443\u0447\u043A\u0438 \u043C\u043E\u043D\u0442\u0430\u0436\u0430, \u0438\u0445 \u043F\u0435\u0440\u0435\u0434\u0430\u044E\u0442 \u0432 render: ${said}.`);
16039
15963
  }
16040
- /** Дождаться всего, что ушло мимо ответа. Боевому пути не нужно. */
16041
- async settled() {
16042
- while (this.#inflight.size > 0) await Promise.allSettled([...this.#inflight]);
15964
+ const index = data.handbook?.index;
15965
+ if (index !== void 0 && index.trim() !== "") {
15966
+ lines.push(`\u0421\u041F\u0420\u0410\u0412\u041E\u0427\u041D\u0418\u041A \u041C\u041E\u041D\u0422\u0410\u0416\u0401\u0420\u0410 \u2014 \u043E\u0433\u043B\u0430\u0432\u043B\u0435\u043D\u0438\u0435, \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u044B \u0447\u0438\u0442\u0430\u0439 plan \u0441 read:
15967
+ ${index.trim()}`);
16043
15968
  }
16044
- /** Задание уже разобрано: отчёт о нём ушёл раньше, в этом или прошлом запуске. */
16045
- jobSeen(id) {
16046
- return this.#state().jobs.includes(id);
15969
+ const style = data.style;
15970
+ if (style?.name !== void 0) {
15971
+ lines.push(
15972
+ style.own === true ? `\u0421\u0442\u0438\u043B\u044C: \u0441\u0432\u043E\u0439 \u043F\u0430\u043A \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \xAB${style.name}\xBB \u2014 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u0435 \u0437\u0430\u043F\u043E\u043C\u0438\u043D\u0430\u0435\u0442\u0441\u044F \u0432 \u043D\u0435\u0433\u043E.` : `\u0421\u0442\u0438\u043B\u044C: \u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043D\u044B\u0439 \xAB${style.name}\xBB, \u0441\u0432\u043E\u0435\u0433\u043E \u043F\u0430\u043A\u0430 \u0443 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u0435\u0449\u0451 \u043D\u0435\u0442.`
15973
+ );
16047
15974
  }
16048
- rememberJob(id) {
16049
- const state = this.#state();
16050
- state.jobs = [...state.jobs.filter((one) => one !== id), id].slice(-50);
16051
- this.#save(state);
15975
+ const mine = data.mine?.summary ?? [];
15976
+ if (mine.length > 0) {
15977
+ lines.push(`\u0421\u0432\u043E\u0451 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 (\u0437\u0430\u043F\u043E\u043C\u043D\u0435\u043D\u043E, \u0443\u0436\u0435 \u0432 \u043A\u0430\u0436\u0434\u043E\u043C \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u0435; \xAB\u0437\u0430\u0431\u0443\u0434\u044C\xBB \u2014 plan_edit forget):
15978
+ ${mine.map((line) => `\u2014 ${line}`).join("\n")}`);
16052
15979
  }
16053
- scrubContext(secrets) {
16054
- return {
16055
- home: this.#deps.osHome ?? homedir(),
16056
- user: this.#deps.osUser ?? safeUser(),
16057
- naparnikHome: this.#deps.home,
16058
- shellDir: this.#deps.shellDir ?? ownDir(),
16059
- secrets
16060
- };
15980
+ for (const said of data.warnings ?? []) lines.push(said);
15981
+ return lines.join("\n");
15982
+ }
15983
+ function otherContract(said, expected) {
15984
+ return `\u0414\u0432\u0438\u0436\u043E\u043A \u0433\u043E\u0432\u043E\u0440\u0438\u0442 \u043D\u0430 \u0434\u043E\u0433\u043E\u0432\u043E\u0440\u0435 \xAB${String(said)}\xBB, \u0430 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0435 \u043F\u043E\u043D\u0438\u043C\u0430\u0435\u0442 \xAB${expected}\xBB. \u041E\u0431\u043D\u043E\u0432\u0438 \u043C\u043E\u043D\u0442\u0430\u0436: install \u0441 what=all \u2014 \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442 \u043D\u043E\u0432\u044B\u0435 \u0432\u0435\u0440\u0441\u0438\u0438 \u0434\u0432\u0438\u0436\u043A\u0430 \u0438 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F.`;
15985
+ }
15986
+ async function askCatalog(engine, pack, video) {
15987
+ const answer = await engine.command([
15988
+ "capabilities",
15989
+ "--pack",
15990
+ pack,
15991
+ ...video !== void 0 ? ["--video", path(video)] : []
15992
+ ]);
15993
+ if (!answer.ok) return `\u0421\u043F\u0438\u0441\u043E\u043A \u0442\u043E\u0433\u043E, \u0447\u0442\u043E \u0443\u043C\u0435\u0435\u0442 \u043C\u043E\u043D\u0442\u0430\u0436, \u043D\u0435 \u043F\u0440\u0438\u0448\u0451\u043B: ${answer.text}`;
15994
+ let data;
15995
+ try {
15996
+ data = JSON.parse(answer.text);
15997
+ } catch {
15998
+ return `\u0421\u043F\u0438\u0441\u043E\u043A \u0442\u043E\u0433\u043E, \u0447\u0442\u043E \u0443\u043C\u0435\u0435\u0442 \u043C\u043E\u043D\u0442\u0430\u0436, \u043D\u0435 \u043F\u0440\u0438\u0448\u0451\u043B: ${NOT_JSON}`;
16061
15999
  }
16062
- #track(work) {
16063
- const guarded = work.catch(() => void 0);
16064
- this.#inflight.add(guarded);
16065
- void guarded.finally(() => this.#inflight.delete(guarded));
16000
+ if (data.contract !== CAPABILITIES_CONTRACT) {
16001
+ return `\u0414\u0432\u0438\u0436\u043E\u043A \u0433\u043E\u0432\u043E\u0440\u0438\u0442 \u043D\u0430 \u0434\u043E\u0433\u043E\u0432\u043E\u0440\u0435 \xAB${String(data.contract)}\xBB, \u0430 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0435 \u043F\u043E\u043D\u0438\u043C\u0430\u0435\u0442 \xAB${CAPABILITIES_CONTRACT}\xBB, \u0438 \u0441\u043F\u0438\u0441\u043E\u043A \u0442\u043E\u0433\u043E, \u0447\u0442\u043E \u0443\u043C\u0435\u0435\u0442 \u043C\u043E\u043D\u0442\u0430\u0436, \u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D. \u041E\u0431\u043D\u043E\u0432\u0438 \u043C\u043E\u043D\u0442\u0430\u0436: install \u0441 what=all \u2014 \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442 \u043D\u043E\u0432\u044B\u0435 \u0432\u0435\u0440\u0441\u0438\u0438 \u0434\u0432\u0438\u0436\u043A\u0430 \u0438 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F.`;
16066
16002
  }
16067
- async #deliver(crash) {
16068
- const report = this.#build(crash);
16069
- if (report === null) return this.#queuedAlready(fingerprintOf2(crash.errorType, crash.frames)) ? "queued" : "sent";
16070
- report.versions.ffmpeg = await this.#ffmpegVersion();
16071
- const verdict = await this.#send(report);
16072
- if (verdict === "sent") return "sent";
16073
- if (verdict === "drop") return "none";
16074
- return this.#enqueue(report) ? "queued" : "none";
16003
+ return catalogInWords(data);
16004
+ }
16005
+ function fromWords(text2, contract) {
16006
+ let data;
16007
+ try {
16008
+ data = JSON.parse(text2);
16009
+ } catch {
16010
+ return { refusal: NOT_JSON };
16075
16011
  }
16076
- #queuedAlready(fingerprint) {
16077
- try {
16078
- return readdirSync(this.#queueDir()).some((f) => f.endsWith(`-${fingerprint}.json`));
16079
- } catch {
16080
- return false;
16081
- }
16012
+ if (contract !== void 0 && data.contract !== contract) {
16013
+ return { refusal: otherContract(data.contract, contract) };
16082
16014
  }
16083
- async #flush() {
16084
- const dir = this.#queueDir();
16085
- let files;
16086
- try {
16087
- files = readdirSync(dir).filter((f) => f.endsWith(".json")).sort();
16088
- } catch {
16089
- return;
16090
- }
16091
- for (const file2 of files.slice(0, FLUSH_PER_PASS)) {
16092
- const path2 = join(dir, file2);
16093
- let report;
16094
- try {
16095
- report = JSON.parse(readFileSync(path2, "utf8"));
16096
- } catch {
16097
- rmSync(path2, { force: true });
16098
- continue;
16099
- }
16100
- const verdict = await this.#send(report);
16101
- if (verdict === "retry") return;
16102
- rmSync(path2, { force: true });
16103
- }
16015
+ if (typeof data.words !== "string" || data.words === "") return { refusal: NOT_JSON };
16016
+ return { words: data.words, data };
16017
+ }
16018
+ function folderAnswer(text2) {
16019
+ const read = fromWords(text2);
16020
+ return "refusal" in read ? toolError(read.refusal) : toolSuccess(read.words, read.data);
16021
+ }
16022
+ var DRAFT_CONTRACT = "naparnik-draft/1";
16023
+ var VIEW_CONTRACT = "naparnik-view/1";
16024
+ var EDIT_CONTRACT = "naparnik-edit/1";
16025
+ function answerOrRefusal(text2, contract) {
16026
+ const read = fromWords(text2, contract);
16027
+ if ("refusal" in read) {
16028
+ const plain = text2.trim();
16029
+ return { refusal: plain.startsWith("{") || plain === "" ? read.refusal : plain };
16104
16030
  }
16105
- /**
16106
- * Отправить и решить, что делать с отчётом дальше.
16107
- *
16108
- * ⚠ В ОЧЕРЕДЬ ИДЁТ ТОЛЬКО ТО, ЧТО ПОВТОР ИСПРАВИТ: нет сети, таймаут, 5xx,
16109
- * 429 и «ключа нет» (человек впишет его завтра). 400 и 413 — отчёт кривой,
16110
- * 401 и 403 — ключ не наш: повтор даст тот же отказ, а очередь с потолком
16111
- * вытесняла бы им живые отчёты.
16112
- */
16113
- async #send(report) {
16114
- if (!CrashReportRequest.safeParse(report).success) return "drop";
16115
- try {
16116
- await this.#deps.client.postMontage(CRASH_REPORT_PATH, report, {
16117
- once: true,
16118
- timeoutMs: this.#deps.timeoutMs ?? SEND_TIMEOUT_MS
16119
- });
16120
- return "sent";
16121
- } catch (trouble) {
16122
- if (trouble instanceof NaparnikAuthError || trouble instanceof NaparnikScopeError) return "drop";
16123
- if (trouble instanceof NaparnikClientError) return trouble.status === 429 ? "retry" : "drop";
16124
- return "retry";
16125
- }
16126
- }
16127
- /**
16128
- * Собрать отчёт: обезличить, обрезать, посчитать отпечаток и решить по
16129
- * частоте. `null` — этот отпечаток уже уходил меньше получаса назад, и
16130
- * повтор записан в счётчик.
16131
- */
16132
- #build(crash) {
16133
- const fingerprint = fingerprintOf2(crash.errorType, crash.frames);
16134
- const state = this.#state();
16135
- const now = this.#now();
16136
- const seen = state.fingerprints[fingerprint];
16137
- if (seen !== void 0 && now - seen.sentAt < RESEND_AFTER_MS) {
16138
- seen.pending += 1;
16139
- this.#save(state);
16140
- return null;
16141
- }
16142
- const repeats = Math.min(seen?.pending ?? 0, CRASH_LIMITS.repeats);
16143
- state.fingerprints[fingerprint] = { sentAt: now, pending: 0 };
16144
- prune(state, now);
16145
- this.#save(state);
16146
- const context = this.scrubContext([...crash.secrets, ...this.#recentSecrets]);
16147
- const clean = (text2) => scrub(text2, context);
16148
- return {
16149
- contract: CRASH_CONTRACT,
16150
- occurred_at: new Date(now).toISOString(),
16151
- where: crash.where,
16152
- tool: crash.tool !== null && CRASH_TOOL_NAME.test(crash.tool) ? crash.tool : null,
16153
- // ⚠ РЕЖЕМ ДО ОБЕЗЛИЧИВАНИЯ С ЗАПАСОМ И ЕЩЁ РАЗ ПОСЛЕ. Обезличивание идёт
16154
- // синхронно, до ответа инструмента, а stderr движка бывает мегабайтами:
16155
- // без первой резки длинный текст подвешивал бы ответ на секунды (ревью
16156
- // безопасности 2026-09-24). Запас — вдвое: метки короче того, что заменяют.
16157
- stage: crash.stage === null ? null : head(clean(head(crash.stage, 2 * CRASH_LIMITS.stage)), CRASH_LIMITS.stage),
16158
- error_type: head(clean(head(crash.errorType, 2 * CRASH_LIMITS.errorType)), CRASH_LIMITS.errorType) || "Error",
16159
- message: head(clean(head(crash.message, 2 * CRASH_LIMITS.message)), CRASH_LIMITS.message),
16160
- trace: crash.keep === "tail" ? tail(clean(crash.trace.slice(-2 * CRASH_LIMITS.trace)), CRASH_LIMITS.trace) : head(clean(head(crash.trace, 2 * CRASH_LIMITS.trace)), CRASH_LIMITS.trace),
16161
- fingerprint,
16162
- repeats,
16163
- os: {
16164
- platform: head(platform(), CRASH_LIMITS.osPlatform),
16165
- release: head(release(), CRASH_LIMITS.osRelease),
16166
- arch: head(arch(), CRASH_LIMITS.osArch)
16167
- },
16168
- host: hostOf(crash.host ?? this.#host),
16169
- versions: {
16170
- shell: head(VERSION, CRASH_LIMITS.shellVersion),
16171
- engine: orNull(this.#deps.engineVersion(), CRASH_LIMITS.engineVersion),
16172
- python: orNull(crash.python, CRASH_LIMITS.pythonVersion),
16173
- ffmpeg: null
16174
- },
16175
- recent_calls: recentCalls(this.#deps.home)
16176
- };
16177
- }
16178
- /** Положить отчёт в очередь. `false` — диск не принял, отчёт пропал. */
16179
- #enqueue(report) {
16180
- try {
16181
- const dir = this.#queueDir();
16182
- mkdirSync(dir, { recursive: true });
16183
- const name = `${String(this.#now()).padStart(15, "0")}-${report.fingerprint}.json`;
16184
- const temporary = join(dir, `${name}.tmp`);
16185
- writeFileSync(temporary, JSON.stringify(report), { mode: 384 });
16186
- renameSync(temporary, join(dir, name));
16187
- const files = readdirSync(dir).filter((f) => f.endsWith(".json")).sort();
16188
- for (const old of files.slice(0, Math.max(0, files.length - QUEUE_CAP))) {
16189
- rmSync(join(dir, old), { force: true });
16190
- }
16191
- return true;
16192
- } catch {
16193
- return false;
16194
- }
16195
- }
16196
- #queueDir() {
16197
- return join(this.#deps.home, "crash-reports", "queue");
16198
- }
16199
- #stateFile() {
16200
- return join(this.#deps.home, "crash-reports", "seen.json");
16201
- }
16202
- #state() {
16203
- try {
16204
- const parsed = JSON.parse(readFileSync(this.#stateFile(), "utf8"));
16205
- return {
16206
- fingerprints: typeof parsed.fingerprints === "object" && parsed.fingerprints !== null ? parsed.fingerprints : {},
16207
- jobs: Array.isArray(parsed.jobs) ? parsed.jobs.filter((j) => typeof j === "string") : []
16208
- };
16209
- } catch {
16210
- return { fingerprints: {}, jobs: [] };
16211
- }
16212
- }
16213
- #save(state) {
16214
- try {
16215
- mkdirSync(dirname(this.#stateFile()), { recursive: true });
16216
- writeFileSync(this.#stateFile(), JSON.stringify(state), { mode: 384 });
16217
- } catch {
16218
- }
16219
- }
16220
- #ffmpegVersion() {
16221
- this.#ffmpeg ??= (this.#deps.ffmpegVersion ?? (() => probeFfmpeg(this.#deps.home)))().catch(() => null);
16222
- return this.#ffmpeg.then((v) => orNull(v, CRASH_LIMITS.ffmpegVersion));
16223
- }
16224
- };
16225
- function prune(state, now) {
16226
- for (const [key, value] of Object.entries(state.fingerprints)) {
16227
- if (now - value.sentAt > 24 * 36e5 && value.pending === 0) delete state.fingerprints[key];
16228
- }
16229
- }
16230
- function withCrashReport(crashes) {
16231
- return (tool) => {
16232
- if (crashes === null) return tool;
16233
- const original = tool.execute.bind(tool);
16234
- return {
16235
- ...tool,
16236
- async execute(input, ...rest) {
16237
- crashes.flushSoon();
16238
- const host = hostFromContext(rest[0]);
16239
- crashes.noteHost(host);
16240
- crashes.noteSecrets(secretsOf(input));
16241
- let result;
16242
- let faults;
16243
- try {
16244
- ({ value: result, faults } = await collectInstallFaults(() => original(input, ...rest)));
16245
- } catch (trouble) {
16246
- const crash = fromThrown(trouble, "tool", tool.name, input, host);
16247
- return toolError(await brokenWords(crash, crashes, crashes.submit(crash)));
16248
- }
16249
- const install2 = firstInstallCrash(faults);
16250
- if (install2 !== null) {
16251
- const crash = { ...install2, where: "tool", tool: tool.name, secrets: secretsOf(input), host };
16252
- return withNotice(result, await brokenWords(crash, crashes, crashes.submit(crash)));
16253
- }
16254
- return caught(result, tool.name, input, host, crashes);
16255
- }
16256
- };
16257
- };
16031
+ return read.data["ok"] === false ? { refusal: read.words } : read;
16258
16032
  }
16259
- async function reportInstallFaults(crashes, work) {
16260
- const { value, faults } = await collectInstallFaults(work);
16261
- const install2 = firstInstallCrash(faults);
16262
- if (install2 !== null) {
16263
- void crashes.submit({ ...install2, stage: `${install2.stage ?? ""} (\u0444\u043E\u043D\u043E\u043C \u043D\u0430 \u0441\u0442\u0430\u0440\u0442\u0435)`, where: "tool", tool: null, secrets: [], host: null });
16264
- }
16265
- return value;
16033
+ async function draftDocument(engine, place, asked, key) {
16034
+ const answer = await engine.command([
16035
+ "document-draft",
16036
+ "--video",
16037
+ asked.video,
16038
+ "--pack",
16039
+ place.pack,
16040
+ ...asked.style !== void 0 ? ["--style", path(asked.style)] : [],
16041
+ ...(asked.also ?? []).flatMap((one) => ["--also", path(one)]),
16042
+ ...asked.ask !== void 0 ? ["--ask", JSON.stringify(asked.ask)] : [],
16043
+ "--json"
16044
+ ], key !== void 0 ? { key } : {});
16045
+ return answer.ok ? answerOrRefusal(answer.text, DRAFT_CONTRACT) : { refusal: answer.text };
16266
16046
  }
16267
- function firstInstallCrash(faults) {
16268
- for (const fault of faults) {
16269
- const parsed = installCrash(fault);
16270
- if (parsed !== null) return parsed;
16271
- }
16272
- return null;
16047
+ async function editOperations(engine, place, asked, key) {
16048
+ const answer = await engine.command([
16049
+ "document-edit",
16050
+ "--project",
16051
+ path(asked.reel),
16052
+ "--ops",
16053
+ JSON.stringify(asked.ops),
16054
+ ...asked.stamps !== void 0 ? ["--stamps", JSON.stringify(asked.stamps)] : [],
16055
+ "--by",
16056
+ asked.by ?? "model",
16057
+ "--pack",
16058
+ place.pack,
16059
+ "--json"
16060
+ ], key !== void 0 ? { key } : {});
16061
+ return answer.ok ? answerOrRefusal(answer.text, EDIT_CONTRACT) : { refusal: answer.text };
16273
16062
  }
16274
- function withNotice(result, notice) {
16275
- if (result.type === "error") return { ...result, error: `${result.error}
16276
-
16277
- ${notice}` };
16278
- return typeof result.content === "string" ? { ...result, content: `${result.content}
16279
-
16280
- ${notice}` } : { ...result, content: [...result.content, { type: "text", text: notice }] };
16063
+ function nameOrRefusal(candidate) {
16064
+ const looksLikePath2 = candidate.includes("/") || candidate.includes("\\") || candidate.startsWith("~") || candidate.startsWith(".") || candidate.length > 1 && candidate[1] === ":";
16065
+ if (!looksLikePath2) return { name: candidate };
16066
+ return {
16067
+ refusal: `\u0417\u0434\u0435\u0441\u044C \u0436\u0434\u0443\u0442 \u0418\u041C\u042F \u0448\u0430\u0433\u0430, \u0430 \u043F\u0440\u0438\u0448\u0451\u043B \u043F\u0443\u0442\u044C: ${candidate}
16068
+ \u0427\u0442\u043E \u0434\u0435\u043B\u0430\u0442\u044C: \u043D\u0430\u0437\u043E\u0432\u0438 \u0448\u0430\u0433 \u043E\u0434\u043D\u0438\u043C \u0441\u043B\u043E\u0432\u043E\u043C \u2014 \u0442\u0430\u043A, \u043A\u0430\u043A \u043D\u0430\u0437\u044B\u0432\u0430\u0435\u0442\u0441\u044F \u0435\u0433\u043E \u043F\u0430\u043F\u043A\u0430. \u0421\u0432\u043E\u0439 \u0448\u0430\u0433 \u043B\u0435\u0436\u0438\u0442 \u0432 \`mine/steps/<\u0438\u043C\u044F>\` \u0432 \u0434\u043E\u043C\u0435 \u041D\u0430\u043F\u0430\u0440\u043D\u0438\u043A\u0430, \u043D\u0430\u0448\u0438 \u043F\u0435\u0440\u0435\u0447\u0438\u0441\u043B\u044F\u0435\u0442 setup, \u0438\u043C\u044F \u0443 \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0441\u0442\u043E\u0438\u0442 \u0432 \u0441\u043A\u043E\u0431\u043A\u0430\u0445.`
16069
+ };
16281
16070
  }
16282
- async function caught(result, name, input, host, crashes) {
16283
- if (result.type === "error") {
16284
- const parsed2 = pythonCrash(result.error);
16285
- if (parsed2 === null) return result;
16286
- const crash2 = { ...parsed2, where: "tool", tool: name, stage: null, secrets: secretsOf(input), host };
16287
- return toolError(await brokenWords(crash2, crashes, crashes.submit(crash2)));
16071
+ var SETUP_CONTRACT = "naparnik-setup/1";
16072
+ var READINESS_CONTRACT = "naparnik-readiness/1";
16073
+ var JSON_CHECK = "--json";
16074
+ function knowsJsonCheck(engineDir) {
16075
+ try {
16076
+ const contract = JSON.parse(readFileSync(join(engineDir, "shell-contract.json"), "utf8"));
16077
+ const flags = contract.env_check_flags;
16078
+ return Array.isArray(flags) && flags.includes(JSON_CHECK);
16079
+ } catch {
16080
+ return false;
16288
16081
  }
16289
- const job = result.structured;
16290
- if (job === void 0 || job["contract"] !== JOB_CONTRACT || job["status"] !== "error") return result;
16291
- const id = typeof job["id"] === "string" ? job["id"] : "";
16292
- if (!/^[\w.-]{1,80}$/.test(id) || crashes.jobSeen(id)) return result;
16293
- const fromLog = pythonCrash(jobLogTail(crashes.home, id));
16294
- const install2 = fromLog === null ? firstInstallCrash(jobInstallFaults(id)) : null;
16295
- const parsed = fromLog ?? install2;
16296
- if (parsed === null) return result;
16297
- crashes.rememberJob(id);
16298
- const stage = install2?.stage ?? (typeof job["now"] === "string" && job["now"] !== "" ? job["now"] : null);
16299
- const crash = { ...parsed, where: "job", tool: name, stage, secrets: secretsOf(input), host };
16300
- return withNotice(result, await brokenWords(crash, crashes, crashes.submit(crash)));
16301
16082
  }
16302
- async function brokenWords(crash, crashes, delivery) {
16303
- const essence = head(scrub(head(crash.essence, 600), crashes.scrubContext(crash.secrets)), 300);
16304
- const fate = crashes.canSend ? await crashes.outcome(delivery) : "none";
16305
- return [
16306
- `\u0421\u043A\u0430\u0436\u0438 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443 \u0442\u0430\u043A, \u0441\u0432\u043E\u0438\u043C\u0438 \u0441\u043B\u043E\u0432\u0430\u043C\u0438 \u043D\u0435 \u043C\u0435\u043D\u044F\u0439: \xAB${humanWords(fate)}\xBB`,
16307
- `\u0421\u0443\u0442\u044C \u0434\u043B\u044F \u0442\u0435\u0431\u044F, \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443 \u043D\u0435 \u0437\u0430\u0447\u0438\u0442\u044B\u0432\u0430\u0439: ${essence}`
16308
- ].join("\n");
16083
+ var STATES = /* @__PURE__ */ new Set(["ok", "missing", "later"]);
16084
+ function megabytes(value) {
16085
+ return typeof value === "number" && value > 0 ? Math.round(value) : 0;
16309
16086
  }
16310
- function humanWords(fate) {
16311
- if (fate === "sent") {
16312
- return `\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043E\u0448\u0438\u0431\u043A\u0430. \u041E\u0442\u0447\u0451\u0442 \u0443\u0436\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D \u0440\u0430\u0437\u0440\u0430\u0431\u043E\u0442\u0447\u0438\u043A\u0430\u043C \u2014 \u043C\u044B \u0435\u0451 \u0438\u0441\u043F\u0440\u0430\u0432\u0438\u043C. \u0415\u0441\u043B\u0438 \u043D\u0443\u0436\u043D\u043E \u0441\u0440\u043E\u0447\u043D\u043E \u2014 \u043D\u0430\u043F\u0438\u0448\u0438 \u043D\u0430\u043C: ${SUPPORT_TOPIC}`;
16087
+ function readReadiness(stdout) {
16088
+ const lines = stdout.trim().split("\n");
16089
+ let raw;
16090
+ try {
16091
+ raw = JSON.parse(lines[lines.length - 1] ?? "");
16092
+ } catch {
16093
+ return null;
16313
16094
  }
16314
- if (fate === "queued") {
16315
- return `\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043E\u0448\u0438\u0431\u043A\u0430. \u041E\u0442\u0447\u0451\u0442 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D \u0438 \u0443\u0439\u0434\u0451\u0442 \u0440\u0430\u0437\u0440\u0430\u0431\u043E\u0442\u0447\u0438\u043A\u0430\u043C, \u043A\u0430\u043A \u043F\u043E\u044F\u0432\u0438\u0442\u0441\u044F \u0441\u0432\u044F\u0437\u044C, \u2014 \u043C\u044B \u0435\u0451 \u0438\u0441\u043F\u0440\u0430\u0432\u0438\u043C. \u0415\u0441\u043B\u0438 \u043D\u0443\u0436\u043D\u043E \u0441\u0440\u043E\u0447\u043D\u043E \u2014 \u043D\u0430\u043F\u0438\u0448\u0438 \u043D\u0430\u043C: ${SUPPORT_TOPIC}`;
16095
+ const data = raw;
16096
+ if (data === null || typeof data !== "object" || data["contract"] !== READINESS_CONTRACT) return null;
16097
+ if (typeof data["words"] !== "string" || !Array.isArray(data["parts"])) return null;
16098
+ const parts = [];
16099
+ for (const item of data["parts"]) {
16100
+ const part = item;
16101
+ if (part === null || typeof part !== "object" || typeof part["name"] !== "string") continue;
16102
+ parts.push({
16103
+ name: part["name"],
16104
+ title: typeof part["title"] === "string" && part["title"] !== "" ? part["title"] : part["name"],
16105
+ ok: part["ok"] === true,
16106
+ mb: megabytes(part["mb"]),
16107
+ on_demand: part["on_demand"] === true,
16108
+ step: typeof part["step"] === "string" ? part["step"] : ""
16109
+ });
16316
16110
  }
16317
- return `\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043E\u0448\u0438\u0431\u043A\u0430. \u041D\u0430\u043F\u0438\u0448\u0438 \u043D\u0430\u043C, \u043C\u044B \u0440\u0430\u0437\u0431\u0435\u0440\u0451\u043C\u0441\u044F: ${SUPPORT_TOPIC}`;
16318
- }
16319
- function watchProcessCrashes(crashes, proc = process, exit = (code) => process.exit(code)) {
16320
- const fall = (trouble) => {
16321
- crashes.persist(fromThrown(trouble, "process", null, {}, null));
16322
- try {
16323
- process.stderr.write(`naparnik-mcp: \u0443\u043F\u0430\u043B \u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u2014 ${trouble instanceof Error ? trouble.stack ?? trouble.message : String(trouble)}
16324
- `);
16325
- } catch {
16111
+ let rows = null;
16112
+ if (Array.isArray(data["rows"])) {
16113
+ rows = [];
16114
+ for (const item of data["rows"]) {
16115
+ const row = item;
16116
+ if (row === null || typeof row !== "object" || typeof row["title"] !== "string") continue;
16117
+ const state = row["state"];
16118
+ if (typeof state !== "string" || !STATES.has(state)) continue;
16119
+ rows.push({ title: row["title"], state, mb: megabytes(row["mb"]) });
16326
16120
  }
16327
- exit(1);
16328
- };
16329
- proc.on("uncaughtException", fall);
16330
- proc.on("unhandledRejection", fall);
16331
- crashes.flushSoon();
16332
- }
16333
- function fromThrown(trouble, where, tool, input, host) {
16334
- const error40 = trouble instanceof Error ? trouble : new Error(String(trouble));
16335
- const errorType = error40.name || error40.constructor.name || "Error";
16336
- const trace = error40.stack ?? `${errorType}: ${error40.message}`;
16337
- const frames = jsFrames(trace);
16338
- return {
16339
- where,
16340
- tool,
16341
- stage: null,
16342
- errorType,
16343
- message: error40.message,
16344
- trace,
16345
- keep: "head",
16346
- frames: frames.slice(0, 3).map((f) => `${f.file}:${f.func}`),
16347
- python: null,
16348
- secrets: secretsOf(input),
16349
- host,
16350
- essence: `${errorType}: ${error40.message}${frames[0] ? ` \u2014 ${frames[0].file}` : ""}`
16351
- };
16352
- }
16353
- function installCrash(fault) {
16354
- const { stage, trouble } = fault;
16355
- if (machineCause(trouble)) return null;
16356
- const said = commandOutput(trouble);
16357
- if (said.includes(PYTHON_TRACEBACK)) {
16358
- const parsed = pythonCrash(said);
16359
- return parsed === null ? null : { ...parsed, stage };
16360
16121
  }
16361
- const error40 = trouble instanceof Error ? trouble : new Error(String(trouble));
16362
- const reason = lastMeaningfulLine(said) ?? lastMeaningfulLine(error40.message) ?? error40.name;
16363
- const command = typeof trouble.cmd === "string" ? trouble.cmd : "";
16364
- const trace = [
16365
- command === "" ? "" : `\u043A\u043E\u043C\u0430\u043D\u0434\u0430: ${command}`,
16366
- said.trim() === "" ? "" : `\u0432\u044B\u0432\u043E\u0434 \u043A\u043E\u043C\u0430\u043D\u0434\u044B:
16367
- ${said.trim().slice(-4e3)}`,
16368
- error40.stack ?? `${error40.name}: ${error40.message}`
16369
- ].filter((part) => part !== "").join("\n\n");
16370
- return {
16371
- stage,
16372
- errorType: "InstallError",
16373
- message: `${stage}: ${reason}`,
16374
- trace,
16375
- keep: "head",
16376
- // ⚠ ПРИЧИНА В ОТПЕЧАТКЕ — БЕЗ ПУТЕЙ И ЧИСЕЛ. Иначе все беды шага склеились
16377
- // бы в один отпечаток, а сервер пишет в бот про отпечаток раз в час: вторая,
16378
- // другая беда того же шага промолчала бы. А с путями и версиями одна и та
16379
- // же беда у двух покупателей считалась бы двумя.
16380
- frames: [`install:${stage}`, reason.replace(/(?:[A-Za-z]:)?[\\/][^\s'"]*/g, "<path>").replace(/\d+/g, "0").slice(0, 160)],
16381
- python: /python@?(\d)\.(\d{1,2})\b/i.exec(command)?.slice(1, 3).join(".") ?? null,
16382
- essence: `\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430, \u0448\u0430\u0433 ${stage}: ${reason}`
16383
- };
16384
- }
16385
- function machineCause(trouble) {
16386
- const t = trouble ?? {};
16387
- if (typeof t.code === "string" && MACHINE_CODES.has(t.code)) return true;
16388
- if (t.code === 2) return true;
16389
- if (t.killed === true || typeof t.signal === "string" && t.signal !== "") return true;
16390
- const text2 = `${typeof t.message === "string" ? t.message : String(trouble)}
16391
- ${commandOutput(trouble)}`;
16392
- return MACHINE_WORDS.test(text2);
16393
- }
16394
- var MACHINE_CODES = /* @__PURE__ */ new Set([
16395
- "ENOSPC",
16396
- "EDQUOT",
16397
- "EACCES",
16398
- "EPERM",
16399
- "EROFS",
16400
- "EBUSY",
16401
- "ENOTFOUND",
16402
- "EAI_AGAIN",
16403
- "ECONNRESET",
16404
- "ECONNREFUSED",
16405
- "ETIMEDOUT",
16406
- "ENETUNREACH",
16407
- "ENETDOWN",
16408
- "EHOSTUNREACH"
16409
- ]);
16410
- var MACHINE_WORDS = new RegExp([
16411
- "No space left on device",
16412
- "Disk quota exceeded",
16413
- "Permission denied",
16414
- "Operation not permitted",
16415
- "Read-only file system",
16416
- "Access is denied",
16417
- "Temporary failure in name resolution",
16418
- "Name or service not known",
16419
- "nodename nor servname",
16420
- "getaddrinfo",
16421
- "Network is unreachable",
16422
- "No route to host",
16423
- "Connection refused",
16424
- "Connection reset",
16425
- "Connection aborted",
16426
- "timed out",
16427
- "Max retries exceeded",
16428
- "NewConnectionError",
16429
- "ConnectTimeoutError",
16430
- "ReadTimeoutError",
16431
- "ProxyError",
16432
- "CERTIFICATE_VERIFY_FAILED"
16433
- ].map((one) => one.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "i");
16434
- function commandOutput(trouble) {
16435
- const t = trouble ?? {};
16436
- return [t.stderr, t.stdout].filter((one) => typeof one === "string" && one.trim() !== "").join("\n");
16437
- }
16438
- function lastMeaningfulLine(text2) {
16439
- const lines = text2.split("\n").map((one) => one.trim()).filter((one) => one !== "" && !/^Command failed:/.test(one));
16440
- return lines[lines.length - 1] ?? null;
16122
+ return { words: data["words"], ready: data["ready"] === true, parts, rows };
16441
16123
  }
16442
- function jsFrames(stack) {
16443
- const frames = [];
16444
- for (const line of stack.split("\n")) {
16445
- const m = /^\s*at (?:(.+?) \()?(.+?):\d+:\d+\)?\s*$/.exec(line);
16446
- if (m === null) continue;
16447
- const file2 = basename(m[2] ?? "").replace(/-[A-Za-z0-9]{6,}(\.[cm]?js)$/, "$1");
16448
- frames.push({ file: file2, func: m[1] ?? "<anonymous>" });
16449
- }
16450
- return frames;
16124
+ function refusalWords(status) {
16125
+ if (status === 401) return `\u041A\u043B\u044E\u0447 \u043D\u0435 \u043F\u043E\u0434\u0445\u043E\u0434\u0438\u0442. \u0412\u043E\u0437\u044C\u043C\u0438 \u043D\u043E\u0432\u044B\u0439 \u0444\u0430\u0439\u043B \u0432 \u0442\u0435\u043B\u0435\u0433\u0440\u0430\u043C-\u0431\u043E\u0442\u0435 ${KEY_SOURCE_HANDLE}.`;
16126
+ if (status === 403) return `\u041F\u043E\u0434\u043F\u0438\u0441\u043A\u0430 \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u043B\u0430\u0441\u044C. \u041F\u0440\u043E\u0434\u043B\u0438 \u0435\u0451 \u0432 \u0442\u0435\u043B\u0435\u0433\u0440\u0430\u043C-\u0431\u043E\u0442\u0435 ${KEY_SOURCE_HANDLE}.`;
16127
+ return "";
16451
16128
  }
16452
- function pythonCrash(text2) {
16453
- const start = text2.lastIndexOf(PYTHON_TRACEBACK);
16454
- if (start < 0) return null;
16455
- const block = text2.slice(start).replace(/\s+$/, "");
16456
- const lines = block.split("\n");
16457
- const frames = [];
16458
- let lastFrame = -1;
16459
- lines.forEach((line, at2) => {
16460
- const m2 = /^\s*File "(.+)", line (\d+), in (.+)$/.exec(line);
16461
- if (m2 === null) return;
16462
- frames.push({ file: m2[1] ?? "", line: m2[2] ?? "", func: (m2[3] ?? "").trim() });
16463
- lastFrame = at2;
16464
- });
16465
- if (frames.length === 0) return null;
16466
- if (frames.some((f) => /[\\/]mine[\\/]steps[\\/]/.test(f.file))) return null;
16467
- const at = lines.findIndex((line, n) => n > lastFrame && line !== "" && !/^\s/.test(line));
16468
- if (at < 0) return null;
16469
- const m = /^([A-Za-z_][\w.]*)(?::\s?(.*))?$/.exec(lines[at] ?? "");
16470
- if (m === null) return null;
16471
- const errorType = m[1] ?? "Error";
16472
- if (/(?:^|\.)(?:KeyboardInterrupt|SystemExit|Explained|Refusal)$/.test(errorType)) return null;
16473
- const message = [m[2] ?? "", ...lines.slice(at + 1)].join("\n").trim();
16474
- const innermost = frames[frames.length - 1];
16475
- const firstLine2 = message.split("\n")[0] ?? "";
16476
- return {
16477
- errorType,
16478
- message,
16479
- trace: block,
16480
- keep: "tail",
16481
- frames: frames.slice(-3).map((f) => `${basename(f.file.replace(/\\/g, "/"))}:${f.func}`),
16482
- python: pythonVersionIn(frames.map((f) => f.file)),
16483
- essence: `${errorType}${firstLine2 === "" ? "" : `: ${firstLine2}`}` + (innermost ? ` \u2014 ${basename(innermost.file.replace(/\\/g, "/"))}:${innermost.line}` : "")
16484
- };
16129
+
16130
+ // src/setup-job.ts
16131
+ var SETUP_JOB_MARK = "setup-";
16132
+ function isSetupJob(id) {
16133
+ return id.startsWith(SETUP_JOB_MARK);
16485
16134
  }
16486
- function pythonVersionIn(files) {
16487
- for (const file2 of files) {
16488
- const m = /python(\d)\.?(\d{1,2})[\\/]/i.exec(file2);
16489
- if (m !== null) return `${m[1]}.${m[2]}`;
16135
+ var TITLE_UPDATE = human("\u041E\u0431\u043D\u043E\u0432\u043B\u044F\u044E \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438 \u0434\u043B\u044F \u043C\u043E\u043D\u0442\u0430\u0436\u0430");
16136
+ var TITLE_INSTALL = human("\u0423\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u044E \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438 \u0434\u043B\u044F \u043C\u043E\u043D\u0442\u0430\u0436\u0430");
16137
+ var SAY_UPDATED = human("\u0413\u043E\u0442\u043E\u0432\u043E \u2014 \u043D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u0443\u0436\u0435 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442.");
16138
+ var SAY_NOTHING = human("\u0412\u0441\u0451 \u0443\u0436\u0435 \u0441\u0442\u043E\u0438\u0442 \u2014 \u043C\u043E\u0436\u043D\u043E \u043C\u043E\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C.");
16139
+ var SAY_INSTALLED = human("\u0413\u043E\u0442\u043E\u0432\u043E \u2014 \u043C\u043E\u0436\u043D\u043E \u043C\u043E\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C.");
16140
+ var ADVICE_ASK = human("\u041D\u0430\u043F\u0438\u0448\u0438 \u041A\u043B\u043E\u0434\u0443 \xAB\u0447\u0442\u043E \u0441\u043B\u0443\u0447\u0438\u043B\u043E\u0441\u044C\xBB \u2014 \u043E\u043D \u0440\u0430\u0441\u0441\u043A\u0430\u0436\u0435\u0442, \u0447\u0442\u043E \u043F\u043E\u0448\u043B\u043E \u043D\u0435 \u0442\u0430\u043A.");
16141
+ var CHECK = human("\u041F\u0440\u043E\u0432\u0435\u0440\u044F\u044E, \u0447\u0442\u043E \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C");
16142
+ var INSTALL_NEW = human("\u0421\u0442\u0430\u0432\u043B\u044E \u043D\u043E\u0432\u0443\u044E \u0432\u0435\u0440\u0441\u0438\u044E");
16143
+ var MISSING = human("\u0421\u0442\u0430\u0432\u043B\u044E \u043D\u0435\u0434\u043E\u0441\u0442\u0430\u044E\u0449\u0435\u0435");
16144
+ var works = /* @__PURE__ */ new Map();
16145
+ var JOBS_DIR = "jobs";
16146
+ var JOBS_KEPT2 = 10;
16147
+ var BEAT_MS = 1e4;
16148
+ var STALE_MS = 6e4;
16149
+ var JOB_ID = /^setup-[\w-]{1,80}$/;
16150
+ function running(home, clock) {
16151
+ for (const work of works.values()) if (work.ended === null) return work.id;
16152
+ if (home === null) return null;
16153
+ for (const record2 of records(home)) {
16154
+ if (record2.ended === null && alive(record2, clock)) return record2.id;
16490
16155
  }
16491
16156
  return null;
16492
16157
  }
16493
- function jobLogTail(home, id) {
16494
- const file2 = join(home, "logs", `${id}.log`);
16495
- let fd = null;
16158
+ function records(home) {
16159
+ let names;
16496
16160
  try {
16497
- fd = openSync(file2, "r");
16498
- const size = fstatSync(fd).size;
16499
- const length = Math.min(size, JOB_LOG_TAIL);
16500
- const buffer = Buffer.alloc(length);
16501
- readSync(fd, buffer, 0, length, size - length);
16502
- return buffer.toString("utf8");
16161
+ names = readdirSync(join(home, JOBS_DIR)).filter((name) => name.endsWith(".json"));
16503
16162
  } catch {
16504
- return "";
16505
- } finally {
16506
- if (fd !== null) try {
16507
- closeSync(fd);
16508
- } catch {
16509
- }
16510
- }
16511
- }
16512
- function fingerprintOf2(errorType, frames) {
16513
- return createHash("sha256").update([errorType, ...frames].join("|")).digest("hex").slice(0, 16);
16514
- }
16515
- function secretsOf(input, depth = 0, out = []) {
16516
- if (out.length >= 50 || depth > 4) return out;
16517
- if (typeof input === "string") out.push(input);
16518
- else if (Array.isArray(input)) for (const one of input) secretsOf(one, depth + 1, out);
16519
- else if (typeof input === "object" && input !== null) {
16520
- for (const one of Object.values(input)) secretsOf(one, depth + 1, out);
16163
+ return [];
16521
16164
  }
16522
- return out;
16165
+ return names.sort().reverse().map((name) => load(home, name.slice(0, -".json".length))).filter((record2) => record2 !== null);
16523
16166
  }
16524
- function recentCalls(home) {
16167
+ function load(home, id) {
16168
+ if (!JOB_ID.test(id)) return null;
16525
16169
  try {
16526
- const text2 = readFileSync(join(home, CALLS_FILE), "utf8").slice(-8192);
16527
- const names = [];
16528
- for (const line of text2.split("\n")) {
16529
- try {
16530
- const tool = JSON.parse(line).tool;
16531
- if (typeof tool === "string" && CRASH_TOOL_NAME.test(tool)) names.push(tool);
16532
- } catch {
16533
- }
16534
- }
16535
- return names.slice(-CRASH_LIMITS.recentCalls);
16170
+ const value = JSON.parse(readFileSync(join(home, JOBS_DIR, `${id}.json`), "utf8"));
16171
+ if (value.id !== id || typeof value.what !== "string" || typeof value.started !== "number" || !Array.isArray(value.stages) || typeof value.at !== "number" || typeof value.pid !== "number" || typeof value.beat !== "number") return null;
16172
+ return {
16173
+ // Запись пишет только `save` этого файла, и слова в ней те же, что в
16174
+ // памяти ведущего процесса, — человеку.
16175
+ id,
16176
+ what: human(value.what),
16177
+ started: value.started,
16178
+ stages: value.stages.map((one) => human(String(one))),
16179
+ at: value.at,
16180
+ message: human(typeof value.message === "string" ? value.message : ""),
16181
+ ended: value.ended ?? null,
16182
+ pid: value.pid,
16183
+ beat: value.beat
16184
+ };
16536
16185
  } catch {
16537
- return [];
16186
+ return null;
16538
16187
  }
16539
16188
  }
16540
- function hostFromContext(context) {
16541
- const host = context?.host;
16542
- return host !== void 0 && typeof host.name === "string" && typeof host.version === "string" ? host : null;
16543
- }
16544
- function hostOf(host) {
16545
- return host === null ? null : { name: head(host.name, CRASH_LIMITS.hostName), version: head(host.version, CRASH_LIMITS.hostVersion) };
16546
- }
16547
- function probeFfmpeg(home) {
16548
- const own = join(home, "bin", platform() === "win32" ? "ffmpeg.exe" : "ffmpeg");
16549
- const program = existsSync(own) ? own : "ffmpeg";
16550
- return new Promise((done) => {
16551
- execFile(program, ["-version"], { timeout: 3e3, encoding: "utf8" }, (trouble, stdout) => {
16552
- if (trouble !== null) return done(null);
16553
- const m = /^ffmpeg version (\S+)/.exec(stdout);
16554
- done(m?.[1] ?? null);
16555
- });
16556
- });
16189
+ function alive(record2, clock) {
16190
+ if (clock() - record2.beat > STALE_MS) return false;
16191
+ try {
16192
+ process.kill(record2.pid, 0);
16193
+ return true;
16194
+ } catch (trouble) {
16195
+ return trouble.code === "EPERM";
16196
+ }
16557
16197
  }
16558
- function ownDir() {
16198
+ function save(work, clock) {
16199
+ if (work.home === null) return;
16559
16200
  try {
16560
- return dirname(fileURLToPath(import.meta.url));
16201
+ const dir = join(work.home, JOBS_DIR);
16202
+ mkdirSync(dir, { recursive: true });
16203
+ const file2 = join(dir, `${work.id}.json`);
16204
+ const temp = `${file2}.${process.pid}.tmp`;
16205
+ const record2 = {
16206
+ id: work.id,
16207
+ what: work.what,
16208
+ started: work.started,
16209
+ stages: work.stages,
16210
+ at: work.at,
16211
+ message: work.message,
16212
+ ended: work.ended,
16213
+ pid: process.pid,
16214
+ beat: clock()
16215
+ };
16216
+ writeFileSync(temp, JSON.stringify(record2), "utf8");
16217
+ renameSync(temp, file2);
16561
16218
  } catch {
16562
- return "";
16563
16219
  }
16564
16220
  }
16565
- function safeUser() {
16221
+ function prune(home) {
16566
16222
  try {
16567
- return userInfo().username;
16223
+ const dir = join(home, JOBS_DIR);
16224
+ const names = readdirSync(dir).filter((name) => JOB_ID.test(name.replace(/\.json$/, ""))).sort();
16225
+ for (const name of names.slice(0, Math.max(0, names.length - JOBS_KEPT2))) rmSync(join(dir, name), { force: true });
16568
16226
  } catch {
16569
- return "";
16570
16227
  }
16571
16228
  }
16572
- function head(text2, max) {
16573
- return text2.length <= max ? text2 : text2.slice(0, max);
16229
+ function fromDisk(home, id, clock) {
16230
+ const record2 = load(home, id);
16231
+ if (record2 === null) return null;
16232
+ if (record2.ended !== null || alive(record2, clock)) return record2;
16233
+ return {
16234
+ ...record2,
16235
+ ended: {
16236
+ at: record2.beat,
16237
+ ending: {
16238
+ ok: false,
16239
+ trouble: human("\u041E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435 \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C: \u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u0430, \u043A\u043E\u0442\u043E\u0440\u0430\u044F \u0435\u0433\u043E \u0432\u0435\u043B\u0430, \u0437\u0430\u043A\u0440\u044B\u043B\u0430\u0441\u044C \u043F\u043E\u0441\u0440\u0435\u0434\u0438 \u0440\u0430\u0431\u043E\u0442\u044B. \u041F\u0440\u0435\u0436\u043D\u044F\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u043D\u0430 \u043C\u0435\u0441\u0442\u0435, \u043C\u043E\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043C\u043E\u0436\u043D\u043E."),
16240
+ advice: human("\u041D\u0430\u043F\u0438\u0448\u0438 \xAB\u043E\u0431\u043D\u043E\u0432\u0438\xBB \u0435\u0449\u0451 \u0440\u0430\u0437 \u2014 \u043D\u0430\u0447\u043D\u0443 \u0437\u0430\u043D\u043E\u0432\u043E."),
16241
+ report: forModel(`\u041F\u0440\u043E\u0446\u0435\u0441\u0441 \u043E\u0431\u043E\u043B\u043E\u0447\u043A\u0438 ${record2.pid}, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u0432\u0451\u043B \u0437\u0430\u0434\u0430\u0447\u0443, \u043F\u0440\u043E\u043F\u0430\u043B \u043D\u0430 \u044D\u0442\u0430\u043F\u0435 \xAB${record2.stages[record2.at - 1] ?? CHECK}\xBB.`)
16242
+ }
16243
+ }
16244
+ };
16574
16245
  }
16575
- function tail(text2, max) {
16576
- if (text2.length <= max) return text2;
16577
- const cut = "\u2026(\u043D\u0430\u0447\u0430\u043B\u043E \u043E\u0431\u0440\u0435\u0437\u0430\u043D\u043E)\n";
16578
- return cut + text2.slice(text2.length - (max - cut.length));
16246
+ function startSetupJob(what, run, clock = Date.now, home = null) {
16247
+ const already = running(home, clock);
16248
+ if (already !== null) return already;
16249
+ const started = clock();
16250
+ const id = `${SETUP_JOB_MARK}${new Date(started).toISOString().replace(/[:.]/g, "-")}`;
16251
+ const work = {
16252
+ id,
16253
+ what,
16254
+ started,
16255
+ stages: [CHECK],
16256
+ at: 1,
16257
+ message: human(""),
16258
+ ended: null,
16259
+ done: Promise.resolve(),
16260
+ home
16261
+ };
16262
+ const control = {
16263
+ plan(stages) {
16264
+ work.stages = stages;
16265
+ work.at = Math.min(work.at, stages.length);
16266
+ save(work, clock);
16267
+ },
16268
+ go(stage) {
16269
+ const index = work.stages.indexOf(stage);
16270
+ if (index >= 0) work.at = index + 1;
16271
+ work.message = human("");
16272
+ save(work, clock);
16273
+ },
16274
+ line(text2) {
16275
+ work.message = text2;
16276
+ save(work, clock);
16277
+ }
16278
+ };
16279
+ works.set(id, work);
16280
+ if (home !== null) prune(home);
16281
+ save(work, clock);
16282
+ const beat = home === null ? null : setInterval(() => save(work, clock), BEAT_MS);
16283
+ beat?.unref();
16284
+ work.done = collectJobInstallFaults(id, () => run(control).catch((trouble) => {
16285
+ noteInstallFault("update-job", trouble);
16286
+ return {
16287
+ ok: false,
16288
+ trouble: human("\u0427\u0442\u043E-\u0442\u043E \u0441\u043B\u043E\u043C\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u0441\u0440\u0435\u0434\u0438 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F \u2014 \u043F\u0440\u0435\u0436\u043D\u044F\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u043D\u0430 \u043C\u0435\u0441\u0442\u0435, \u043C\u043E\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043C\u043E\u0436\u043D\u043E."),
16289
+ advice: ADVICE_ASK,
16290
+ report: forModel(String(trouble instanceof Error ? trouble.message : trouble))
16291
+ };
16292
+ })).then((ending) => {
16293
+ work.ended = { at: clock(), ending };
16294
+ if (beat !== null) clearInterval(beat);
16295
+ save(work, clock);
16296
+ });
16297
+ return id;
16579
16298
  }
16580
- function orNull(value, max) {
16581
- return value === null || value === "" ? null : head(value, max);
16299
+ async function setupJobDone(id) {
16300
+ await works.get(id)?.done;
16582
16301
  }
16583
- var SETUP_CONTRACT = "naparnik-setup/1";
16584
- var READINESS_CONTRACT = "naparnik-readiness/1";
16585
- var JSON_CHECK = "--json";
16586
- function knowsJsonCheck(engineDir) {
16587
- try {
16588
- const contract = JSON.parse(readFileSync(join(engineDir, "shell-contract.json"), "utf8"));
16589
- const flags = contract.env_check_flags;
16590
- return Array.isArray(flags) && flags.includes(JSON_CHECK);
16591
- } catch {
16592
- return false;
16302
+ function setupJobEnding(id) {
16303
+ return works.get(id)?.ended?.ending ?? null;
16304
+ }
16305
+ function ownSetupJobRunning() {
16306
+ for (const work of works.values()) if (work.ended === null) return true;
16307
+ return false;
16308
+ }
16309
+ function quiet(line) {
16310
+ return line.replace(/^⚠\s*/, "").replace(/^Что делать:/, "\u0421\u043E\u0432\u0435\u0442:");
16311
+ }
16312
+ function setupJobText(id, clock = Date.now, home = null) {
16313
+ const work = works.get(id) ?? (home === null ? null : fromDisk(home, id, clock));
16314
+ if (work === null) return null;
16315
+ const head2 = `\u0417\u0430\u0434\u0430\u043D\u0438\u0435 ${work.id}: ${work.what}`;
16316
+ if (work.ended === null) {
16317
+ const sec2 = Math.max(0, Math.round((clock() - work.started) / 1e3));
16318
+ return [
16319
+ head2,
16320
+ `\u042D\u0442\u0430\u043F ${work.at} \u0438\u0437 ${work.stages.length}: ${work.stages[work.at - 1] ?? CHECK}`,
16321
+ ...work.message === "" ? [] : [work.message],
16322
+ `\u0415\u0449\u0451 \u0441\u0447\u0438\u0442\u0430\u0435\u0442, ${sec2} \u0441.`
16323
+ ].join("\n");
16324
+ }
16325
+ const sec = Math.max(0, Math.round((work.ended.at - work.started) / 1e3));
16326
+ const ending = work.ended.ending;
16327
+ const report = ending.report.split("\n").map((line) => line.trim()).filter((line) => line !== "").map(quiet);
16328
+ if (ending.ok) {
16329
+ return [head2, `\u0413\u043E\u0442\u043E\u0432\u043E \u0437\u0430 ${sec} \u0441.`, `\u0427\u0435\u043B\u043E\u0432\u0435\u043A\u0443: ${ending.say}`, ...report].join("\n");
16330
+ }
16331
+ return [
16332
+ head2,
16333
+ ...report,
16334
+ `\u041D\u0435 \u043F\u043E\u043B\u0443\u0447\u0438\u043B\u043E\u0441\u044C \u0437\u0430 ${sec} \u0441: ${ending.trouble}`,
16335
+ `\u0427\u0442\u043E \u0434\u0435\u043B\u0430\u0442\u044C: ${ending.advice}`
16336
+ ].join("\n");
16337
+ }
16338
+ function setupJobReport(id, home = null, clock = Date.now) {
16339
+ const work = works.get(id) ?? (home === null ? null : fromDisk(home, id, clock));
16340
+ if (work === null) return null;
16341
+ const job = blankJob();
16342
+ job.id = work.id;
16343
+ job.what = work.what;
16344
+ job.at = work.at;
16345
+ job.total = work.stages.length;
16346
+ job.now = work.stages[work.at - 1] ?? CHECK;
16347
+ if (work.ended === null) {
16348
+ job.sec = Math.max(0, Math.round((clock() - work.started) / 1e3));
16349
+ job.message = work.message;
16350
+ const download = downloadOf(work.message);
16351
+ if (download !== null) Object.assign(job, download);
16352
+ return job;
16353
+ }
16354
+ const ending = work.ended.ending;
16355
+ job.sec = Math.max(0, Math.round((work.ended.at - work.started) / 1e3));
16356
+ job.outcome = ending.report;
16357
+ if (ending.ok) {
16358
+ job.status = "done";
16359
+ job.say = ending.say;
16360
+ } else {
16361
+ job.status = "error";
16362
+ job.trouble = ending.trouble;
16363
+ job.advice = ending.advice;
16364
+ }
16365
+ return job;
16366
+ }
16367
+ function weightExact(bytes) {
16368
+ const mb = bytes / 1024 / 1024;
16369
+ if (mb >= 1e3) return `${(mb / 1024).toFixed(1).replace(".", ",")} \u0413\u0411`;
16370
+ return `${mb.toFixed(1).replace(".", ",")} \u041C\u0411`;
16371
+ }
16372
+ async function runUpdate(job, deps) {
16373
+ const { engine, key, baseUrl, what } = deps;
16374
+ const home = engine.home();
16375
+ const wantShell = what !== "engine";
16376
+ const hasEngine = engine.place() !== null;
16377
+ const wantEngine = what !== "shell" && hasEngine && !engine.pathSetByHand();
16378
+ const shellUrl = engine.variable("NAPARNIK_BUNDLE_MANIFEST") ?? bundleUrlFor(baseUrl);
16379
+ const [shellAsk, engineAsk] = await Promise.all([
16380
+ wantShell ? fetchShellManifest(shellUrl, deps.pointerMs, key) : Promise.resolve(null),
16381
+ wantEngine ? fetchManifest(manifestUrl(engine, baseUrl), deps.pointerMs, key) : Promise.resolve(null)
16382
+ ]);
16383
+ const shellWas = installedShell(home);
16384
+ const shell = decideShell(shellWas, shellAsk?.ok === true ? shellAsk.manifest : null);
16385
+ const engineWas = engine.freshVersion();
16386
+ const fresh = decide(engineWas, engineAsk?.ok === true ? engineAsk.manifest : null);
16387
+ const shellStage = shell.what === "newer_available" ? human(`\u0421\u043A\u0430\u0447\u0438\u0432\u0430\u044E \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0435 \xB7 ${weightExact(shell.manifest.body?.bytes ?? 0)}`) : null;
16388
+ const engineStage = fresh.what === "newer_available" ? human(`\u0421\u043A\u0430\u0447\u0438\u0432\u0430\u044E \u0434\u0432\u0438\u0436\u043E\u043A \xB7 ${weightExact(fresh.manifest.bytes)}`) : null;
16389
+ const missing = what === "all";
16390
+ job.plan([
16391
+ CHECK,
16392
+ ...shellStage === null ? [] : [shellStage],
16393
+ ...engineStage === null ? [] : [engineStage, INSTALL_NEW],
16394
+ ...missing ? [MISSING] : []
16395
+ ]);
16396
+ const steps = [];
16397
+ let updated = false;
16398
+ let installed = false;
16399
+ for (const [ask2, whose] of [[shellAsk, "\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F"], [engineAsk, "\u0434\u0432\u0438\u0436\u043A\u0430"]]) {
16400
+ if (ask2 === null || ask2.ok) continue;
16401
+ const refusal = refusalWords(ask2.status ?? 0);
16402
+ steps.push({
16403
+ report: `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043F\u0440\u043E\u0441\u0438\u0442\u044C, \u0435\u0441\u0442\u044C \u043B\u0438 \u043D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F ${whose}: ${ask2.why}.`,
16404
+ trouble: human(refusal !== "" ? refusal : "\u041D\u0435 \u043F\u043E\u043B\u0443\u0447\u0438\u043B\u043E\u0441\u044C \u0441\u0432\u044F\u0437\u0430\u0442\u044C\u0441\u044F \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u043C \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0439.")
16405
+ });
16593
16406
  }
16594
- }
16595
- var STATES = /* @__PURE__ */ new Set(["ok", "missing", "later"]);
16596
- function megabytes(value) {
16597
- return typeof value === "number" && value > 0 ? Math.round(value) : 0;
16598
- }
16599
- function readReadiness(stdout) {
16600
- const lines = stdout.trim().split("\n");
16601
- let raw;
16602
- try {
16603
- raw = JSON.parse(lines[lines.length - 1] ?? "");
16604
- } catch {
16605
- return null;
16407
+ if (shellAsk?.ok === true && shell.what === "nothing") {
16408
+ steps.push({ report: `\u0423 \u0432\u0430\u0441 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u0441\u0430\u043C\u0430\u044F \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u044F\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u2014 ${shellWas}.` });
16606
16409
  }
16607
- const data = raw;
16608
- if (data === null || typeof data !== "object" || data["contract"] !== READINESS_CONTRACT) return null;
16609
- if (typeof data["words"] !== "string" || !Array.isArray(data["parts"])) return null;
16610
- const parts = [];
16611
- for (const item of data["parts"]) {
16612
- const part = item;
16613
- if (part === null || typeof part !== "object" || typeof part["name"] !== "string") continue;
16614
- parts.push({
16615
- name: part["name"],
16616
- title: typeof part["title"] === "string" && part["title"] !== "" ? part["title"] : part["name"],
16617
- ok: part["ok"] === true,
16618
- mb: megabytes(part["mb"]),
16619
- on_demand: part["on_demand"] === true,
16620
- step: typeof part["step"] === "string" ? part["step"] : ""
16410
+ if (shell.what === "launcher_too_old") {
16411
+ steps.push({
16412
+ report: `\u041D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F ${shell.manifest.version} \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043D\u043E\u0432\u043E\u0433\u043E \u0444\u0430\u0439\u043B\u0430 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F.`,
16413
+ trouble: human(`\u0414\u043B\u044F \u043D\u043E\u0432\u043E\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u043D\u0443\u0436\u0435\u043D \u043D\u043E\u0432\u044B\u0439 \u0444\u0430\u0439\u043B \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F. \u0412\u043E\u0437\u044C\u043C\u0438 \u0435\u0433\u043E \u0432 \u0442\u0435\u043B\u0435\u0433\u0440\u0430\u043C-\u0431\u043E\u0442\u0435 ${KEY_SOURCE_BOT}.`)
16621
16414
  });
16622
16415
  }
16623
- let rows = null;
16624
- if (Array.isArray(data["rows"])) {
16625
- rows = [];
16626
- for (const item of data["rows"]) {
16627
- const row = item;
16628
- if (row === null || typeof row !== "object" || typeof row["title"] !== "string") continue;
16629
- const state = row["state"];
16630
- if (typeof state !== "string" || !STATES.has(state)) continue;
16631
- rows.push({ title: row["title"], state, mb: megabytes(row["mb"]) });
16416
+ if (fresh.what === "shell_too_old") {
16417
+ steps.push({
16418
+ report: `\u041D\u043E\u0432\u0430\u044F \u043D\u0430\u0447\u0438\u043D\u043A\u0430 ${fresh.manifest.version} \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u043D\u0435 \u0441\u0442\u0430\u0440\u0448\u0435 ${fresh.manifest.minShellVersion}.`,
16419
+ trouble: human("\u041D\u043E\u0432\u043E\u043C\u0443 \u0434\u0432\u0438\u0436\u043A\u0443 \u043D\u0443\u0436\u043D\u043E \u043D\u043E\u0432\u043E\u0435 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0435 \u2014 \u0435\u0433\u043E \u043F\u043E\u043A\u0430 \u043D\u0435\u0442 \u043D\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435.")
16420
+ });
16421
+ }
16422
+ if (shell.what === "newer_available" && shellStage !== null) {
16423
+ job.go(shellStage);
16424
+ const outcome = await installShell({ home, manifest: shell.manifest, key: key ?? null });
16425
+ if (outcome.ok) {
16426
+ updated = true;
16427
+ steps.push({ report: outcome.text });
16428
+ } else {
16429
+ steps.push({ report: outcome.text, trouble: human("\u041D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u043D\u0435 \u0432\u0441\u0442\u0430\u043B\u0430 \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043F\u0440\u0435\u0436\u043D\u044F\u044F.") });
16632
16430
  }
16633
16431
  }
16634
- return { words: data["words"], ready: data["ready"] === true, parts, rows };
16635
- }
16636
- var NOT_A_ROW = /* @__PURE__ */ new Set(["system"]);
16637
- var TITLE_READY = "\u0412\u0441\u0451 \u0433\u043E\u0442\u043E\u0432\u043E \u2014 \u043C\u043E\u0436\u043D\u043E \u043D\u0430\u0447\u0438\u043D\u0430\u0442\u044C \u043C\u043E\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C :)";
16638
- var TITLE_UNKNOWN = "\u041F\u0440\u043E\u0432\u0435\u0440\u044F\u044E, \u0447\u0442\u043E \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E";
16639
- function weight(mb) {
16640
- if (mb >= 1e3) return `${(mb / 1024).toFixed(1).replace(".", ",")} \u0413\u0411`;
16641
- return `${mb} \u041C\u0411`;
16642
- }
16643
- function rowWords(state, mb) {
16644
- if (state === "ok") return "\u0435\u0441\u0442\u044C";
16645
- if (state === "later") return `\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u044E, \u0435\u0441\u043B\u0438 \u0434\u0430\u0448\u044C \u0441\u043E\u0433\u043B\u0430\u0441\u0438\u0435${mb ? ` \xB7 ${weight(mb)}` : ""}`;
16646
- return mb ? `\u043D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u043E \u0441\u043A\u0430\u0447\u0430\u0442\u044C \xB7 ${weight(mb)}` : "\u043D\u0435 \u0445\u0432\u0430\u0442\u0430\u0435\u0442";
16647
- }
16648
- function firstPhrase(notes) {
16649
- const paragraph = notes.trim().split(/\n\s*\n/)[0] ?? "";
16650
- const plain = paragraph.replaceAll("*", "").replace(/^[#\s-]+/, "").replace(/\s+/g, " ").trim();
16651
- const sentence = /^(.+?[.!?…])(?:\s|$)/.exec(plain);
16652
- return (sentence?.[1] ?? plain).trim();
16432
+ if (fresh.what === "newer_available" && engineStage !== null) {
16433
+ job.go(engineStage);
16434
+ const outcome = await deploy(engine, fresh.manifest, void 0, key, {
16435
+ // Строки хода скачивания и сборки печатает движок человеку: «Скачано …».
16436
+ line: (text2) => job.line(human(text2)),
16437
+ downloaded: () => {
16438
+ job.go(INSTALL_NEW);
16439
+ job.line(human("\u0421\u043E\u0431\u0438\u0440\u0430\u044E \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 \u043D\u043E\u0432\u043E\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u2014 \u044D\u0442\u043E \u0434\u043E \u043F\u0430\u0440\u044B \u043C\u0438\u043D\u0443\u0442."));
16440
+ }
16441
+ });
16442
+ if (outcome.ok) {
16443
+ updated = true;
16444
+ steps.push({ report: outcome.text });
16445
+ } else {
16446
+ const cause = causeOf(outcome.text);
16447
+ steps.push({
16448
+ report: outcome.text,
16449
+ trouble: human(`\u041D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u0434\u0432\u0438\u0436\u043A\u0430 \u043D\u0435 \u0432\u0441\u0442\u0430\u043B\u0430${cause === "" ? "" : `: ${cause}`}. \u0420\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043F\u0440\u0435\u0436\u043D\u044F\u044F${engineWas === null ? "" : ` ${engineWas}`}, \u043C\u043E\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043C\u043E\u0436\u043D\u043E.`)
16450
+ });
16451
+ }
16452
+ }
16453
+ if (missing) {
16454
+ job.go(MISSING);
16455
+ const step = await followEngineJob(job, deps.engine, deps.pace, deps.installMissing);
16456
+ if (step.trouble === void 0 && step.installed) installed = true;
16457
+ steps.push(step);
16458
+ }
16459
+ const report = forModel(steps.map((step) => step.report).join("\n"));
16460
+ const trouble = steps.find((step) => step.trouble !== void 0)?.trouble;
16461
+ if (trouble !== void 0) return { ok: false, trouble, advice: ADVICE_ASK, report };
16462
+ if (updated) return { ok: true, say: SAY_UPDATED, report };
16463
+ return { ok: true, say: installed ? SAY_INSTALLED : SAY_NOTHING, report };
16653
16464
  }
16654
- function refusalWords(status) {
16655
- if (status === 401) return `\u041A\u043B\u044E\u0447 \u043D\u0435 \u043F\u043E\u0434\u0445\u043E\u0434\u0438\u0442. \u0412\u043E\u0437\u044C\u043C\u0438 \u043D\u043E\u0432\u044B\u0439 \u0444\u0430\u0439\u043B \u0432 \u0442\u0435\u043B\u0435\u0433\u0440\u0430\u043C-\u0431\u043E\u0442\u0435 ${KEY_SOURCE_HANDLE}.`;
16656
- if (status === 403) return `\u041F\u043E\u0434\u043F\u0438\u0441\u043A\u0430 \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u043B\u0430\u0441\u044C. \u041F\u0440\u043E\u0434\u043B\u0438 \u0435\u0451 \u0432 \u0442\u0435\u043B\u0435\u0433\u0440\u0430\u043C-\u0431\u043E\u0442\u0435 ${KEY_SOURCE_HANDLE}.`;
16657
- return "";
16465
+ function causeOf(text2) {
16466
+ const line = text2.split("\n").map((one) => one.trim()).find((one) => one !== "" && !/^(Новая версия|Версия) \S+ не встала/.test(one) && !/^Что делать:/.test(one)) ?? "";
16467
+ const phrase = (line.split(/\.\s|\s—\s|;\s/)[0] ?? "").replace(/\s*\([^)]*\)/g, "").replace(/\.$/, "").trim();
16468
+ return phrase === "" ? "" : phrase[0].toLowerCase() + phrase.slice(1);
16658
16469
  }
16659
- function exactWeight(mb) {
16660
- if (mb >= 1e3) return weight(mb);
16661
- return `${mb.toFixed(1).replace(".", ",")} \u041C\u0411`;
16470
+ async function runHeavy(job, deps) {
16471
+ job.plan([deps.title]);
16472
+ job.go(deps.title);
16473
+ const step = await followEngineJob(job, deps.engine, deps.pace, deps.start);
16474
+ const report = forModel(step.report);
16475
+ if (step.trouble !== void 0) return { ok: false, trouble: step.trouble, advice: ADVICE_ASK, report };
16476
+ return { ok: true, say: SAY_INSTALLED, report };
16662
16477
  }
16663
- function whatIsNew(shell, engine) {
16664
- const parts = [];
16665
- for (const [news, name] of [[engine, "\u0434\u0432\u0438\u0436\u043E\u043A"], [shell, "\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0435"]]) {
16666
- if (!news?.fresh) continue;
16667
- parts.push(news.mb > 0 ? `${name} \xB7 ${exactWeight(news.mb)}` : name);
16478
+ async function followEngineJob(job, engine, pace, start) {
16479
+ const answer = await start();
16480
+ if (answer.type === "error") {
16481
+ return { report: answer.error, trouble: human("\u041D\u0435 \u043F\u043E\u043B\u0443\u0447\u0438\u043B\u043E\u0441\u044C \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C."), installed: false };
16668
16482
  }
16669
- return parts.join(" \u0438 ");
16670
- }
16671
- function updateWords(shell, engine) {
16672
- if (!shell?.fresh && !engine?.fresh) return "";
16673
- const notes = shell?.fresh ? firstPhrase(shell.notes) : "";
16674
- return [
16675
- `\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u0430 \u043D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u043C\u043E\u043D\u0442\u0430\u0436\u0451\u0440\u0430: ${whatIsNew(shell, engine)}.${notes ? ` \u041F\u043E\u044F\u0432\u0438\u043B\u043E\u0441\u044C: ${notes}` : ""}`,
16676
- "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0434\u043E \u043D\u043E\u0432\u043E\u0439 \u0432\u0435\u0440\u0441\u0438\u0438?",
16677
- "\u041F\u0440\u043E\u0441\u0442\u043E \u043D\u0430\u043F\u0438\u0448\u0438 \xAB\u043E\u0431\u043D\u043E\u0432\u0438\xBB."
16678
- ].join("\n");
16679
- }
16680
- function cardRows(readiness) {
16681
- const rows = readiness?.rows ?? (readiness?.parts ?? []).filter((part) => !NOT_A_ROW.has(part.name)).map((part) => ({
16682
- title: part.title,
16683
- state: part.ok ? "ok" : part.on_demand ? "later" : "missing",
16684
- mb: part.ok ? 0 : part.mb
16685
- }));
16686
- return rows.map((row) => ({ ...row, mb: row.state === "ok" ? 0 : row.mb, say: rowWords(row.state, row.mb) }));
16687
- }
16688
- var FOR_MODEL = /\bsetup\b|\bwait\b|what=|consent|\bjob\b|Скажи человеку|Спроси человека|скажи ему|перескажи/i;
16689
- function forHuman(line) {
16690
- return FOR_MODEL.test(line) ? "" : line;
16691
- }
16692
- function setupCard(o) {
16693
- const rows = cardRows(o.readiness);
16694
- const missingMb = rows.filter((row) => row.state === "missing").reduce((all, row) => all + row.mb, 0);
16695
- const ready = o.readiness === null ? false : o.readiness.rows !== null ? rows.every((row) => row.state !== "missing") : o.readiness.ready;
16696
- let title;
16697
- let next;
16698
- if (o.noEngine || o.noPython) {
16699
- title = "\u041D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u043E \u0441\u043A\u0430\u0447\u0430\u0442\u044C \u043C\u043E\u043D\u0442\u0430\u0436 \u0434\u043B\u044F \u043F\u043E\u043B\u043D\u043E\u0446\u0435\u043D\u043D\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u044B";
16700
- next = `\u041D\u0430\u043F\u0438\u0448\u0438 \xAB\u0441\u043A\u0430\u0447\u0430\u0439\xBB \u2014 \u0441\u043A\u0430\u0447\u0430\u044E ${o.noEngine ? "\u043C\u043E\u043D\u0442\u0430\u0436" : "Python"}.`;
16701
- } else if (o.readiness === null) {
16702
- title = o.title ?? TITLE_UNKNOWN;
16703
- next = forHuman(o.note ?? "");
16704
- } else if (!ready) {
16705
- title = missingMb ? `\u041D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u043E \u0441\u043A\u0430\u0447\u0430\u0442\u044C ${weight(missingMb)} \u0434\u043B\u044F \u043F\u043E\u043B\u043D\u043E\u0446\u0435\u043D\u043D\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u044B` : "\u041D\u0435 \u0432\u0441\u0451 \u0433\u043E\u0442\u043E\u0432\u043E \u0434\u043B\u044F \u043F\u043E\u043B\u043D\u043E\u0446\u0435\u043D\u043D\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u044B";
16706
- next = missingMb ? `\u041D\u0430\u043F\u0438\u0448\u0438 \xAB\u0441\u043A\u0430\u0447\u0430\u0439\xBB \u2014 \u0441\u043A\u0430\u0447\u0430\u044E ${weight(missingMb)}.` : "\u041D\u0430\u043F\u0438\u0448\u0438 \xAB\u0441\u043A\u0430\u0447\u0430\u0439\xBB \u2014 \u043F\u043E\u0441\u0442\u0430\u0432\u043B\u044E, \u0447\u0435\u0433\u043E \u043D\u0435 \u0445\u0432\u0430\u0442\u0430\u0435\u0442.";
16707
- } else {
16708
- title = TITLE_READY;
16709
- next = "";
16483
+ const words = typeof answer.content === "string" ? answer.content : answer.content.map((block) => block.type === "text" ? block.text : "").join("\n");
16484
+ const id = typeof answer.structured?.["id"] === "string" ? answer.structured["id"] : "";
16485
+ if (id === "") return { report: words, installed: false };
16486
+ const rest = pace ?? realPace;
16487
+ let state = await engine.command(["job", id]);
16488
+ while (state.ok && stillWorking(state.text)) {
16489
+ const now = readJob(state.text);
16490
+ if (now !== null) job.line(now.message !== "" ? now.message : now.now);
16491
+ await rest.rest(POLL_EVERY_MS);
16492
+ state = await engine.command(["job", id]);
16710
16493
  }
16711
- return {
16712
- contract: SETUP_CONTRACT,
16713
- title,
16714
- meta: `\u0432\u0435\u0440\u0441\u0438\u044F ${o.shellVersion}`,
16715
- ready,
16716
- rows,
16717
- missingMb,
16718
- update: updateWords(o.shell, o.engine),
16719
- trouble: refusalWords(o.shell?.status ?? 0) || refusalWords(o.engine?.status ?? 0),
16720
- next
16721
- };
16494
+ const end = state.ok ? readJob(state.text) : null;
16495
+ if (end === null || end.status !== "done") {
16496
+ return {
16497
+ report: `${words}
16498
+ ${state.text}`,
16499
+ // Беда задачи движка — слова человеку от самого движка (строка «Не
16500
+ // получилось …» его задания), их и отдаём в окно.
16501
+ trouble: human(end?.trouble !== void 0 && end.trouble !== "" ? end.trouble : "\u041D\u0435 \u043F\u043E\u043B\u0443\u0447\u0438\u043B\u043E\u0441\u044C \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043D\u0435\u0434\u043E\u0441\u0442\u0430\u044E\u0449\u0435\u0435."),
16502
+ installed: false
16503
+ };
16504
+ }
16505
+ return { report: `${words}
16506
+ ${state.text}`, installed: true };
16722
16507
  }
16723
16508
  var WIDGET_MIME = "text/html;profile=mcp-app";
16724
16509
  function widgetStamp(html) {
@@ -17403,8 +17188,10 @@ function runbarWidgetHtml() {
17403
17188
  var box = byId('dl');
17404
17189
  if (!box) return false;
17405
17190
  var got = Number(data.got) || 0;
17406
- if (!got) { box.hidden = true; return false; }
17407
17191
  var of = Number(data.of) || 0;
17192
+ // \u041D\u043E\u043B\u044C \u0438\u0437 \u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E\u0433\u043E \u0446\u0435\u043B\u043E\u0433\u043E \u2014 \u0443\u0436\u0435 \u0448\u043A\u0430\u043B\u0430: \xAB0 \u0438\u0437 450 \u041C\u0411 \xB7 0%\xBB \u0433\u043E\u0432\u043E\u0440\u0438\u0442, \u0441\u043A\u043E\u043B\u044C\u043A\u043E
17193
+ // \u043F\u0440\u0435\u0434\u0441\u0442\u043E\u0438\u0442, \u0430 \u0441\u0442\u0440\u043E\u043A\u0430 \xAB\u0421\u043A\u0430\u0447\u0430\u043D\u043E 0 \u041C\u0411\xBB \u043F\u043B\u0430\u0448\u043A\u043E\u0439 \u0447\u0438\u0442\u0430\u043B\u0430\u0441\u044C \u0431\u044B \u043A\u0430\u043A \u043F\u043E\u043B\u043E\u043C\u043A\u0430.
17194
+ if (!got && !of) { box.hidden = true; return false; }
17408
17195
  byId('dlMeter').hidden = !of;
17409
17196
  fill('dlFill', of ? data.percent : 0);
17410
17197
  var cap = byId('dlCap');
@@ -17655,985 +17442,1118 @@ function runbarWidgetHtml() {
17655
17442
  </html>`;
17656
17443
  }
17657
17444
 
17658
- // src/tool-kit.ts
17659
- function manifestUrl(engine, baseUrl) {
17660
- return engine.variable("NAPARNIK_UPDATE_MANIFEST") ?? manifestUrlFor(baseUrl);
17661
- }
17662
- function aboutDefaults(map2) {
17663
- if (map2 === void 0 || Object.keys(map2).length === 0) return "\u0441\u043C\u043E\u0442\u0440\u0438 setup";
17664
- return Object.entries(map2).map(([engine, model]) => `${model} (${engine})`).join(", ");
17665
- }
17666
- function aboutModels(passport) {
17667
- if (passport === null) return "\u0421\u043F\u0438\u0441\u043E\u043A \u043C\u043E\u0434\u0435\u043B\u0435\u0439 \u0441\u043C\u043E\u0442\u0440\u0438 \u0432 setup";
17668
- return `\u041A\u0430\u043A\u0438\u0435 \u0435\u0441\u0442\u044C: ${passport["models"].join(", ")}; \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u2014 \u0441\u0432\u043E\u0451 \u043D\u0430 \u0434\u0432\u0438\u0436\u043E\u043A: ${aboutDefaults(passport["default_model"])}. \u041D\u0435 \u043D\u0430\u0437\u044B\u0432\u0430\u0439 \u043C\u043E\u0434\u0435\u043B\u044C \u0431\u0435\u0437 \u043D\u0443\u0436\u0434\u044B: \u043F\u0430\u043A \u0441\u0430\u043C \u0432\u043E\u0437\u044C\u043C\u0451\u0442 \u0442\u0443, \u0447\u0442\u043E \u0431\u044B\u0441\u0442\u0440\u0435\u0435 \u043D\u0430 \u044D\u0442\u043E\u043C \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440\u0435`;
17669
- }
17670
- function fromList(list, description) {
17671
- return list !== void 0 && list.length > 0 ? external_exports.enum([...list]).describe(description) : external_exports.string().describe(description);
17672
- }
17673
- function asToolResult(outcome) {
17674
- return outcome.ok ? toolSuccess(outcome.text) : toolError(outcome.text);
17675
- }
17676
- function asJobResult(outcome) {
17677
- if (!outcome.ok) return toolError(outcome.text);
17678
- const job = readJob(outcome.text);
17679
- return job === null ? toolSuccess(outcome.text) : toolSuccess(outcome.text, job);
17680
- }
17681
- var CAPABILITIES_CONTRACT = "naparnik-capabilities/1";
17682
- var HEAVY_WORK = {
17683
- model: "\u0440\u0430\u0441\u043F\u043E\u0437\u043D\u0430\u0432\u0430\u043D\u0438\u0435 \u0440\u0435\u0447\u0438",
17684
- cutout: "\u0432\u044B\u0440\u0435\u0437 \u0433\u0435\u0440\u043E\u044F",
17685
- voice: "\u0447\u0438\u0441\u0442\u043A\u0430 \u0433\u043E\u043B\u043E\u0441\u0430"
17686
- };
17687
- var KNOB_KIND = {
17688
- file: "\u0444\u0430\u0439\u043B",
17689
- string: "\u0441\u0442\u0440\u043E\u043A\u0430",
17690
- number: "\u0447\u0438\u0441\u043B\u043E",
17691
- "yes-no": "\u0434\u0430/\u043D\u0435\u0442"
17692
- };
17693
- var FROM_MINE = "mine";
17694
- function catalogInWords(data) {
17695
- const lines = [];
17696
- const version2 = data.engine?.version;
17697
- lines.push(`\u0427\u0442\u043E \u0443\u043C\u0435\u0435\u0442 \u043C\u043E\u043D\u0442\u0430\u0436 \u043D\u0430 \u044D\u0442\u043E\u043C \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440\u0435${version2 !== void 0 ? `. \u0414\u0432\u0438\u0436\u043E\u043A ${version2}` : ""}.`);
17698
- const steps = data.steps ?? [];
17699
- const ready = steps.filter((s) => s.ready !== false);
17700
- const later = steps.filter((s) => s.ready === false);
17701
- if (ready.length > 0) {
17702
- lines.push(`\u0423\u0436\u0435 \u0441\u0442\u043E\u0438\u0442: ${ready.map((s) => `${s.title} (${s.name})`).join(", ")}.`);
17703
- }
17704
- if (later.length > 0) {
17705
- const said = later.map((s) => {
17706
- const work = (s.needs ?? []).map((n) => HEAVY_WORK[n] ?? n).join(" \u0438 ");
17707
- const step = (s.needs ?? [])[0];
17708
- return `${s.title} (${s.name}) \u2014 ${work}${step !== void 0 ? ` (setup what="${step}")` : ""}`;
17709
- }).join("; ");
17710
- lines.push(
17711
- `\u0414\u043E\u043A\u0430\u0447\u0430\u0435\u0442\u0441\u044F \u043F\u0435\u0440\u0435\u0434 \u043F\u0435\u0440\u0432\u044B\u043C \u043C\u043E\u043D\u0442\u0430\u0436\u043E\u043C, \u0441\u043F\u0440\u043E\u0441\u0438 \u0441\u043E\u0433\u043B\u0430\u0441\u0438\u0435 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430: ${said}.`
17712
- );
17713
- }
17714
- const recipes = data.recipes ?? [];
17715
- if (recipes.length > 0) {
17716
- lines.push(`\u0413\u043E\u0442\u043E\u0432\u044B\u0435 \u043D\u0430\u0431\u043E\u0440\u044B \u0448\u0430\u0433\u043E\u0432: ${recipes.map((r) => `${r.title} (${r.name})`).join(", ")}.`);
17445
+ // src/setup-tool.ts
17446
+ var ASKED_POINTER_MS = 15e3;
17447
+ var EXTRAS_LATER = "\u041C\u043E\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0443\u0436\u0435 \u043C\u043E\u0436\u043D\u043E. \u0424\u043E\u043D\u043E\u043C \u0434\u043E\u043A\u0430\u0447\u0438\u0432\u0430\u044E \u0442\u043E, \u0431\u0435\u0437 \u0447\u0435\u0433\u043E \u043C\u043E\u043D\u0442\u0430\u0436 \u0438 \u0442\u0430\u043A \u0438\u0434\u0451\u0442: \u0448\u0443\u043C\u043E\u0434\u0430\u0432 \u0433\u043E\u043B\u043E\u0441\u0430 \u2014 \u043E\u043A\u043E\u043B\u043E 30 \u041C\u0411, \u043A\u0430\u0440\u0442\u0443 \u0433\u0435\u0440\u043E\u044F \u0438 \u0431\u0440\u0430\u0443\u0437\u0435\u0440 \u0434\u043B\u044F \u0433\u0440\u0430\u0444\u0438\u043A\u0438 \u2014 \u0434\u043E 150 \u041C\u0411. \u0411\u0440\u0430\u0443\u0437\u0435\u0440 \u043A\u0430\u0447\u0430\u0435\u0442\u0441\u044F, \u0442\u043E\u043B\u044C\u043A\u043E \u0435\u0441\u043B\u0438 \u0435\u0433\u043E \u0441\u0431\u043E\u0440\u043A\u0430 \u0434\u043B\u044F \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u044B \u0443\u0436\u0435 \u0432\u044B\u043B\u043E\u0436\u0435\u043D\u0430: \u0433\u0440\u0430\u0444\u0438\u043A\u0430 \u0432 \u043C\u043E\u043D\u0442\u0430\u0436 \u0435\u0449\u0451 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0430. \u041D\u0435 \u0434\u043E\u043A\u0430\u0447\u0430\u0435\u0442\u0441\u044F \u2014 \u043C\u043E\u043D\u0442\u0430\u0436 \u043D\u0435 \u0443\u043F\u0430\u0434\u0451\u0442, \u0430 setup \u0441\u043A\u0430\u0436\u0435\u0442 \u0441\u043B\u043E\u0432\u0430\u043C\u0438, \u0447\u0435\u0433\u043E \u043D\u0435\u0442.";
17448
+ async function inspectMachine(engine, asked) {
17449
+ const video = asked.video;
17450
+ const model = asked.model;
17451
+ const place = engine.place();
17452
+ if (place === null) return { result: toolError(engine.whyNoEngine()), readiness: null };
17453
+ const args = [
17454
+ "env-check",
17455
+ "--pack",
17456
+ place.pack,
17457
+ ...video !== void 0 ? ["--video", path(video)] : [],
17458
+ ...model !== void 0 ? ["--model", model] : []
17459
+ ];
17460
+ let readiness = null;
17461
+ let words;
17462
+ if (knowsJsonCheck(place.engine)) {
17463
+ const answer = await askReadiness(engine, place, args);
17464
+ if ("ok" in answer && !answer.ok) return { result: toolError(answer.text), readiness: null };
17465
+ if ("parts" in answer) readiness = answer;
17466
+ words = "parts" in answer ? answer.words : answer.text;
17467
+ } else {
17468
+ const outcome = await engine.command(args);
17469
+ if (!outcome.ok) return { result: toolError(outcome.text), readiness: null };
17470
+ words = outcome.text;
17717
17471
  }
17718
- const inForce = data.recipe;
17719
- if (inForce?.name !== void 0) {
17720
- lines.push(
17721
- inForce.origin === FROM_MINE ? `\u0420\u0435\u0446\u0435\u043F\u0442 \u0432 \u0434\u0435\u043B\u0435: \u0441\u0432\u043E\u0439 \xAB${inForce.name}\xBB \u2014 ${inForce.path ?? ""}.` : `\u0420\u0435\u0446\u0435\u043F\u0442 \u0432 \u0434\u0435\u043B\u0435: \u043D\u0430\u0448 \xAB${inForce.name}\xBB. \u0421\u0432\u043E\u0439 \u0437\u0430\u0432\u043E\u0434\u0438\u0442\u0441\u044F \u0447\u0435\u0440\u0435\u0437 plan_edit \u0441\u043E step.`
17722
- );
17472
+ const catalog = await askCatalog(engine, place.pack, video);
17473
+ return { result: toolSuccess(`${words}
17474
+
17475
+ ${catalog}`), readiness };
17476
+ }
17477
+ async function askReadiness(engine, place, args) {
17478
+ const python = await engine.pythonTrouble();
17479
+ if (python !== null) return { ok: false, text: python };
17480
+ let stdout;
17481
+ try {
17482
+ stdout = (await engine.run(place, [...args, JSON_CHECK])).stdout;
17483
+ } catch (err) {
17484
+ const e = err;
17485
+ if (e.code !== 2 || typeof e.stdout !== "string") return parseFailure(err, "\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u043C\u0430\u0448\u0438\u043D\u044B \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u043B\u0430\u0441\u044C");
17486
+ stdout = e.stdout;
17723
17487
  }
17724
- const questions = data.questions ?? [];
17725
- if (questions.length > 0) {
17726
- const sections = [...new Map(
17727
- questions.filter((q) => q.section).map((q) => [q.section ?? "", q.group ?? ""])
17728
- )].map(([id, tab]) => `${tab} (${id})`);
17729
- lines.push(
17730
- "\u0421\u043F\u0440\u043E\u0441\u0438\u0442\u044C \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0430\u043C\u0438 \u2014 choose_variant(questions=[\u2026]) \u0438\u043C\u0435\u043D\u0430\u043C\u0438 \u0432\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0438\u043B\u0438 \u0440\u0430\u0437\u0434\u0435\u043B\u043E\u0432, \u0438 \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u0440\u043E \u0442\u043E, \u0447\u0442\u043E \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u0432\u044B\u0431\u0440\u0430\u043B \u0438\u043B\u0438 \u043F\u043E\u043F\u0440\u043E\u0441\u0438\u043B." + (sections.length > 0 ? ` \u0420\u0430\u0437\u0434\u0435\u043B\u044B: ${sections.join(", ")}.` : "")
17731
- );
17732
- for (const q of questions) {
17733
- const tab = q.group ? `[${q.group}] ` : "";
17734
- if (q.kind === "range") {
17735
- const dials = (q.sliders ?? []).map((s) => `${s.title} (${s.name})`).join(", ");
17736
- lines.push(`\u2014 ${tab}${q.title} (${q.name}), \u043F\u043E\u043B\u0437\u0443\u043D\u043A\u0430\u043C\u0438: ${dials}.`);
17737
- continue;
17488
+ return readReadiness(stdout) ?? { ok: true, text: stdout.trim() };
17489
+ }
17490
+ async function installStep(engine, asked, key, baseUrl, tell) {
17491
+ const step = asked.step;
17492
+ const model = asked.model;
17493
+ const consent = asked.consent;
17494
+ if (engine.place() === null) {
17495
+ let candidate = readCandidate(engine.versionsDir());
17496
+ if (candidate === null) {
17497
+ if (tell !== void 0) for (const line of PAYLOAD_LADDER) tell(line);
17498
+ const result = await installPayload(engine, {
17499
+ consent,
17500
+ manifestUrl: manifestUrl(engine, baseUrl),
17501
+ ...key !== void 0 ? { key } : {}
17502
+ });
17503
+ if (!result.ok) return toolError(result.text);
17504
+ candidate = readCandidate(engine.versionsDir());
17505
+ if (candidate === null) {
17506
+ return toolError("\u041D\u0430\u0447\u0438\u043D\u043A\u0430 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C \u0431\u0435\u0437 \u0443\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u044F \u043A\u0430\u043D\u0434\u0438\u0434\u0430\u0442\u0430 \u2014 current \u043D\u0435 \u043C\u0435\u043D\u044F\u043B. \u0427\u0442\u043E \u0434\u0435\u043B\u0430\u0442\u044C: \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0435 setup.");
17738
17507
  }
17739
- const plain = q.kind === "plain" ? ", \u0431\u0435\u0437 \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0438" : "";
17740
- const named = q.variants ?? [];
17741
- const variants = named.length > 0 ? named.map((v) => `${v.title} (${v.name})`).join(", ") : "\u0432\u0430\u0440\u0438\u0430\u043D\u0442\u044B \u0441\u0447\u0438\u0442\u0430\u044E\u0442\u0441\u044F \u043F\u043E \u043A\u0430\u0434\u0440\u0443 \u0440\u043E\u043B\u0438\u043A\u0430";
17742
- const total = typeof q.total === "number" ? q.total : named.length;
17743
- const rest = total > named.length ? ` \u0438 \u0435\u0449\u0451 ${total - named.length}` : "";
17744
- lines.push(`\u2014 ${tab}${q.title} (${q.name})${plain}: ${variants}${rest}.`);
17508
+ }
17509
+ if (!consent) {
17510
+ return toolError(
17511
+ `\u041A\u0430\u043D\u0434\u0438\u0434\u0430\u0442 ${candidate.version} \u0443\u0436\u0435 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043B\u0435\u043D, \u043D\u043E \u0435\u0449\u0451 \u043D\u0435 \u0440\u0430\u0431\u043E\u0447\u0438\u0439. \u0421\u043F\u0440\u043E\u0441\u0438 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0441\u043E\u0431\u0440\u0430\u0442\u044C \u0435\u0433\u043E \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 \u0438 \u043F\u043E\u0437\u043E\u0432\u0438 install \u0441 what=all.`
17512
+ );
17513
+ }
17514
+ const extras = step === "all" ? "later" : "now";
17515
+ const activated = await continuePreparedCandidate(engine, candidate, void 0, extras);
17516
+ if (!activated.ok) return toolError(activated.text);
17517
+ engine.forgetPython();
17518
+ if (step === "python") return toolSuccess(activated.text);
17519
+ if (step === "all") {
17520
+ const ready = `${activated.text}
17521
+ \u041A\u0430\u043D\u0434\u0438\u0434\u0430\u0442 \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u043B \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438 \u043F\u043E \u0441\u0432\u043E\u0435\u043C\u0443 \u0437\u0430\u043C\u043A\u0443 \u0438 \u043F\u0440\u043E\u0448\u0451\u043B env-check. \u0412\u0441\u0451 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u0433\u043E\u0442\u043E\u0432\u043E.`;
17522
+ const later = await installStep(engine, asked, key, baseUrl, tell);
17523
+ if (later.type === "error") {
17524
+ return toolSuccess(
17525
+ `${ready}
17526
+
17527
+ \u041E\u0441\u0442\u0430\u043B\u044C\u043D\u043E\u0435 \u0444\u043E\u043D\u043E\u043C \u0437\u0430\u0432\u0435\u0441\u0442\u0438 \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C: ${later.error}
17528
+ \u041C\u043E\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u044D\u0442\u043E \u043D\u0435 \u043C\u0435\u0448\u0430\u0435\u0442. \u0427\u0442\u043E \u0434\u0435\u043B\u0430\u0442\u044C: \u043F\u043E\u0437\u043E\u0432\u0438 install \u0441 what=all \u0435\u0449\u0451 \u0440\u0430\u0437.`
17529
+ );
17530
+ }
17531
+ return together([toolSuccess(`${ready}
17532
+ ${EXTRAS_LATER}`), later]);
17745
17533
  }
17746
17534
  }
17747
- const knobs = data.knobs ?? [];
17748
- if (knobs.length > 0) {
17749
- const said = knobs.map((k) => `${k.name} (${k.kind !== void 0 ? KNOB_KIND[k.kind] ?? k.kind : "\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435"})`).join(", ");
17750
- lines.push(`\u0420\u0443\u0447\u043A\u0438 \u043C\u043E\u043D\u0442\u0430\u0436\u0430, \u0438\u0445 \u043F\u0435\u0440\u0435\u0434\u0430\u044E\u0442 \u0432 render: ${said}.`);
17751
- }
17752
- const index = data.handbook?.index;
17753
- if (index !== void 0 && index.trim() !== "") {
17754
- lines.push(`\u0421\u041F\u0420\u0410\u0412\u041E\u0427\u041D\u0418\u041A \u041C\u041E\u041D\u0422\u0410\u0416\u0401\u0420\u0410 \u2014 \u043E\u0433\u043B\u0430\u0432\u043B\u0435\u043D\u0438\u0435, \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u044B \u0447\u0438\u0442\u0430\u0439 plan \u0441 read:
17755
- ${index.trim()}`);
17756
- }
17757
- const style = data.style;
17758
- if (style?.name !== void 0) {
17759
- lines.push(
17760
- style.own === true ? `\u0421\u0442\u0438\u043B\u044C: \u0441\u0432\u043E\u0439 \u043F\u0430\u043A \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \xAB${style.name}\xBB \u2014 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u0435 \u0437\u0430\u043F\u043E\u043C\u0438\u043D\u0430\u0435\u0442\u0441\u044F \u0432 \u043D\u0435\u0433\u043E.` : `\u0421\u0442\u0438\u043B\u044C: \u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043D\u044B\u0439 \xAB${style.name}\xBB, \u0441\u0432\u043E\u0435\u0433\u043E \u043F\u0430\u043A\u0430 \u0443 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u0435\u0449\u0451 \u043D\u0435\u0442.`
17761
- );
17762
- }
17763
- const mine = data.mine?.summary ?? [];
17764
- if (mine.length > 0) {
17765
- lines.push(`\u0421\u0432\u043E\u0451 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 (\u0437\u0430\u043F\u043E\u043C\u043D\u0435\u043D\u043E, \u0443\u0436\u0435 \u0432 \u043A\u0430\u0436\u0434\u043E\u043C \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u0435; \xAB\u0437\u0430\u0431\u0443\u0434\u044C\xBB \u2014 plan_edit forget):
17766
- ${mine.map((line) => `\u2014 ${line}`).join("\n")}`);
17535
+ const place = engine.place();
17536
+ if (place === null) return toolError(engine.whyNoEngine());
17537
+ if (consent && await engine.pythonTrouble() !== null) {
17538
+ try {
17539
+ await engine.bringPython(place);
17540
+ } catch (trouble) {
17541
+ return toolError(`\u041F\u0438\u0442\u043E\u043D \u043C\u043E\u043D\u0442\u0430\u0436\u0430 \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C: ${describeFailure(trouble)}. \u0427\u0442\u043E \u0434\u0435\u043B\u0430\u0442\u044C: \u043F\u043E\u0432\u0442\u043E\u0440\u0438 setup.`);
17542
+ }
17767
17543
  }
17768
- for (const said of data.warnings ?? []) lines.push(said);
17769
- return lines.join("\n");
17770
- }
17771
- function otherContract(said, expected) {
17772
- return `\u0414\u0432\u0438\u0436\u043E\u043A \u0433\u043E\u0432\u043E\u0440\u0438\u0442 \u043D\u0430 \u0434\u043E\u0433\u043E\u0432\u043E\u0440\u0435 \xAB${String(said)}\xBB, \u0430 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0435 \u043F\u043E\u043D\u0438\u043C\u0430\u0435\u0442 \xAB${expected}\xBB. \u041E\u0431\u043D\u043E\u0432\u0438 \u0442\u043E, \u0447\u0442\u043E \u0441\u0442\u0430\u0440\u0435\u0435: setup \u0441 what=engine \u0434\u043B\u044F \u0434\u0432\u0438\u0436\u043A\u0430, what=shell \u0434\u043B\u044F \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F.`;
17773
- }
17774
- async function askCatalog(engine, pack, video) {
17775
- const answer = await engine.command([
17776
- "capabilities",
17544
+ const common = [
17777
17545
  "--pack",
17778
- pack,
17779
- ...video !== void 0 ? ["--video", path(video)] : []
17780
- ]);
17781
- if (!answer.ok) return `\u0421\u043F\u0438\u0441\u043E\u043A \u0442\u043E\u0433\u043E, \u0447\u0442\u043E \u0443\u043C\u0435\u0435\u0442 \u043C\u043E\u043D\u0442\u0430\u0436, \u043D\u0435 \u043F\u0440\u0438\u0448\u0451\u043B: ${answer.text}`;
17782
- let data;
17783
- try {
17784
- data = JSON.parse(answer.text);
17785
- } catch {
17786
- return `\u0421\u043F\u0438\u0441\u043E\u043A \u0442\u043E\u0433\u043E, \u0447\u0442\u043E \u0443\u043C\u0435\u0435\u0442 \u043C\u043E\u043D\u0442\u0430\u0436, \u043D\u0435 \u043F\u0440\u0438\u0448\u0451\u043B: ${NOT_JSON}`;
17546
+ place.pack,
17547
+ "--step",
17548
+ step,
17549
+ ...model !== void 0 ? ["--model", model] : [],
17550
+ ...consent ? ["--consent"] : []
17551
+ ];
17552
+ if (!LONG_STEPS.has(step)) return asToolResult(await engine.command(["env-install", ...common]));
17553
+ const python = await engine.pythonTrouble();
17554
+ if (python !== null) return toolError(python);
17555
+ if (tell !== void 0) for (const line of installLadder(step)) tell(line);
17556
+ const launch = await engine.background({
17557
+ place,
17558
+ executable: engine.enginePython(),
17559
+ // Установка берёт тот же слот тяжёлой работы, а на слоте стоит заслон
17560
+ // подписки в самом движке — без ключа она откажет платящему.
17561
+ key,
17562
+ startJob: [
17563
+ "install-create",
17564
+ "--pack",
17565
+ place.pack,
17566
+ "--step",
17567
+ step,
17568
+ ...model !== void 0 ? ["--model", model] : []
17569
+ ],
17570
+ args: (id) => [place.consolePy, "env-install", ...common, "--job", id],
17571
+ advice: "\u043F\u043E\u0437\u043E\u0432\u0438 setup \u2014 \u0441\u043A\u043E\u0440\u0435\u0435 \u0432\u0441\u0435\u0433\u043E, \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043D\u0435\u043F\u043E\u043B\u043D\u0430\u044F"
17572
+ });
17573
+ if ("refusal" in launch) return toolError(launch.refusal);
17574
+ return toolSuccess(
17575
+ `\u0417\u0430\u0434\u0430\u0447\u0430 ${launch.id}: ${installTitle(step)} (\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430, \u0448\u0430\u0433 \xAB${step}\xBB).
17576
+ \u0418\u0434\u0451\u0442 \u043D\u0430 \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440\u0435 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F \u0438 \u043F\u0435\u0440\u0435\u0436\u0438\u0432\u0430\u0435\u0442 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u044B. \u041F\u043E\u0437\u043E\u0432\u0438 wait \u0441 job "${launch.id}" \u2014 \u043E\u043D \u043F\u043E\u0434\u043E\u0436\u0434\u0451\u0442 \u0438 \u043F\u043E \u0445\u043E\u0434\u0443 \u0440\u0430\u0441\u0441\u043A\u0430\u0436\u0435\u0442, \u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0441\u043A\u0430\u0447\u0430\u043D\u043E \u0438 \u043E \u0447\u0451\u043C \u043D\u0430\u0434\u043E \u0441\u043F\u0440\u043E\u0441\u0438\u0442\u044C \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430. \u041F\u0435\u0440\u0435\u0441\u043A\u0430\u0437\u044B\u0432\u0430\u0439 \u044D\u0442\u043E \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443 \u0441\u0440\u0430\u0437\u0443, \u0430 \u043D\u0435 \u0432 \u043A\u043E\u043D\u0446\u0435. ${AWAY_NOTE}`,
17577
+ startedJob({
17578
+ id: launch.id,
17579
+ what: installTitle(step),
17580
+ ahead: human(installLadder(step).join(" ")),
17581
+ message: human("\u0418\u0434\u0451\u0442 \u043C\u0438\u043D\u0443\u0442\u0430\u043C\u0438: \u043A\u0430\u0447\u0430\u0435\u0442\u0441\u044F \u0438 \u0441\u043E\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044F \u0442\u043E, \u0447\u0435\u043C \u043C\u043E\u043D\u0442\u0438\u0440\u0443\u0435\u0442\u0441\u044F \u0432\u0438\u0434\u0435\u043E.")
17582
+ })
17583
+ );
17584
+ }
17585
+ async function updateEngine(engine, key, baseUrl, timeoutMs = ASKED_POINTER_MS) {
17586
+ if (engine.pathSetByHand()) {
17587
+ return toolSuccess(
17588
+ "\u041F\u0430\u043F\u043A\u0430 \u0434\u0432\u0438\u0436\u043A\u0430 \u0437\u0430\u0434\u0430\u043D\u0430 \u0432\u0440\u0443\u0447\u043D\u0443\u044E \u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u043E\u0439 NAPARNIK_ENGINE \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E: \u0441\u0430\u043C\u043E\u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0441 \u0432\u0435\u0440\u0441\u0438\u043E\u043D\u043D\u043E\u0439 \u0440\u0430\u0441\u043A\u043B\u0430\u0434\u043A\u043E\u0439 \u0432 ~/.naparnik/engine, \u0430 \u044D\u0442\u0430 \u043F\u0430\u043F\u043A\u0430 \u043C\u0438\u043C\u043E \u043D\u0435\u0451. \u0421\u043A\u0430\u0436\u0438 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443, \u0447\u0442\u043E \u0434\u0432\u0438\u0436\u043E\u043A \u0437\u0434\u0435\u0441\u044C \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0442\u0435\u043C \u0436\u0435 \u0441\u043F\u043E\u0441\u043E\u0431\u043E\u043C, \u043A\u0430\u043A\u0438\u043C \u0431\u044B\u043B \u043F\u043E\u043B\u043E\u0436\u0435\u043D."
17589
+ );
17787
17590
  }
17788
- if (data.contract !== CAPABILITIES_CONTRACT) {
17789
- return `\u0414\u0432\u0438\u0436\u043E\u043A \u0433\u043E\u0432\u043E\u0440\u0438\u0442 \u043D\u0430 \u0434\u043E\u0433\u043E\u0432\u043E\u0440\u0435 \xAB${String(data.contract)}\xBB, \u0430 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0435 \u043F\u043E\u043D\u0438\u043C\u0430\u0435\u0442 \xAB${CAPABILITIES_CONTRACT}\xBB, \u0438 \u0441\u043F\u0438\u0441\u043E\u043A \u0442\u043E\u0433\u043E, \u0447\u0442\u043E \u0443\u043C\u0435\u0435\u0442 \u043C\u043E\u043D\u0442\u0430\u0436, \u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D. \u041E\u0431\u043D\u043E\u0432\u0438 \u0442\u043E, \u0447\u0442\u043E \u0441\u0442\u0430\u0440\u0435\u0435: setup \u0441 what=engine \u0434\u043B\u044F \u0434\u0432\u0438\u0436\u043A\u0430, what=shell \u0434\u043B\u044F \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F.`;
17591
+ if (engine.place() === null) {
17592
+ return toolSuccess(
17593
+ "\u0414\u0432\u0438\u0436\u043A\u0430 \u043C\u043E\u043D\u0442\u0430\u0436\u0430 \u043D\u0430 \u044D\u0442\u043E\u043C \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440\u0435 \u0435\u0449\u0451 \u043D\u0435\u0442 \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E.\n\u0427\u0442\u043E \u0434\u0435\u043B\u0430\u0442\u044C: \u043F\u043E\u0437\u043E\u0432\u0438 install \u0441 what=all, \u043E\u043D \u0441\u043A\u0430\u0447\u0430\u0435\u0442 \u043D\u0430\u0447\u0438\u043D\u043A\u0443 \u0441 \u043D\u0443\u043B\u044F (\u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0441\u043E\u0433\u043B\u0430\u0441\u0438\u044F \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430)."
17594
+ );
17790
17595
  }
17791
- return catalogInWords(data);
17792
- }
17793
- function fromWords(text2, contract) {
17794
- let data;
17795
- try {
17796
- data = JSON.parse(text2);
17797
- } catch {
17798
- return { refusal: NOT_JSON };
17596
+ const outcome = await fetchManifest(manifestUrl(engine, baseUrl), timeoutMs, key);
17597
+ const decision = decide(engine.freshVersion(), outcome.ok ? outcome.manifest : null);
17598
+ if (decision.what === "nothing") {
17599
+ const ownDir2 = engine.freshVersion();
17600
+ return toolSuccess(
17601
+ !outcome.ok ? `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043F\u0440\u043E\u0441\u0438\u0442\u044C, \u0435\u0441\u0442\u044C \u043B\u0438 \u043D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u043D\u0430\u0447\u0438\u043D\u043A\u0438: ${outcome.why}. \u042D\u0442\u043E \u043D\u0435 \u043F\u043E\u043B\u043E\u043C\u043A\u0430: \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F (${ownDir2 ?? "\u043D\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430"}) \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043A\u0430\u043A \u0440\u0430\u0431\u043E\u0442\u0430\u043B\u0430.` : `\u041D\u0430\u0447\u0438\u043D\u043A\u0430 \u0441\u0432\u0435\u0436\u0430\u044F: \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 ${ownDir2 ?? "\u043D\u0435\u0442"}, \u043D\u043E\u0432\u0435\u0435 \u043D\u0435 \u0432\u044B\u0448\u043B\u043E.`
17602
+ );
17799
17603
  }
17800
- if (contract !== void 0 && data.contract !== contract) {
17801
- return { refusal: otherContract(data.contract, contract) };
17604
+ if (decision.what === "shell_too_old") {
17605
+ return toolSuccess(
17606
+ `\u041D\u043E\u0432\u0430\u044F \u043D\u0430\u0447\u0438\u043D\u043A\u0430 ${decision.manifest.version} \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u043D\u0435 \u0441\u0442\u0430\u0440\u0448\u0435 ${decision.manifest.minShellVersion}, \u0430 \u0441\u0442\u043E\u0438\u0442 ${VERSION}. \u0421\u043A\u0430\u0436\u0438 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443, \u0447\u0442\u043E \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u043D\u0430\u0434\u043E \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u0430\u043C\u043E \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0435 \u041D\u0430\u043F\u0430\u0440\u043D\u0438\u043A\u0430, \u0438 \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u0442\u043E\u043C \u043D\u0430\u0447\u0438\u043D\u043A\u0443. \u0421\u0442\u0430\u0432\u0438\u0442\u044C \u043D\u0435 \u0441\u0442\u0430\u043B.`
17607
+ );
17802
17608
  }
17803
- if (typeof data.words !== "string" || data.words === "") return { refusal: NOT_JSON };
17804
- return { words: data.words, data };
17609
+ return asToolResult(await deploy(engine, decision.manifest, void 0, key));
17805
17610
  }
17806
- function folderAnswer(text2) {
17807
- const read = fromWords(text2);
17808
- return "refusal" in read ? toolError(read.refusal) : toolSuccess(read.words, read.data);
17611
+ function setupTool(engine, passport, key, baseUrl) {
17612
+ return defineTool({
17613
+ name: "setup",
17614
+ title: "\u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C, \u0432\u0441\u0451 \u043B\u0438 \u0433\u043E\u0442\u043E\u0432\u043E \u0434\u043B\u044F \u043C\u043E\u043D\u0442\u0430\u0436\u0430",
17615
+ description: "\u041F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u0442 \u0434\u0435\u043B\u043E\u043C, \u0447\u0442\u043E \u0441\u0442\u043E\u0438\u0442 \u0438 \u0447\u0435\u0433\u043E \u043D\u0435 \u0445\u0432\u0430\u0442\u0430\u0435\u0442, \u0447\u0442\u043E \u0434\u043E\u043A\u0430\u0447\u0430\u0435\u0442\u0441\u044F \u043F\u043E \u043F\u0440\u043E\u0441\u044C\u0431\u0435 \u0438 \u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0432\u0435\u0441\u0438\u0442, \u0438 \u0447\u0442\u043E \u0443\u043C\u0435\u0435\u0442 \u043C\u043E\u043D\u0442\u0430\u0436 (\u0448\u0430\u0433\u0438, \u0432\u043E\u043F\u0440\u043E\u0441\u044B \u0434\u043B\u044F choose_variant, \u0447\u0435\u0439 \u0441\u0442\u0438\u043B\u044C). \u0417\u043E\u0432\u0438 \u041F\u0415\u0420\u0412\u042B\u041C, \u0434\u043E \u0440\u0430\u0431\u043E\u0442\u044B \u0441 \u0432\u0438\u0434\u0435\u043E. \u0421\u0442\u0430\u0432\u0438\u0442 install. \u0421\u043B\u043E\u043C\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u0441\u043B\u0435 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F \u2014 rollback.",
17616
+ args: external_exports.object({
17617
+ model: fromList(
17618
+ passport?.["models"],
17619
+ `\u041C\u043E\u0434\u0435\u043B\u044C \u0440\u0430\u0441\u043F\u043E\u0437\u043D\u0430\u0432\u0430\u043D\u0438\u044F \u0440\u0435\u0447\u0438, \u043A\u0430\u043A\u0443\u044E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C. ${aboutModels(passport)}`
17620
+ ).optional(),
17621
+ rollback: external_exports.enum(["engine", "shell"]).describe(
17622
+ "\u0412\u0435\u0440\u043D\u0443\u0442\u044C \u043F\u0440\u0435\u0436\u043D\u044E\u044E \u0432\u0435\u0440\u0441\u0438\u044E: engine \u2014 \u0434\u0432\u0438\u0436\u043E\u043A, shell \u2014 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0435"
17623
+ ).optional()
17624
+ }),
17625
+ async execute(args) {
17626
+ if (args.rollback === "engine") return asToolResult(rollback(engine));
17627
+ if (args.rollback === "shell") return asToolResult(rollbackShell(engine.home()));
17628
+ return lookAround(engine, args.model, key, baseUrl);
17629
+ }
17630
+ });
17809
17631
  }
17810
- var DRAFT_CONTRACT = "naparnik-draft/1";
17811
- var VIEW_CONTRACT = "naparnik-view/1";
17812
- var EDIT_CONTRACT = "naparnik-edit/1";
17813
- function answerOrRefusal(text2, contract) {
17814
- const read = fromWords(text2, contract);
17815
- if ("refusal" in read) {
17816
- const plain = text2.trim();
17817
- return { refusal: plain.startsWith("{") || plain === "" ? read.refusal : plain };
17632
+ async function lookAround(engine, model, key, baseUrl) {
17633
+ if (engine.place() === null) {
17634
+ const payload = await installPayload(engine, {
17635
+ consent: false,
17636
+ manifestUrl: manifestUrl(engine, baseUrl),
17637
+ ...key !== void 0 ? { key } : {}
17638
+ });
17639
+ return toolSuccess(`${payload.text}
17640
+ ${INSTALL_HINT}`);
17818
17641
  }
17819
- return read.data["ok"] === false ? { refusal: read.words } : read;
17820
- }
17821
- async function draftDocument(engine, place, asked, key) {
17822
- const answer = await engine.command([
17823
- "document-draft",
17824
- "--video",
17825
- asked.video,
17826
- "--pack",
17827
- place.pack,
17828
- ...asked.style !== void 0 ? ["--style", path(asked.style)] : [],
17829
- ...(asked.also ?? []).flatMap((one) => ["--also", path(one)]),
17830
- ...asked.ask !== void 0 ? ["--ask", JSON.stringify(asked.ask)] : [],
17831
- "--json"
17832
- ], key !== void 0 ? { key } : {});
17833
- return answer.ok ? answerOrRefusal(answer.text, DRAFT_CONTRACT) : { refusal: answer.text };
17834
- }
17835
- async function editOperations(engine, place, asked, key) {
17836
- const answer = await engine.command([
17837
- "document-edit",
17838
- "--project",
17839
- path(asked.reel),
17840
- "--ops",
17841
- JSON.stringify(asked.ops),
17842
- ...asked.stamps !== void 0 ? ["--stamps", JSON.stringify(asked.stamps)] : [],
17843
- "--by",
17844
- asked.by ?? "model",
17845
- "--pack",
17846
- place.pack,
17847
- "--json"
17848
- ], key !== void 0 ? { key } : {});
17849
- return answer.ok ? answerOrRefusal(answer.text, EDIT_CONTRACT) : { refusal: answer.text };
17850
- }
17851
- function nameOrRefusal(candidate) {
17852
- const looksLikePath2 = candidate.includes("/") || candidate.includes("\\") || candidate.startsWith("~") || candidate.startsWith(".") || candidate.length > 1 && candidate[1] === ":";
17853
- if (!looksLikePath2) return { name: candidate };
17854
- return {
17855
- refusal: `\u0417\u0434\u0435\u0441\u044C \u0436\u0434\u0443\u0442 \u0418\u041C\u042F \u0448\u0430\u0433\u0430, \u0430 \u043F\u0440\u0438\u0448\u0451\u043B \u043F\u0443\u0442\u044C: ${candidate}
17856
- \u0427\u0442\u043E \u0434\u0435\u043B\u0430\u0442\u044C: \u043D\u0430\u0437\u043E\u0432\u0438 \u0448\u0430\u0433 \u043E\u0434\u043D\u0438\u043C \u0441\u043B\u043E\u0432\u043E\u043C \u2014 \u0442\u0430\u043A, \u043A\u0430\u043A \u043D\u0430\u0437\u044B\u0432\u0430\u0435\u0442\u0441\u044F \u0435\u0433\u043E \u043F\u0430\u043F\u043A\u0430. \u0421\u0432\u043E\u0439 \u0448\u0430\u0433 \u043B\u0435\u0436\u0438\u0442 \u0432 \`mine/steps/<\u0438\u043C\u044F>\` \u0432 \u0434\u043E\u043C\u0435 \u041D\u0430\u043F\u0430\u0440\u043D\u0438\u043A\u0430, \u043D\u0430\u0448\u0438 \u043F\u0435\u0440\u0435\u0447\u0438\u0441\u043B\u044F\u0435\u0442 setup, \u0438\u043C\u044F \u0443 \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0441\u0442\u043E\u0438\u0442 \u0432 \u0441\u043A\u043E\u0431\u043A\u0430\u0445.`
17857
- };
17642
+ const noPython = await engine.pythonTrouble();
17643
+ if (noPython !== null) return toolSuccess(`${noPython}
17644
+ ${INSTALL_HINT}`);
17645
+ const machine = await inspectMachine(engine, { model });
17646
+ if (machine.result.type === "error") return machine.result;
17647
+ return toolSuccess(`${wordsOf(machine.result.content)}
17648
+
17649
+ ${INSTALL_HINT}`);
17858
17650
  }
17859
- var SETUP_JOB_MARK = "setup-";
17860
- function isSetupJob(id) {
17861
- return id.startsWith(SETUP_JOB_MARK);
17651
+ var INSTALL_HINT = "\u0421\u0442\u0430\u0432\u0438\u0442 install \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u0441\u043B\u0435 \xAB\u0434\u0430\xBB \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u043D\u0430 \u043D\u0430\u0437\u0432\u0430\u043D\u043D\u043E\u0435 \u0441 \u0432\u0435\u0441\u043E\u043C: what=all \u2014 \u0432\u0441\u0451 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u0438 \u043D\u043E\u0432\u044B\u0435 \u0432\u0435\u0440\u0441\u0438\u0438; speech, cutout, voice \u2014 \u0442\u044F\u0436\u0451\u043B\u043E\u0435 \u043F\u043E \u043F\u0440\u043E\u0441\u044C\u0431\u0435. \u0413\u0434\u0435 \u043D\u0430\u043F\u0438\u0441\u0430\u043D\u043E \xABsetup \u0441 what=\u2026\xBB, \u0437\u043E\u0432\u0438 install \u0441 \u0442\u0435\u043C \u0436\u0435 what.";
17652
+ function installTool(engine, passport, key, baseUrl) {
17653
+ return defineTool({
17654
+ name: "install",
17655
+ title: "\u041F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0434\u043B\u044F \u043C\u043E\u043D\u0442\u0430\u0436\u0430",
17656
+ description: "\u0421\u0442\u0430\u0432\u0438\u0442 \u0438 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0435\u0442 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u0441\u043B\u0435 \xAB\u0434\u0430\xBB \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u043D\u0430 \u0442\u043E, \u0447\u0442\u043E \u043D\u0430\u0437\u0432\u0430\u043B\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 setup \u0441 \u0432\u0435\u0441\u043E\u043C. \u041E\u0442\u0432\u0435\u0442 \u2014 \u043D\u043E\u043C\u0435\u0440 \u0437\u0430\u0434\u0430\u0447\u0438: \u0436\u0434\u0438 wait. \u041F\u043E\u0432\u0442\u043E\u0440\u043D\u044B\u0439 \u0432\u044B\u0437\u043E\u0432 \u0432\u0435\u0440\u043D\u0451\u0442 \u0442\u0443 \u0436\u0435 \u0437\u0430\u0434\u0430\u0447\u0443.",
17657
+ ui: { resourceUri: RUNBAR_RESOURCE_URI },
17658
+ args: external_exports.object({
17659
+ what: external_exports.enum(["all", "speech", "cutout", "voice"]).describe(
17660
+ "all \u2014 \u0432\u0441\u0451 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u0438 \u043D\u043E\u0432\u044B\u0435 \u0432\u0435\u0440\u0441\u0438\u0438; speech \u2014 \u0440\u0430\u0441\u043F\u043E\u0437\u043D\u0430\u0432\u0430\u043D\u0438\u0435 \u0440\u0435\u0447\u0438 (\u0441\u0443\u0431\u0442\u0438\u0442\u0440\u044B); cutout \u2014 \u0432\u044B\u0440\u0435\u0437 \u0433\u0435\u0440\u043E\u044F (\u043D\u043E\u0432\u044B\u0439 \u0444\u043E\u043D); voice \u2014 \u0447\u0438\u0441\u0442\u043A\u0430 \u0433\u043E\u043B\u043E\u0441\u0430"
17661
+ ),
17662
+ model: fromList(
17663
+ passport?.["models"],
17664
+ `\u041C\u043E\u0434\u0435\u043B\u044C \u0440\u0430\u0441\u043F\u043E\u0437\u043D\u0430\u0432\u0430\u043D\u0438\u044F \u0440\u0435\u0447\u0438 \u0434\u043B\u044F what=speech. ${aboutModels(passport)}`
17665
+ ).optional()
17666
+ }),
17667
+ async execute(args) {
17668
+ return startInstall(engine, args.what, args.model, key, baseUrl);
17669
+ }
17670
+ });
17862
17671
  }
17863
- var TITLE_UPDATE = "\u041E\u0431\u043D\u043E\u0432\u043B\u044F\u044E \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438 \u0434\u043B\u044F \u043C\u043E\u043D\u0442\u0430\u0436\u0430";
17864
- var TITLE_INSTALL = "\u0423\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u044E \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438 \u0434\u043B\u044F \u043C\u043E\u043D\u0442\u0430\u0436\u0430";
17865
- var SAY_RESTART = "\u0413\u043E\u0442\u043E\u0432\u043E. \u041F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438 Claude \u2014 \u043D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u0437\u0430\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043F\u043E\u0441\u043B\u0435 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430.";
17866
- var SAY_NOTHING = "\u0412\u0441\u0451 \u0443\u0436\u0435 \u0441\u0442\u043E\u0438\u0442 \u2014 \u043C\u043E\u0436\u043D\u043E \u043C\u043E\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C.";
17867
- var SAY_INSTALLED = "\u0413\u043E\u0442\u043E\u0432\u043E \u2014 \u043C\u043E\u0436\u043D\u043E \u043C\u043E\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C.";
17868
- var ADVICE_ASK = "\u041D\u0430\u043F\u0438\u0448\u0438 \u041A\u043B\u043E\u0434\u0443 \xAB\u0447\u0442\u043E \u0441\u043B\u0443\u0447\u0438\u043B\u043E\u0441\u044C\xBB \u2014 \u043E\u043D \u0440\u0430\u0441\u0441\u043A\u0430\u0436\u0435\u0442, \u0447\u0442\u043E \u043F\u043E\u0448\u043B\u043E \u043D\u0435 \u0442\u0430\u043A.";
17869
- var CHECK = "\u041F\u0440\u043E\u0432\u0435\u0440\u044F\u044E, \u0447\u0442\u043E \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C";
17870
- var INSTALL_NEW = "\u0421\u0442\u0430\u0432\u043B\u044E \u043D\u043E\u0432\u0443\u044E \u0432\u0435\u0440\u0441\u0438\u044E";
17871
- var MISSING = "\u0421\u0442\u0430\u0432\u043B\u044E \u043D\u0435\u0434\u043E\u0441\u0442\u0430\u044E\u0449\u0435\u0435";
17872
- var works = /* @__PURE__ */ new Map();
17873
- var JOBS_DIR = "jobs";
17874
- var JOBS_KEPT2 = 10;
17875
- var BEAT_MS = 1e4;
17876
- var STALE_MS = 6e4;
17877
- var JOB_ID = /^setup-[\w-]{1,80}$/;
17878
- function running(home, clock) {
17879
- for (const work of works.values()) if (work.ended === null) return work.id;
17880
- if (home === null) return null;
17881
- for (const record2 of records(home)) {
17882
- if (record2.ended === null && alive(record2, clock)) return record2.id;
17672
+ function startInstall(engine, what, model, key, baseUrl) {
17673
+ const id = what === "all" ? startSetupJob(engine.place() === null ? TITLE_INSTALL : TITLE_UPDATE, (job) => runUpdate(job, {
17674
+ engine,
17675
+ key,
17676
+ baseUrl,
17677
+ what: "all",
17678
+ pointerMs: ASKED_POINTER_MS,
17679
+ installMissing: () => installMissing(engine, key, baseUrl)
17680
+ }), Date.now, engine.home()) : startSetupJob(installTitle(HEAVY_STEPS[what]), (job) => runHeavy(job, {
17681
+ engine,
17682
+ title: installTitle(HEAVY_STEPS[what]),
17683
+ start: () => installStep(engine, { step: HEAVY_STEPS[what], model, consent: true }, key, baseUrl)
17684
+ }), Date.now, engine.home());
17685
+ const report = setupJobReport(id, engine.home());
17686
+ const ahead = what === "all" ? "" : `${installLadder(HEAVY_STEPS[what]).join(" ")}
17687
+ `;
17688
+ return toolSuccess(
17689
+ `\u0417\u0430\u0434\u0430\u0447\u0430 ${id}: ${report?.what ?? what}.
17690
+ ${ahead}\u0418\u0434\u0451\u0442 \u043D\u0430 \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440\u0435 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F, \u043F\u043E\u043B\u043E\u0441\u0430 \u0443 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0441\u0430\u043C\u0430. \u041F\u043E\u0437\u043E\u0432\u0438 wait \u0441 job "` + id + '" \u2014 \u0434\u043E\u0436\u0434\u0451\u0442\u0441\u044F \u0438 \u0441\u043A\u0430\u0436\u0435\u0442, \u0447\u0435\u043C \u043A\u043E\u043D\u0447\u0438\u043B\u043E\u0441\u044C; install \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u043E \u043D\u0435 \u0437\u043E\u0432\u0438. \u0413\u043E\u0442\u043E\u0432\u043E \u2014 \u043F\u0435\u0440\u0435\u0441\u043A\u0430\u0436\u0438 \u0441\u0442\u0440\u043E\u043A\u0443 \xAB\u0427\u0435\u043B\u043E\u0432\u0435\u043A\u0443\xBB.',
17691
+ report ?? void 0
17692
+ );
17693
+ }
17694
+ var HEAVY_STEPS = {
17695
+ speech: "model",
17696
+ cutout: "cutout",
17697
+ voice: "voice"
17698
+ };
17699
+ async function installMissing(engine, key, baseUrl) {
17700
+ const place = engine.place();
17701
+ if (place !== null && await engine.pythonTrouble() === null && await nothingMissing(engine, place)) {
17702
+ return toolSuccess("\u0412\u0441\u0451 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u0434\u043B\u044F \u043C\u043E\u043D\u0442\u0430\u0436\u0430 \u043D\u0430 \u043C\u0435\u0441\u0442\u0435 \u2014 \u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E.");
17883
17703
  }
17884
- return null;
17704
+ return installStep(engine, { step: "all", consent: true }, key, baseUrl);
17885
17705
  }
17886
- function records(home) {
17887
- let names;
17706
+ async function nothingMissing(engine, place) {
17888
17707
  try {
17889
- names = readdirSync(join(home, JOBS_DIR)).filter((name) => name.endsWith(".json"));
17708
+ await engine.run(place, ["env-check", "--pack", place.pack, "--first-install"]);
17709
+ return true;
17890
17710
  } catch {
17891
- return [];
17711
+ return false;
17892
17712
  }
17893
- return names.sort().reverse().map((name) => load(home, name.slice(0, -".json".length))).filter((record2) => record2 !== null);
17894
17713
  }
17895
- function load(home, id) {
17896
- if (!JOB_ID.test(id)) return null;
17897
- try {
17898
- const value = JSON.parse(readFileSync(join(home, JOBS_DIR, `${id}.json`), "utf8"));
17899
- if (value.id !== id || typeof value.what !== "string" || typeof value.started !== "number" || !Array.isArray(value.stages) || typeof value.at !== "number" || typeof value.pid !== "number" || typeof value.beat !== "number") return null;
17714
+ function together(results) {
17715
+ const text2 = results.map((one) => one.type === "success" ? wordsOf(one.content) : one.error).filter((words) => words !== "").join("\n\n");
17716
+ for (const one of results) {
17717
+ if (one.type === "success" && one.structured !== void 0) return toolSuccess(text2, one.structured);
17718
+ }
17719
+ return results.some((one) => one.type === "error") ? toolError(text2) : toolSuccess(text2);
17720
+ }
17721
+ function wordsOf(content) {
17722
+ if (typeof content === "string") return content;
17723
+ return content.map((block) => block.type === "text" ? block.text : "").join("\n");
17724
+ }
17725
+ var CALLS_FILE = "calls.jsonl";
17726
+ var CALLS_PREVIOUS = "calls.1.jsonl";
17727
+ var CALLS_CAP_BYTES = 1e6;
17728
+ function withCallLog(home, now = () => Date.now()) {
17729
+ return (tool) => {
17730
+ const original = tool.execute.bind(tool);
17900
17731
  return {
17901
- id,
17902
- what: value.what,
17903
- started: value.started,
17904
- stages: value.stages.map(String),
17905
- at: value.at,
17906
- message: typeof value.message === "string" ? value.message : "",
17907
- ended: value.ended ?? null,
17908
- pid: value.pid,
17909
- beat: value.beat
17732
+ ...tool,
17733
+ async execute(input, ...rest) {
17734
+ const started = now();
17735
+ const result = await original(input, ...rest);
17736
+ write(home, {
17737
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
17738
+ tool: tool.name,
17739
+ ok: result.type === "success",
17740
+ ms: now() - started
17741
+ });
17742
+ return result;
17743
+ }
17910
17744
  };
17911
- } catch {
17912
- return null;
17913
- }
17745
+ };
17914
17746
  }
17915
- function alive(record2, clock) {
17916
- if (clock() - record2.beat > STALE_MS) return false;
17747
+ function write(home, line) {
17917
17748
  try {
17918
- process.kill(record2.pid, 0);
17919
- return true;
17920
- } catch (trouble) {
17921
- return trouble.code === "EPERM";
17749
+ mkdirSync(home, { recursive: true });
17750
+ const file2 = join(home, CALLS_FILE);
17751
+ rotate(file2, join(home, CALLS_PREVIOUS));
17752
+ appendFileSync(file2, `${JSON.stringify(line)}
17753
+ `, { encoding: "utf8", mode: 384 });
17754
+ } catch {
17922
17755
  }
17923
17756
  }
17924
- function save(work, clock) {
17925
- if (work.home === null) return;
17757
+ function rotate(file2, previous) {
17926
17758
  try {
17927
- const dir = join(work.home, JOBS_DIR);
17928
- mkdirSync(dir, { recursive: true });
17929
- const file2 = join(dir, `${work.id}.json`);
17930
- const temp = `${file2}.${process.pid}.tmp`;
17931
- const record2 = {
17932
- id: work.id,
17933
- what: work.what,
17934
- started: work.started,
17935
- stages: work.stages,
17936
- at: work.at,
17937
- message: work.message,
17938
- ended: work.ended,
17939
- pid: process.pid,
17940
- beat: clock()
17941
- };
17942
- writeFileSync(temp, JSON.stringify(record2), "utf8");
17943
- renameSync(temp, file2);
17759
+ if (statSync(file2).size >= CALLS_CAP_BYTES) renameSync(file2, previous);
17944
17760
  } catch {
17945
17761
  }
17946
- }
17947
- function prune2(home) {
17948
- try {
17949
- const dir = join(home, JOBS_DIR);
17950
- const names = readdirSync(dir).filter((name) => JOB_ID.test(name.replace(/\.json$/, ""))).sort();
17951
- for (const name of names.slice(0, Math.max(0, names.length - JOBS_KEPT2))) rmSync(join(dir, name), { force: true });
17952
- } catch {
17762
+ }
17763
+
17764
+ // src/contracts/crash-report.ts
17765
+ var CRASH_REPORT_PATH = "/crash-report";
17766
+ var CRASH_CONTRACT = "naparnik-crash/1";
17767
+ var CRASH_LIMITS = {
17768
+ bodyBytes: 65536,
17769
+ errorType: 120,
17770
+ message: 500,
17771
+ trace: 8e3,
17772
+ stage: 120,
17773
+ osPlatform: 32,
17774
+ osRelease: 64,
17775
+ osArch: 16,
17776
+ hostName: 64,
17777
+ hostVersion: 32,
17778
+ shellVersion: 32,
17779
+ engineVersion: 64,
17780
+ pythonVersion: 16,
17781
+ ffmpegVersion: 64,
17782
+ recentCalls: 10,
17783
+ repeats: 1e5
17784
+ };
17785
+ var CRASH_TOOL_NAME = /^[a-z_]{1,64}$/;
17786
+ var CRASH_FINGERPRINT = /^[0-9a-f]{16}$/;
17787
+ var text = (max) => external_exports.string().max(max);
17788
+ var CrashReportRequest = external_exports.object({
17789
+ contract: external_exports.literal(CRASH_CONTRACT),
17790
+ occurred_at: external_exports.string().datetime({ offset: true }),
17791
+ where: external_exports.enum(["tool", "job", "process"]),
17792
+ tool: external_exports.string().regex(CRASH_TOOL_NAME).nullable(),
17793
+ stage: text(CRASH_LIMITS.stage).nullable(),
17794
+ error_type: external_exports.string().min(1).max(CRASH_LIMITS.errorType),
17795
+ message: text(CRASH_LIMITS.message),
17796
+ trace: text(CRASH_LIMITS.trace),
17797
+ fingerprint: external_exports.string().regex(CRASH_FINGERPRINT),
17798
+ repeats: external_exports.number().int().min(0).max(CRASH_LIMITS.repeats),
17799
+ os: external_exports.object({
17800
+ platform: text(CRASH_LIMITS.osPlatform),
17801
+ release: text(CRASH_LIMITS.osRelease),
17802
+ arch: text(CRASH_LIMITS.osArch)
17803
+ }).strict(),
17804
+ host: external_exports.object({
17805
+ name: text(CRASH_LIMITS.hostName),
17806
+ version: text(CRASH_LIMITS.hostVersion)
17807
+ }).strict().nullable(),
17808
+ versions: external_exports.object({
17809
+ shell: text(CRASH_LIMITS.shellVersion),
17810
+ engine: text(CRASH_LIMITS.engineVersion).nullable(),
17811
+ python: text(CRASH_LIMITS.pythonVersion).nullable(),
17812
+ ffmpeg: text(CRASH_LIMITS.ffmpegVersion).nullable()
17813
+ }).strict(),
17814
+ recent_calls: external_exports.array(external_exports.string().regex(CRASH_TOOL_NAME)).max(CRASH_LIMITS.recentCalls)
17815
+ }).strict();
17816
+ var SECRET_MIN = 4;
17817
+ var HUMAN_FILE = /[^\s"'«»“”/\\<>()]{0,255}\.(?:mp4|mov|m4v|mkv|avi|webm|mts|wav|mp3|m4a|aac|flac|ogg|jpg|jpeg|png|heic|gif|srt|vtt)\b/gi;
17818
+ var PATH = /(?:<naparnik>|<shell>|~|[A-Za-z]:)?(?:[\\/][^\s"'«»“”<>|,;()[\]{}\\/]+)+[\\/]?/g;
17819
+ var QUOTED = /(["'])([^"'\n]*)\1|«([^»\n]*)»|“([^”\n]*)”/g;
17820
+ var KEY = /npk_[A-Za-z0-9_-]{6,}/g;
17821
+ function scrub(text2, context) {
17822
+ let out = text2.replace(KEY, "<key>");
17823
+ for (const secret of secretForms(context.secrets)) {
17824
+ out = replaceAll(out, secret, "<video>");
17825
+ }
17826
+ out = replaceAll(out, context.shellDir, "<shell>");
17827
+ out = replaceAll(out, context.naparnikHome, "<naparnik>");
17828
+ out = replaceAll(out, context.home, "~");
17829
+ out = out.replace(/(?:\/Users|\/home|[A-Za-z]:\\Users|[A-Za-z]:\/Users)[\\/][^\s"'«»“”\\/]+/g, "~");
17830
+ out = out.replace(QUOTED, (whole, _q, plain, fir, curly) => {
17831
+ const inside2 = plain ?? fir ?? curly ?? "";
17832
+ if (!looksHuman(inside2)) return whole;
17833
+ const [open, close] = plain !== void 0 ? [_q, _q] : fir !== void 0 ? ["\xAB", "\xBB"] : ["\u201C", "\u201D"];
17834
+ return `${open}${looksLikePath(inside2) ? pathOrMark(inside2) : "<text>"}${close}`;
17835
+ });
17836
+ out = out.replace(PATH, (found) => pathOrMark(found));
17837
+ out = out.replace(HUMAN_FILE, "<file>");
17838
+ out = swallowSpacedTails(out);
17839
+ if (context.user.length >= 3) out = replaceAll(out, context.user, "<user>");
17840
+ return out;
17841
+ }
17842
+ function swallowSpacedTails(text2) {
17843
+ return text2.replace(/<(?:path|file)>[^\n:<>"'«»“”]{0,512}<(?:path|file)>/g, "<path>").replace(/(<(?:path|file)>)([^\n:<>"'«»“”]{0,512})/g, (whole, mark, tail2) => /[^\x00-\x7F]/.test(tail2) ? mark : whole);
17844
+ }
17845
+ function secretForms(secrets) {
17846
+ const forms = /* @__PURE__ */ new Set();
17847
+ for (const raw of secrets) {
17848
+ const value = raw.trim();
17849
+ if (value.length < SECRET_MIN) continue;
17850
+ forms.add(value);
17851
+ if (!looksLikePath(value)) continue;
17852
+ if (isAbsolute(value)) forms.add(resolve(value));
17853
+ const name = basename(value);
17854
+ const stem = name.slice(0, name.length - extname(name).length);
17855
+ for (const one of [name, stem, `${stem}.montage`]) {
17856
+ if (one.length >= SECRET_MIN) forms.add(one);
17857
+ }
17858
+ }
17859
+ return [...forms].sort((a, b) => b.length - a.length);
17860
+ }
17861
+ function pathOrMark(found) {
17862
+ const flat = found.replace(/\\/g, "/");
17863
+ const ours = /^(?:<naparnik>|~\/\.naparnik)\/(?:engine|shell|bin)(?:\/|$)/.test(flat) || flat.startsWith("<shell>") || /\/(?:site-packages|dist-packages|node_modules)\//.test(flat) || /\/lib\/python\d/i.test(flat) || /\/Python\d+\/Lib\//i.test(flat);
17864
+ return ours ? found : "<path>";
17865
+ }
17866
+ var HUMAN_FILE_ONE = new RegExp(HUMAN_FILE.source, "i");
17867
+ function looksLikePath(value) {
17868
+ return /[\\/]/.test(value) || HUMAN_FILE_ONE.test(value);
17869
+ }
17870
+ function looksHuman(inside2) {
17871
+ return /[^\x00-\x7F]/.test(inside2) || /\s/.test(inside2) || looksLikePath(inside2);
17872
+ }
17873
+ function replaceAll(text2, what, by) {
17874
+ if (what === "") return text2;
17875
+ return text2.split(what).join(by);
17876
+ }
17877
+
17878
+ // src/crash-report.ts
17879
+ var RESEND_AFTER_MS = 30 * 6e4;
17880
+ var QUEUE_CAP = 20;
17881
+ var FLUSH_PER_PASS = 5;
17882
+ var FLUSH_EVERY_MS = 6e4;
17883
+ var SEND_TIMEOUT_MS = 3e3;
17884
+ var JOB_LOG_TAIL = 64 * 1024;
17885
+ var PYTHON_TRACEBACK = "Traceback (most recent call last):";
17886
+ var SUPPORT_TOPIC = "https://t.me/c/3763431875/10";
17887
+ var WORDS_GRACE_MS = 1e3;
17888
+ var CrashReporter = class {
17889
+ #deps;
17890
+ #now;
17891
+ #inflight = /* @__PURE__ */ new Set();
17892
+ #lastFlush = Number.NEGATIVE_INFINITY;
17893
+ #host = null;
17894
+ /**
17895
+ * Строки из аргументов недавних вызовов — только в памяти процесса.
17896
+ *
17897
+ * ⚠ ЗАЧЕМ. Задание упало — отчёт о нём приходит на `wait`, а в аргументах
17898
+ * `wait` лишь номер задания; путь к ролику был в аргументах `render`,
17899
+ * который задание запустил. Без этой памяти путь с пробелами из трейса ffmpeg
17900
+ * вычищался бы только общими правилами, а они его режут на куски (ревью
17901
+ * безопасности 2026-09-24). На диск не пишется: это ровно то, что мы
17902
+ * обещали не хранить.
17903
+ */
17904
+ #recentSecrets = [];
17905
+ #ffmpeg = null;
17906
+ constructor(deps) {
17907
+ this.#deps = deps;
17908
+ this.#now = deps.now ?? (() => Date.now());
17909
+ }
17910
+ get canSend() {
17911
+ return this.#deps.canSend;
17912
+ }
17913
+ get home() {
17914
+ return this.#deps.home;
17915
+ }
17916
+ /** Запомнить программу-хост: при падении всего процесса контекста вызова нет. */
17917
+ /** Запомнить строки аргументов вызова — чтобы вычеркнуть их из будущих отчётов. */
17918
+ noteSecrets(secrets) {
17919
+ const fresh = secrets.filter((one) => one.length >= 4);
17920
+ if (fresh.length === 0) return;
17921
+ this.#recentSecrets = [...this.#recentSecrets.filter((one) => !fresh.includes(one)), ...fresh].slice(-50);
17922
+ }
17923
+ noteHost(host) {
17924
+ if (host !== null) this.#host = host;
17953
17925
  }
17954
- }
17955
- function fromDisk(home, id, clock) {
17956
- const record2 = load(home, id);
17957
- if (record2 === null) return null;
17958
- if (record2.ended !== null || alive(record2, clock)) return record2;
17959
- return {
17960
- ...record2,
17961
- ended: {
17962
- at: record2.beat,
17963
- ending: {
17964
- ok: false,
17965
- trouble: "\u041E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435 \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C: \u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u0430, \u043A\u043E\u0442\u043E\u0440\u0430\u044F \u0435\u0433\u043E \u0432\u0435\u043B\u0430, \u0437\u0430\u043A\u0440\u044B\u043B\u0430\u0441\u044C \u043F\u043E\u0441\u0440\u0435\u0434\u0438 \u0440\u0430\u0431\u043E\u0442\u044B. \u041F\u0440\u0435\u0436\u043D\u044F\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u043D\u0430 \u043C\u0435\u0441\u0442\u0435, \u043C\u043E\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043C\u043E\u0436\u043D\u043E.",
17966
- advice: "\u041D\u0430\u043F\u0438\u0448\u0438 \xAB\u043E\u0431\u043D\u043E\u0432\u0438\xBB \u0435\u0449\u0451 \u0440\u0430\u0437 \u2014 \u043D\u0430\u0447\u043D\u0443 \u0437\u0430\u043D\u043E\u0432\u043E.",
17967
- report: `\u041F\u0440\u043E\u0446\u0435\u0441\u0441 \u043E\u0431\u043E\u043B\u043E\u0447\u043A\u0438 ${record2.pid}, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u0432\u0451\u043B \u0437\u0430\u0434\u0430\u0447\u0443, \u043F\u0440\u043E\u043F\u0430\u043B \u043D\u0430 \u044D\u0442\u0430\u043F\u0435 \xAB${record2.stages[record2.at - 1] ?? CHECK}\xBB.`
17968
- }
17926
+ /**
17927
+ * Отправить отчёт. Ответ инструмента ждёт итог через {@link wordsFor}, а
17928
+ * всё прочее — нет; для тестов и смоука — `settled()`.
17929
+ */
17930
+ submit(crash) {
17931
+ const delivery = this.#deliver(crash).catch(() => "none");
17932
+ this.#track(delivery.then(() => void 0));
17933
+ return delivery;
17934
+ }
17935
+ /**
17936
+ * Итог отправки для слов человеку — не дольше таймаута отправки с запасом.
17937
+ * Не успела — отчёт ещё в пути и либо уйдёт, либо ляжет в очередь: «уйдёт»
17938
+ * правда в обоих случаях.
17939
+ */
17940
+ async outcome(delivery) {
17941
+ let timer;
17942
+ const late = new Promise((done) => {
17943
+ timer = setTimeout(() => done("queued"), (this.#deps.timeoutMs ?? SEND_TIMEOUT_MS) + WORDS_GRACE_MS);
17944
+ timer.unref?.();
17945
+ });
17946
+ try {
17947
+ return await Promise.race([delivery, late]);
17948
+ } finally {
17949
+ clearTimeout(timer);
17969
17950
  }
17970
- };
17971
- }
17972
- function startSetupJob(what, run, clock = Date.now, home = null) {
17973
- const already = running(home, clock);
17974
- if (already !== null) return already;
17975
- const started = clock();
17976
- const id = `${SETUP_JOB_MARK}${new Date(started).toISOString().replace(/[:.]/g, "-")}`;
17977
- const work = {
17978
- id,
17979
- what,
17980
- started,
17981
- stages: [CHECK],
17982
- at: 1,
17983
- message: "",
17984
- ended: null,
17985
- done: Promise.resolve(),
17986
- home
17987
- };
17988
- const control = {
17989
- plan(stages) {
17990
- work.stages = stages;
17991
- work.at = Math.min(work.at, stages.length);
17992
- save(work, clock);
17993
- },
17994
- go(stage) {
17995
- const index = work.stages.indexOf(stage);
17996
- if (index >= 0) work.at = index + 1;
17997
- work.message = "";
17998
- save(work, clock);
17999
- },
18000
- line(text2) {
18001
- work.message = text2;
18002
- save(work, clock);
17951
+ }
17952
+ /**
17953
+ * Процесс падает: отчёт кладётся на диск СИНХРОННО и уходит на следующем
17954
+ * запуске. Сеть здесь не трогаем — процессу осталось жить миллисекунды, и
17955
+ * недоставленный наполовину запрос хуже файла, который точно дойдёт.
17956
+ */
17957
+ persist(crash) {
17958
+ try {
17959
+ const report = this.#build(crash);
17960
+ if (report !== null) this.#enqueue(report);
17961
+ } catch {
18003
17962
  }
18004
- };
18005
- works.set(id, work);
18006
- if (home !== null) prune2(home);
18007
- save(work, clock);
18008
- const beat = home === null ? null : setInterval(() => save(work, clock), BEAT_MS);
18009
- beat?.unref();
18010
- work.done = collectJobInstallFaults(id, () => run(control).catch((trouble) => {
18011
- noteInstallFault("update-job", trouble);
17963
+ }
17964
+ /** Досылка очереди — не чаще раза в минуту. */
17965
+ flushSoon() {
17966
+ const now = this.#now();
17967
+ if (now - this.#lastFlush < FLUSH_EVERY_MS) return;
17968
+ this.#lastFlush = now;
17969
+ this.#track(this.#flush());
17970
+ }
17971
+ /** Дождаться всего, что ушло мимо ответа. Боевому пути не нужно. */
17972
+ async settled() {
17973
+ while (this.#inflight.size > 0) await Promise.allSettled([...this.#inflight]);
17974
+ }
17975
+ /** Задание уже разобрано: отчёт о нём ушёл раньше, в этом или прошлом запуске. */
17976
+ jobSeen(id) {
17977
+ return this.#state().jobs.includes(id);
17978
+ }
17979
+ rememberJob(id) {
17980
+ const state = this.#state();
17981
+ state.jobs = [...state.jobs.filter((one) => one !== id), id].slice(-50);
17982
+ this.#save(state);
17983
+ }
17984
+ scrubContext(secrets) {
18012
17985
  return {
18013
- ok: false,
18014
- trouble: "\u0427\u0442\u043E-\u0442\u043E \u0441\u043B\u043E\u043C\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u0441\u0440\u0435\u0434\u0438 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F \u2014 \u043F\u0440\u0435\u0436\u043D\u044F\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u043D\u0430 \u043C\u0435\u0441\u0442\u0435, \u043C\u043E\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043C\u043E\u0436\u043D\u043E.",
18015
- advice: ADVICE_ASK,
18016
- report: String(trouble instanceof Error ? trouble.message : trouble)
17986
+ home: this.#deps.osHome ?? homedir(),
17987
+ user: this.#deps.osUser ?? safeUser(),
17988
+ naparnikHome: this.#deps.home,
17989
+ shellDir: this.#deps.shellDir ?? ownDir(),
17990
+ secrets
18017
17991
  };
18018
- })).then((ending) => {
18019
- work.ended = { at: clock(), ending };
18020
- if (beat !== null) clearInterval(beat);
18021
- save(work, clock);
18022
- });
18023
- return id;
18024
- }
18025
- function quiet(line) {
18026
- return line.replace(/^⚠\s*/, "").replace(/^Что делать:/, "\u0421\u043E\u0432\u0435\u0442:");
18027
- }
18028
- function setupJobText(id, clock = Date.now, home = null) {
18029
- const work = works.get(id) ?? (home === null ? null : fromDisk(home, id, clock));
18030
- if (work === null) return null;
18031
- const head2 = `\u0417\u0430\u0434\u0430\u043D\u0438\u0435 ${work.id}: ${work.what}`;
18032
- if (work.ended === null) {
18033
- const sec2 = Math.max(0, Math.round((clock() - work.started) / 1e3));
18034
- return [
18035
- head2,
18036
- `\u042D\u0442\u0430\u043F ${work.at} \u0438\u0437 ${work.stages.length}: ${work.stages[work.at - 1] ?? CHECK}`,
18037
- ...work.message === "" ? [] : [work.message],
18038
- `\u0415\u0449\u0451 \u0441\u0447\u0438\u0442\u0430\u0435\u0442, ${sec2} \u0441.`
18039
- ].join("\n");
18040
17992
  }
18041
- const sec = Math.max(0, Math.round((work.ended.at - work.started) / 1e3));
18042
- const ending = work.ended.ending;
18043
- const report = ending.report.split("\n").map((line) => line.trim()).filter((line) => line !== "").map(quiet);
18044
- if (ending.ok) {
18045
- return [head2, `\u0413\u043E\u0442\u043E\u0432\u043E \u0437\u0430 ${sec} \u0441.`, `\u0427\u0435\u043B\u043E\u0432\u0435\u043A\u0443: ${ending.say}`, ...report].join("\n");
17993
+ #track(work) {
17994
+ const guarded = work.catch(() => void 0);
17995
+ this.#inflight.add(guarded);
17996
+ void guarded.finally(() => this.#inflight.delete(guarded));
18046
17997
  }
18047
- return [
18048
- head2,
18049
- ...report,
18050
- `\u041D\u0435 \u043F\u043E\u043B\u0443\u0447\u0438\u043B\u043E\u0441\u044C \u0437\u0430 ${sec} \u0441: ${ending.trouble}`,
18051
- `\u0427\u0442\u043E \u0434\u0435\u043B\u0430\u0442\u044C: ${ending.advice}`
18052
- ].join("\n");
18053
- }
18054
- function setupJobReport(id, home = null) {
18055
- const text2 = setupJobText(id, Date.now, home);
18056
- return text2 === null ? null : readJob(text2);
18057
- }
18058
- function weightExact(bytes) {
18059
- const mb = bytes / 1024 / 1024;
18060
- if (mb >= 1e3) return `${(mb / 1024).toFixed(1).replace(".", ",")} \u0413\u0411`;
18061
- return `${mb.toFixed(1).replace(".", ",")} \u041C\u0411`;
18062
- }
18063
- async function runUpdate(job, deps) {
18064
- const { engine, key, baseUrl, what } = deps;
18065
- const home = engine.home();
18066
- const wantShell = what !== "engine";
18067
- const hasEngine = engine.place() !== null;
18068
- const wantEngine = what !== "shell" && hasEngine && !engine.pathSetByHand();
18069
- const shellUrl = engine.variable("NAPARNIK_BUNDLE_MANIFEST") ?? bundleUrlFor(baseUrl);
18070
- const [shellAsk, engineAsk] = await Promise.all([
18071
- wantShell ? fetchShellManifest(shellUrl, deps.pointerMs, key) : Promise.resolve(null),
18072
- wantEngine ? fetchManifest(manifestUrl(engine, baseUrl), deps.pointerMs, key) : Promise.resolve(null)
18073
- ]);
18074
- const shellWas = installedShell(home);
18075
- const shell = decideShell(shellWas, shellAsk?.ok === true ? shellAsk.manifest : null);
18076
- const engineWas = engine.freshVersion();
18077
- const fresh = decide(engineWas, engineAsk?.ok === true ? engineAsk.manifest : null);
18078
- const shellStage = shell.what === "newer_available" ? `\u0421\u043A\u0430\u0447\u0438\u0432\u0430\u044E \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0435 \xB7 ${weightExact(shell.manifest.body?.bytes ?? 0)}` : null;
18079
- const engineStage = fresh.what === "newer_available" ? `\u0421\u043A\u0430\u0447\u0438\u0432\u0430\u044E \u0434\u0432\u0438\u0436\u043E\u043A \xB7 ${weightExact(fresh.manifest.bytes)}` : null;
18080
- const missing = what === "all" && engineStage === null;
18081
- job.plan([
18082
- CHECK,
18083
- ...shellStage === null ? [] : [shellStage],
18084
- ...engineStage === null ? [] : [engineStage, INSTALL_NEW],
18085
- ...missing ? [MISSING] : []
18086
- ]);
18087
- const steps = [];
18088
- let updated = false;
18089
- let installed = false;
18090
- for (const [ask2, whose] of [[shellAsk, "\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F"], [engineAsk, "\u0434\u0432\u0438\u0436\u043A\u0430"]]) {
18091
- if (ask2 === null || ask2.ok) continue;
18092
- const refusal = refusalWords(ask2.status ?? 0);
18093
- steps.push({
18094
- report: `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043F\u0440\u043E\u0441\u0438\u0442\u044C, \u0435\u0441\u0442\u044C \u043B\u0438 \u043D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F ${whose}: ${ask2.why}.`,
18095
- trouble: refusal !== "" ? refusal : "\u041D\u0435 \u043F\u043E\u043B\u0443\u0447\u0438\u043B\u043E\u0441\u044C \u0441\u0432\u044F\u0437\u0430\u0442\u044C\u0441\u044F \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u043C \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0439."
18096
- });
17998
+ async #deliver(crash) {
17999
+ const report = this.#build(crash);
18000
+ if (report === null) return this.#queuedAlready(fingerprintOf2(crash.errorType, crash.frames)) ? "queued" : "sent";
18001
+ report.versions.ffmpeg = await this.#ffmpegVersion();
18002
+ const verdict = await this.#send(report);
18003
+ if (verdict === "sent") return "sent";
18004
+ if (verdict === "drop") return "none";
18005
+ return this.#enqueue(report) ? "queued" : "none";
18097
18006
  }
18098
- if (shellAsk?.ok === true && shell.what === "nothing") {
18099
- steps.push({ report: `\u0423 \u0432\u0430\u0441 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u0441\u0430\u043C\u0430\u044F \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u044F\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u2014 ${shellWas}.` });
18007
+ #queuedAlready(fingerprint) {
18008
+ try {
18009
+ return readdirSync(this.#queueDir()).some((f) => f.endsWith(`-${fingerprint}.json`));
18010
+ } catch {
18011
+ return false;
18012
+ }
18100
18013
  }
18101
- if (shell.what === "launcher_too_old") {
18102
- steps.push({
18103
- report: `\u041D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F ${shell.manifest.version} \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043D\u043E\u0432\u043E\u0433\u043E \u0444\u0430\u0439\u043B\u0430 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F.`,
18104
- trouble: `\u0414\u043B\u044F \u043D\u043E\u0432\u043E\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u043D\u0443\u0436\u0435\u043D \u043D\u043E\u0432\u044B\u0439 \u0444\u0430\u0439\u043B \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F. \u0412\u043E\u0437\u044C\u043C\u0438 \u0435\u0433\u043E \u0432 \u0442\u0435\u043B\u0435\u0433\u0440\u0430\u043C-\u0431\u043E\u0442\u0435 ${KEY_SOURCE_BOT}.`
18105
- });
18014
+ async #flush() {
18015
+ const dir = this.#queueDir();
18016
+ let files;
18017
+ try {
18018
+ files = readdirSync(dir).filter((f) => f.endsWith(".json")).sort();
18019
+ } catch {
18020
+ return;
18021
+ }
18022
+ for (const file2 of files.slice(0, FLUSH_PER_PASS)) {
18023
+ const path2 = join(dir, file2);
18024
+ let report;
18025
+ try {
18026
+ report = JSON.parse(readFileSync(path2, "utf8"));
18027
+ } catch {
18028
+ rmSync(path2, { force: true });
18029
+ continue;
18030
+ }
18031
+ const verdict = await this.#send(report);
18032
+ if (verdict === "retry") return;
18033
+ rmSync(path2, { force: true });
18034
+ }
18106
18035
  }
18107
- if (fresh.what === "shell_too_old") {
18108
- steps.push({
18109
- report: `\u041D\u043E\u0432\u0430\u044F \u043D\u0430\u0447\u0438\u043D\u043A\u0430 ${fresh.manifest.version} \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u043D\u0435 \u0441\u0442\u0430\u0440\u0448\u0435 ${fresh.manifest.minShellVersion}.`,
18110
- trouble: "\u041D\u043E\u0432\u043E\u043C\u0443 \u0434\u0432\u0438\u0436\u043A\u0443 \u043D\u0443\u0436\u043D\u043E \u043D\u043E\u0432\u043E\u0435 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0435 \u2014 \u0435\u0433\u043E \u043F\u043E\u043A\u0430 \u043D\u0435\u0442 \u043D\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435."
18111
- });
18036
+ /**
18037
+ * Отправить и решить, что делать с отчётом дальше.
18038
+ *
18039
+ * ⚠ В ОЧЕРЕДЬ ИДЁТ ТОЛЬКО ТО, ЧТО ПОВТОР ИСПРАВИТ: нет сети, таймаут, 5xx,
18040
+ * 429 и «ключа нет» (человек впишет его завтра). 400 и 413 — отчёт кривой,
18041
+ * 401 и 403 — ключ не наш: повтор даст тот же отказ, а очередь с потолком
18042
+ * вытесняла бы им живые отчёты.
18043
+ */
18044
+ async #send(report) {
18045
+ if (!CrashReportRequest.safeParse(report).success) return "drop";
18046
+ try {
18047
+ await this.#deps.client.postMontage(CRASH_REPORT_PATH, report, {
18048
+ once: true,
18049
+ timeoutMs: this.#deps.timeoutMs ?? SEND_TIMEOUT_MS
18050
+ });
18051
+ return "sent";
18052
+ } catch (trouble) {
18053
+ if (trouble instanceof NaparnikAuthError || trouble instanceof NaparnikScopeError) return "drop";
18054
+ if (trouble instanceof NaparnikClientError) return trouble.status === 429 ? "retry" : "drop";
18055
+ return "retry";
18056
+ }
18112
18057
  }
18113
- if (shell.what === "newer_available" && shellStage !== null) {
18114
- job.go(shellStage);
18115
- const outcome = await installShell({ home, manifest: shell.manifest, key: key ?? null });
18116
- if (outcome.ok) {
18117
- updated = true;
18118
- steps.push({ report: outcome.text });
18119
- } else {
18120
- steps.push({ report: outcome.text, trouble: "\u041D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u043D\u0435 \u0432\u0441\u0442\u0430\u043B\u0430 \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043F\u0440\u0435\u0436\u043D\u044F\u044F." });
18058
+ /**
18059
+ * Собрать отчёт: обезличить, обрезать, посчитать отпечаток и решить по
18060
+ * частоте. `null` — этот отпечаток уже уходил меньше получаса назад, и
18061
+ * повтор записан в счётчик.
18062
+ */
18063
+ #build(crash) {
18064
+ const fingerprint = fingerprintOf2(crash.errorType, crash.frames);
18065
+ const state = this.#state();
18066
+ const now = this.#now();
18067
+ const seen = state.fingerprints[fingerprint];
18068
+ if (seen !== void 0 && now - seen.sentAt < RESEND_AFTER_MS) {
18069
+ seen.pending += 1;
18070
+ this.#save(state);
18071
+ return null;
18121
18072
  }
18073
+ const repeats = Math.min(seen?.pending ?? 0, CRASH_LIMITS.repeats);
18074
+ state.fingerprints[fingerprint] = { sentAt: now, pending: 0 };
18075
+ prune2(state, now);
18076
+ this.#save(state);
18077
+ const context = this.scrubContext([...crash.secrets, ...this.#recentSecrets]);
18078
+ const clean = (text2) => scrub(text2, context);
18079
+ return {
18080
+ contract: CRASH_CONTRACT,
18081
+ occurred_at: new Date(now).toISOString(),
18082
+ where: crash.where,
18083
+ tool: crash.tool !== null && CRASH_TOOL_NAME.test(crash.tool) ? crash.tool : null,
18084
+ // ⚠ РЕЖЕМ ДО ОБЕЗЛИЧИВАНИЯ С ЗАПАСОМ И ЕЩЁ РАЗ ПОСЛЕ. Обезличивание идёт
18085
+ // синхронно, до ответа инструмента, а stderr движка бывает мегабайтами:
18086
+ // без первой резки длинный текст подвешивал бы ответ на секунды (ревью
18087
+ // безопасности 2026-09-24). Запас — вдвое: метки короче того, что заменяют.
18088
+ stage: crash.stage === null ? null : head(clean(head(crash.stage, 2 * CRASH_LIMITS.stage)), CRASH_LIMITS.stage),
18089
+ error_type: head(clean(head(crash.errorType, 2 * CRASH_LIMITS.errorType)), CRASH_LIMITS.errorType) || "Error",
18090
+ message: head(clean(head(crash.message, 2 * CRASH_LIMITS.message)), CRASH_LIMITS.message),
18091
+ trace: crash.keep === "tail" ? tail(clean(crash.trace.slice(-2 * CRASH_LIMITS.trace)), CRASH_LIMITS.trace) : head(clean(head(crash.trace, 2 * CRASH_LIMITS.trace)), CRASH_LIMITS.trace),
18092
+ fingerprint,
18093
+ repeats,
18094
+ os: {
18095
+ platform: head(platform(), CRASH_LIMITS.osPlatform),
18096
+ release: head(release(), CRASH_LIMITS.osRelease),
18097
+ arch: head(arch(), CRASH_LIMITS.osArch)
18098
+ },
18099
+ host: hostOf(crash.host ?? this.#host),
18100
+ versions: {
18101
+ shell: head(VERSION, CRASH_LIMITS.shellVersion),
18102
+ engine: orNull(this.#deps.engineVersion(), CRASH_LIMITS.engineVersion),
18103
+ python: orNull(crash.python, CRASH_LIMITS.pythonVersion),
18104
+ ffmpeg: null
18105
+ },
18106
+ recent_calls: recentCalls(this.#deps.home)
18107
+ };
18122
18108
  }
18123
- if (fresh.what === "newer_available" && engineStage !== null) {
18124
- job.go(engineStage);
18125
- const outcome = await deploy(engine, fresh.manifest, void 0, key, {
18126
- line: (text2) => job.line(text2),
18127
- downloaded: () => {
18128
- job.go(INSTALL_NEW);
18129
- job.line("\u0421\u043E\u0431\u0438\u0440\u0430\u044E \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 \u043D\u043E\u0432\u043E\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u2014 \u044D\u0442\u043E \u0434\u043E \u043F\u0430\u0440\u044B \u043C\u0438\u043D\u0443\u0442.");
18109
+ /** Положить отчёт в очередь. `false` — диск не принял, отчёт пропал. */
18110
+ #enqueue(report) {
18111
+ try {
18112
+ const dir = this.#queueDir();
18113
+ mkdirSync(dir, { recursive: true });
18114
+ const name = `${String(this.#now()).padStart(15, "0")}-${report.fingerprint}.json`;
18115
+ const temporary = join(dir, `${name}.tmp`);
18116
+ writeFileSync(temporary, JSON.stringify(report), { mode: 384 });
18117
+ renameSync(temporary, join(dir, name));
18118
+ const files = readdirSync(dir).filter((f) => f.endsWith(".json")).sort();
18119
+ for (const old of files.slice(0, Math.max(0, files.length - QUEUE_CAP))) {
18120
+ rmSync(join(dir, old), { force: true });
18130
18121
  }
18131
- });
18132
- if (outcome.ok) {
18133
- updated = true;
18134
- steps.push({ report: outcome.text });
18135
- } else {
18136
- const cause = causeOf(outcome.text);
18137
- steps.push({
18138
- report: outcome.text,
18139
- trouble: `\u041D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u0434\u0432\u0438\u0436\u043A\u0430 \u043D\u0435 \u0432\u0441\u0442\u0430\u043B\u0430${cause === "" ? "" : `: ${cause}`}. \u0420\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043F\u0440\u0435\u0436\u043D\u044F\u044F${engineWas === null ? "" : ` ${engineWas}`}, \u043C\u043E\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043C\u043E\u0436\u043D\u043E.`
18140
- });
18122
+ return true;
18123
+ } catch {
18124
+ return false;
18141
18125
  }
18142
18126
  }
18143
- if (missing) {
18144
- job.go(MISSING);
18145
- const step = await deliverMissing(job, deps);
18146
- if (step.trouble === void 0 && step.installed) installed = true;
18147
- steps.push(step);
18127
+ #queueDir() {
18128
+ return join(this.#deps.home, "crash-reports", "queue");
18148
18129
  }
18149
- const report = steps.map((step) => step.report).join("\n");
18150
- const trouble = steps.find((step) => step.trouble !== void 0)?.trouble;
18151
- if (trouble !== void 0) return { ok: false, trouble, advice: ADVICE_ASK, report };
18152
- if (updated) return { ok: true, say: SAY_RESTART, report };
18153
- return { ok: true, say: installed ? SAY_INSTALLED : SAY_NOTHING, report };
18154
- }
18155
- function causeOf(text2) {
18156
- const line = text2.split("\n").map((one) => one.trim()).find((one) => one !== "" && !/^(Новая версия|Версия) \S+ не встала/.test(one) && !/^Что делать:/.test(one)) ?? "";
18157
- const phrase = (line.split(/\.\s|\s—\s|;\s/)[0] ?? "").replace(/\s*\([^)]*\)/g, "").replace(/\.$/, "").trim();
18158
- return phrase === "" ? "" : phrase[0].toLowerCase() + phrase.slice(1);
18159
- }
18160
- async function deliverMissing(job, deps) {
18161
- const answer = await deps.installMissing();
18162
- if (answer.type === "error") {
18163
- return { report: answer.error, trouble: "\u041D\u0435 \u043F\u043E\u043B\u0443\u0447\u0438\u043B\u043E\u0441\u044C \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043D\u0435\u0434\u043E\u0441\u0442\u0430\u044E\u0449\u0435\u0435.", installed: false };
18130
+ #stateFile() {
18131
+ return join(this.#deps.home, "crash-reports", "seen.json");
18164
18132
  }
18165
- const words = typeof answer.content === "string" ? answer.content : answer.content.map((block) => block.type === "text" ? block.text : "").join("\n");
18166
- const id = typeof answer.structured?.["id"] === "string" ? answer.structured["id"] : "";
18167
- if (id === "") return { report: words, installed: false };
18168
- const pace = deps.pace ?? realPace;
18169
- let state = await deps.engine.command(["job", id]);
18170
- while (state.ok && stillWorking(state.text)) {
18171
- const now = readJob(state.text);
18172
- if (now !== null) job.line(now.message !== "" ? now.message : now.now);
18173
- await pace.rest(POLL_EVERY_MS);
18174
- state = await deps.engine.command(["job", id]);
18133
+ #state() {
18134
+ try {
18135
+ const parsed = JSON.parse(readFileSync(this.#stateFile(), "utf8"));
18136
+ return {
18137
+ fingerprints: typeof parsed.fingerprints === "object" && parsed.fingerprints !== null ? parsed.fingerprints : {},
18138
+ jobs: Array.isArray(parsed.jobs) ? parsed.jobs.filter((j) => typeof j === "string") : []
18139
+ };
18140
+ } catch {
18141
+ return { fingerprints: {}, jobs: [] };
18142
+ }
18175
18143
  }
18176
- const end = state.ok ? readJob(state.text) : null;
18177
- if (end === null || end.status !== "done") {
18178
- return {
18179
- report: `${words}
18180
- ${state.text}`,
18181
- trouble: end?.trouble !== void 0 && end.trouble !== "" ? end.trouble : "\u041D\u0435 \u043F\u043E\u043B\u0443\u0447\u0438\u043B\u043E\u0441\u044C \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043D\u0435\u0434\u043E\u0441\u0442\u0430\u044E\u0449\u0435\u0435.",
18182
- installed: false
18183
- };
18144
+ #save(state) {
18145
+ try {
18146
+ mkdirSync(dirname(this.#stateFile()), { recursive: true });
18147
+ writeFileSync(this.#stateFile(), JSON.stringify(state), { mode: 384 });
18148
+ } catch {
18149
+ }
18184
18150
  }
18185
- return { report: `${words}
18186
- ${state.text}`, installed: true };
18187
- }
18188
-
18189
- // src/setup-tool.ts
18190
- var ASKED_POINTER_MS = 15e3;
18191
- var NEWS_POINTER_MS = 5e3;
18192
- var EXTRAS_LATER = "\u041C\u043E\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0443\u0436\u0435 \u043C\u043E\u0436\u043D\u043E. \u0424\u043E\u043D\u043E\u043C \u0434\u043E\u043A\u0430\u0447\u0438\u0432\u0430\u044E \u0442\u043E, \u0431\u0435\u0437 \u0447\u0435\u0433\u043E \u043C\u043E\u043D\u0442\u0430\u0436 \u0438 \u0442\u0430\u043A \u0438\u0434\u0451\u0442: \u0448\u0443\u043C\u043E\u0434\u0430\u0432 \u0433\u043E\u043B\u043E\u0441\u0430 \u2014 \u043E\u043A\u043E\u043B\u043E 30 \u041C\u0411, \u043A\u0430\u0440\u0442\u0443 \u0433\u0435\u0440\u043E\u044F \u0438 \u0431\u0440\u0430\u0443\u0437\u0435\u0440 \u0434\u043B\u044F \u0433\u0440\u0430\u0444\u0438\u043A\u0438 \u2014 \u0434\u043E 150 \u041C\u0411. \u0411\u0440\u0430\u0443\u0437\u0435\u0440 \u043A\u0430\u0447\u0430\u0435\u0442\u0441\u044F, \u0442\u043E\u043B\u044C\u043A\u043E \u0435\u0441\u043B\u0438 \u0435\u0433\u043E \u0441\u0431\u043E\u0440\u043A\u0430 \u0434\u043B\u044F \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u044B \u0443\u0436\u0435 \u0432\u044B\u043B\u043E\u0436\u0435\u043D\u0430: \u0433\u0440\u0430\u0444\u0438\u043A\u0430 \u0432 \u043C\u043E\u043D\u0442\u0430\u0436 \u0435\u0449\u0451 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0430. \u041D\u0435 \u0434\u043E\u043A\u0430\u0447\u0430\u0435\u0442\u0441\u044F \u2014 \u043C\u043E\u043D\u0442\u0430\u0436 \u043D\u0435 \u0443\u043F\u0430\u0434\u0451\u0442, \u0430 setup \u0441\u043A\u0430\u0436\u0435\u0442 \u0441\u043B\u043E\u0432\u0430\u043C\u0438, \u0447\u0435\u0433\u043E \u043D\u0435\u0442.";
18193
- async function inspectMachine(engine, asked) {
18194
- const video = asked.video;
18195
- const model = asked.model;
18196
- const place = engine.place();
18197
- if (place === null) return { result: toolError(engine.whyNoEngine()), readiness: null };
18198
- const args = [
18199
- "env-check",
18200
- "--pack",
18201
- place.pack,
18202
- ...video !== void 0 ? ["--video", path(video)] : [],
18203
- ...model !== void 0 ? ["--model", model] : []
18204
- ];
18205
- let readiness = null;
18206
- let words;
18207
- if (knowsJsonCheck(place.engine)) {
18208
- const answer = await askReadiness(engine, place, args);
18209
- if ("ok" in answer && !answer.ok) return { result: toolError(answer.text), readiness: null };
18210
- if ("parts" in answer) readiness = answer;
18211
- words = "parts" in answer ? answer.words : answer.text;
18212
- } else {
18213
- const outcome = await engine.command(args);
18214
- if (!outcome.ok) return { result: toolError(outcome.text), readiness: null };
18215
- words = outcome.text;
18151
+ #ffmpegVersion() {
18152
+ this.#ffmpeg ??= (this.#deps.ffmpegVersion ?? (() => probeFfmpeg(this.#deps.home)))().catch(() => null);
18153
+ return this.#ffmpeg.then((v) => orNull(v, CRASH_LIMITS.ffmpegVersion));
18216
18154
  }
18217
- const catalog = await askCatalog(engine, place.pack, video);
18218
- return { result: toolSuccess(`${words}
18219
-
18220
- ${catalog}`), readiness };
18221
- }
18222
- async function askReadiness(engine, place, args) {
18223
- const python = await engine.pythonTrouble();
18224
- if (python !== null) return { ok: false, text: python };
18225
- let stdout;
18226
- try {
18227
- stdout = (await engine.run(place, [...args, JSON_CHECK])).stdout;
18228
- } catch (err) {
18229
- const e = err;
18230
- if (e.code !== 2 || typeof e.stdout !== "string") return parseFailure(err, "\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u043C\u0430\u0448\u0438\u043D\u044B \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u043B\u0430\u0441\u044C");
18231
- stdout = e.stdout;
18155
+ };
18156
+ function prune2(state, now) {
18157
+ for (const [key, value] of Object.entries(state.fingerprints)) {
18158
+ if (now - value.sentAt > 24 * 36e5 && value.pending === 0) delete state.fingerprints[key];
18232
18159
  }
18233
- return readReadiness(stdout) ?? { ok: true, text: stdout.trim() };
18234
18160
  }
18235
- async function installStep(engine, asked, key, baseUrl, tell) {
18236
- const step = asked.step;
18237
- const model = asked.model;
18238
- const consent = asked.consent;
18239
- if (engine.place() === null) {
18240
- let candidate = readCandidate(engine.versionsDir());
18241
- if (candidate === null) {
18242
- if (tell !== void 0) for (const line of PAYLOAD_LADDER) tell(line);
18243
- const result = await installPayload(engine, {
18244
- consent,
18245
- manifestUrl: manifestUrl(engine, baseUrl),
18246
- ...key !== void 0 ? { key } : {}
18247
- });
18248
- if (!result.ok) return toolError(result.text);
18249
- candidate = readCandidate(engine.versionsDir());
18250
- if (candidate === null) {
18251
- return toolError("\u041D\u0430\u0447\u0438\u043D\u043A\u0430 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C \u0431\u0435\u0437 \u0443\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u044F \u043A\u0430\u043D\u0434\u0438\u0434\u0430\u0442\u0430 \u2014 current \u043D\u0435 \u043C\u0435\u043D\u044F\u043B. \u0427\u0442\u043E \u0434\u0435\u043B\u0430\u0442\u044C: \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0435 setup.");
18252
- }
18253
- }
18254
- if (!consent) {
18255
- return toolError(
18256
- `\u041A\u0430\u043D\u0434\u0438\u0434\u0430\u0442 ${candidate.version} \u0443\u0436\u0435 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043B\u0435\u043D, \u043D\u043E \u0435\u0449\u0451 \u043D\u0435 \u0440\u0430\u0431\u043E\u0447\u0438\u0439. \u0421\u043F\u0440\u043E\u0441\u0438 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0441\u043E\u0431\u0440\u0430\u0442\u044C \u0435\u0433\u043E \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 \u0438 \u043F\u043E\u0437\u043E\u0432\u0438 setup \u0441 consent=true.`
18257
- );
18258
- }
18259
- const extras = step === "all" ? "later" : "now";
18260
- const activated = await continuePreparedCandidate(engine, candidate, void 0, extras);
18261
- if (!activated.ok) return toolError(activated.text);
18262
- engine.forgetPlace();
18263
- engine.forgetPython();
18264
- if (step === "python") return toolSuccess(activated.text);
18265
- if (step === "all") {
18266
- const ready = `${activated.text}
18267
- \u041A\u0430\u043D\u0434\u0438\u0434\u0430\u0442 \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u043B \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438 \u043F\u043E \u0441\u0432\u043E\u0435\u043C\u0443 \u0437\u0430\u043C\u043A\u0443 \u0438 \u043F\u0440\u043E\u0448\u0451\u043B env-check. \u0412\u0441\u0451 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u0433\u043E\u0442\u043E\u0432\u043E.`;
18268
- const later = await installStep(engine, asked, key, baseUrl, tell);
18269
- if (later.type === "error") {
18270
- return toolSuccess(
18271
- `${ready}
18272
-
18273
- \u041E\u0441\u0442\u0430\u043B\u044C\u043D\u043E\u0435 \u0444\u043E\u043D\u043E\u043C \u0437\u0430\u0432\u0435\u0441\u0442\u0438 \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C: ${later.error}
18274
- \u041C\u043E\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u044D\u0442\u043E \u043D\u0435 \u043C\u0435\u0448\u0430\u0435\u0442. \u0427\u0442\u043E \u0434\u0435\u043B\u0430\u0442\u044C: \u043F\u043E\u0437\u043E\u0432\u0438 setup \u0441 what=all \u0435\u0449\u0451 \u0440\u0430\u0437.`
18275
- );
18161
+ function withCrashReport(crashes) {
18162
+ return (tool) => {
18163
+ if (crashes === null) return tool;
18164
+ const original = tool.execute.bind(tool);
18165
+ return {
18166
+ ...tool,
18167
+ async execute(input, ...rest) {
18168
+ crashes.flushSoon();
18169
+ const host = hostFromContext(rest[0]);
18170
+ crashes.noteHost(host);
18171
+ crashes.noteSecrets(secretsOf(input));
18172
+ let result;
18173
+ let faults;
18174
+ try {
18175
+ ({ value: result, faults } = await collectInstallFaults(() => original(input, ...rest)));
18176
+ } catch (trouble) {
18177
+ const crash = fromThrown(trouble, "tool", tool.name, input, host);
18178
+ return toolError(await brokenWords(crash, crashes, crashes.submit(crash)));
18179
+ }
18180
+ const install2 = firstInstallCrash(faults);
18181
+ if (install2 !== null) {
18182
+ const crash = { ...install2, where: "tool", tool: tool.name, secrets: secretsOf(input), host };
18183
+ return withNotice(result, await brokenWords(crash, crashes, crashes.submit(crash)));
18184
+ }
18185
+ return caught(result, tool.name, input, host, crashes);
18276
18186
  }
18277
- return together([toolSuccess(`${ready}
18278
- ${EXTRAS_LATER}`), later]);
18279
- }
18187
+ };
18188
+ };
18189
+ }
18190
+ async function reportInstallFaults(crashes, work) {
18191
+ const { value, faults } = await collectInstallFaults(work);
18192
+ const install2 = firstInstallCrash(faults);
18193
+ if (install2 !== null) {
18194
+ void crashes.submit({ ...install2, stage: `${install2.stage ?? ""} (\u0444\u043E\u043D\u043E\u043C \u043D\u0430 \u0441\u0442\u0430\u0440\u0442\u0435)`, where: "tool", tool: null, secrets: [], host: null });
18280
18195
  }
18281
- const place = engine.place();
18282
- if (place === null) return toolError(engine.whyNoEngine());
18283
- if (consent && await engine.pythonTrouble() !== null) {
18284
- try {
18285
- await engine.bringPython(place);
18286
- } catch (trouble) {
18287
- return toolError(`\u041F\u0438\u0442\u043E\u043D \u043C\u043E\u043D\u0442\u0430\u0436\u0430 \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C: ${describeFailure(trouble)}. \u0427\u0442\u043E \u0434\u0435\u043B\u0430\u0442\u044C: \u043F\u043E\u0432\u0442\u043E\u0440\u0438 setup.`);
18288
- }
18196
+ return value;
18197
+ }
18198
+ function reportJobFaults(crashes, id, where) {
18199
+ const install2 = firstInstallCrash(jobInstallFaults(id));
18200
+ if (install2 !== null) {
18201
+ void crashes.submit({ ...install2, stage: `${install2.stage ?? ""} (${where})`, where: "job", tool: null, secrets: [], host: null });
18289
18202
  }
18290
- const common = [
18291
- "--pack",
18292
- place.pack,
18293
- "--step",
18294
- step,
18295
- ...model !== void 0 ? ["--model", model] : [],
18296
- ...consent ? ["--consent"] : []
18297
- ];
18298
- if (!LONG_STEPS.has(step)) return asToolResult(await engine.command(["env-install", ...common]));
18299
- const python = await engine.pythonTrouble();
18300
- if (python !== null) return toolError(python);
18301
- if (tell !== void 0) for (const line of installLadder(step)) tell(line);
18302
- const launch = await engine.background({
18303
- place,
18304
- executable: engine.enginePython(),
18305
- // Установка берёт тот же слот тяжёлой работы, а на слоте стоит заслон
18306
- // подписки в самом движке — без ключа она откажет платящему.
18307
- key,
18308
- startJob: [
18309
- "install-create",
18310
- "--pack",
18311
- place.pack,
18312
- "--step",
18313
- step,
18314
- ...model !== void 0 ? ["--model", model] : []
18315
- ],
18316
- args: (id) => [place.consolePy, "env-install", ...common, "--job", id],
18317
- advice: "\u043F\u043E\u0437\u043E\u0432\u0438 setup \u2014 \u0441\u043A\u043E\u0440\u0435\u0435 \u0432\u0441\u0435\u0433\u043E, \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043D\u0435\u043F\u043E\u043B\u043D\u0430\u044F"
18318
- });
18319
- if ("refusal" in launch) return toolError(launch.refusal);
18320
- return toolSuccess(
18321
- `\u0417\u0430\u0434\u0430\u0447\u0430 ${launch.id}: ${installTitle(step)} (\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430, \u0448\u0430\u0433 \xAB${step}\xBB).
18322
- \u0418\u0434\u0451\u0442 \u043D\u0430 \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440\u0435 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F \u0438 \u043F\u0435\u0440\u0435\u0436\u0438\u0432\u0430\u0435\u0442 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u044B. \u041F\u043E\u0437\u043E\u0432\u0438 wait \u0441 job "${launch.id}" \u2014 \u043E\u043D \u043F\u043E\u0434\u043E\u0436\u0434\u0451\u0442 \u0438 \u043F\u043E \u0445\u043E\u0434\u0443 \u0440\u0430\u0441\u0441\u043A\u0430\u0436\u0435\u0442, \u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0441\u043A\u0430\u0447\u0430\u043D\u043E \u0438 \u043E \u0447\u0451\u043C \u043D\u0430\u0434\u043E \u0441\u043F\u0440\u043E\u0441\u0438\u0442\u044C \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430. \u041F\u0435\u0440\u0435\u0441\u043A\u0430\u0437\u044B\u0432\u0430\u0439 \u044D\u0442\u043E \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443 \u0441\u0440\u0430\u0437\u0443, \u0430 \u043D\u0435 \u0432 \u043A\u043E\u043D\u0446\u0435. ${AWAY_NOTE}`,
18323
- startedJob({
18324
- id: launch.id,
18325
- what: installTitle(step),
18326
- ahead: installLadder(step).join(" "),
18327
- message: "\u0418\u0434\u0451\u0442 \u043C\u0438\u043D\u0443\u0442\u0430\u043C\u0438: \u043A\u0430\u0447\u0430\u0435\u0442\u0441\u044F \u0438 \u0441\u043E\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044F \u0442\u043E, \u0447\u0435\u043C \u043C\u043E\u043D\u0442\u0438\u0440\u0443\u0435\u0442\u0441\u044F \u0432\u0438\u0434\u0435\u043E."
18328
- })
18329
- );
18330
18203
  }
18331
- function tenths(bytes) {
18332
- return Math.round(bytes / 1024 / 1024 * 10) / 10;
18204
+ function firstInstallCrash(faults) {
18205
+ for (const fault of faults) {
18206
+ const parsed = installCrash(fault);
18207
+ if (parsed !== null) return parsed;
18208
+ }
18209
+ return null;
18210
+ }
18211
+ function withNotice(result, notice) {
18212
+ if (result.type === "error") return { ...result, error: `${result.error}
18213
+
18214
+ ${notice}` };
18215
+ return typeof result.content === "string" ? { ...result, content: `${result.content}
18216
+
18217
+ ${notice}` } : { ...result, content: [...result.content, { type: "text", text: notice }] };
18333
18218
  }
18334
- async function updateEngine(engine, consent, key, baseUrl, timeoutMs = ASKED_POINTER_MS, seen = () => {
18335
- }) {
18336
- if (engine.pathSetByHand()) {
18337
- return toolSuccess(
18338
- "\u041F\u0430\u043F\u043A\u0430 \u0434\u0432\u0438\u0436\u043A\u0430 \u0437\u0430\u0434\u0430\u043D\u0430 \u0432\u0440\u0443\u0447\u043D\u0443\u044E \u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u043E\u0439 NAPARNIK_ENGINE \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E: \u0441\u0430\u043C\u043E\u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0441 \u0432\u0435\u0440\u0441\u0438\u043E\u043D\u043D\u043E\u0439 \u0440\u0430\u0441\u043A\u043B\u0430\u0434\u043A\u043E\u0439 \u0432 ~/.naparnik/engine, \u0430 \u044D\u0442\u0430 \u043F\u0430\u043F\u043A\u0430 \u043C\u0438\u043C\u043E \u043D\u0435\u0451. \u0421\u043A\u0430\u0436\u0438 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443, \u0447\u0442\u043E \u0434\u0432\u0438\u0436\u043E\u043A \u0437\u0434\u0435\u0441\u044C \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0442\u0435\u043C \u0436\u0435 \u0441\u043F\u043E\u0441\u043E\u0431\u043E\u043C, \u043A\u0430\u043A\u0438\u043C \u0431\u044B\u043B \u043F\u043E\u043B\u043E\u0436\u0435\u043D."
18339
- );
18219
+ async function caught(result, name, input, host, crashes) {
18220
+ if (result.type === "error") {
18221
+ const parsed2 = pythonCrash(result.error);
18222
+ if (parsed2 === null) return result;
18223
+ const crash2 = { ...parsed2, where: "tool", tool: name, stage: null, secrets: secretsOf(input), host };
18224
+ return toolError(await brokenWords(crash2, crashes, crashes.submit(crash2)));
18340
18225
  }
18341
- if (engine.place() === null) {
18342
- return toolSuccess(
18343
- "\u0414\u0432\u0438\u0436\u043A\u0430 \u043C\u043E\u043D\u0442\u0430\u0436\u0430 \u043D\u0430 \u044D\u0442\u043E\u043C \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440\u0435 \u0435\u0449\u0451 \u043D\u0435\u0442 \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E.\n\u0427\u0442\u043E \u0434\u0435\u043B\u0430\u0442\u044C: \u043F\u043E\u0437\u043E\u0432\u0438 setup \u0441 what=all, \u043E\u043D \u0441\u043A\u0430\u0447\u0430\u0435\u0442 \u043D\u0430\u0447\u0438\u043D\u043A\u0443 \u0441 \u043D\u0443\u043B\u044F (\u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0441\u043E\u0433\u043B\u0430\u0441\u0438\u044F \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430)."
18344
- );
18226
+ const job = result.structured;
18227
+ if (job === void 0 || job["contract"] !== JOB_CONTRACT || job["status"] !== "error") return result;
18228
+ const id = typeof job["id"] === "string" ? job["id"] : "";
18229
+ if (!/^[\w.-]{1,80}$/.test(id) || crashes.jobSeen(id)) return result;
18230
+ const fromLog = pythonCrash(jobLogTail(crashes.home, id));
18231
+ const install2 = fromLog === null ? firstInstallCrash(jobInstallFaults(id)) : null;
18232
+ const parsed = fromLog ?? install2;
18233
+ if (parsed === null) return result;
18234
+ crashes.rememberJob(id);
18235
+ const stage = install2?.stage ?? (typeof job["now"] === "string" && job["now"] !== "" ? job["now"] : null);
18236
+ const crash = { ...parsed, where: "job", tool: name, stage, secrets: secretsOf(input), host };
18237
+ return withNotice(result, await brokenWords(crash, crashes, crashes.submit(crash)));
18238
+ }
18239
+ async function brokenWords(crash, crashes, delivery) {
18240
+ const essence = head(scrub(head(crash.essence, 600), crashes.scrubContext(crash.secrets)), 300);
18241
+ const fate = crashes.canSend ? await crashes.outcome(delivery) : "none";
18242
+ return [
18243
+ `\u0421\u043A\u0430\u0436\u0438 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443 \u0442\u0430\u043A, \u0441\u0432\u043E\u0438\u043C\u0438 \u0441\u043B\u043E\u0432\u0430\u043C\u0438 \u043D\u0435 \u043C\u0435\u043D\u044F\u0439: \xAB${humanWords(fate)}\xBB`,
18244
+ `\u0421\u0443\u0442\u044C \u0434\u043B\u044F \u0442\u0435\u0431\u044F, \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443 \u043D\u0435 \u0437\u0430\u0447\u0438\u0442\u044B\u0432\u0430\u0439: ${essence}`
18245
+ ].join("\n");
18246
+ }
18247
+ function humanWords(fate) {
18248
+ if (fate === "sent") {
18249
+ return `\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043E\u0448\u0438\u0431\u043A\u0430. \u041E\u0442\u0447\u0451\u0442 \u0443\u0436\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D \u0440\u0430\u0437\u0440\u0430\u0431\u043E\u0442\u0447\u0438\u043A\u0430\u043C \u2014 \u043C\u044B \u0435\u0451 \u0438\u0441\u043F\u0440\u0430\u0432\u0438\u043C. \u0415\u0441\u043B\u0438 \u043D\u0443\u0436\u043D\u043E \u0441\u0440\u043E\u0447\u043D\u043E \u2014 \u043D\u0430\u043F\u0438\u0448\u0438 \u043D\u0430\u043C: ${SUPPORT_TOPIC}`;
18345
18250
  }
18346
- const outcome = await fetchManifest(manifestUrl(engine, baseUrl), timeoutMs, key);
18347
- const decision = decide(engine.freshVersion(), outcome.ok ? outcome.manifest : null);
18348
- seen({
18349
- installed: engine.freshVersion(),
18350
- fresh: decision.what === "nothing" ? null : decision.manifest.version,
18351
- mb: decision.what === "nothing" ? 0 : tenths(decision.manifest.bytes),
18352
- status: outcome.ok ? 0 : outcome.status ?? 0,
18353
- notes: ""
18354
- });
18355
- if (decision.what === "nothing") {
18356
- const ownDir2 = engine.freshVersion();
18357
- return toolSuccess(
18358
- !outcome.ok ? `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043F\u0440\u043E\u0441\u0438\u0442\u044C, \u0435\u0441\u0442\u044C \u043B\u0438 \u043D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u043D\u0430\u0447\u0438\u043D\u043A\u0438: ${outcome.why}. \u042D\u0442\u043E \u043D\u0435 \u043F\u043E\u043B\u043E\u043C\u043A\u0430: \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F (${ownDir2 ?? "\u043D\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430"}) \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043A\u0430\u043A \u0440\u0430\u0431\u043E\u0442\u0430\u043B\u0430.` : `\u041D\u0430\u0447\u0438\u043D\u043A\u0430 \u0441\u0432\u0435\u0436\u0430\u044F: \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 ${ownDir2 ?? "\u043D\u0435\u0442"}, \u043D\u043E\u0432\u0435\u0435 \u043D\u0435 \u0432\u044B\u0448\u043B\u043E.`
18359
- );
18251
+ if (fate === "queued") {
18252
+ return `\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043E\u0448\u0438\u0431\u043A\u0430. \u041E\u0442\u0447\u0451\u0442 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D \u0438 \u0443\u0439\u0434\u0451\u0442 \u0440\u0430\u0437\u0440\u0430\u0431\u043E\u0442\u0447\u0438\u043A\u0430\u043C, \u043A\u0430\u043A \u043F\u043E\u044F\u0432\u0438\u0442\u0441\u044F \u0441\u0432\u044F\u0437\u044C, \u2014 \u043C\u044B \u0435\u0451 \u0438\u0441\u043F\u0440\u0430\u0432\u0438\u043C. \u0415\u0441\u043B\u0438 \u043D\u0443\u0436\u043D\u043E \u0441\u0440\u043E\u0447\u043D\u043E \u2014 \u043D\u0430\u043F\u0438\u0448\u0438 \u043D\u0430\u043C: ${SUPPORT_TOPIC}`;
18360
18253
  }
18361
- if (decision.what === "shell_too_old") {
18362
- return toolSuccess(
18363
- `\u041D\u043E\u0432\u0430\u044F \u043D\u0430\u0447\u0438\u043D\u043A\u0430 ${decision.manifest.version} \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u043D\u0435 \u0441\u0442\u0430\u0440\u0448\u0435 ${decision.manifest.minShellVersion}, \u0430 \u0441\u0442\u043E\u0438\u0442 ${VERSION}. \u0421\u043A\u0430\u0436\u0438 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443, \u0447\u0442\u043E \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u043D\u0430\u0434\u043E \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u0430\u043C\u043E \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0435 \u041D\u0430\u043F\u0430\u0440\u043D\u0438\u043A\u0430, \u0438 \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u0442\u043E\u043C \u043D\u0430\u0447\u0438\u043D\u043A\u0443. \u0421\u0442\u0430\u0432\u0438\u0442\u044C \u043D\u0435 \u0441\u0442\u0430\u043B.`
18364
- );
18254
+ return `\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043E\u0448\u0438\u0431\u043A\u0430. \u041D\u0430\u043F\u0438\u0448\u0438 \u043D\u0430\u043C, \u043C\u044B \u0440\u0430\u0437\u0431\u0435\u0440\u0451\u043C\u0441\u044F: ${SUPPORT_TOPIC}`;
18255
+ }
18256
+ function watchProcessCrashes(crashes, proc = process, exit = (code) => process.exit(code)) {
18257
+ const fall = (trouble) => {
18258
+ crashes.persist(fromThrown(trouble, "process", null, {}, null));
18259
+ try {
18260
+ process.stderr.write(`naparnik-mcp: \u0443\u043F\u0430\u043B \u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u2014 ${trouble instanceof Error ? trouble.stack ?? trouble.message : String(trouble)}
18261
+ `);
18262
+ } catch {
18263
+ }
18264
+ exit(1);
18265
+ };
18266
+ proc.on("uncaughtException", fall);
18267
+ proc.on("unhandledRejection", fall);
18268
+ crashes.flushSoon();
18269
+ }
18270
+ function fromThrown(trouble, where, tool, input, host) {
18271
+ const error40 = trouble instanceof Error ? trouble : new Error(String(trouble));
18272
+ const errorType = error40.name || error40.constructor.name || "Error";
18273
+ const trace = error40.stack ?? `${errorType}: ${error40.message}`;
18274
+ const frames = jsFrames(trace);
18275
+ return {
18276
+ where,
18277
+ tool,
18278
+ stage: null,
18279
+ errorType,
18280
+ message: error40.message,
18281
+ trace,
18282
+ keep: "head",
18283
+ frames: frames.slice(0, 3).map((f) => `${f.file}:${f.func}`),
18284
+ python: null,
18285
+ secrets: secretsOf(input),
18286
+ host,
18287
+ essence: `${errorType}: ${error40.message}${frames[0] ? ` \u2014 ${frames[0].file}` : ""}`
18288
+ };
18289
+ }
18290
+ function installCrash(fault) {
18291
+ const { stage, trouble } = fault;
18292
+ if (machineCause(trouble)) return null;
18293
+ const said = commandOutput(trouble);
18294
+ if (said.includes(PYTHON_TRACEBACK)) {
18295
+ const parsed = pythonCrash(said);
18296
+ return parsed === null ? null : { ...parsed, stage };
18365
18297
  }
18366
- if (!consent) {
18367
- const mb = (decision.manifest.bytes / 1024 / 1024).toFixed(1);
18368
- return toolSuccess(
18369
- `\u0415\u0441\u0442\u044C \u043D\u043E\u0432\u0430\u044F \u043D\u0430\u0447\u0438\u043D\u043A\u0430: ${engine.freshVersion() ?? "\u043D\u0435\u0442"} \u2192 ${decision.manifest.version}, ${mb} \u041C\u0411.
18370
- ${VOICE_CLEANER_NOTE}
18371
- \u0421\u043F\u0440\u043E\u0441\u0438 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430, \u043A\u0430\u0447\u0430\u0442\u044C \u043B\u0438 \u2014 \u044D\u0442\u043E \u0435\u0433\u043E \u0442\u0440\u0430\u0444\u0438\u043A \u0438 \u0435\u0433\u043E \u0434\u0438\u0441\u043A. \u0421\u043E\u0433\u043B\u0430\u0441\u0438\u043B\u0441\u044F \u2014 \u0437\u043E\u0432\u0438 setup \u0441 what=engine \u0438 consent=true.`
18372
- );
18298
+ const error40 = trouble instanceof Error ? trouble : new Error(String(trouble));
18299
+ const reason = lastMeaningfulLine(said) ?? lastMeaningfulLine(error40.message) ?? error40.name;
18300
+ const command = typeof trouble.cmd === "string" ? trouble.cmd : "";
18301
+ const trace = [
18302
+ command === "" ? "" : `\u043A\u043E\u043C\u0430\u043D\u0434\u0430: ${command}`,
18303
+ said.trim() === "" ? "" : `\u0432\u044B\u0432\u043E\u0434 \u043A\u043E\u043C\u0430\u043D\u0434\u044B:
18304
+ ${said.trim().slice(-4e3)}`,
18305
+ error40.stack ?? `${error40.name}: ${error40.message}`
18306
+ ].filter((part) => part !== "").join("\n\n");
18307
+ return {
18308
+ stage,
18309
+ errorType: "InstallError",
18310
+ message: `${stage}: ${reason}`,
18311
+ trace,
18312
+ keep: "head",
18313
+ // ⚠ ПРИЧИНА В ОТПЕЧАТКЕ — БЕЗ ПУТЕЙ И ЧИСЕЛ. Иначе все беды шага склеились
18314
+ // бы в один отпечаток, а сервер пишет в бот про отпечаток раз в час: вторая,
18315
+ // другая беда того же шага промолчала бы. А с путями и версиями одна и та
18316
+ // же беда у двух покупателей считалась бы двумя.
18317
+ frames: [`install:${stage}`, reason.replace(/(?:[A-Za-z]:)?[\\/][^\s'"]*/g, "<path>").replace(/\d+/g, "0").slice(0, 160)],
18318
+ python: /python@?(\d)\.(\d{1,2})\b/i.exec(command)?.slice(1, 3).join(".") ?? null,
18319
+ essence: `\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430, \u0448\u0430\u0433 ${stage}: ${reason}`
18320
+ };
18321
+ }
18322
+ function machineCause(trouble) {
18323
+ const t = trouble ?? {};
18324
+ if (typeof t.code === "string" && MACHINE_CODES.has(t.code)) return true;
18325
+ if (t.code === 2) return true;
18326
+ if (t.killed === true || typeof t.signal === "string" && t.signal !== "") return true;
18327
+ const text2 = `${typeof t.message === "string" ? t.message : String(trouble)}
18328
+ ${commandOutput(trouble)}`;
18329
+ return MACHINE_WORDS.test(text2);
18330
+ }
18331
+ var MACHINE_CODES = /* @__PURE__ */ new Set([
18332
+ "ENOSPC",
18333
+ "EDQUOT",
18334
+ "EACCES",
18335
+ "EPERM",
18336
+ "EROFS",
18337
+ "EBUSY",
18338
+ "ENOTFOUND",
18339
+ "EAI_AGAIN",
18340
+ "ECONNRESET",
18341
+ "ECONNREFUSED",
18342
+ "ETIMEDOUT",
18343
+ "ENETUNREACH",
18344
+ "ENETDOWN",
18345
+ "EHOSTUNREACH"
18346
+ ]);
18347
+ var MACHINE_WORDS = new RegExp([
18348
+ "No space left on device",
18349
+ "Disk quota exceeded",
18350
+ "Permission denied",
18351
+ "Operation not permitted",
18352
+ "Read-only file system",
18353
+ "Access is denied",
18354
+ "Temporary failure in name resolution",
18355
+ "Name or service not known",
18356
+ "nodename nor servname",
18357
+ "getaddrinfo",
18358
+ "Network is unreachable",
18359
+ "No route to host",
18360
+ "Connection refused",
18361
+ "Connection reset",
18362
+ "Connection aborted",
18363
+ "timed out",
18364
+ "Max retries exceeded",
18365
+ "NewConnectionError",
18366
+ "ConnectTimeoutError",
18367
+ "ReadTimeoutError",
18368
+ "ProxyError",
18369
+ "CERTIFICATE_VERIFY_FAILED"
18370
+ ].map((one) => one.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "i");
18371
+ function commandOutput(trouble) {
18372
+ const t = trouble ?? {};
18373
+ return [t.stderr, t.stdout].filter((one) => typeof one === "string" && one.trim() !== "").join("\n");
18374
+ }
18375
+ function lastMeaningfulLine(text2) {
18376
+ const lines = text2.split("\n").map((one) => one.trim()).filter((one) => one !== "" && !/^Command failed:/.test(one));
18377
+ return lines[lines.length - 1] ?? null;
18378
+ }
18379
+ function jsFrames(stack) {
18380
+ const frames = [];
18381
+ for (const line of stack.split("\n")) {
18382
+ const m = /^\s*at (?:(.+?) \()?(.+?):\d+:\d+\)?\s*$/.exec(line);
18383
+ if (m === null) continue;
18384
+ const file2 = basename(m[2] ?? "").replace(/-[A-Za-z0-9]{6,}(\.[cm]?js)$/, "$1");
18385
+ frames.push({ file: file2, func: m[1] ?? "<anonymous>" });
18373
18386
  }
18374
- return asToolResult(await deploy(engine, decision.manifest, void 0, key));
18387
+ return frames;
18375
18388
  }
18376
- async function updateShell(engine, consent, key, baseUrl, timeoutMs = ASKED_POINTER_MS, seen = () => {
18377
- }) {
18378
- const home = engine.home();
18379
- const url2 = engine.variable("NAPARNIK_BUNDLE_MANIFEST") ?? bundleUrlFor(baseUrl);
18380
- const outcome = await fetchShellManifest(url2, timeoutMs, key);
18381
- const installed = installedShell(home);
18382
- const decision = decideShell(installed, outcome.ok ? outcome.manifest : null);
18383
- seen({
18384
- installed,
18385
- fresh: decision.what === "nothing" ? null : decision.manifest.version,
18386
- mb: decision.what === "nothing" ? 0 : tenths(decision.manifest.body?.bytes ?? 0),
18387
- status: outcome.ok ? 0 : outcome.status ?? 0,
18388
- notes: decision.what === "nothing" ? "" : decision.manifest.notes ?? ""
18389
+ function pythonCrash(text2) {
18390
+ const start = text2.lastIndexOf(PYTHON_TRACEBACK);
18391
+ if (start < 0) return null;
18392
+ const block = text2.slice(start).replace(/\s+$/, "");
18393
+ const lines = block.split("\n");
18394
+ const frames = [];
18395
+ let lastFrame = -1;
18396
+ lines.forEach((line, at2) => {
18397
+ const m2 = /^\s*File "(.+)", line (\d+), in (.+)$/.exec(line);
18398
+ if (m2 === null) return;
18399
+ frames.push({ file: m2[1] ?? "", line: m2[2] ?? "", func: (m2[3] ?? "").trim() });
18400
+ lastFrame = at2;
18389
18401
  });
18390
- if (decision.what === "nothing") {
18391
- if (!outcome.ok) {
18392
- return toolSuccess(
18393
- `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043F\u0440\u043E\u0441\u0438\u0442\u044C, \u0435\u0441\u0442\u044C \u043B\u0438 \u043D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F: ${outcome.why}. \u042D\u0442\u043E \u043D\u0435 \u043F\u043E\u043B\u043E\u043C\u043A\u0430: \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F (${installed}) \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043A\u0430\u043A \u0440\u0430\u0431\u043E\u0442\u0430\u043B\u0430.`
18394
- );
18395
- }
18396
- return toolSuccess(
18397
- outcome.manifest.body === null ? `\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E ${installed}; \u043D\u0430 \u0440\u0430\u0437\u0434\u0430\u0447\u0435 ${outcome.manifest.version}, \u043D\u043E \u0444\u0430\u0439\u043B\u0430 \u0434\u043B\u044F \u0441\u0430\u043C\u043E\u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F \u0442\u0430\u043C \u043F\u043E\u043A\u0430 \u043D\u0435\u0442. \u041D\u0438\u0447\u0435\u0433\u043E \u0434\u0435\u043B\u0430\u0442\u044C \u043D\u0435 \u043D\u0443\u0436\u043D\u043E, \u043E\u043D \u043F\u043E\u044F\u0432\u0438\u0442\u0441\u044F \u0441 \u0431\u043B\u0438\u0436\u0430\u0439\u0448\u0438\u043C \u0432\u044B\u043A\u0430\u0442\u043E\u043C.` : `\u0423 \u0432\u0430\u0441 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u0441\u0430\u043C\u0430\u044F \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u044F\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u2014 ${installed}.`
18398
- );
18399
- }
18400
- if (decision.what === "launcher_too_old") {
18401
- return toolSuccess(
18402
- `\u041D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F ${decision.manifest.version} \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043D\u043E\u0432\u043E\u0433\u043E \u0444\u0430\u0439\u043B\u0430 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u2014 \u0442\u043E\u0433\u043E, \u0447\u0442\u043E \u0441\u0442\u0430\u0432\u0438\u0442\u0441\u044F \u043F\u0435\u0440\u0435\u0442\u0430\u0441\u043A\u0438\u0432\u0430\u043D\u0438\u0435\u043C. \u0421\u043A\u0430\u0436\u0438 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443: \u043D\u0430\u043F\u0438\u0448\u0438\u0442\u0435 \u0431\u043E\u0442\u0443 ${KEY_SOURCE_BOT}, \u043E\u043D \u043F\u0440\u0438\u0448\u043B\u0451\u0442 \u0441\u0432\u0435\u0436\u0438\u0439 \u0444\u0430\u0439\u043B; \u0432\u0441\u0451 \u043E\u0441\u0442\u0430\u043B\u044C\u043D\u043E\u0435 \u2014 \u043A\u043B\u044E\u0447, \u0434\u0432\u0438\u0436\u043E\u043A, \u0441\u0442\u0438\u043B\u044C, \u0437\u0430\u0434\u0430\u0447\u0438 \u2014 \u043E\u0441\u0442\u0430\u043D\u0435\u0442\u0441\u044F \u043D\u0430 \u043C\u0435\u0441\u0442\u0435. \u0421\u0442\u0430\u0432\u0438\u0442\u044C \u043D\u0435 \u0441\u0442\u0430\u043B.`
18403
- );
18404
- }
18405
- if (!consent) {
18406
- const mb = ((decision.manifest.body?.bytes ?? 0) / 1024 / 1024).toFixed(1);
18407
- return toolSuccess(
18408
- `\u0415\u0441\u0442\u044C \u043D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F: ${installed} \u2192 ${decision.manifest.version}, ${mb} \u041C\u0411.
18409
- ` + (decision.manifest.notes === null ? "" : `\u0427\u0442\u043E \u043D\u043E\u0432\u043E\u0433\u043E:
18410
- ${decision.manifest.notes}
18411
- `) + "\u0421\u043A\u0430\u0436\u0438 \u044D\u0442\u043E \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443 \u0438 \u0441\u043F\u0440\u043E\u0441\u0438, \u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043B\u0438. \u0421\u043E\u0433\u043B\u0430\u0441\u0438\u043B\u0441\u044F \u2014 \u0437\u043E\u0432\u0438 setup \u0441 what=shell \u0441 consent=true. \u0417\u0430\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043F\u043E\u0441\u043B\u0435 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430 \u041A\u043B\u043E\u0434\u0430 \u0438\u043B\u0438 Codex."
18412
- );
18402
+ if (frames.length === 0) return null;
18403
+ if (frames.some((f) => /[\\/]mine[\\/]steps[\\/]/.test(f.file))) return null;
18404
+ const at = lines.findIndex((line, n) => n > lastFrame && line !== "" && !/^\s/.test(line));
18405
+ if (at < 0) return null;
18406
+ const m = /^([A-Za-z_][\w.]*)(?::\s?(.*))?$/.exec(lines[at] ?? "");
18407
+ if (m === null) return null;
18408
+ const errorType = m[1] ?? "Error";
18409
+ if (/(?:^|\.)(?:KeyboardInterrupt|SystemExit|Explained|Refusal)$/.test(errorType)) return null;
18410
+ const message = [m[2] ?? "", ...lines.slice(at + 1)].join("\n").trim();
18411
+ const innermost = frames[frames.length - 1];
18412
+ const firstLine = message.split("\n")[0] ?? "";
18413
+ return {
18414
+ errorType,
18415
+ message,
18416
+ trace: block,
18417
+ keep: "tail",
18418
+ frames: frames.slice(-3).map((f) => `${basename(f.file.replace(/\\/g, "/"))}:${f.func}`),
18419
+ python: pythonVersionIn(frames.map((f) => f.file)),
18420
+ essence: `${errorType}${firstLine === "" ? "" : `: ${firstLine}`}` + (innermost ? ` \u2014 ${basename(innermost.file.replace(/\\/g, "/"))}:${innermost.line}` : "")
18421
+ };
18422
+ }
18423
+ function pythonVersionIn(files) {
18424
+ for (const file2 of files) {
18425
+ const m = /python(\d)\.?(\d{1,2})[\\/]/i.exec(file2);
18426
+ if (m !== null) return `${m[1]}.${m[2]}`;
18413
18427
  }
18414
- return asToolResult(await installShell({
18415
- home,
18416
- manifest: decision.manifest,
18417
- key: key ?? null
18418
- }));
18428
+ return null;
18419
18429
  }
18420
- var UPDATES = /* @__PURE__ */ new Set(["all", "engine", "shell"]);
18421
- var HEAVY_STEPS = {
18422
- speech: "model",
18423
- cutout: "cutout",
18424
- voice: "voice"
18425
- };
18426
- function setupTool(engine, passport, key, baseUrl) {
18427
- return defineTool({
18428
- name: "setup",
18429
- title: "\u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0438 \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0432\u0441\u0451 \u0434\u043B\u044F \u043C\u043E\u043D\u0442\u0430\u0436\u0430",
18430
- description: "\u0412\u0441\u0451, \u0447\u0435\u043C \u043C\u043E\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043D\u0430 \u044D\u0442\u043E\u043C \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440\u0435, \u2014 \u043E\u0434\u043D\u0438\u043C \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u043E\u043C. \u0411\u0435\u0437 \u0430\u0440\u0433\u0443\u043C\u0435\u043D\u0442\u043E\u0432 \u041F\u0420\u041E\u0412\u0415\u0420\u042F\u0415\u0422 \u0434\u0435\u043B\u043E\u043C: \u0447\u0442\u043E \u0441\u0442\u043E\u0438\u0442 \u0438 \u0447\u0435\u0433\u043E \u043D\u0435 \u0445\u0432\u0430\u0442\u0430\u0435\u0442, \u0447\u0442\u043E \u0434\u043E\u043A\u0430\u0447\u0430\u0435\u0442\u0441\u044F \u043F\u043E \u043F\u0440\u043E\u0441\u044C\u0431\u0435 \u0438 \u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0432\u0435\u0441\u0438\u0442, \u0447\u0442\u043E \u0443\u043C\u0435\u0435\u0442 \u043C\u043E\u043D\u0442\u0430\u0436 (\u0448\u0430\u0433\u0438, \u0432\u043E\u043F\u0440\u043E\u0441\u044B \u0434\u043B\u044F \u043A\u0430\u0440\u0442\u043E\u0447\u0435\u043A choose_variant, \u0447\u0435\u0439 \u0440\u0435\u0446\u0435\u043F\u0442 \u0438 \u0447\u0435\u0439 \u0441\u0442\u0438\u043B\u044C) \u0438 \u0432\u044B\u0448\u043B\u0430 \u043B\u0438 \u043D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u0434\u0432\u0438\u0436\u043A\u0430 \u0438\u043B\u0438 \u0441\u0430\u043C\u043E\u0433\u043E \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F. \u0417\u043E\u0432\u0438 \u041F\u0415\u0420\u0412\u042B\u041C, \u0434\u043E \u043B\u044E\u0431\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u044B \u0441 \u0432\u0438\u0434\u0435\u043E, \u0438 \u0438\u0437 \u0441\u0432\u043E\u0435\u0439 \u043F\u0430\u043C\u044F\u0442\u0438 \u043E\u0431 \u044D\u0442\u043E\u043C \u043D\u0435 \u0440\u0430\u0441\u0441\u043A\u0430\u0437\u044B\u0432\u0430\u0439.\n\u0421\u0442\u0430\u0432\u0438\u0442 \u0438 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0435\u0442 \u0441 what, \u043D\u043E \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u0441\u043B\u0435 \xAB\u0434\u0430\xBB \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430: \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u0441\u043A\u0430\u0436\u0438 \u0435\u043C\u0443 \u0441\u043B\u043E\u0432\u0430\u043C\u0438, \u0447\u0442\u043E \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0448\u044C \u0438 \u0441\u043A\u043E\u043B\u044C\u043A\u043E \u044D\u0442\u043E \u0432\u0435\u0441\u0438\u0442, \u043F\u043E\u0442\u043E\u043C \u0437\u043E\u0432\u0438 \u0441 \u0442\u0435\u043C \u0436\u0435 what \u0438 consent=true. \xAB\u0421\u043A\u0430\u0447\u0430\u0439\xBB \u0438 \xAB\u043E\u0431\u043D\u043E\u0432\u0438\xBB \u2014 \u0442\u043E \u0436\u0435 \xAB\u0434\u0430\xBB \u043D\u0430 \u0442\u043E, \u0447\u0442\u043E \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0443\u0436\u0435 \u043D\u0430\u0437\u0432\u0430\u043B\u0430 \u0441 \u0432\u0435\u0441\u043E\u043C: \u0437\u043E\u0432\u0438 what=all \u0441 consent=true \u041E\u0414\u0418\u041D \u0440\u0430\u0437 \u2014 \u043E\u0442\u0432\u0435\u0442 \u043D\u043E\u043C\u0435\u0440\u043E\u043C \u0437\u0430\u0434\u0430\u0447\u0438, \u0434\u0430\u043B\u044C\u0448\u0435 wait, setup \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u043E \u043D\u0435 \u0437\u043E\u0432\u0438. \u0411\u0435\u0437 consent \u043D\u0438\u0447\u0435\u0433\u043E \u043D\u0435 \u043A\u0430\u0447\u0430\u0435\u0442\u0441\u044F \u2014 \u043F\u0440\u0438\u0434\u0443\u0442 \u0432\u0435\u0441 \u0438 \u0432\u043E\u043F\u0440\u043E\u0441. \u0422\u044F\u0436\u0451\u043B\u043E\u0435 \u0441\u0442\u0430\u0432\u0438\u0442\u0441\u044F \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u0434 \u043F\u0440\u043E\u0441\u044C\u0431\u0443: \u043D\u0435 \u043F\u0440\u043E\u0441\u0438\u043B\u0438 \u0441\u0443\u0431\u0442\u0438\u0442\u0440\u044B \u2014 \u043D\u0435 \u0441\u0442\u0430\u0432\u044C speech, \u043D\u0435 \u043F\u0440\u043E\u0441\u0438\u043B\u0438 \u043D\u043E\u0432\u044B\u0439 \u0444\u043E\u043D \u2014 \u043D\u0435 \u0441\u0442\u0430\u0432\u044C cutout.\n\u0414\u043E\u043B\u0433\u0430\u044F \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u043D\u043E\u043C\u0435\u0440\u043E\u043C \u0437\u0430\u0434\u0430\u0447\u0438 \u2014 \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u0435\u0451 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u043E\u043C wait. \u041D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u0434\u0432\u0438\u0436\u043A\u0430 \u0438\u043B\u0438 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u0437\u0430\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043F\u043E\u0441\u043B\u0435 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430 \u041A\u043B\u043E\u0434\u0430 \u0438\u043B\u0438 Codex, \u0441\u043A\u0430\u0436\u0438 \u044D\u0442\u043E \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443. \u0421\u043B\u043E\u043C\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u0441\u043B\u0435 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F \u2014 rollback. \u041F\u043E\u0434\u043F\u0438\u0441\u043A\u0430 \u043D\u0435 \u043D\u0443\u0436\u043D\u0430: \u0443\u0437\u043D\u0430\u0442\u044C, \u0437\u0430 \u0447\u0442\u043E \u043F\u043B\u0430\u0442\u044F\u0442, \u043C\u043E\u0436\u043D\u043E \u0434\u043E \u043E\u043F\u043B\u0430\u0442\u044B, \u0430 \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u0435 \u0441\u0430\u043C\u043E \u0443\u043F\u0438\u0440\u0430\u0435\u0442\u0441\u044F \u0432 \u043A\u043B\u044E\u0447 \u043D\u0430 \u0440\u0430\u0437\u0434\u0430\u0447\u0435.",
18431
- // Полоса хода работы в ленте: установка качает десятки минут, и человек
18432
- // видит, что качается и сколько уже скачано. ⚠ Ожидание задачи — у
18433
- // отдельного инструмента `wait`, без полосы: полоса объявлена на
18434
- // инструменте, и ждать тем же инструментом значило бы рисовать новую
18435
- // полосу на каждый вызов.
18436
- ui: { resourceUri: RUNBAR_RESOURCE_URI },
18437
- args: external_exports.object({
18438
- what: external_exports.enum(["all", "engine", "shell", "speech", "cutout", "voice"]).describe(
18439
- "\u0427\u0442\u043E \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0438\u043B\u0438 \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C. all \u2014 \u0432\u0441\u0451 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435, \u0447\u0435\u0433\u043E \u043D\u0435 \u0445\u0432\u0430\u0442\u0430\u0435\u0442, \u0438 \u043D\u043E\u0432\u044B\u0435 \u0432\u0435\u0440\u0441\u0438\u0438, \u043E \u043A\u043E\u0442\u043E\u0440\u044B\u0445 \u0441\u043A\u0430\u0437\u0430\u043B\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0430; engine \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u043D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u0434\u0432\u0438\u0436\u043A\u0430; shell \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u043D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u0441\u0430\u043C\u043E\u0433\u043E \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F; speech \u2014 \u0440\u0430\u0441\u043F\u043E\u0437\u043D\u0430\u0432\u0430\u043D\u0438\u0435 \u0440\u0435\u0447\u0438 (\u0441\u0443\u0431\u0442\u0438\u0442\u0440\u044B); cutout \u2014 \u0432\u044B\u0440\u0435\u0437 \u0433\u0435\u0440\u043E\u044F (\u043D\u043E\u0432\u044B\u0439 \u0444\u043E\u043D); voice \u2014 \u0447\u0438\u0441\u0442\u043A\u0430 \u0433\u043E\u043B\u043E\u0441\u0430"
18440
- ).optional(),
18441
- consent: external_exports.boolean().describe(
18442
- "\u0427\u0435\u043B\u043E\u0432\u0435\u043A \u0420\u0410\u0417\u0420\u0415\u0428\u0418\u041B \u0441\u043A\u0430\u0447\u0430\u0442\u044C \u0442\u043E, \u0447\u0442\u043E \u043D\u0430\u0437\u0432\u0430\u043D\u043E \u0432 what. \u0421\u0442\u0430\u0432\u044C \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u0441\u043B\u0435 \u0442\u043E\u0433\u043E, \u043A\u0430\u043A \u043D\u0430\u0437\u0432\u0430\u043B \u0435\u043C\u0443, \u0447\u0442\u043E \u0438 \u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0432\u0435\u0441\u0438\u0442, \u0438 \u043E\u043D \u0441\u043E\u0433\u043B\u0430\u0441\u0438\u043B\u0441\u044F \u2014 \u044D\u0442\u043E \u0435\u0433\u043E \u0442\u0440\u0430\u0444\u0438\u043A \u0438 \u0435\u0433\u043E \u0434\u0438\u0441\u043A"
18443
- ).optional(),
18444
- model: fromList(
18445
- passport?.["models"],
18446
- `\u041C\u043E\u0434\u0435\u043B\u044C \u0440\u0430\u0441\u043F\u043E\u0437\u043D\u0430\u0432\u0430\u043D\u0438\u044F \u0440\u0435\u0447\u0438: \u043A\u0430\u043A\u0443\u044E \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0442\u044C \u0438\u043B\u0438 \u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0441 what="speech". ${aboutModels(passport)}`
18447
- ).optional(),
18448
- rollback: external_exports.enum(["engine", "shell"]).describe(
18449
- "\u0412\u0435\u0440\u043D\u0443\u0442\u044C \u043F\u0440\u0435\u0436\u043D\u044E\u044E \u0432\u0435\u0440\u0441\u0438\u044E: engine \u2014 \u0434\u0432\u0438\u0436\u043E\u043A, shell \u2014 \u0441\u0430\u043C\u043E \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0435. \u0417\u043E\u0432\u0438, \u043A\u043E\u0433\u0434\u0430 \u043F\u043E\u0441\u043B\u0435 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F \u0447\u0442\u043E-\u0442\u043E \u043F\u0435\u0440\u0435\u0441\u0442\u0430\u043B\u043E \u0440\u0430\u0431\u043E\u0442\u0430\u0442\u044C: \u043F\u0440\u0435\u0436\u043D\u044F\u044F \u043B\u0435\u0436\u0438\u0442 \u043D\u0430 \u0434\u0438\u0441\u043A\u0435 \u0440\u044F\u0434\u043E\u043C. \u0417\u0430\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043F\u043E\u0441\u043B\u0435 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430"
18450
- ).optional()
18451
- // ⚠ СОЧЕТАНИЯ СВЕРЯЮТСЯ В САМОЙ СХЕМЕ, А НЕ В ТЕЛЕ. Так правило видит
18452
- // сверка аргументов на регистрации, и кривой вызов отбивается до всякой
18453
- // сети (`tool-args.ts`); в теле оно стало бы правдой, о которой сверка не
18454
- // знает.
18455
- }).check((ctx) => {
18456
- const asked = ctx.value;
18457
- if (asked.rollback !== void 0 && asked.what !== void 0) {
18458
- ctx.issues.push({
18459
- code: "custom",
18460
- message: "\xABrollback\xBB \u0438 \xABwhat\xBB \u2014 \u0440\u0430\u0437\u043D\u044B\u0435 \u043F\u0440\u043E\u0441\u044C\u0431\u044B: \u043E\u0442\u043A\u0430\u0442 \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043F\u0440\u0435\u0436\u043D\u044E\u044E \u0432\u0435\u0440\u0441\u0438\u044E, \u0430 what \u0441\u0442\u0430\u0432\u0438\u0442 \u043D\u043E\u0432\u043E\u0435. \u041F\u043E\u0437\u043E\u0432\u0438 \u0441 \u043E\u0434\u043D\u0438\u043C \u0438\u0437 \u043D\u0438\u0445",
18461
- input: asked,
18462
- path: ["rollback"]
18463
- });
18464
- }
18465
- if (asked.consent === true && asked.what === void 0 && asked.rollback === void 0) {
18466
- ctx.issues.push({
18467
- code: "custom",
18468
- message: "\xABconsent\xBB \u0431\u0435\u0437 \xABwhat\xBB: \u043D\u0435 \u0441\u043A\u0430\u0437\u0430\u043D\u043E, \u043D\u0430 \u0447\u0442\u043E \u0441\u043E\u0433\u043B\u0430\u0441\u0438\u043B\u0441\u044F \u0447\u0435\u043B\u043E\u0432\u0435\u043A. \u041D\u0430\u0437\u043E\u0432\u0438 what \u2014 \u0442\u043E, \u0447\u0442\u043E \u0442\u044B \u0435\u043C\u0443 \u043F\u0440\u0435\u0434\u043B\u0430\u0433\u0430\u043B",
18469
- input: asked,
18470
- path: ["consent"]
18471
- });
18472
- }
18473
- }),
18474
- async execute(args, ctx) {
18475
- const news = { engine: null, shell: null };
18476
- const said = await answer(args, ctx?.emitProgress, news);
18477
- return withCard(said, () => setupCard({
18478
- readiness: null,
18479
- engine: news.engine ?? { installed: engine.freshVersion(), fresh: null, mb: 0, status: 0, notes: "" },
18480
- shell: news.shell,
18481
- note: firstLine(said),
18482
- shellVersion: VERSION
18483
- }));
18484
- }
18485
- });
18486
- async function answer(args, tell, news) {
18487
- if (args.rollback === "engine") return asToolResult(rollback(engine));
18488
- if (args.rollback === "shell") return asToolResult(rollbackShell(engine.home()));
18489
- const consent = args.consent === true;
18490
- const what = args.what;
18491
- if (what === void 0) return lookAround(engine, args.model, key, baseUrl);
18492
- if (consent && UPDATES.has(what)) return startUpdate(engine, what, key, baseUrl);
18493
- if (what === "engine") {
18494
- return updateEngine(engine, consent, key, baseUrl, ASKED_POINTER_MS, (seen) => {
18495
- if (!consent) news.engine = seen;
18496
- });
18497
- }
18498
- if (what === "shell") {
18499
- return updateShell(engine, consent, key, baseUrl, ASKED_POINTER_MS, (seen) => {
18500
- if (!consent) news.shell = seen;
18501
- });
18502
- }
18503
- if (what === "all") {
18504
- return lookAround(engine, args.model, key, baseUrl);
18430
+ function jobLogTail(home, id) {
18431
+ const file2 = join(home, "logs", `${id}.log`);
18432
+ let fd = null;
18433
+ try {
18434
+ fd = openSync(file2, "r");
18435
+ const size = fstatSync(fd).size;
18436
+ const length = Math.min(size, JOB_LOG_TAIL);
18437
+ const buffer = Buffer.alloc(length);
18438
+ readSync(fd, buffer, 0, length, size - length);
18439
+ return buffer.toString("utf8");
18440
+ } catch {
18441
+ return "";
18442
+ } finally {
18443
+ if (fd !== null) try {
18444
+ closeSync(fd);
18445
+ } catch {
18505
18446
  }
18506
- const step = HEAVY_STEPS[what];
18507
- return consent ? installStep(engine, { step, model: args.model, consent: true }, key, baseUrl, tell) : askBeforeHeavy(engine, step, args.model, key, baseUrl);
18508
18447
  }
18509
18448
  }
18510
- async function lookAround(engine, model, key, baseUrl) {
18511
- let engineNews = null;
18512
- let shellNews = null;
18513
- const toEngine = (news2) => {
18514
- engineNews = news2;
18515
- };
18516
- const toShell = (news2) => {
18517
- shellNews = news2;
18518
- };
18519
- const card = (said, o) => withCard(said, () => setupCard({
18520
- readiness: o.readiness ?? null,
18521
- engine: engineNews ?? { installed: engine.freshVersion(), fresh: null, mb: 0, status: 0, notes: "" },
18522
- shell: shellNews,
18523
- ...o.noEngine === true ? { noEngine: true } : {},
18524
- ...o.noPython === true ? { noPython: true } : {},
18525
- note: o.note ?? firstLine(said),
18526
- shellVersion: VERSION
18527
- }));
18528
- const shell = updateShell(engine, false, key, baseUrl, NEWS_POINTER_MS, toShell);
18529
- if (engine.place() === null) {
18530
- const payload = await installPayload(engine, {
18531
- consent: false,
18532
- manifestUrl: manifestUrl(engine, baseUrl),
18533
- ...key !== void 0 ? { key } : {}
18534
- });
18535
- return card(together([toolSuccess(payload.text), await shell]), { noEngine: true });
18449
+ function fingerprintOf2(errorType, frames) {
18450
+ return createHash("sha256").update([errorType, ...frames].join("|")).digest("hex").slice(0, 16);
18451
+ }
18452
+ function secretsOf(input, depth = 0, out = []) {
18453
+ if (out.length >= 50 || depth > 4) return out;
18454
+ if (typeof input === "string") out.push(input);
18455
+ else if (Array.isArray(input)) for (const one of input) secretsOf(one, depth + 1, out);
18456
+ else if (typeof input === "object" && input !== null) {
18457
+ for (const one of Object.values(input)) secretsOf(one, depth + 1, out);
18536
18458
  }
18537
- const noPython = await engine.pythonTrouble();
18538
- if (noPython !== null) {
18539
- return card(together([
18540
- toolSuccess(noPython),
18541
- await updateEngine(engine, false, key, baseUrl, NEWS_POINTER_MS, toEngine),
18542
- await shell
18543
- ]), { noPython: true });
18544
- }
18545
- const [machine, news, own] = await Promise.all([
18546
- inspectMachine(engine, { model }),
18547
- updateEngine(engine, false, key, baseUrl, NEWS_POINTER_MS, toEngine),
18548
- shell
18549
- ]);
18550
- return card(
18551
- together([newsForModel(shellNews, engineNews), machine.result, news, own]),
18552
- { readiness: machine.readiness, note: firstLine(together([machine.result, news, own])) }
18553
- );
18459
+ return out;
18554
18460
  }
18555
- function newsForModel(shell, engine) {
18556
- if (!shell?.fresh && !engine?.fresh) return toolSuccess("");
18557
- const numbers = [
18558
- engine?.fresh ? `\u0434\u0432\u0438\u0436\u043E\u043A ${engine.installed ?? "\u043D\u0435\u0442"} \u2192 ${engine.fresh}` : "",
18559
- shell?.fresh ? `\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0435 ${shell.installed ?? VERSION} \u2192 ${shell.fresh}` : ""
18560
- ].filter((one) => one !== "").join(", ");
18561
- return toolSuccess(
18562
- `\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u0430 \u043D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u043C\u043E\u043D\u0442\u0430\u0436\u0451\u0440\u0430: ${whatIsNew(shell, engine)} (${numbers}). \u0421\u043A\u0430\u0436\u0438 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443, \u0447\u0442\u043E \u0438\u043C\u0435\u043D\u043D\u043E \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u0441\u044F \u0438 \u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0432\u0435\u0441\u0438\u0442. \xAB\u041E\u0431\u043D\u043E\u0432\u0438\xBB \u2014 \u044D\u0442\u043E \xAB\u0434\u0430\xBB: \u043E\u0434\u0438\u043D \u0432\u044B\u0437\u043E\u0432 setup \u0441 what=all \u0438 consent=true, \u0434\u0430\u043B\u044C\u0448\u0435 wait \u043F\u043E \u043D\u043E\u043C\u0435\u0440\u0443 \u0437\u0430\u0434\u0430\u0447\u0438.`
18563
- );
18461
+ function recentCalls(home) {
18462
+ try {
18463
+ const text2 = readFileSync(join(home, CALLS_FILE), "utf8").slice(-8192);
18464
+ const names = [];
18465
+ for (const line of text2.split("\n")) {
18466
+ try {
18467
+ const tool = JSON.parse(line).tool;
18468
+ if (typeof tool === "string" && CRASH_TOOL_NAME.test(tool)) names.push(tool);
18469
+ } catch {
18470
+ }
18471
+ }
18472
+ return names.slice(-CRASH_LIMITS.recentCalls);
18473
+ } catch {
18474
+ return [];
18475
+ }
18564
18476
  }
18565
- function withCard(result, card) {
18566
- if (result.type !== "success" || result.structured !== void 0) return result;
18567
- return toolSuccess(wordsOf(result.content), card());
18477
+ function hostFromContext(context) {
18478
+ const host = context?.host;
18479
+ return host !== void 0 && typeof host.name === "string" && typeof host.version === "string" ? host : null;
18568
18480
  }
18569
- function firstLine(result) {
18570
- const text2 = result.type === "success" ? wordsOf(result.content) : result.error;
18571
- return text2.split("\n").map((line) => line.trim()).find((line) => line !== "") ?? "";
18481
+ function hostOf(host) {
18482
+ return host === null ? null : { name: head(host.name, CRASH_LIMITS.hostName), version: head(host.version, CRASH_LIMITS.hostVersion) };
18572
18483
  }
18573
- function startUpdate(engine, what, key, baseUrl) {
18574
- const title = engine.place() === null ? TITLE_INSTALL : TITLE_UPDATE;
18575
- const id = startSetupJob(title, (job) => runUpdate(job, {
18576
- engine,
18577
- key,
18578
- baseUrl,
18579
- what,
18580
- pointerMs: ASKED_POINTER_MS,
18581
- installMissing: () => installMissing(engine, key, baseUrl, void 0)
18582
- }), Date.now, engine.home());
18583
- const report = setupJobReport(id, engine.home());
18584
- return toolSuccess(
18585
- `\u0417\u0430\u0434\u0430\u0447\u0430 ${id}: ${report?.what ?? title}.
18586
- \u0418\u0434\u0451\u0442 \u043D\u0430 \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440\u0435 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F: \u043F\u0440\u043E\u0432\u0435\u0440\u044E, \u0447\u0442\u043E \u0432\u044B\u0448\u043B\u043E, \u0441\u043A\u0430\u0447\u0430\u044E \u0438 \u043F\u043E\u0441\u0442\u0430\u0432\u043B\u044E. \u041F\u043E\u043B\u043E\u0441\u0430 \u0443 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0441\u0430\u043C\u0430. \u041F\u043E\u0437\u043E\u0432\u0438 wait \u0441 job "` + id + '" \u2014 \u0434\u043E\u0436\u0434\u0451\u0442\u0441\u044F \u0438 \u0441\u043A\u0430\u0436\u0435\u0442, \u0447\u0435\u043C \u043A\u043E\u043D\u0447\u0438\u043B\u043E\u0441\u044C; setup \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u043E \u043D\u0435 \u0437\u043E\u0432\u0438. \u0413\u043E\u0442\u043E\u0432\u043E \u2014 \u043F\u0435\u0440\u0435\u0441\u043A\u0430\u0436\u0438 \u0441\u0442\u0440\u043E\u043A\u0443 \xAB\u0427\u0435\u043B\u043E\u0432\u0435\u043A\u0443\xBB.',
18587
- report ?? void 0
18588
- );
18484
+ function probeFfmpeg(home) {
18485
+ const own = join(home, "bin", platform() === "win32" ? "ffmpeg.exe" : "ffmpeg");
18486
+ const program = existsSync(own) ? own : "ffmpeg";
18487
+ return new Promise((done) => {
18488
+ execFile(program, ["-version"], { timeout: 3e3, encoding: "utf8" }, (trouble, stdout) => {
18489
+ if (trouble !== null) return done(null);
18490
+ const m = /^ffmpeg version (\S+)/.exec(stdout);
18491
+ done(m?.[1] ?? null);
18492
+ });
18493
+ });
18589
18494
  }
18590
- async function installMissing(engine, key, baseUrl, tell) {
18591
- const place = engine.place();
18592
- if (place !== null && await engine.pythonTrouble() === null && await nothingMissing(engine, place)) {
18593
- return toolSuccess("\u0412\u0441\u0451 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u0434\u043B\u044F \u043C\u043E\u043D\u0442\u0430\u0436\u0430 \u043D\u0430 \u043C\u0435\u0441\u0442\u0435 \u2014 \u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E.");
18495
+ function ownDir() {
18496
+ try {
18497
+ return dirname(fileURLToPath(import.meta.url));
18498
+ } catch {
18499
+ return "";
18594
18500
  }
18595
- return installStep(engine, { step: "all", consent: true }, key, baseUrl, tell);
18596
18501
  }
18597
- async function nothingMissing(engine, place) {
18502
+ function safeUser() {
18598
18503
  try {
18599
- await engine.run(place, ["env-check", "--pack", place.pack, "--first-install"]);
18600
- return true;
18504
+ return userInfo().username;
18601
18505
  } catch {
18602
- return false;
18506
+ return "";
18603
18507
  }
18604
18508
  }
18605
- async function askBeforeHeavy(engine, step, model, key, baseUrl) {
18606
- const place = engine.place();
18607
- if (place === null) {
18608
- const payload = await installPayload(engine, {
18609
- consent: false,
18610
- manifestUrl: manifestUrl(engine, baseUrl),
18611
- ...key !== void 0 ? { key } : {}
18612
- });
18613
- return toolSuccess(payload.text);
18614
- }
18615
- const noPython = await engine.pythonTrouble();
18616
- if (noPython !== null) return toolSuccess(noPython);
18617
- return asToolResult(await engine.command([
18618
- "env-install",
18619
- "--pack",
18620
- place.pack,
18621
- "--step",
18622
- step,
18623
- ...model !== void 0 ? ["--model", model] : []
18624
- ]));
18509
+ function head(text2, max) {
18510
+ return text2.length <= max ? text2 : text2.slice(0, max);
18625
18511
  }
18626
- function together(results) {
18627
- const text2 = results.map((one) => one.type === "success" ? wordsOf(one.content) : one.error).filter((words) => words !== "").join("\n\n");
18628
- for (const one of results) {
18629
- if (one.type === "success" && one.structured !== void 0) return toolSuccess(text2, one.structured);
18630
- }
18631
- return results.some((one) => one.type === "error") ? toolError(text2) : toolSuccess(text2);
18512
+ function tail(text2, max) {
18513
+ if (text2.length <= max) return text2;
18514
+ const cut = "\u2026(\u043D\u0430\u0447\u0430\u043B\u043E \u043E\u0431\u0440\u0435\u0437\u0430\u043D\u043E)\n";
18515
+ return cut + text2.slice(text2.length - (max - cut.length));
18632
18516
  }
18633
- function wordsOf(content) {
18634
- if (typeof content === "string") return content;
18635
- return content.map((block) => block.type === "text" ? block.text : "").join("\n");
18517
+ function orNull(value, max) {
18518
+ return value === null || value === "" ? null : head(value, max);
18519
+ }
18520
+
18521
+ // src/update-nudge.ts
18522
+ function usable2(value) {
18523
+ return typeof value === "string" && value !== "";
18636
18524
  }
18525
+ var processHook = null;
18526
+ function whenNewerReleased(hook) {
18527
+ processHook = hook;
18528
+ }
18529
+ function createUpdateNudge(parts = {}) {
18530
+ const shell = parts.shell ?? (() => installedShell(new Engine().home()));
18531
+ const installedEngine = parts.installedEngine ?? (() => new Engine().freshVersion());
18532
+ const update = parts.update ?? (() => {
18533
+ processHook?.();
18534
+ });
18535
+ const asked = /* @__PURE__ */ new Set();
18536
+ return (info) => {
18537
+ if (info === null || typeof info !== "object") return null;
18538
+ const safely = (read, fallback) => {
18539
+ try {
18540
+ return read();
18541
+ } catch {
18542
+ return fallback;
18543
+ }
18544
+ };
18545
+ const newShell = usable2(info.shell) && info.shell !== safely(shell, info.shell) ? info.shell : "";
18546
+ const engineNow = safely(installedEngine, null);
18547
+ const newEngine = usable2(info.engine) && engineNow !== null && info.engine !== engineNow ? info.engine : "";
18548
+ if (newShell === "" && newEngine === "") return null;
18549
+ const seen = `${newShell}|${newEngine}`;
18550
+ if (asked.has(seen)) return null;
18551
+ asked.add(seen);
18552
+ update();
18553
+ return null;
18554
+ };
18555
+ }
18556
+ var processUpdateNudge = createUpdateNudge();
18637
18557
  var TRIAL_AXES = ["motion", "timing", "place", "camera", "color"];
18638
18558
  var TRIALS_MAX = 10;
18639
18559
  async function startRender(engine, asked, key, tell) {
@@ -18704,9 +18624,10 @@ async function startDocumentRender(engine, reel, key, tell, trials = {}) {
18704
18624
  \u0414\u0430\u043B\u044C\u0448\u0435 \u043F\u043E\u0437\u043E\u0432\u0438 wait \u0441 \u044D\u0442\u0438\u043C \u043D\u043E\u043C\u0435\u0440\u043E\u043C. ${AWAY_NOTE}`,
18705
18625
  startedJob({
18706
18626
  id: launch.id,
18707
- what,
18708
- ahead: ladder.join(" "),
18709
- message: "\u0420\u043E\u043B\u0438\u043A \u0441\u043E\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044F \u043D\u0430 \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440\u0435 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F."
18627
+ // Лестница и заголовок — слова человеку: полоса показывает их как есть.
18628
+ what: human(what),
18629
+ ahead: human(ladder.join(" ")),
18630
+ message: human("\u0420\u043E\u043B\u0438\u043A \u0441\u043E\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044F \u043D\u0430 \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440\u0435 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F.")
18710
18631
  })
18711
18632
  );
18712
18633
  }
@@ -18716,6 +18637,12 @@ var RENDER_LADDER = [
18716
18637
  ];
18717
18638
  var TRIALS_STEP = "\u041F\u043E\u0442\u043E\u043C \u0441\u043E\u0431\u0435\u0440\u0443 \u0432\u0435\u0440\u0441\u0438\u0438 \u0434\u043B\u044F \u043F\u0440\u043E\u0431\u043D\u043E\u0433\u043E \u0440\u0435\u0436\u0438\u043C\u0430 \u043F\u043E \u043E\u0434\u043D\u043E\u0439: \u0440\u0435\u0447\u044C \u0438 \u0437\u0432\u0443\u043A \u0442\u0435 \u0436\u0435, \u0434\u0440\u0443\u0433\u043E\u0435 \u0434\u0432\u0438\u0436\u0435\u043D\u0438\u0435, \u043C\u0435\u0441\u0442\u0430 \u0438 \u043A\u0440\u0443\u043F\u043D\u043E\u0441\u0442\u044C.";
18718
18639
  async function followWork(engine, asked, pace = realPace, tell) {
18640
+ const result = await followAny(engine, asked, pace, tell);
18641
+ if (!isSetupJob(asked.job) || result.type !== "success") return result;
18642
+ const report = setupJobReport(asked.job, engine.home());
18643
+ return report === null ? result : { ...result, structured: report };
18644
+ }
18645
+ async function followAny(engine, asked, pace, tell) {
18719
18646
  const ask2 = async () => {
18720
18647
  if (!isSetupJob(asked.job)) return engine.command(["job", asked.job]);
18721
18648
  const text2 = setupJobText(asked.job, Date.now, engine.home());
@@ -19509,14 +19436,14 @@ var FONTS_CONTRACT = "naparnik-fonts/1";
19509
19436
  var CAPABILITIES_CONTRACT2 = "naparnik-capabilities/1";
19510
19437
  var OWN_STYLE_FOLDER = "style";
19511
19438
  function contractRefusal(theirs) {
19512
- return `\u0414\u0432\u0438\u0436\u043E\u043A \u0433\u043E\u0432\u043E\u0440\u0438\u0442 \u043D\u0430 \u0434\u043E\u0433\u043E\u0432\u043E\u0440\u0435 \xAB${theirs}\xBB, \u0430 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0435 \u043F\u043E\u043D\u0438\u043C\u0430\u0435\u0442 \xAB${CHOICES_CONTRACT}\xBB. \u041E\u0431\u043D\u043E\u0432\u0438 \u0442\u043E, \u0447\u0442\u043E \u0441\u0442\u0430\u0440\u0435\u0435: setup \u0441 what=engine \u0434\u043B\u044F \u0434\u0432\u0438\u0436\u043A\u0430, what=shell \u0434\u043B\u044F \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F.`;
19439
+ return `\u0414\u0432\u0438\u0436\u043E\u043A \u0433\u043E\u0432\u043E\u0440\u0438\u0442 \u043D\u0430 \u0434\u043E\u0433\u043E\u0432\u043E\u0440\u0435 \xAB${theirs}\xBB, \u0430 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0435 \u043F\u043E\u043D\u0438\u043C\u0430\u0435\u0442 \xAB${CHOICES_CONTRACT}\xBB. \u041E\u0431\u043D\u043E\u0432\u0438 \u043C\u043E\u043D\u0442\u0430\u0436: install \u0441 what=all \u2014 \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442 \u043D\u043E\u0432\u044B\u0435 \u0432\u0435\u0440\u0441\u0438\u0438 \u0434\u0432\u0438\u0436\u043A\u0430 \u0438 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F.`;
19513
19440
  }
19514
19441
  function notJson(text2) {
19515
19442
  const head2 = text2.trim().slice(0, 160).replace(/\s+/g, " ");
19516
19443
  return NOT_JSON2 + (head2 ? `
19517
19444
  \u0427\u0442\u043E \u043F\u0440\u0438\u0448\u043B\u043E \u0432\u043C\u0435\u0441\u0442\u043E JSON: \xAB${head2}\u2026\xBB` : "");
19518
19445
  }
19519
- var NOT_JSON2 = "\u0414\u0432\u0438\u0436\u043E\u043A \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043D\u0435 \u0442\u0435\u043C, \u0447\u0435\u0433\u043E \u0436\u0434\u0430\u043B\u0438. \u041E\u0431\u043D\u043E\u0432\u0438 \u0434\u0432\u0438\u0436\u043E\u043A \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u043E\u043C setup \u0441 what=engine \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438.";
19446
+ var NOT_JSON2 = "\u0414\u0432\u0438\u0436\u043E\u043A \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043D\u0435 \u0442\u0435\u043C, \u0447\u0435\u0433\u043E \u0436\u0434\u0430\u043B\u0438. \u041E\u0431\u043D\u043E\u0432\u0438 \u043C\u043E\u043D\u0442\u0430\u0436 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u043E\u043C install \u0441 what=all \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438.";
19520
19447
  var FIRST_BATCH = 12;
19521
19448
  var ALL_OF_THEM = 1e3;
19522
19449
  var SAID_LIMIT = 6;
@@ -19669,7 +19596,7 @@ async function fontsBranch(engine, alphabet) {
19669
19596
  );
19670
19597
  }
19671
19598
  return toolError(
19672
- "\u0412 \u043D\u0430\u0431\u043E\u0440\u0435 \u043D\u0435 \u043D\u0430\u0448\u043B\u043E\u0441\u044C \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u0448\u0440\u0438\u0444\u0442\u0430. \u041E\u0431\u043D\u043E\u0432\u0438 \u043D\u0430\u0447\u0438\u043D\u043A\u0443 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u043E\u043C setup \u0441 what=engine: \u043F\u043E\u0445\u043E\u0436\u0435, \u043E\u043D\u0430 \u0440\u0430\u0441\u043F\u0430\u043A\u043E\u0432\u0430\u043D\u0430 \u0441\u0442\u0430\u0440\u043E\u0439 \u0432\u0435\u0440\u0441\u0438\u0435\u0439."
19599
+ "\u0412 \u043D\u0430\u0431\u043E\u0440\u0435 \u043D\u0435 \u043D\u0430\u0448\u043B\u043E\u0441\u044C \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u0448\u0440\u0438\u0444\u0442\u0430. \u041E\u0431\u043D\u043E\u0432\u0438 \u043D\u0430\u0447\u0438\u043D\u043A\u0443 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u043E\u043C install \u0441 what=all: \u043F\u043E\u0445\u043E\u0436\u0435, \u043E\u043D\u0430 \u0440\u0430\u0441\u043F\u0430\u043A\u043E\u0432\u0430\u043D\u0430 \u0441\u0442\u0430\u0440\u043E\u0439 \u0432\u0435\u0440\u0441\u0438\u0435\u0439."
19673
19600
  );
19674
19601
  }
19675
19602
  const short = listed.map((one) => ({
@@ -22629,40 +22556,6 @@ function chooseWidgetHtml() {
22629
22556
  </html>`;
22630
22557
  }
22631
22558
 
22632
- // src/update-nudge.ts
22633
- function usable2(value) {
22634
- return typeof value === "string" && value !== "";
22635
- }
22636
- function createUpdateNudge(parts = {}) {
22637
- const shell = parts.shell ?? VERSION;
22638
- const installedEngine = parts.installedEngine ?? (() => new Engine().currentVersion());
22639
- let said = false;
22640
- return (info) => {
22641
- if (said || info === null || typeof info !== "object") return null;
22642
- const lines = [];
22643
- if (usable2(info.shell) && info.shell !== shell) {
22644
- lines.push(`\u0412\u044B\u0448\u043B\u043E \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F ${info.shell} \u2014 \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C: setup \u0441 what=shell (\u0441\u043F\u0440\u043E\u0441\u0438 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430)`);
22645
- }
22646
- if (usable2(info.engine)) {
22647
- let installed = null;
22648
- try {
22649
- installed = installedEngine();
22650
- } catch {
22651
- installed = null;
22652
- }
22653
- if (installed !== null && installed !== info.engine) {
22654
- lines.push(
22655
- `\u0412\u044B\u0448\u043B\u043E \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435 \u0434\u0432\u0438\u0436\u043A\u0430 \u043C\u043E\u043D\u0442\u0430\u0436\u0430 ${info.engine} \u2014 \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C: setup \u0441 what=engine (\u0441\u043F\u0440\u043E\u0441\u0438 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430)`
22656
- );
22657
- }
22658
- }
22659
- if (lines.length === 0) return null;
22660
- said = true;
22661
- return lines.join("\n");
22662
- };
22663
- }
22664
- var processUpdateNudge = createUpdateNudge();
22665
-
22666
22559
  // src/subscription.ts
22667
22560
  var TOO_OFTEN = 429;
22668
22561
  var STATUS_PATH = SDK_SUBSCRIPTION_STATUS_PATH;
@@ -22793,6 +22686,9 @@ function buildAll(client, engine, passport, key, baseUrl, pace = realPace, awake
22793
22686
  // Порядок — это порядок работы, и модель читает список сверху вниз:
22794
22687
  // проверить машину, придумать словами, поправить словами, собрать, дождаться.
22795
22688
  setupTool(engine, passport, key, baseUrl),
22689
+ // Ставит — отдельной дверью с окном-полосой: у `setup` окна нет, и одно
22690
+ // «скачай» даёт одну задачу и одно окно (владелец 2026-09-25).
22691
+ installTool(engine, passport, key, baseUrl),
22796
22692
  planTool(engine, key),
22797
22693
  planEditTool(engine, key),
22798
22694
  renderTool(engine, passport, key),
@@ -23026,4 +22922,4 @@ ${inTable}`, file2);
23026
22922
  };
23027
22923
  }
23028
22924
 
23029
- export { CrashReporter, DEFAULT_HOME, DeliveryError, Engine, ForeignBlockError, INSTALL_STEPS, KEEP_COPIES, KEEP_VERSIONS, LOCK_FILE, LONG_STEPS, MANIFEST_TIMEOUT_MS, MANIFEST_URL, MARKER, MIN_ENGINE, McpServer, NaparnikApiClient, NaparnikAuthError, NaparnikClientError, NaparnikConfigError, NaparnikError, NaparnikInsufficientBalanceError, NaparnikScopeError, NaparnikServerError, NaparnikTimeoutError, PAUSE_HOURS, SERVER_NAME, START_SEC, StdioServerTransport, TOOL_SEC, USER_AGENT, VERSION, WHERE_TO_GET_KEY, Wakefulness, bringPython, buildAll, buildBlock, buildResources, bundleUrlFor, chooseBody, compare, configPath, currentMachineFingerprint, decide, decideShell, defineTool, depFolderName, deploy, downloadFile, fetchManifest, fetchShellManifest, fileOrRefusal, findBlock, fromHost, fromNpxCache, handOverToBody, inside, install, installPayload, installStep, installedShell, jsonSchemaOf, keyFromBlock, keyInTable, lastLine, loadConfig, lockedPython, manifestUrlFor, markChecked, markChecked2, markCurrent2 as markCurrent, parseEngineManifest, parseFailure, path, platformKey, previousVersion2 as previousVersion, probe, readConfig, realPace, refusalFor, remove, reportInstallFaults, rollback, rollbackShell, shellDir, systemBlocker, tarEntries, tidyOld2 as tidyOld, timeToCheck, timeToCheck2, unpackArchive, updateEngine, watchProcessCrashes, withArgs };
22925
+ export { CrashReporter, DEFAULT_HOME, DeliveryError, Engine, ForeignBlockError, INSTALL_STEPS, KEEP_COPIES, KEEP_VERSIONS, LOCK_FILE, LONG_STEPS, MANIFEST_TIMEOUT_MS, MANIFEST_URL, MARKER, MIN_ENGINE, McpServer, NaparnikApiClient, NaparnikAuthError, NaparnikClientError, NaparnikConfigError, NaparnikError, NaparnikInsufficientBalanceError, NaparnikScopeError, NaparnikServerError, NaparnikTimeoutError, PAUSE_HOURS, SERVER_NAME, START_SEC, StdioServerTransport, TITLE_UPDATE, TOOL_SEC, USER_AGENT, VERSION, WHERE_TO_GET_KEY, Wakefulness, bringPython, buildAll, buildBlock, buildResources, chooseBody, compare, configPath, currentMachineFingerprint, decide, defineTool, depFolderName, deploy, describeFailure, downloadFile, fetchManifest, fileOrRefusal, findBlock, forModel, fromHost, fromNpxCache, handOverToBody, inside, install, installMissing, installPayload, installStep, installedShell, jsonSchemaOf, keyFromBlock, keyInTable, lastLine, loadConfig, lockedPython, markChecked2 as markChecked, markCurrent2 as markCurrent, noteInstallFault, ownSetupJobRunning, parseEngineManifest, parseFailure, path, platformKey, previousVersion2 as previousVersion, probe, readConfig, realPace, refusalFor, remove, reportInstallFaults, reportJobFaults, rollback, rollbackShell, runUpdate, setupJobDone, setupJobEnding, shellDir, startSetupJob, systemBlocker, tarEntries, tidyOld2 as tidyOld, timeToCheck2 as timeToCheck, toolError, unpackArchive, updateEngine, watchProcessCrashes, whenNewerReleased, withArgs };