@naparnik/mcp 0.1.0 → 0.10.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1359 -0
- package/LICENSE.md +8 -1
- package/README.md +279 -23
- package/THIRD-PARTY.md +59 -0
- package/dist/chunk-MBUPRPJQ.js +21484 -0
- package/dist/cli.d.ts +25 -0
- package/dist/cli.js +466 -15
- package/dist/index.d.ts +1490 -16
- package/dist/index.js +1 -1
- package/package.json +15 -11
- package/dist/chunk-T6UWKM37.js +0 -4989
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
2
|
|
|
3
3
|
interface NaparnikMcpConfig {
|
|
4
4
|
apiKey: string;
|
|
@@ -18,23 +18,1194 @@ declare function loadConfig(env?: NodeJS.ProcessEnv): NaparnikMcpConfig;
|
|
|
18
18
|
|
|
19
19
|
declare class NaparnikApiClient {
|
|
20
20
|
private readonly config;
|
|
21
|
-
|
|
21
|
+
private readonly machine;
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
* ограничение VerbFilter на сервере, отдельного GET-пути нет.
|
|
23
|
+
* Какая начинка стоит на этой машине. `null` — не стоит вовсе.
|
|
25
24
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* ⚠ ЗАВИСИМОСТЬЮ, А НЕ ЧТЕНИЕМ ДИСКА ЗДЕСЬ. По той же причине, что и
|
|
26
|
+
* отпечаток: клиент, который сам лезет в файлы, нечем проверить, не трогая
|
|
27
|
+
* настоящую машину. Спрашивается на каждом запросе, а не один раз при
|
|
28
|
+
* запуске: начинка ставится и обновляется прямо во время работы, и
|
|
29
|
+
* запомненная версия врала бы ровно после установки.
|
|
29
30
|
*/
|
|
30
|
-
|
|
31
|
+
private readonly engineVersion;
|
|
32
|
+
/**
|
|
33
|
+
* @param config ключ, адрес и таймауты
|
|
34
|
+
* @param machine отпечаток этой машины или `null`, если опознать не удалось
|
|
35
|
+
*
|
|
36
|
+
* ⚠ ОТПЕЧАТОК ПРИХОДИТ СНАРУЖИ, А НЕ СЧИТАЕТСЯ ЗДЕСЬ. Счёт лезет в систему —
|
|
37
|
+
* запускает команду, читает файл в домашней папке, — и клиент, который это
|
|
38
|
+
* делает сам, нечем проверить: тест либо трогает настоящую машину, либо
|
|
39
|
+
* подменяет модуль. Зависимостью же обе стороны проверяются простой
|
|
40
|
+
* функцией: «прислали отпечаток» и «машину не опознали».
|
|
41
|
+
*/
|
|
42
|
+
constructor(config: NaparnikMcpConfig, machine: () => string | null,
|
|
43
|
+
/**
|
|
44
|
+
* Какая начинка стоит на этой машине. `null` — не стоит вовсе.
|
|
45
|
+
*
|
|
46
|
+
* ⚠ ЗАВИСИМОСТЬЮ, А НЕ ЧТЕНИЕМ ДИСКА ЗДЕСЬ. По той же причине, что и
|
|
47
|
+
* отпечаток: клиент, который сам лезет в файлы, нечем проверить, не трогая
|
|
48
|
+
* настоящую машину. Спрашивается на каждом запросе, а не один раз при
|
|
49
|
+
* запуске: начинка ставится и обновляется прямо во время работы, и
|
|
50
|
+
* запомненная версия врала бы ровно после установки.
|
|
51
|
+
*/
|
|
52
|
+
engineVersion?: () => string | null);
|
|
53
|
+
/**
|
|
54
|
+
* POST к НАШЕМУ серверу монтажа.
|
|
55
|
+
*
|
|
56
|
+
* ⚠ ОТДЕЛЬНЫЙ МЕТОД, А НЕ ФЛАГ В `post`. Адресатов ровно два, и они разные во
|
|
57
|
+
* всём: у нашего сервера свой контракт и свой релизный цикл, у чужого API
|
|
58
|
+
* Напарника — чужие. Пока метод был один, вопрос о подписке уходил под чужой
|
|
59
|
+
* префикс, и в день гашения монолита отличить «наш сервер молчит» от «чужого
|
|
60
|
+
* больше нет» было бы нечем. Вызывающий обязан знать, куда идёт, — поэтому
|
|
61
|
+
* знание вынесено в имя метода, а не спрятано в параметр.
|
|
62
|
+
*/
|
|
63
|
+
postMontage<T>(path: string, body?: unknown, opts?: {
|
|
31
64
|
long?: boolean;
|
|
65
|
+
billable?: boolean;
|
|
66
|
+
timeoutMs?: number;
|
|
67
|
+
once?: boolean;
|
|
32
68
|
}): Promise<T>;
|
|
69
|
+
/**
|
|
70
|
+
* GET к НАШЕМУ серверу монтажа: ответ разбирается как JSON.
|
|
71
|
+
*
|
|
72
|
+
* ⚠ ОТДЕЛЬНЫЙ МЕТОД РЯДОМ С `postMontage`, А НЕ ФЛАГ В НЁМ. Тело и заголовки
|
|
73
|
+
* у GET другие: пустого JSON-тела он не шлёт вовсе, а `Content-Type` без тела
|
|
74
|
+
* — это заголовок, обещающий то, чего нет. Прятать это в параметр значило бы
|
|
75
|
+
* заводить в одном методе две формы запроса, которые расходятся молча.
|
|
76
|
+
*/
|
|
77
|
+
getMontage<T>(path: string, opts?: {
|
|
78
|
+
timeoutMs?: number;
|
|
79
|
+
}): Promise<T>;
|
|
80
|
+
/**
|
|
81
|
+
* GET к нашему серверу за ТЕКСТОМ, а не за JSON.
|
|
82
|
+
*
|
|
83
|
+
* ⚠ ТЕКСТ НУЖЕН ОТДЕЛЬНО, ПОТОМУ ЧТО ЗНАНИЯ ЕДУТ РАЗМЕТКОЙ. Оберни мы их в
|
|
84
|
+
* JSON ради единообразия — модель получала бы экранированные переводы строк
|
|
85
|
+
* и первым делом просила бы их распутать. Разбор ОШИБОК при этом остаётся
|
|
86
|
+
* общим: у ошибок тело json и на этом пути тоже.
|
|
87
|
+
*/
|
|
88
|
+
getMontageText(path: string, opts?: {
|
|
89
|
+
timeoutMs?: number;
|
|
90
|
+
accept?: string;
|
|
91
|
+
}): Promise<string>;
|
|
92
|
+
/**
|
|
93
|
+
* Отправка. Адрес собирает `postMontage` — других адресатов у пакета нет.
|
|
94
|
+
*
|
|
95
|
+
* ⚠ БЫЛО ДВА, ОСТАЛСЯ ОДИН. Второй метод ходил в PHP API монолита: рилсы,
|
|
96
|
+
* баланс, распаковка бренда, генерация. Полка данных убрана из поставки
|
|
97
|
+
* 2026-09-02 вместе с обменом стилем — монолит гасится (docs/architecture.md,
|
|
98
|
+
* раздел 20), и инструмент, который в тот день начнёт отвечать «сервер не
|
|
99
|
+
* ответил», хуже отсутствующего.
|
|
100
|
+
*
|
|
101
|
+
* Метод оставлен приватным намеренно: снаружи виден только `postMontage`, и
|
|
102
|
+
* добавить хождение на чужой сервер, не объявив нового публичного метода,
|
|
103
|
+
* теперь нельзя.
|
|
104
|
+
*/
|
|
105
|
+
private send;
|
|
33
106
|
private attempt;
|
|
107
|
+
/**
|
|
108
|
+
* Заголовки запроса. Ключ есть всегда, отпечаток машины — когда он есть.
|
|
109
|
+
*
|
|
110
|
+
* ⚠ ОТПЕЧАТКА МОЖЕТ НЕ БЫТЬ, И ЭТО НЕ ОШИБКА. На урезанной машине команду не
|
|
111
|
+
* запустить и в домашнюю папку не записать. Пустой заголовок был бы хуже
|
|
112
|
+
* отсутствующего: сервер отличает «машину не назвали» от «назвали не ту», и
|
|
113
|
+
* пустая строка спутала бы одно с другим. Поэтому либо значение, либо ничего.
|
|
114
|
+
*
|
|
115
|
+
* Почему это заголовок, а не поле тела, — в контракте
|
|
116
|
+
* (`contracts/sdk-headers.ts`); второй копии этого объяснения быть не должно.
|
|
117
|
+
*/
|
|
118
|
+
private headers;
|
|
34
119
|
private parse;
|
|
35
120
|
}
|
|
36
121
|
|
|
37
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Где внутри распакованной портативной сборки лежит сам интерпретатор.
|
|
124
|
+
*
|
|
125
|
+
* ⚠ ТРИ ВАРИАНТА, А НЕ ДВА, И РОВНО ТЕ ЖЕ, ЧТО У ДВИЖКА
|
|
126
|
+
* (`engine/env.py` → `_INSIDE_PYTHON`). Копия здесь неизбежна — питон мы
|
|
127
|
+
* ищем ДО того, как есть чем запустить питон, — но прошлая копия знала два
|
|
128
|
+
* варианта из трёх: на сборке, где внутри лежит только `bin/python`, движок
|
|
129
|
+
* питон видел, а сервер MCP нет. Обе стороны сверяет тест границы
|
|
130
|
+
* (`engine.test.ts`), и он читает питоновский исходник, а не повторяет список.
|
|
131
|
+
*/
|
|
132
|
+
declare const INSIDE_PYTHON: string[];
|
|
133
|
+
/** Дом по умолчанию — ровно тот же литерал, что в `engine/home.py`. */
|
|
134
|
+
declare const DEFAULT_HOME = "~/.naparnik";
|
|
135
|
+
/**
|
|
136
|
+
* Шаги установки, которые идут минутами и часами: их нельзя держать внутри
|
|
137
|
+
* вызова инструмента. Секундами считается только `check`.
|
|
138
|
+
*
|
|
139
|
+
* ⚠ «python» здесь вместе с портативной сборкой: шаг, который раньше лишь
|
|
140
|
+
* советовал сходить на python.org, теперь может качать от 24 до 104 МБ.
|
|
141
|
+
* Оставь его быстрым — и на медленной связи клиентская программа оборвала бы
|
|
142
|
+
* вызов по своему таймауту, а скачивание осталось бы сиротой; повтор начал бы
|
|
143
|
+
* второе.
|
|
144
|
+
*/
|
|
145
|
+
declare const LONG_STEPS: Set<string>;
|
|
146
|
+
/**
|
|
147
|
+
* Все шаги установки, которые понимает движок.
|
|
148
|
+
*
|
|
149
|
+
* ⚠ ЭТО ЗЕРКАЛО `install.py` → `STEPS`, и оно уже один раз разошлось: движок печатал
|
|
150
|
+
* «Шаг установки „вырез“», а здешний список о таком шаге не знал — клиент
|
|
151
|
+
* оказывался в тупике, потому что инструмент не принимал значение, которое
|
|
152
|
+
* движок сам же и назвал. Правится сверху вниз: сначала `install.py` → `STEPS`,
|
|
153
|
+
* потом сюда. Расхождение сторожит `engine.test.ts` — он читает питоновский
|
|
154
|
+
* исходник, а не повторяет список.
|
|
155
|
+
*/
|
|
156
|
+
declare const INSTALL_STEPS: string[];
|
|
157
|
+
/**
|
|
158
|
+
* Паспорт пака: имена этапов, список моделей, чем запускать.
|
|
159
|
+
*
|
|
160
|
+
* Спрашивается у движка ОДНОЙ командой, а не переписывается сюда. Пока его не
|
|
161
|
+
* было, списки этапов и моделей жили здесь второй правдой, а точка входа была
|
|
162
|
+
* прибита строкой "scripts/edit.py".
|
|
163
|
+
*/
|
|
164
|
+
/**
|
|
165
|
+
* ⚠ КЛЮЧИ РУССКИЕ, ПОТОМУ ЧТО ЭТО ОТВЕТ ПИТОНА, А НЕ НАШ ОБЪЕКТ. Паспорт
|
|
166
|
+
* печатает движок монтажа (`pack.json` и команда паспорта), и имена полей — его.
|
|
167
|
+
* Переименование здесь ничего не меняет на той стороне: оболочка просто
|
|
168
|
+
* перестала бы находить поля и объявила бы «паспорт не прочитан» на каждом
|
|
169
|
+
* запуске. Обе стороны сверяет тест границы (`engine.test.ts`), и он читает
|
|
170
|
+
* питоновский исходник, а не повторяет список.
|
|
171
|
+
*/
|
|
172
|
+
interface PackPassport {
|
|
173
|
+
'name': string;
|
|
174
|
+
'title': string;
|
|
175
|
+
'entry': string;
|
|
176
|
+
'stages': string[];
|
|
177
|
+
'models': string[];
|
|
178
|
+
/**
|
|
179
|
+
* Движок распознавания → модель по умолчанию. Карта, а не одно имя: на
|
|
180
|
+
* видеокарте Apple берётся medium, на процессоре turbo — разница в скорости
|
|
181
|
+
* десятикратная, замерено.
|
|
182
|
+
*/
|
|
183
|
+
'default_model': Record<string, string>;
|
|
184
|
+
/**
|
|
185
|
+
* Что монтаж умеет СВЕРХ обязательного пути: имя ключа → вид и пояснение.
|
|
186
|
+
*
|
|
187
|
+
* Объявляется паком, а не здесь: набор зависит от того, что пак умеет, и
|
|
188
|
+
* оболочка знать его заранее не может. Пока этого не было, шесть ключей
|
|
189
|
+
* (фон, свет, экран, слои, музыка, без акцентов) существовали только для
|
|
190
|
+
* того, кто печатает в терминале, — через инструмент их попросить было нечем.
|
|
191
|
+
* Старый пак поля не отдаёт: тогда схема остаётся с обязательными ключами.
|
|
192
|
+
*/
|
|
193
|
+
'capabilities'?: Record<string, {
|
|
194
|
+
'kind': string;
|
|
195
|
+
'about': string;
|
|
196
|
+
}>;
|
|
197
|
+
}
|
|
198
|
+
/** Ответ команды движка в виде, пригодном для инструмента. */
|
|
199
|
+
type CommandOutcome = {
|
|
200
|
+
ok: true;
|
|
201
|
+
text: string;
|
|
202
|
+
} | {
|
|
203
|
+
ok: false;
|
|
204
|
+
text: string;
|
|
205
|
+
};
|
|
206
|
+
/** Где лежит установленная сборка движка. */
|
|
207
|
+
interface EnginePlace {
|
|
208
|
+
/** Папка с `console.py`. */
|
|
209
|
+
engine: string;
|
|
210
|
+
/** Папка пака монтажа: `.venv`, `pack.json`, точка входа. */
|
|
211
|
+
pack: string;
|
|
212
|
+
/** Полный путь к `console.py`. */
|
|
213
|
+
consolePy: string;
|
|
214
|
+
}
|
|
215
|
+
declare class Engine {
|
|
216
|
+
private readonly env;
|
|
217
|
+
private passportCache;
|
|
218
|
+
private versionAtStart;
|
|
219
|
+
private pythonChecked;
|
|
220
|
+
constructor(env?: NodeJS.ProcessEnv);
|
|
221
|
+
/**
|
|
222
|
+
* Корень Напарника на машине клиента.
|
|
223
|
+
*
|
|
224
|
+
* ⚠ ПУСТАЯ СТРОКА — ЭТО «НЕ ЗАДАНО», А НЕ «ТЕКУЩАЯ ПАПКА». Прочитай мы её
|
|
225
|
+
* как путь — дом уехал бы в рабочий каталог процесса, и питон с TypeScript
|
|
226
|
+
* разошлись бы в том, где вообще живут задачи и стиль. Тот же `or` стоит в
|
|
227
|
+
* `engine/home.py`.
|
|
228
|
+
*
|
|
229
|
+
* ⚠ И ШАБЛОН — ТОЖЕ «НЕ ЗАДАНО», см. `fromHost` в `config.ts`. Пустое поле
|
|
230
|
+
* настроек приезжает от Claude Desktop строкой `${user_config.engine_home}`,
|
|
231
|
+
* и до 2026-09-10 движок искался в папке с таким именем: у человека он лежал
|
|
232
|
+
* в `~/.naparnik`, а расширение отвечало «не установлен — монтировать нечем».
|
|
233
|
+
*/
|
|
234
|
+
home(): string;
|
|
235
|
+
/**
|
|
236
|
+
* Значение переменной окружения — или undefined, если она пуста либо не
|
|
237
|
+
* задана. Наружу нужна затем, что окружение у Движка своё (передано в
|
|
238
|
+
* конструктор), а `process.env` в тестах не тот же самый.
|
|
239
|
+
*
|
|
240
|
+
* ⚠ ПУСТАЯ СТРОКА И ШАБЛОН СЧИТАЮТСЯ «НЕ ЗАДАНО» — по той же причине, что
|
|
241
|
+
* и у дома: незаполненное поле настроек приезжает от Claude Desktop либо
|
|
242
|
+
* пустым, либо строкой `${user_config.…}` (`fromHost` в `config.ts`).
|
|
243
|
+
*/
|
|
244
|
+
variable(name: string): string | undefined;
|
|
245
|
+
/** Папка версий движка: `<дом>/engine`. */
|
|
246
|
+
versionsDir(): string;
|
|
247
|
+
/**
|
|
248
|
+
* Какая версия движка объявлена рабочей.
|
|
249
|
+
*
|
|
250
|
+
* Обычный ТЕКСТОВЫЙ файл, а не симлинк: создание симлинка на Windows требует
|
|
251
|
+
* режима разработчика или прав администратора, а строка в файле переносима и
|
|
252
|
+
* переименовывается атомарно.
|
|
253
|
+
*
|
|
254
|
+
* ⚠ ЗАПОМИНАЕТСЯ НА ВСЮ ЖИЗНЬ ПРОЦЕССА, И ЭТО НЕСУЩЕЕ. Так работает обещание
|
|
255
|
+
* «подмена начинки — только на старте»: обновление меняет файл `current`, а
|
|
256
|
+
* запущенный процесс продолжает считать на той версии, с которой начал.
|
|
257
|
+
* Читай мы файл каждый раз — задача, заведённая старой версией, дочитывалась
|
|
258
|
+
* бы новой. У движка кэш по отпечаткам, и в отпечаток входит код шага: смесь
|
|
259
|
+
* двух версий внутри одной задачи не упала бы, а тихо смешала результаты.
|
|
260
|
+
*
|
|
261
|
+
* `freshVersion()` даёт незакэшированное значение — она нужна ровно двум местам:
|
|
262
|
+
* самому обновлению и рассказу человеку о том, что новая версия уже готова и
|
|
263
|
+
* подхватится после перезапуска.
|
|
264
|
+
*/
|
|
265
|
+
currentVersion(): string | null;
|
|
266
|
+
/** Что записано в файле `current` прямо сейчас, мимо памяти процесса. */
|
|
267
|
+
freshVersion(): string | null;
|
|
268
|
+
/**
|
|
269
|
+
* Где установлен движок — или null, если нигде.
|
|
270
|
+
*
|
|
271
|
+
* Порядок: явное указание переменной (лаборатория, тесты, ручная сборка) →
|
|
272
|
+
* версионная раскладка `<дом>/engine/<текущая>`. Третьего варианта нет
|
|
273
|
+
* намеренно: каждый «а ещё поищем вот тут» — это место, где TypeScript и
|
|
274
|
+
* питон однажды разойдутся.
|
|
275
|
+
*/
|
|
276
|
+
/**
|
|
277
|
+
* Указан ли движок переменной вручную.
|
|
278
|
+
*
|
|
279
|
+
* ⚠ ЭТО НЕ МЕЛОЧЬ ДЛЯ ОБНОВЛЕНИЯ. Ручной путь ПОБЕЖДАЕТ версионную
|
|
280
|
+
* раскладку, а обновление умеет только её: оно распакует новую версию в
|
|
281
|
+
* `<дом>/engine/<версия>` и переставит указатель — на который эта машина не
|
|
282
|
+
* смотрит вовсе. Получилось бы обновление, которое честно отчитывается об
|
|
283
|
+
* успехе и не меняет ничего.
|
|
284
|
+
*/
|
|
285
|
+
pathSetByHand(): boolean;
|
|
286
|
+
place(): EnginePlace | null;
|
|
287
|
+
/** Явное место кандидата: не читает и не меняет current. */
|
|
288
|
+
placeForVersion(version: string): EnginePlace | null;
|
|
289
|
+
private assemble;
|
|
290
|
+
/**
|
|
291
|
+
* Почему монтировать нечем — словами для нейросети клиента.
|
|
292
|
+
*
|
|
293
|
+
* ⚠ ЭТО НЕ ОШИБКА ИНСТРУМЕНТА, а состояние машины. Отдай мы сюда `spawn
|
|
294
|
+
* python3 ENOENT` или молчание — модель начала бы извиняться и гадать
|
|
295
|
+
* вместо того, чтобы объяснить человеку один недостающий шаг.
|
|
296
|
+
*/
|
|
297
|
+
whyNoEngine(): string;
|
|
298
|
+
/**
|
|
299
|
+
* Забыть, где движок, — после того как мы его только что поставили.
|
|
300
|
+
*
|
|
301
|
+
* ⚠ БЕЗ ЭТОГО ПЕРВАЯ УСТАНОВКА НЕ ВИДНА САМОЙ СЕБЕ. `currentVersion`
|
|
302
|
+
* запоминается на всю жизнь процесса нарочно (подмена начинки — только на
|
|
303
|
+
* старте), и сразу после доставки следующий же вызов получил бы
|
|
304
|
+
* закэшированное «версии нет» про версию, которую мы сами и разложили.
|
|
305
|
+
* Здесь это не нарушает обещания: смешивать две версии внутри задачи нечем,
|
|
306
|
+
* до этого момента версии не было вовсе.
|
|
307
|
+
*/
|
|
308
|
+
forgetPlace(): void;
|
|
309
|
+
/** Забыть ответ на вопрос «есть ли питон» — после того как мы его принесли. */
|
|
310
|
+
forgetPython(): void;
|
|
311
|
+
/** Python окружения пака — им считается монтаж. Может ещё не существовать. */
|
|
312
|
+
packPython(): string | null;
|
|
313
|
+
/**
|
|
314
|
+
* Портативный python, который движок доставил сам (шаг «python» установки).
|
|
315
|
+
*
|
|
316
|
+
* ⚠ ЗНАТЬ ПРО НЕГО ОБЯЗАНА И ЭТА СТОРОНА. Он лежит в `<дом>/bin/python` и в
|
|
317
|
+
* PATH не попадает намеренно — PATH человека мы не трогаем. Пропусти его
|
|
318
|
+
* здесь, и получится: установка принесла питон, окружение на нём собралось,
|
|
319
|
+
* а сервер MCP по-прежнему зовёт системный «python», которого нет или
|
|
320
|
+
* который заглушка из Microsoft Store.
|
|
321
|
+
*/
|
|
322
|
+
ourPython(): string | null;
|
|
323
|
+
/**
|
|
324
|
+
* Python для команд движка.
|
|
325
|
+
*
|
|
326
|
+
* Окружения пака может не быть вовсе — именно это и проверяет `check_setup`,
|
|
327
|
+
* поэтому запускать проверку его питоном нельзя: получилась бы курица и
|
|
328
|
+
* яйцо. Команды движка написаны на голой стандартной библиотеке и идут
|
|
329
|
+
* системным python'ом, когда своего ещё нет.
|
|
330
|
+
*/
|
|
331
|
+
enginePython(): string;
|
|
332
|
+
/** Python подготовки кандидата — намеренно без fallback на venv старого пака. */
|
|
333
|
+
bootstrapPython(): string;
|
|
334
|
+
/** Python ровно названного кандидата, а не текущей версии. */
|
|
335
|
+
candidatePython(place: EnginePlace): string | null;
|
|
336
|
+
/**
|
|
337
|
+
* Среда для КАЖДОГО питона, которого мы запускаем.
|
|
338
|
+
*
|
|
339
|
+
* ⚠ БЕЗ ЭТОГО НА WINDOWS НЕ РАБОТАЕТ НИЧЕГО. Пока в питоне не включён режим
|
|
340
|
+
* UTF-8, он печатает в кодовой странице системы (cp1251 на русской, cp1252
|
|
341
|
+
* на английской), а Node декодирует ребёнка как UTF-8 — и весь ответ
|
|
342
|
+
* приезжал нечитаемым: «????? ???: /??? ??????.mp4». Отдельно ломался
|
|
343
|
+
* preview: строка JSON у нас с русскими ключами, и поле `файл` выходило
|
|
344
|
+
* undefined. Свой код мы чиним вызовом (`engine/encoding.py`), а чужие
|
|
345
|
+
* библиотеки внутри того же процесса — только средой. Нужны оба заслона.
|
|
346
|
+
*/
|
|
347
|
+
pythonEnv(): NodeJS.ProcessEnv;
|
|
348
|
+
/**
|
|
349
|
+
* Есть ли на машине python вообще — и словами, если нет.
|
|
350
|
+
*
|
|
351
|
+
* Весь движок (включая проверку и установку) идёт через запуск python'а.
|
|
352
|
+
* Когда его нет, `execFile` бросает ENOENT, и клиентской нейросети
|
|
353
|
+
* доставалось «spawn python3 ENOENT» — текст, из которого человеку не понять
|
|
354
|
+
* ничего. Шаг «python», написанный ровно для этого случая, при этом
|
|
355
|
+
* недостижим по устройству: он сам запускается python'ом.
|
|
356
|
+
*
|
|
357
|
+
* На Windows есть отдельная беда: в PATH лежит заглушка Microsoft Store —
|
|
358
|
+
* `python` там есть, он печатает рекламу магазина и выходит с кодом 9009.
|
|
359
|
+
* Поэтому проверяем не наличие файла, а ответ на вопрос о версии.
|
|
360
|
+
*/
|
|
361
|
+
pythonTrouble(): Promise<string | null>;
|
|
362
|
+
/**
|
|
363
|
+
* Запустить команду движка и отдать её текст как есть.
|
|
364
|
+
*
|
|
365
|
+
* Умолчание таймаута заведомо БОЛЬШЕ внутренних таймаутов движка (самый
|
|
366
|
+
* долгий — опрос окружения пака, 180 с). Внешний таймаут короче внутреннего
|
|
367
|
+
* означает, что внутренний недостижим: пробник убивался снаружи, и его
|
|
368
|
+
* объяснение не доезжало до человека вовсе.
|
|
369
|
+
*/
|
|
370
|
+
command(args: string[], { maxBuffer, timeout, key, }?: {
|
|
371
|
+
maxBuffer?: number;
|
|
372
|
+
timeout?: number;
|
|
373
|
+
key?: string;
|
|
374
|
+
}): Promise<CommandOutcome>;
|
|
375
|
+
/**
|
|
376
|
+
* Сырой запуск консоли движка. Наружу нужен затем, что часть работы
|
|
377
|
+
* (заведение задания, показ кадра) читает не текст, а JSON.
|
|
378
|
+
*/
|
|
379
|
+
run(place: EnginePlace, args: string[], { maxBuffer, timeout, key, }?: {
|
|
380
|
+
maxBuffer?: number;
|
|
381
|
+
timeout?: number;
|
|
382
|
+
key?: string;
|
|
383
|
+
}): Promise<{
|
|
384
|
+
stdout: string;
|
|
385
|
+
stderr: string;
|
|
386
|
+
}>;
|
|
387
|
+
/** Запуск через явно выбранный Python нужен проверке кандидата до current. */
|
|
388
|
+
runWith(executable: string, place: EnginePlace, args: string[], { maxBuffer, timeout, key, }?: {
|
|
389
|
+
maxBuffer?: number;
|
|
390
|
+
timeout?: number;
|
|
391
|
+
key?: string;
|
|
392
|
+
}): Promise<{
|
|
393
|
+
stdout: string;
|
|
394
|
+
stderr: string;
|
|
395
|
+
}>;
|
|
396
|
+
/** Одна строка вывода команды — например, номер заведённого задания. */
|
|
397
|
+
asString(place: EnginePlace, args: string[], key?: string): Promise<string>;
|
|
398
|
+
/**
|
|
399
|
+
* Паспорт пака. Читается один раз за жизнь процесса: состав пака при
|
|
400
|
+
* работающей программе не меняется, а спрашивать его подпроцессом на каждый
|
|
401
|
+
* вызов инструмента — лишние полсекунды на ровном месте.
|
|
402
|
+
*
|
|
403
|
+
* ⚠ ТАЙМАУТ КОРОТКИЙ, И ЭТО НЕСУЩЕЕ. Паспорт читается НА СТАРТЕ, до
|
|
404
|
+
* регистрации инструментов, — то есть внутри рукопожатия, которое клиент
|
|
405
|
+
* ждёт по своему таймауту (у Codex это 10 секунд по умолчанию). Команда
|
|
406
|
+
* честно занимает доли секунды: запустить питон и прочитать pack.json.
|
|
407
|
+
* Не уложилась — с паком что-то не так, и правильный размен здесь такой:
|
|
408
|
+
* потерять подсказки со списками моделей, но поднять сервер. Обратный
|
|
409
|
+
* размен означал бы «сервер не запустился» и молчание вместо объяснения.
|
|
410
|
+
*/
|
|
411
|
+
passport(): Promise<PackPassport | null>;
|
|
412
|
+
/**
|
|
413
|
+
* Запустить фоновую работу движка, заранее заведя под неё задание.
|
|
414
|
+
*
|
|
415
|
+
* Задание заводится ДО запуска и отдельной командой: если запуск не удастся,
|
|
416
|
+
* задача всё равно видна, и человеку есть что показать. Формат файла знает
|
|
417
|
+
* только питон — эта сторона его не пишет, чтобы у файла не было двух хозяев
|
|
418
|
+
* на разных языках.
|
|
419
|
+
*
|
|
420
|
+
* Предохранителя параллельности здесь нет намеренно. Он в питоне
|
|
421
|
+
* (`engine/state.slot`) — файловые замки в `<дом>/busy`. Счётчик в
|
|
422
|
+
* памяти защищал только свой процесс: Claude Desktop и Claude Code поднимают
|
|
423
|
+
* по своему, и вместе они давали вдвое больше тяжёлой работы, чем разрешено.
|
|
424
|
+
*/
|
|
425
|
+
background(opts: {
|
|
426
|
+
place: EnginePlace;
|
|
427
|
+
executable: string;
|
|
428
|
+
args: (id: string) => string[];
|
|
429
|
+
startJob: string[];
|
|
430
|
+
advice: string;
|
|
431
|
+
/**
|
|
432
|
+
* Ключ подписки — ТОЛЬКО этому запуску, не общей среде питона.
|
|
433
|
+
*
|
|
434
|
+
* ⚠ БЕЗ НЕГО МОНТАЖ ОТКАЗЫВАЕТ КАЖДОМУ ПЛАТЯЩЕМУ. С 2026-09-09 движок
|
|
435
|
+
* спрашивает подписку сам (`client/engine/subscription.py`) и берёт ключ
|
|
436
|
+
* из `NAPARNIK_API_KEY`, а `pythonEnv()` его оттуда вычищает намеренно:
|
|
437
|
+
* иначе ключ уезжает в каждого потомка питона — ffmpeg, pip, чужой код
|
|
438
|
+
* пака. Поэтому не «вернуть ключ в среду», а доложить его точечно, ровно
|
|
439
|
+
* как уже сделано у скачивания движка (`run`, поле `key`).
|
|
440
|
+
*/
|
|
441
|
+
key: string | undefined;
|
|
442
|
+
}): Promise<{
|
|
443
|
+
id: string;
|
|
444
|
+
} | {
|
|
445
|
+
refusal: string;
|
|
446
|
+
}>;
|
|
447
|
+
/**
|
|
448
|
+
* Файл, куда фоновая работа пишет свой stderr.
|
|
449
|
+
*
|
|
450
|
+
* Лежит в доме, а не рядом с видео: у установки видео нет вовсе, а ответ на
|
|
451
|
+
* вопрос «что она успела сказать перед смертью» нужен одинаково обоим.
|
|
452
|
+
* Держим последние `KEEP_LOGS` — полоса загрузки модели пишет сотни
|
|
453
|
+
* килобайт, и вечная папка таких файлов была бы платой человека за то, что
|
|
454
|
+
* он однажды монтировал.
|
|
455
|
+
*/
|
|
456
|
+
private openLog;
|
|
457
|
+
/**
|
|
458
|
+
* Присмотр за фоновой работой: перевести её смерть в состояние задания.
|
|
459
|
+
*
|
|
460
|
+
* Один на монтаж и на установку. Двумя копиями это было бы двумя разными
|
|
461
|
+
* ответами человеку на одну и ту же беду.
|
|
462
|
+
*/
|
|
463
|
+
private watch;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Путь, каким его понимает питон: с раскрытой тильдой и от нашей рабочей
|
|
467
|
+
* папки, а не от чужой.
|
|
468
|
+
*
|
|
469
|
+
* Нейросеть пишет «~/Movies/ролик.mp4» постоянно — тем более что описание
|
|
470
|
+
* `style_new` само подсказывает «~/.naparnik/стиль» как образец.
|
|
471
|
+
* `existsSync` тильду не раскрывает, и монтаж отвечал «Файла нет» о
|
|
472
|
+
* существующем файле. Питон везде делает expanduser; тут делаем то же самое.
|
|
473
|
+
*/
|
|
474
|
+
declare function path(candidate: string): string;
|
|
475
|
+
/**
|
|
476
|
+
* Путь к существующему файлу — или человеческий отказ.
|
|
477
|
+
*
|
|
478
|
+
* Копий этой проверки было четыре, слово в слово, и текст у них был хуже
|
|
479
|
+
* питоновского: там на этот случай уже сказано, что ролик мог остаться в
|
|
480
|
+
* «Загрузках» или его переименовали. Одна дверь.
|
|
481
|
+
*/
|
|
482
|
+
declare function fileOrRefusal(candidate: string): {
|
|
483
|
+
path: string;
|
|
484
|
+
} | {
|
|
485
|
+
refusal: string;
|
|
486
|
+
};
|
|
487
|
+
/**
|
|
488
|
+
* Разбор неудачи команды движка — ОДИН на все её виды.
|
|
489
|
+
*
|
|
490
|
+
* Своё сообщение команды важнее сообщения запускателя: «Command failed: …» с
|
|
491
|
+
* полным путём к python'у человеку не говорит ничего. Код 2 значит «чего-то не
|
|
492
|
+
* хватает» или «задача не удалась» — это НОРМАЛЬНЫЙ ответ, а не сбой
|
|
493
|
+
* инструмента: подай его ошибкой, и нейросеть начнёт извиняться вместо того,
|
|
494
|
+
* чтобы доставить недостающее.
|
|
495
|
+
*/
|
|
496
|
+
declare function parseFailure(err: unknown, fallback: string): CommandOutcome;
|
|
497
|
+
/** Последняя непустая строка вывода — там, где команда печатает результат. */
|
|
498
|
+
declare function lastLine(output: string): string;
|
|
499
|
+
|
|
500
|
+
/** Определение ресурса в MCP */
|
|
501
|
+
interface McpResource {
|
|
502
|
+
/** URI ресурса (уникальный идентификатор) */
|
|
503
|
+
uri: string;
|
|
504
|
+
/** Человекочитаемое имя */
|
|
505
|
+
name: string;
|
|
506
|
+
/** Описание ресурса (опционально) */
|
|
507
|
+
description?: string;
|
|
508
|
+
/** MIME-тип содержимого (опционально) */
|
|
509
|
+
mimeType?: string;
|
|
510
|
+
}
|
|
511
|
+
/** Текстовое содержимое ресурса */
|
|
512
|
+
interface McpResourceTextContent {
|
|
513
|
+
uri: string;
|
|
514
|
+
mimeType?: string;
|
|
515
|
+
text: string;
|
|
516
|
+
/** Метаданные расширений: у виджета MCP Apps здесь `ui.csp`, `ui.prefersBorder` */
|
|
517
|
+
_meta?: Record<string, unknown>;
|
|
518
|
+
}
|
|
519
|
+
/** Бинарное содержимое ресурса */
|
|
520
|
+
interface McpResourceBlobContent {
|
|
521
|
+
uri: string;
|
|
522
|
+
mimeType?: string;
|
|
523
|
+
/** Base64-кодированные данные */
|
|
524
|
+
blob: string;
|
|
525
|
+
}
|
|
526
|
+
/** Содержимое ресурса — текст или бинарные данные */
|
|
527
|
+
type McpResourceItemContent = McpResourceTextContent | McpResourceBlobContent;
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Типы и утилиты для инструментов агентного цикла.
|
|
531
|
+
*
|
|
532
|
+
* Порт из packages/agent-core/src/tools/tool.ts с расширениями:
|
|
533
|
+
* - readOnly флаг для параллельного выполнения
|
|
534
|
+
* - group — категория инструмента для фильтрации и UI
|
|
535
|
+
* - emitProgress в ToolContext — стриминг прогресса без возврата из execute
|
|
536
|
+
* - agentId в ToolContext — для мульти-агентных сценариев
|
|
537
|
+
*/
|
|
538
|
+
/**
|
|
539
|
+
* Примитивный тип JSON Schema для параметра инструмента.
|
|
540
|
+
* Ограниченное подмножество JSON Schema — только то, что понимают LLM-провайдеры.
|
|
541
|
+
*/
|
|
542
|
+
type JSONSchemaType = {
|
|
543
|
+
type: 'string';
|
|
544
|
+
description?: string;
|
|
545
|
+
enum?: string[];
|
|
546
|
+
/** Максимальная длина строки — защита от DoS через гигантский input. */
|
|
547
|
+
maxLength?: number;
|
|
548
|
+
/** Минимальная длина строки. */
|
|
549
|
+
minLength?: number;
|
|
550
|
+
} | {
|
|
551
|
+
type: 'number';
|
|
552
|
+
description?: string;
|
|
553
|
+
} | {
|
|
554
|
+
type: 'integer';
|
|
555
|
+
description?: string;
|
|
556
|
+
} | {
|
|
557
|
+
type: 'boolean';
|
|
558
|
+
description?: string;
|
|
559
|
+
} | {
|
|
560
|
+
type: 'array';
|
|
561
|
+
items: JSONSchemaType;
|
|
562
|
+
description?: string;
|
|
563
|
+
} | {
|
|
564
|
+
type: 'object';
|
|
565
|
+
properties: Record<string, JSONSchemaType>;
|
|
566
|
+
required?: string[];
|
|
567
|
+
description?: string;
|
|
568
|
+
/**
|
|
569
|
+
* Разрешить произвольные ключи в объекте (валидный JSON Schema keyword).
|
|
570
|
+
* Полезно для object-параметров с динамическими полями (params интеграций),
|
|
571
|
+
* где нужно явно подсказать LLM что это словарь произвольных ключей.
|
|
572
|
+
* При использовании совмещать с `properties: {}` для соответствия общему
|
|
573
|
+
* стилю кодовой базы.
|
|
574
|
+
*/
|
|
575
|
+
additionalProperties?: boolean;
|
|
576
|
+
};
|
|
577
|
+
/** Схема входных параметров инструмента (JSON Schema объект верхнего уровня) */
|
|
578
|
+
interface ToolInputSchema {
|
|
579
|
+
type: 'object';
|
|
580
|
+
properties: Record<string, JSONSchemaType>;
|
|
581
|
+
required?: string[];
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Контекст, передаваемый в каждый вызов инструмента.
|
|
585
|
+
*
|
|
586
|
+
* readFileTimestamps — критически важен для защиты от устаревших записей:
|
|
587
|
+
* перед перезаписью файла инструменты write/edit проверяют, что файл был
|
|
588
|
+
* прочитан в текущей сессии и не изменился с момента чтения.
|
|
589
|
+
*/
|
|
590
|
+
interface ToolContext {
|
|
591
|
+
/** Уникальный идентификатор текущей сессии */
|
|
592
|
+
sessionId: string;
|
|
593
|
+
/** Сигнал отмены — инструменты должны проверять его при длительных операциях */
|
|
594
|
+
abortSignal?: AbortSignal;
|
|
595
|
+
/** Среда выполнения — влияет на доступные операции */
|
|
596
|
+
env: 'electron' | 'server' | 'test';
|
|
597
|
+
/** Рабочая директория для файловых операций */
|
|
598
|
+
workingDirectory?: string;
|
|
599
|
+
/**
|
|
600
|
+
* Временные метки последнего чтения файлов (путь → timestamp в мс).
|
|
601
|
+
*
|
|
602
|
+
* Заполняется инструментом чтения при каждом успешном обращении.
|
|
603
|
+
* Инструменты записи и редактирования проверяют этот словарь перед записью,
|
|
604
|
+
* чтобы обнаружить устаревшие правки (stale write detection).
|
|
605
|
+
*
|
|
606
|
+
* Ключ: абсолютный путь к файлу.
|
|
607
|
+
* Значение: Date.now() в момент прочтения.
|
|
608
|
+
*/
|
|
609
|
+
readFileTimestamps: Record<string, number>;
|
|
610
|
+
/**
|
|
611
|
+
* Содержимое прочитанных файлов (путь → текст).
|
|
612
|
+
*
|
|
613
|
+
* Опциональный словарь для post-compact restore — заполняется инструментами
|
|
614
|
+
* чтения когда featureGates.postCompactRestore=true.
|
|
615
|
+
* Позволяет postCompactRestore() восстановить содержимое без повторного чтения с диска.
|
|
616
|
+
*
|
|
617
|
+
* Ключ: абсолютный путь к файлу (совпадает с readFileTimestamps).
|
|
618
|
+
* Значение: текстовое содержимое на момент последнего чтения.
|
|
619
|
+
*/
|
|
620
|
+
readFileContents?: Record<string, string>;
|
|
621
|
+
/**
|
|
622
|
+
* Функция для эмиссии событий прогресса во время выполнения инструмента.
|
|
623
|
+
* Позволяет стримить промежуточные результаты в UI без завершения execute().
|
|
624
|
+
* Отсутствует, если потребитель не поддерживает стриминг прогресса.
|
|
625
|
+
*/
|
|
626
|
+
emitProgress?: (content: string) => void;
|
|
627
|
+
/**
|
|
628
|
+
* Идентификатор агента в мульти-агентных сценариях.
|
|
629
|
+
* Используется для изоляции состояния между агентами и корректного логирования.
|
|
630
|
+
*/
|
|
631
|
+
agentId?: string;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Текстовый блок в результате инструмента.
|
|
635
|
+
* Используется внутри mixed-content для аннотаций к изображениям.
|
|
636
|
+
*/
|
|
637
|
+
interface ToolTextBlock {
|
|
638
|
+
type: 'text';
|
|
639
|
+
text: string;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Блок изображения в результате инструмента (vision support).
|
|
643
|
+
*
|
|
644
|
+
* Tool возвращает картинку «по-человечески»: base64 + mediaType.
|
|
645
|
+
* Конвертация в нативный формат API провайдера (Anthropic source/Google inlineData/
|
|
646
|
+
* OpenAI image_url) — обязанность нижележащего слоя (tool-dispatch + LLM provider).
|
|
647
|
+
*
|
|
648
|
+
* Используется для:
|
|
649
|
+
* - чтения сканированных документов (vision fallback при отказе OCR)
|
|
650
|
+
* - анализа скриншотов
|
|
651
|
+
* - обработки фото с телефона пользователя
|
|
652
|
+
*/
|
|
653
|
+
interface ToolImageBlock {
|
|
654
|
+
type: 'image';
|
|
655
|
+
/** Base64-кодированные данные изображения (без префикса data:...) */
|
|
656
|
+
base64: string;
|
|
657
|
+
/** MIME-тип — модели нужен корректный формат для декодирования */
|
|
658
|
+
mediaType: 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp';
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Блок-ссылка на изображение в vision-cache (disk storage).
|
|
662
|
+
*
|
|
663
|
+
* Альтернатива ToolImageBlock когда vision-cache сконфигурирован
|
|
664
|
+
* (`ctx.visionCache` есть). Tool пишет raw bytes в cache и возвращает
|
|
665
|
+
* file_ref вместо inline base64. Это снимает нагрузку с RAM/БД/PHP:
|
|
666
|
+
* tool_result в messages.content становится ~200 байт вместо ~500 KB.
|
|
667
|
+
*
|
|
668
|
+
* Раскрытие в base64 происходит непосредственно перед `provider.send()`
|
|
669
|
+
* через `expandVisionRefs` в `super-agent-core/src/loop/query.ts`.
|
|
670
|
+
*
|
|
671
|
+
* @see super-agent-core/src/loop/vision-cache.ts
|
|
672
|
+
*/
|
|
673
|
+
interface ToolImageRefBlock {
|
|
674
|
+
type: 'image_ref';
|
|
675
|
+
/** Относительный путь от rootPath: `<sessionId>/<sha256>.<ext>` */
|
|
676
|
+
pathSuffix: string;
|
|
677
|
+
mediaType: 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp';
|
|
678
|
+
}
|
|
679
|
+
/** Объединённый тип контентного блока в результате инструмента */
|
|
680
|
+
type ToolContentBlock = ToolTextBlock | ToolImageBlock | ToolImageRefBlock;
|
|
681
|
+
/**
|
|
682
|
+
* Успешный результат инструмента.
|
|
683
|
+
*
|
|
684
|
+
* Может быть простой строкой (обычный случай) или массивом блоков text+image
|
|
685
|
+
* для мультимодальных результатов (vision). Image-блоки требуются чтобы
|
|
686
|
+
* модель могла «видеть» картинку, а не получать строку base64 в text-блоке.
|
|
687
|
+
*/
|
|
688
|
+
interface ToolResultSuccess {
|
|
689
|
+
type: 'success';
|
|
690
|
+
content: string | ToolContentBlock[];
|
|
691
|
+
/**
|
|
692
|
+
* Данные для ВИДЖЕТА, мимо контекста модели.
|
|
693
|
+
*
|
|
694
|
+
* ⚠ РАЗДЕЛЕНИЕ НЕ РАДИ ПОРЯДКА, А РАДИ ДЕНЕГ И ТОЛКУ. Настоящий кадр ролика
|
|
695
|
+
* в base64 весит восемь килобайт; три кадра — двадцать четыре, и модель
|
|
696
|
+
* перечитывает их каждый ход, ничего в них не видя: base64 в тексте она
|
|
697
|
+
* «посмотреть» не может. Картинки нужны ГЛАЗАМ человека, то есть форме, а
|
|
698
|
+
* модели хватает названий словами. Уезжает как `structuredContent` в ответе
|
|
699
|
+
* `tools/call` — так это и разведено в спеке MCP.
|
|
700
|
+
*/
|
|
701
|
+
structured?: Record<string, unknown>;
|
|
702
|
+
}
|
|
703
|
+
/** Результат с ошибкой без машиночитаемого кода */
|
|
704
|
+
interface ToolResultErrorBase {
|
|
705
|
+
type: 'error';
|
|
706
|
+
error: string;
|
|
707
|
+
}
|
|
708
|
+
/** Результат с ошибкой с машиночитаемым кодом */
|
|
709
|
+
interface ToolResultErrorWithCode {
|
|
710
|
+
type: 'error';
|
|
711
|
+
error: string;
|
|
712
|
+
/** Код ошибки для программной обработки (retry logic, UI, логирование) */
|
|
713
|
+
code: string;
|
|
714
|
+
}
|
|
715
|
+
/** Результат с ошибкой — с кодом или без */
|
|
716
|
+
type ToolResultError = ToolResultErrorBase | ToolResultErrorWithCode;
|
|
717
|
+
/** Финальный результат выполнения инструмента */
|
|
718
|
+
type ToolResult = ToolResultSuccess | ToolResultError;
|
|
719
|
+
/**
|
|
720
|
+
* Категория инструмента — для фильтрации, UI-группировки и политик доступа.
|
|
721
|
+
* Аналог group:* из конфигурации OpenClaw.
|
|
722
|
+
*/
|
|
723
|
+
type ToolGroup = 'fs' | 'runtime' | 'web' | 'memory' | 'knowledge' | 'ui' | 'system' | 'automation' | 'confirmation' | 'collaboration' | 'integration' | 'skill';
|
|
724
|
+
/**
|
|
725
|
+
* Универсальный интерфейс инструмента агента.
|
|
726
|
+
*
|
|
727
|
+
* Инструменты регистрируются в агентском цикле и вызываются моделью
|
|
728
|
+
* по имени через механизм tool_use. Каждый инструмент описывает себя
|
|
729
|
+
* через JSON Schema и реализует функцию execute.
|
|
730
|
+
*
|
|
731
|
+
* @example
|
|
732
|
+
* ```typescript
|
|
733
|
+
* const readFileTool: Tool = {
|
|
734
|
+
* name: 'read_file',
|
|
735
|
+
* description: 'Читает содержимое файла по указанному пути',
|
|
736
|
+
* inputSchema: {
|
|
737
|
+
* type: 'object',
|
|
738
|
+
* properties: {
|
|
739
|
+
* path: { type: 'string', description: 'Путь к файлу' }
|
|
740
|
+
* },
|
|
741
|
+
* required: ['path']
|
|
742
|
+
* },
|
|
743
|
+
* readOnly: true,
|
|
744
|
+
* group: 'fs',
|
|
745
|
+
* async execute(input, ctx) {
|
|
746
|
+
* // ...
|
|
747
|
+
* }
|
|
748
|
+
* }
|
|
749
|
+
* ```
|
|
750
|
+
*/
|
|
751
|
+
interface Tool<TInput = any> {
|
|
752
|
+
/** Уникальное имя инструмента (snake_case) */
|
|
753
|
+
name: string;
|
|
754
|
+
/** Человекочитаемое описание для модели */
|
|
755
|
+
description: string;
|
|
756
|
+
/**
|
|
757
|
+
* Название по-русски — для ЧЕЛОВЕКА, в отличие от `description` для модели.
|
|
758
|
+
* Уезжает хосту как `annotations.title` и показывается в настройках вместо
|
|
759
|
+
* имени из кода: без него человек читает «Choose variant», решая, разрешать
|
|
760
|
+
* ли инструмент.
|
|
761
|
+
*/
|
|
762
|
+
title?: string;
|
|
763
|
+
/** JSON Schema входных параметров */
|
|
764
|
+
inputSchema: ToolInputSchema;
|
|
765
|
+
/**
|
|
766
|
+
* Флаг «только чтение».
|
|
767
|
+
* readOnly-инструменты не изменяют внешнее состояние и могут выполняться
|
|
768
|
+
* параллельно с другими readOnly-инструментами без риска конфликтов.
|
|
769
|
+
* Агентский цикл использует этот флаг для параллельного выполнения.
|
|
770
|
+
*/
|
|
771
|
+
readOnly?: boolean;
|
|
772
|
+
/**
|
|
773
|
+
* Переписывает то, что у человека уже есть.
|
|
774
|
+
*
|
|
775
|
+
* По умолчанию `false`, и это не оптимизм: монтаж кладёт НОВЫЙ файл рядом с
|
|
776
|
+
* исходником, обновление держит прежнюю версию на диске, установка кладёт
|
|
777
|
+
* недостающее в свою папку — ничего из этого не затирает чужого. `true`
|
|
778
|
+
* ставится там, где вызов переписывает содержимое, которое человек считал
|
|
779
|
+
* своим: например, готовый фильтр цвета в его паке стиля.
|
|
780
|
+
*/
|
|
781
|
+
destructive?: boolean;
|
|
782
|
+
/**
|
|
783
|
+
* Виджет инструмента (MCP Apps): адрес `ui://…` ресурса, HTML которого хост
|
|
784
|
+
* рисует в ленте вместо текстового ответа. Ресурс регистрируется на
|
|
785
|
+
* сервере отдельно (`McpServer.addResource`); здесь только ссылка, и в
|
|
786
|
+
* `tools/list` она уезжает как `_meta.ui.resourceUri`.
|
|
787
|
+
*/
|
|
788
|
+
ui?: {
|
|
789
|
+
resourceUri: string;
|
|
790
|
+
};
|
|
791
|
+
/**
|
|
792
|
+
* Флаг «обратимое действие» — действие остаётся в рамках текущего диалога
|
|
793
|
+
* с этим пользователем и не выходит наружу.
|
|
794
|
+
*
|
|
795
|
+
* `true` (по умолчанию) — безопасно вызывать без явного человеческого approve:
|
|
796
|
+
* recall, web_search, view, create_file (в workspace), send_file_to_chat,
|
|
797
|
+
* send_task_to_agent (внутреннему агенту), bash_tool (sandbox-изолирован),
|
|
798
|
+
* integration_call с read-методами.
|
|
799
|
+
*
|
|
800
|
+
* `false` — действие выходит наружу и должно сопровождаться явной формулировкой
|
|
801
|
+
* подтверждения от пользователя (через диалог) перед выполнением:
|
|
802
|
+
* отправка наружу (письмо/SMS внешнему адресату), запись в CRM/ERP,
|
|
803
|
+
* финансовые операции, удаление persistent данных.
|
|
804
|
+
*
|
|
805
|
+
* Используется для генерации списков в system prompt и (в будущем)
|
|
806
|
+
* для автоматического enforcement в loop.
|
|
807
|
+
*/
|
|
808
|
+
reversible?: boolean;
|
|
809
|
+
/**
|
|
810
|
+
* Группа инструмента — для фильтрации, UI-группировки и политик доступа.
|
|
811
|
+
* Соответствует group:* из конфигурации OpenClaw sandbox.
|
|
812
|
+
*/
|
|
813
|
+
group?: ToolGroup;
|
|
814
|
+
/**
|
|
815
|
+
* Флаг «отложенная загрузка схемы» (lazy tool loading).
|
|
816
|
+
*
|
|
817
|
+
* Если `true` — JSON Schema этого tool'а НЕ передаётся в LLM API в каждом
|
|
818
|
+
* запросе. Модель видит только имя + 1-строчное описание в специальном
|
|
819
|
+
* блоке `<deferred_tools>` системного промпта и подгружает полную схему
|
|
820
|
+
* через ToolSearch tool когда нужно её вызвать.
|
|
821
|
+
*
|
|
822
|
+
* Используется для редких/тяжёлых tools чтобы экономить токены input'а:
|
|
823
|
+
* - integration_call (динамический, описание ~жирное)
|
|
824
|
+
* - browse_page (тяжёлый prompt)
|
|
825
|
+
* - observer_*, credential_*, skill_* (admin-flow, редко)
|
|
826
|
+
*
|
|
827
|
+
* НЕ deferr'им: bash, view, read, write, edit, grep, glob, web_*,
|
|
828
|
+
* todo_write, Task, ToolSearch, memory tools — это core.
|
|
829
|
+
*
|
|
830
|
+
* После вызова ToolSearch с `select:<name>` или релевантного keyword-search
|
|
831
|
+
* — схема активируется на оставшийся срок жизни сессии (обычный tools[] payload).
|
|
832
|
+
*/
|
|
833
|
+
isDeferred?: boolean;
|
|
834
|
+
/**
|
|
835
|
+
* Максимальный размер `tool_result.content` в символах ДО persistence-cap.
|
|
836
|
+
*
|
|
837
|
+
* Семантика 1-в-1 как у Claude Code 2.1.88 (`Tool.ts:466`):
|
|
838
|
+
* - Эффективный порог = `min(maxResultSizeChars, DEFAULT_MAX_RESULT_SIZE_CHARS=50_000)`.
|
|
839
|
+
* Tool может объявить **БОЛЬШЕ** 50K — глобальный cap всё равно применит 50K.
|
|
840
|
+
* Tool может объявить **МЕНЬШЕ** 50K (например Grep 20K) — будет применён tool-specific лимит.
|
|
841
|
+
* - `Infinity` = hard opt-out, никогда не persist (для FileRead/view —
|
|
842
|
+
* нет смысла записывать файл во второй файл).
|
|
843
|
+
* - `undefined` = поведение как 100_000 (clamp до 50K).
|
|
844
|
+
*
|
|
845
|
+
* Применяется в `maybePersistLargeToolResult` (слой A) сразу после
|
|
846
|
+
* выполнения tool'а в `tool-dispatch.ts`, ДО того как tool_result попадёт
|
|
847
|
+
* в messages history. Большие результаты пишутся на диск
|
|
848
|
+
* `<rootPath>/<sessionId>/<tool_use_id>.{json,txt}`, в messages летит
|
|
849
|
+
* preview ~2000 символов внутри `<persisted-output>` тегов.
|
|
850
|
+
*
|
|
851
|
+
* Подробности — `new-version/docs/claude-code-optimizations.md` (Слой A).
|
|
852
|
+
*/
|
|
853
|
+
maxResultSizeChars?: number;
|
|
854
|
+
/**
|
|
855
|
+
* Выполняет инструмент с заданными параметрами.
|
|
856
|
+
* @param input - Валидированные входные параметры
|
|
857
|
+
* @param ctx - Контекст выполнения (сессия, сигнал отмены, среда, временные метки файлов)
|
|
858
|
+
* @returns Результат выполнения или ошибка
|
|
859
|
+
*/
|
|
860
|
+
execute(input: TInput, ctx: ToolContext): Promise<ToolResult>;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Аргументы инструмента: ОДНА запись на объявление модели и на проверку входа.
|
|
865
|
+
*
|
|
866
|
+
* ⚠ ЧТО ЗДЕСЬ БЫЛО ДО 2026-09-09 И ПОЧЕМУ ЭТО ПРИШЛОСЬ ПЕРЕДЕЛАТЬ. Форма
|
|
867
|
+
* аргументов была написана ДВАЖДЫ: JSON Schema рядом с каждым инструментом (её
|
|
868
|
+
* читает модель — иначе она не знает, что слать) и разбор этой же схемы руками в
|
|
869
|
+
* `arg-check.ts` (без него `arguments` от модели уходили в питон как есть).
|
|
870
|
+
* Обе копии были верны по отдельности и разойтись могли только МОЛЧА: схема
|
|
871
|
+
* обещает модели одно, проверка требует другого, а узнаёт об этом человек —
|
|
872
|
+
* отказом на то, что ему разрешили, или опиской, уехавшей внутрь платной
|
|
873
|
+
* операции. Ни один тест такого расхождения не видел: каждая половина
|
|
874
|
+
* проверялась своей.
|
|
875
|
+
*
|
|
876
|
+
* Теперь схема пишется на zod один раз, а JSON Schema ВЫВОДИТСЯ из неё
|
|
877
|
+
* (`docs/architecture.md` р. 24, «Один источник правды — схема на zod»).
|
|
878
|
+
* Расхождение стало невозможным по построению, а не по договорённости.
|
|
879
|
+
*
|
|
880
|
+
* ⚠ ПОЧЕМУ `zod/v4`, А НЕ КОРЕНЬ ПАКЕТА. Вывод JSON Schema из схемы умеет сама
|
|
881
|
+
* библиотека — `z.toJSONSchema`, и он есть только в четвёртой версии. Пакет
|
|
882
|
+
* `zod@3.25` отдаёт её подпутём `zod/v4`, поэтому ставить ничего не пришлось. И
|
|
883
|
+
* поэтому же на `zod/v4` переведены ВСЕ места пакета: две копии библиотеки в
|
|
884
|
+
* бандле — это 726 КБ вместо 398 КБ на машине покупателя (замерено сборкой), а
|
|
885
|
+
* бандл уезжает целиком, `node_modules` внутри `.mcpb` нет.
|
|
886
|
+
*
|
|
887
|
+
* ⚠ СВОЮ ФУНКЦИЮ ВЫВОДА МЫ НЕ ПИШЕМ НАМЕРЕННО. Наше подмножество JSON Schema
|
|
888
|
+
* маленькое (строка, число, флаг, список, перечисление, пояснение), и обойти
|
|
889
|
+
* его в шестьдесят строк можно. Но такая функция читает приватное устройство
|
|
890
|
+
* схем zod, то есть становится ровно той второй правдой, от которой мы здесь
|
|
891
|
+
* избавляемся, — только уже про чужой формат.
|
|
892
|
+
*/
|
|
893
|
+
|
|
894
|
+
/** Схема аргументов инструмента: всегда объект — таков контракт MCP. */
|
|
895
|
+
type ArgsSchema = z.ZodObject;
|
|
896
|
+
/**
|
|
897
|
+
* Инструмент, у которого схема аргументов есть.
|
|
898
|
+
*
|
|
899
|
+
* ⚠ ПОЛЕ ОБЯЗАТЕЛЬНОЕ, И ЭТО НЕСУЩЕЕ. Заслон навешивается при сборке набора
|
|
900
|
+
* (`tool-registry.ts`), а не внутри каждого `execute`. Будь схема
|
|
901
|
+
* необязательной, инструмент без неё зарегистрировался бы молча и остался бы
|
|
902
|
+
* единственным без проверки — то есть повторилась бы беда, из-за которой заслон
|
|
903
|
+
* и появился. Здесь это ловит не тест, а тип: собрать такой инструмент нечем.
|
|
904
|
+
*/
|
|
905
|
+
interface CheckedTool extends Tool {
|
|
906
|
+
readonly argsSchema: ArgsSchema;
|
|
907
|
+
}
|
|
908
|
+
/** Что не так с аргументами — или `null`, если всё сходится. */
|
|
909
|
+
declare function refusalFor(schema: ArgsSchema, input: Record<string, unknown>): string | null;
|
|
910
|
+
/**
|
|
911
|
+
* Объявление для модели — выводом из схемы.
|
|
912
|
+
*
|
|
913
|
+
* `$schema` снимается: клиент MCP ждёт схему аргументов, а не самостоятельный
|
|
914
|
+
* документ, и лишний ключ у части клиентов уезжает в промпт модели как есть.
|
|
915
|
+
*
|
|
916
|
+
* `required` отдаётся списком всегда, даже пустым: пропавшее поле разные клиенты
|
|
917
|
+
* читают по-разному, пустой список — одинаково. Так же выглядели все схемы до
|
|
918
|
+
* перехода на zod, и менять форму объявления мы не собирались.
|
|
919
|
+
*/
|
|
920
|
+
declare function jsonSchemaOf(schema: ArgsSchema): ToolInputSchema;
|
|
921
|
+
/**
|
|
922
|
+
* Инструмент из схемы: объявление выведено, вход разобран, тело получает свой тип.
|
|
923
|
+
*
|
|
924
|
+
* ⚠ РАЗБОР ЕСТЬ И ЗДЕСЬ, И В ОБЁРТКЕ `withArgs`, И ЭТО НЕ ДВЕ ПРАВДЫ. Правило
|
|
925
|
+
* одно — схема, — а вызовов проверки два, потому что задачи разные. Обёртка
|
|
926
|
+
* стоит на регистрации и отбивает описку ДО заслона подписки, то есть до сети.
|
|
927
|
+
* Разбор здесь страхует прямой вызов: обёртку однажды снимут «для отладки» или
|
|
928
|
+
* забудут навесить на новую полку, и без этой страховки `execute` получил бы
|
|
929
|
+
* `arguments` от модели как есть — ровно то, из-за чего заслон и появился.
|
|
930
|
+
* Второй `safeParse` над уже чистым объектом стоит микросекунды против минут
|
|
931
|
+
* работы питона.
|
|
932
|
+
*/
|
|
933
|
+
declare function defineTool<S extends ArgsSchema>(spec: {
|
|
934
|
+
name: string;
|
|
935
|
+
description: string;
|
|
936
|
+
args: S;
|
|
937
|
+
/** Название по-русски — его человек видит в настройках хоста */
|
|
938
|
+
title?: string;
|
|
939
|
+
readOnly?: boolean;
|
|
940
|
+
/** Переписывает то, что у человека уже есть (`protocol/tool.ts`) */
|
|
941
|
+
destructive?: boolean;
|
|
942
|
+
/**
|
|
943
|
+
* Виджет инструмента (MCP Apps): адрес `ui://…` ресурса с HTML формы.
|
|
944
|
+
*
|
|
945
|
+
* ⚠ ПРОХОДИТ ЧЕРЕЗ ЭТУ ЖЕ ДВЕРЬ НАМЕРЕННО. Инструмент с формой в ленте — это
|
|
946
|
+
* всё тот же инструмент: аргументы у него разбираются той же схемой, и
|
|
947
|
+
* заводить ему второй способ объявиться значило бы завести полку, на которой
|
|
948
|
+
* заслон аргументов однажды забудут навесить.
|
|
949
|
+
*/
|
|
950
|
+
ui?: {
|
|
951
|
+
resourceUri: string;
|
|
952
|
+
};
|
|
953
|
+
/**
|
|
954
|
+
* Тело инструмента. Контекст вызова — вторым доводом и необязательным.
|
|
955
|
+
*
|
|
956
|
+
* ⚠ НЕОБЯЗАТЕЛЕН ПОТОМУ, ЧТО ЕГО МОЖЕТ НЕ БЫТЬ: прямой вызов из теста
|
|
957
|
+
* контекста не передаёт, а у чужого клиента может не быть `emitProgress`
|
|
958
|
+
* (рассказ о ходе работы он не просил). Инструменты, которым ход не нужен,
|
|
959
|
+
* второй довод просто не объявляют.
|
|
960
|
+
*/
|
|
961
|
+
execute: (args: z.output<S>, ctx?: ToolContext) => Promise<ToolResult>;
|
|
962
|
+
}): CheckedTool;
|
|
963
|
+
/**
|
|
964
|
+
* Тот же инструмент, но со сверкой аргументов перед вызовом.
|
|
965
|
+
*
|
|
966
|
+
* Обёртка, а не правка каждого `execute`: инструментов четырнадцать, и
|
|
967
|
+
* четырнадцать копий одной проверки — это четырнадцать мест, где её однажды
|
|
968
|
+
* забудут. Порядок в `tool-registry.ts` несущий: эта обёртка СНАРУЖИ заслона
|
|
969
|
+
* подписки, иначе описка в аргументах сначала гнала бы запрос в сеть.
|
|
970
|
+
*/
|
|
971
|
+
declare function withArgs<T extends CheckedTool>(tool: T): T;
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Ход работы словами: что делаю сейчас, что дальше, сколько примерно осталось.
|
|
975
|
+
*
|
|
976
|
+
* Решение владельца 2026-09-10: «ты сидишь, ждёшь и не понимаешь, что
|
|
977
|
+
* происходит, на каком этапе, сколько осталось — и с установкой то же самое».
|
|
978
|
+
* Отсюда два куска, и они про разное время:
|
|
979
|
+
*
|
|
980
|
+
* - ЛЕСТНИЦА ШАГОВ — говорится СРАЗУ, до того как работа уйдёт в фон: что
|
|
981
|
+
* будет сделано и в каком порядке. Ближние шаги подробнее, дальние одной
|
|
982
|
+
* строкой;
|
|
983
|
+
* - `followJob` — рассказ ПО ХОДУ: пока задание считается, каждая новая
|
|
984
|
+
* строка его состояния уходит человеку, и опрашивать инструмент вручную
|
|
985
|
+
* раз в полминуты больше не нужно.
|
|
986
|
+
*
|
|
987
|
+
* ⚠ ВНУТРЕННОСТЕЙ В ЭТИХ СЛОВАХ БЫТЬ НЕ МОЖЕТ: ни команд, ни путей в недрах,
|
|
988
|
+
* ни имён библиотек, ни трейсбеков. Человек должен понять, где он сейчас, а не
|
|
989
|
+
* узнать, как продукт устроен. Сторожит это `progress.test.ts` — по настоящим
|
|
990
|
+
* строкам, собранным с прогона инструментов, а не по копии списка слов.
|
|
991
|
+
*
|
|
992
|
+
* ⚠ ПОЧЕМУ ЛЕСТНИЦА ПЕРЕВОДИТ ИМЕНА ЭТАПОВ, А НЕ БЕРЁТ ГОТОВЫЕ СЛОВА. Список
|
|
993
|
+
* этапов один и лежит в паспорте пака (`stages`), но человеческих названий у
|
|
994
|
+
* этапов паспорт не отдаёт — они лежат в паспортах шагов (`step.json`, ключ
|
|
995
|
+
* `title`), и оболочка их не читает. Поэтому здесь СЛОВАРЬ ИМЯ → РЕЧЬ, а не
|
|
996
|
+
* второй список этапов: порядок и состав по-прежнему приходят из паспорта, а
|
|
997
|
+
* незнакомое имя просто не попадает в рассказ. Появятся названия в паспорте —
|
|
998
|
+
* словарь уйдёт, и это будет правильнее.
|
|
999
|
+
*/
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Время и ожидание — снаружи, а не таймером в теле.
|
|
1003
|
+
*
|
|
1004
|
+
* ⚠ ЭТО НЕ УКРАШЕНИЕ РАДИ ТЕСТА, А ЕДИНСТВЕННЫЙ СПОСОБ ЕГО НАПИСАТЬ. Ждать
|
|
1005
|
+
* по-настоящему в тесте нельзя (`docs/testing.md`: ожидания по таймеру и
|
|
1006
|
+
* `sleep` запрещены), а цикл опроса без подмены времени проверяется только
|
|
1007
|
+
* минутным прогоном — то есть не проверяется никогда.
|
|
1008
|
+
*/
|
|
1009
|
+
interface Pace {
|
|
1010
|
+
/** Сколько прошло, в миллисекундах. */
|
|
1011
|
+
now: () => number;
|
|
1012
|
+
/** Подождать столько миллисекунд. */
|
|
1013
|
+
rest: (ms: number) => Promise<void>;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Набор инструментов покупателя — одним списком и под одними обёртками.
|
|
1018
|
+
*
|
|
1019
|
+
* ⚠ ИХ ВОСЕМЬ, И ЭТО РЕШЕНИЕ ВЛАДЕЛЬЦА 2026-09-17 («у модели шесть рабочих
|
|
1020
|
+
* инструментов»). Рабочих действительно шесть: `setup`, `plan`, `plan_edit`,
|
|
1021
|
+
* `render`, `preview`, `choose_variant`. Ещё два добавлены механикой хоста, а
|
|
1022
|
+
* не желанием: `wait` — потому что полоса хода работы объявлена НА
|
|
1023
|
+
* ИНСТРУМЕНТЕ (`protocol/server.ts`), и ожидание тем же `render` рисовало бы
|
|
1024
|
+
* новую полосу на каждый вызов; `variant_chosen` — потому что в Codex виджетов
|
|
1025
|
+
* нет, выбор приходит словами, и звать запись модель должна сама.
|
|
1026
|
+
*
|
|
1027
|
+
* ⚠ ДО 2026-09-18 ИХ БЫЛО ДВАДЦАТЬ ПЯТЬ, И ЭТО БЫЛО ДОРОГО НЕ КРАСОТОЙ. Каждое
|
|
1028
|
+
* объявление модель перечитывает каждым ходом, а ход стоит 8–19 секунд
|
|
1029
|
+
* (`docs/architecture.md` р. 26). Двадцать пять дверей к пятнадцати работам
|
|
1030
|
+
* означали и второй счёт: модель путала соседние двери — «покажи варианты» шло
|
|
1031
|
+
* в `style_check`, который без движка отвечал отказом. Одна дверь на работу,
|
|
1032
|
+
* ветку внутри выбирают аргументы.
|
|
1033
|
+
*
|
|
1034
|
+
* ⚠ ЗАЧЕМ ОТДЕЛЬНЫЙ МОДУЛЬ РАДИ ДВУХ СТРОК. Проверять надо ТОТ САМЫЙ список,
|
|
1035
|
+
* который уезжает клиенту, а не собранную рядом копию: сними обёртку сверки —
|
|
1036
|
+
* и тест по копии остался бы зелёным (проверено мутацией, так и вышло). Пока
|
|
1037
|
+
* список жил в `cli.ts`, отдельно от сборки сервера его было не достать.
|
|
1038
|
+
*
|
|
1039
|
+
* ⚠ И ЭТОТ ДОВОД ПЕРЕПИСАН ДВАЖДЫ. Сначала здесь стояло «`cli.ts` при импорте
|
|
1040
|
+
* ПОДНИМАЕТ СЕРВЕР» — неправда с тех пор, как авто-запуск закрыт проверкой
|
|
1041
|
+
* `launchedDirectly()`. Потом вместо этого стояла ссылка на `cli.test.ts`, а
|
|
1042
|
+
* он ушёл 2026-09-17 вместе с агентами покупателя: проверять в старте стало
|
|
1043
|
+
* нечего. Довод остался один, тот, что в начале: ЗДЕСЬ СОБИРАЕТСЯ НАСТОЯЩИЙ
|
|
1044
|
+
* СПИСОК, и тесты берут его отсюда.
|
|
1045
|
+
*
|
|
1046
|
+
* ⚠ ПОЛКА ОСТАЛАСЬ ОДНА, И ЭТО РЕШЕНИЕ, А НЕ УПРОЩЕНИЕ. Раньше их было три:
|
|
1047
|
+
* монтаж, данные Напарника (рилсы, баланс, распаковка, генерация) и обмен
|
|
1048
|
+
* фирменным стилем. Вторая и третья ходили в PHP API монолита —
|
|
1049
|
+
* `company.naparnik.ai`, — а монолит гасится (docs/architecture.md, раздел 20).
|
|
1050
|
+
* Инструмент, который в день гашения начнёт врать «сервер не ответил», хуже
|
|
1051
|
+
* отсутствующего: человек будет чинить связь вместо того, чтобы узнать, что
|
|
1052
|
+
* фичи больше нет. Поэтому обе полки убраны из поставки целиком.
|
|
1053
|
+
*
|
|
1054
|
+
* Что осталось: монтаж, который идёт НА КОМПЬЮТЕРЕ КЛИЕНТА, и заслон подписки,
|
|
1055
|
+
* который спрашивает НАШ сервер. Больше этот пакет никуда не ходит.
|
|
1056
|
+
*/
|
|
1057
|
+
|
|
1058
|
+
declare function buildAll(client: NaparnikApiClient, engine: Engine, passport: PackPassport | null,
|
|
1059
|
+
/**
|
|
1060
|
+
* Ключ Напарника. Нужен трём разным работам, и все три — за деньги:
|
|
1061
|
+
* установщику движка (раздача закрыта авторизацией), черновику плана и цвету
|
|
1062
|
+
* по двум кадрам (обе команды движка стоят в `UNDER_SUBSCRIPTION`).
|
|
1063
|
+
*/
|
|
1064
|
+
key: string | undefined,
|
|
1065
|
+
/**
|
|
1066
|
+
* Адрес нашего сервера. Он же адрес раздачи: указатель начинки лежит на том
|
|
1067
|
+
* же хосте, что и API (`config.ts`, `DEFAULT_BASE_URL`).
|
|
1068
|
+
*/
|
|
1069
|
+
baseUrl: string,
|
|
1070
|
+
/**
|
|
1071
|
+
* Время и ожидание — для цикла `wait`, который ждёт задание и рассказывает ход.
|
|
1072
|
+
*
|
|
1073
|
+
* ⚠ ПАРАМЕТРОМ, А НЕ ТАЙМЕРОМ В ТЕЛЕ, И ЭТО ЕДИНСТВЕННЫЙ СПОСОБ ПРОВЕРИТЬ
|
|
1074
|
+
* ЦИКЛ. Ожидания по таймеру и `sleep` в тестах запрещены (`docs/testing.md`),
|
|
1075
|
+
* а цикл без подмены времени проверялся бы минутным прогоном — то есть
|
|
1076
|
+
* никогда. Умолчание обычное, вызывающему про это знать не надо.
|
|
1077
|
+
*/
|
|
1078
|
+
pace?: Pace): Tool[];
|
|
1079
|
+
/**
|
|
1080
|
+
* Ресурсы виджетов — парой к `buildAll`, из того же файла: кто собирает
|
|
1081
|
+
* сервер из инструментов, здесь же берёт и HTML к ним. Сторож пары — тест
|
|
1082
|
+
* «у каждого инструмента с виджетом есть ресурс» в `tool-declaration.test.ts`.
|
|
1083
|
+
*/
|
|
1084
|
+
declare function buildResources(): Array<{
|
|
1085
|
+
resource: McpResource;
|
|
1086
|
+
contents: McpResourceItemContent[];
|
|
1087
|
+
}>;
|
|
1088
|
+
|
|
1089
|
+
/** Что скачали и чем это оказалось. */
|
|
1090
|
+
interface Downloaded {
|
|
1091
|
+
file: string;
|
|
1092
|
+
bytes: number;
|
|
1093
|
+
sha256: string;
|
|
1094
|
+
}
|
|
1095
|
+
/** Беда доставки, у которой есть человеческий текст. */
|
|
1096
|
+
declare class DeliveryError extends Error {
|
|
1097
|
+
constructor(message: string);
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Скачать файл целиком и сверить сумму.
|
|
1101
|
+
*
|
|
1102
|
+
* Пишем во времянку рядом и переименовываем: недокачанный файл не должен ни
|
|
1103
|
+
* секунды выглядеть готовым — на этом уже обжигались в питоновской докачке.
|
|
1104
|
+
*/
|
|
1105
|
+
declare function downloadFile(opts: {
|
|
1106
|
+
url: string;
|
|
1107
|
+
dest: string;
|
|
1108
|
+
sha256: string;
|
|
1109
|
+
bytes?: number;
|
|
1110
|
+
timeoutMs?: number;
|
|
1111
|
+
report?: (text: string) => void;
|
|
1112
|
+
/**
|
|
1113
|
+
* Ключ Напарника. Раздача движка закрыта авторизацией: без ключа приедет 403.
|
|
1114
|
+
* Необязателен, потому что этим же загрузчиком качается открытая поставка
|
|
1115
|
+
* расширения — заставлять её носить ключ значило бы обещать связь, которой нет.
|
|
1116
|
+
*/
|
|
1117
|
+
/**
|
|
1118
|
+
* Ключ Напарника — или явное `null`, если его нет.
|
|
1119
|
+
*
|
|
1120
|
+
* ⚠ ПОЛЕ ОБЯЗАТЕЛЬНОЕ, ХОТЯ ЗНАЧЕНИЕ МОЖЕТ БЫТЬ ПУСТЫМ. Пока оно было
|
|
1121
|
+
* необязательным (`key?`), забыть его по дороге было нечем поймать: ключ
|
|
1122
|
+
* протаскивается через шесть вызовов подряд, и пропуск на любом из них давал
|
|
1123
|
+
* молчаливое скачивание без авторизации. Раньше это было незаметно — раздача
|
|
1124
|
+
* стояла открытой. Теперь она закрыта, и та же описка превратилась бы в
|
|
1125
|
+
* «не принял ключ» у платящего человека, причём без единой подсказки, что
|
|
1126
|
+
* ключ просто не доехал. Обязательное поле заставляет вызывающего сказать
|
|
1127
|
+
* вслух, что ключа нет.
|
|
1128
|
+
*/
|
|
1129
|
+
key: string | null;
|
|
1130
|
+
}): Promise<Downloaded>;
|
|
1131
|
+
interface TarEntry {
|
|
1132
|
+
name: string;
|
|
1133
|
+
kind: 'file' | 'dir' | 'symlink' | 'hardlink';
|
|
1134
|
+
mode: number;
|
|
1135
|
+
body: Buffer;
|
|
1136
|
+
target: string;
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Разобрать `.tar` (уже распакованный из gzip) на записи.
|
|
1140
|
+
*
|
|
1141
|
+
* ⚠ СВОЙ РАЗБОР, А НЕ ВЫЗОВ `tar`. Системный `tar` есть не везде (на Windows
|
|
1142
|
+
* он появился только в свежих сборках), а на macOS он ещё и молча впитывает
|
|
1143
|
+
* обратно свои двойники `._имя` — то есть показывает не то, что получит
|
|
1144
|
+
* клиент. Формат простой и стабильный: заголовок в 512 байт, тело кратно 512.
|
|
1145
|
+
*/
|
|
1146
|
+
declare function tarEntries(data: Buffer): TarEntry[];
|
|
1147
|
+
/**
|
|
1148
|
+
* Остаётся ли путь внутри целевой папки.
|
|
1149
|
+
*
|
|
1150
|
+
* ⚠ ПРОВЕРЯЕМ СОБРАННЫМ ПУТЁМ, А НЕ НАЛИЧИЕМ «..» В СТРОКЕ. Архив приезжает из
|
|
1151
|
+
* интернета, и «../../.ssh/authorized_keys» в имени члена — известный приём
|
|
1152
|
+
* (zip slip); хитрое имя обходит проверку строкой, но не проверку путём. Тот
|
|
1153
|
+
* же заслон и теми же словами стоит в питоне (`install._inside`).
|
|
1154
|
+
*/
|
|
1155
|
+
declare function inside(root: string, path: string): boolean;
|
|
1156
|
+
/**
|
|
1157
|
+
* Разложить `.tar.gz` целиком в папку.
|
|
1158
|
+
*
|
|
1159
|
+
* Всё, что пытается выйти за `dest` (именем члена или целью ссылки), — ОТКАЗ
|
|
1160
|
+
* ЦЕЛИКОМ, а не «пропустим этот файл». Наполовину распакованное дерево
|
|
1161
|
+
* выглядит установленным и падает потом, в чужом месте.
|
|
1162
|
+
*/
|
|
1163
|
+
declare function unpackArchive(archive: string, dest: string): void;
|
|
1164
|
+
|
|
1165
|
+
interface StepOutcome {
|
|
1166
|
+
ok: boolean;
|
|
1167
|
+
text: string;
|
|
1168
|
+
}
|
|
1169
|
+
interface FirstInstallTestHooks {
|
|
1170
|
+
afterPublishRename?(): void;
|
|
1171
|
+
}
|
|
1172
|
+
/** Где внутри начинки лежит общая таблица источников питона. */
|
|
1173
|
+
declare const SOURCES_FILE = "python-sources.json";
|
|
1174
|
+
interface PythonSource {
|
|
1175
|
+
url: string;
|
|
1176
|
+
sha256: string;
|
|
1177
|
+
mb: number;
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Ключ платформы ровно в том виде, в каком его пишет питон:
|
|
1181
|
+
* `platform.system()`-`platform.machine()`. Совпадение обязано быть точным —
|
|
1182
|
+
* таблица одна на две стороны.
|
|
1183
|
+
*/
|
|
1184
|
+
declare function platformKey(os?: NodeJS.Platform, arch?: string): string | null;
|
|
1185
|
+
/** Прочитать таблицу питона из установленной начинки. */
|
|
1186
|
+
declare function pythonSource(engineDir: string, key?: string | null): PythonSource | null;
|
|
1187
|
+
/**
|
|
1188
|
+
* Доставить начинку, когда движка нет вовсе.
|
|
1189
|
+
*
|
|
1190
|
+
* Согласие спрашивается ровно так же, как у остальных шагов установки: это
|
|
1191
|
+
* трафик человека, его диск и чужой исполняемый код на его машине.
|
|
1192
|
+
*/
|
|
1193
|
+
declare function installPayload(engine: Engine, opts: {
|
|
1194
|
+
consent: boolean;
|
|
1195
|
+
manifestUrl: string;
|
|
1196
|
+
key?: string | undefined;
|
|
1197
|
+
}, hooks?: FirstInstallTestHooks): Promise<StepOutcome>;
|
|
1198
|
+
/**
|
|
1199
|
+
* Доставить портативный python, когда системного нет.
|
|
1200
|
+
*
|
|
1201
|
+
* ⚠ ЭТО НЕ ДУБЛЬ ШАГА «python» ДВИЖКА, А ЕГО ЕДИНСТВЕННЫЙ ДОСТУПНЫЙ ПУТЬ НА
|
|
1202
|
+
* ЧИСТОЙ МАШИНЕ. Как только питон появился, шаг движка снова работает и делает
|
|
1203
|
+
* ровно то же самое — оттуда и таблица источников, и место установки.
|
|
1204
|
+
*/
|
|
1205
|
+
declare function installPython(engine: Engine, opts: {
|
|
1206
|
+
consent: boolean;
|
|
1207
|
+
version?: string;
|
|
1208
|
+
}): Promise<StepOutcome>;
|
|
38
1209
|
|
|
39
1210
|
/**
|
|
40
1211
|
* Иерархия ошибок клиента Напарника.
|
|
@@ -72,19 +1243,27 @@ declare class NaparnikScopeError extends NaparnikError {
|
|
|
72
1243
|
*/
|
|
73
1244
|
declare class NaparnikInsufficientBalanceError extends NaparnikError {
|
|
74
1245
|
readonly details: {
|
|
75
|
-
/**
|
|
76
|
-
|
|
1246
|
+
/**
|
|
1247
|
+
* Что упёрлось. Пока единственная причина — баланс компании; поле
|
|
1248
|
+
* оставлено перечислением, чтобы форма не менялась, если появится
|
|
1249
|
+
* второй источник отказа.
|
|
1250
|
+
*/
|
|
1251
|
+
limit: 'company_balance';
|
|
77
1252
|
balanceRub: number;
|
|
78
1253
|
requiredRub: number;
|
|
79
|
-
/** Куда идти
|
|
1254
|
+
/** Куда идти пополнять. */
|
|
80
1255
|
actionUrl: string;
|
|
81
1256
|
};
|
|
82
1257
|
constructor(message: string, details: {
|
|
83
|
-
/**
|
|
84
|
-
|
|
1258
|
+
/**
|
|
1259
|
+
* Что упёрлось. Пока единственная причина — баланс компании; поле
|
|
1260
|
+
* оставлено перечислением, чтобы форма не менялась, если появится
|
|
1261
|
+
* второй источник отказа.
|
|
1262
|
+
*/
|
|
1263
|
+
limit: 'company_balance';
|
|
85
1264
|
balanceRub: number;
|
|
86
1265
|
requiredRub: number;
|
|
87
|
-
/** Куда идти
|
|
1266
|
+
/** Куда идти пополнять. */
|
|
88
1267
|
actionUrl: string;
|
|
89
1268
|
});
|
|
90
1269
|
}
|
|
@@ -115,4 +1294,299 @@ declare class NaparnikTimeoutError extends NaparnikError {
|
|
|
115
1294
|
declare class NaparnikConfigError extends NaparnikError {
|
|
116
1295
|
}
|
|
117
1296
|
|
|
118
|
-
|
|
1297
|
+
/**
|
|
1298
|
+
* Версия оболочки.
|
|
1299
|
+
*
|
|
1300
|
+
* ⚠ РАНЬШЕ ЭТОТ ФАЙЛ СОБИРАЛСЯ, ТЕПЕРЬ ПРАВИТСЯ РУКАМИ. Генератор жил в
|
|
1301
|
+
* монолите и в этот репозиторий пока не переехал — он в одной связке
|
|
1302
|
+
* со сборкой поставки, а движок монтажа ещё в монолите. Значит правится два
|
|
1303
|
+
* места сразу: `version` в package.json и строка ниже.
|
|
1304
|
+
*
|
|
1305
|
+
* Чтобы «два места» не разъехались молча, за ними смотрит `version.test.ts`.
|
|
1306
|
+
* Версия — это то, чем оболочка представляется серверу и по чему сервер решает,
|
|
1307
|
+
* годится ли она для новой начинки: разойдись она с пакетом, и в логах будет
|
|
1308
|
+
* стоять чужое число, а обновление начнёт считать оболочку не той.
|
|
1309
|
+
*/
|
|
1310
|
+
declare const VERSION = "0.10.11";
|
|
1311
|
+
/** Как мы представляемся серверу Напарника. По нему в логах видно оболочку. */
|
|
1312
|
+
declare const USER_AGENT = "naparnik-mcp/0.10.11";
|
|
1313
|
+
/**
|
|
1314
|
+
* Самая старая начинка (движок монтажа), с которой эта оболочка работает.
|
|
1315
|
+
* Обновление начинки ниже этой версии не ставится, а выше — требует оболочки
|
|
1316
|
+
* не старше, чем объявлено в самой начинке.
|
|
1317
|
+
*
|
|
1318
|
+
* ⚠ ЭТО ВТОРАЯ ПОЛОВИНА ЗАМКА, И ПОДНИМАТЬ ЕЁ НАДО ВМЕСТЕ С ПЕРВОЙ. Первая —
|
|
1319
|
+
* `min_shell` в `client/engine/version.json` («ниже этой оболочки начинку не
|
|
1320
|
+
* отдавать»), вторая здесь («ниже этой начинки обновление не брать»). Подними
|
|
1321
|
+
* одну — и свежая оболочка честно поставит себе начинку, которая лежит в
|
|
1322
|
+
* раздаче СЕЙЧАС, старого вида. Обе половины названы в `docs/architecture.md`
|
|
1323
|
+
* р. 24 и поднимаются одним коммитом.
|
|
1324
|
+
*
|
|
1325
|
+
* ⚠ ПОЧЕМУ ИМЕННО «2026.09.13-1». Волна цвета подняла договор
|
|
1326
|
+
* вопросов формы на naparnik-choices/3 и завела команды capabilities и
|
|
1327
|
+
* choose-apply: начинка старше их не знает, и эта оболочка с ней осталась бы без
|
|
1328
|
+
* каталога и без памяти выбора — с отказом словами, но без формы. Номер сборки
|
|
1329
|
+
* выводится выкладкой как «<дата UTC>-<номер конвейера>»
|
|
1330
|
+
* (`tools/delivery/build-engine.ts`), значит сборка волны и любая следующая за
|
|
1331
|
+
* ней дают число не меньше этого, а всё прежнее отсекается. Число совпадает с
|
|
1332
|
+
* `version` в `client/engine/version.json`: там оно тоже поднято волной.
|
|
1333
|
+
* Прежний замок — «2026.09.09-192», волна латиницы.
|
|
1334
|
+
*
|
|
1335
|
+
* ⚠ ЧИСЛО ОТСЕКАЕТ ПРОШЛУЮ ВОЛНУ, А НЕ УГАДЫВАЕТ СВОЮ. Сборка этой волны
|
|
1336
|
+
* получит номер «2026.09.13-<номер конвейера>» — он больше, и замок её
|
|
1337
|
+
* пропустит; начинка прошлой волны («2026.09.12-230») не знает вида «range» и
|
|
1338
|
+
* отсекается. Ревью 2026-09-13 поймало ровно это: обе половины замка обязаны
|
|
1339
|
+
* подниматься вместе, иначе свежая оболочка честно ставит себе начинку без
|
|
1340
|
+
* ползунков и встречает человека отказом договора.
|
|
1341
|
+
*
|
|
1342
|
+
* ⚠ ДАТА В НОМЕРЕ — UTC, А НЕ МЕСТНАЯ. Сначала здесь стояло «2026.09.13-1» —
|
|
1343
|
+
* по московскому календарю верно, а выкладка в тот же час собрала
|
|
1344
|
+
* «2026.09.12-230»: по UTC было ещё двенадцатое. Замок отверг собственную
|
|
1345
|
+
* сборку, и прогон покупателя на стенде честно встал на install_engine со
|
|
1346
|
+
* словами «раздача ещё не обновилась». Номер берётся из УЖЕ СОБРАННОГО
|
|
1347
|
+
* артефакта этой волны, а не сочиняется по календарю.
|
|
1348
|
+
*/
|
|
1349
|
+
declare const MIN_ENGINE = "2026.09.19-355";
|
|
1350
|
+
|
|
1351
|
+
/**
|
|
1352
|
+
* Что вышло из установки или отката — одними словами у начинки и у тела.
|
|
1353
|
+
*
|
|
1354
|
+
* ⚠ ТИП ЗДЕСЬ, А НЕ У КАЖДОГО СВОЙ. Копий было две, и вторая приехала с
|
|
1355
|
+
* подписью «теми же словами, что и у начинки» — то есть автор уже знал, что это
|
|
1356
|
+
* копия. Модель читает оба ответа одинаково; разойдись форма, и половина
|
|
1357
|
+
* разговора про обновление стала бы другой.
|
|
1358
|
+
*/
|
|
1359
|
+
interface InstallOutcome {
|
|
1360
|
+
ok: boolean;
|
|
1361
|
+
text: string;
|
|
1362
|
+
}
|
|
1363
|
+
/** Прежний публичный минимум хранения; GC не применяет его без leases. */
|
|
1364
|
+
declare const KEEP_VERSIONS = 2;
|
|
1365
|
+
/** Не чаще раза в столько часов дёргаем сеть на старте. */
|
|
1366
|
+
declare const PAUSE_HOURS = 6;
|
|
1367
|
+
/**
|
|
1368
|
+
* Сравнение версий по числовым кускам: «2026.08.20-1» и «0.3.0» одинаково
|
|
1369
|
+
* раскладываются на числа. Строковое сравнение здесь врёт на первом же
|
|
1370
|
+
* двузначном числе («0.10.0» оказывалось бы меньше «0.9.0»).
|
|
1371
|
+
*/
|
|
1372
|
+
declare function compare(a: string, b: string): number;
|
|
1373
|
+
|
|
1374
|
+
/** Указатель на свежую начинку — НАШ объект, собранный разбором ниже. */
|
|
1375
|
+
interface EngineManifest {
|
|
1376
|
+
/** Версия начинки. Она же станет именем папки на диске человека. */
|
|
1377
|
+
version: string;
|
|
1378
|
+
/** Полный адрес архива. Обязан лежать на том же хосте, что и указатель. */
|
|
1379
|
+
url: string;
|
|
1380
|
+
sha256: string;
|
|
1381
|
+
bytes: number;
|
|
1382
|
+
/** Ниже этой версии оболочка эту начинку не примет. */
|
|
1383
|
+
minShellVersion: string;
|
|
1384
|
+
}
|
|
1385
|
+
/** Что вышло из разбора: объект — или причина, по которой его нет. */
|
|
1386
|
+
type ManifestOutcome = Parsed<EngineManifest>;
|
|
1387
|
+
/**
|
|
1388
|
+
* Разобрать указатель.
|
|
1389
|
+
*
|
|
1390
|
+
* @param raw что приехало по сети — доверять нечему, тип `unknown` намеренно
|
|
1391
|
+
* @param from адрес самого указателя: по нему проверяется, что архив лежит рядом
|
|
1392
|
+
*/
|
|
1393
|
+
declare function parseEngineManifest(raw: unknown, from: string): ManifestOutcome;
|
|
1394
|
+
/**
|
|
1395
|
+
* Что вышло из разбора: наш объект — или причина, по которой его нет.
|
|
1396
|
+
* Одна форма на оба указателя.
|
|
1397
|
+
*/
|
|
1398
|
+
type Parsed<T> = {
|
|
1399
|
+
ok: true;
|
|
1400
|
+
manifest: T;
|
|
1401
|
+
} | {
|
|
1402
|
+
ok: false;
|
|
1403
|
+
why: string;
|
|
1404
|
+
};
|
|
1405
|
+
|
|
1406
|
+
/** Умолчание: раздача прода. Одна правда с адресом API — `config.ts`. */
|
|
1407
|
+
declare const MANIFEST_URL: string;
|
|
1408
|
+
/** Потолок ожидания манифеста — общий с указателем оболочки. */
|
|
1409
|
+
declare const MANIFEST_TIMEOUT_MS = 2000;
|
|
1410
|
+
/**
|
|
1411
|
+
* Указатель начинки и его разбор живут в `manifest.ts` — там же кончается чужой
|
|
1412
|
+
* формат с русскими ключами. Здесь ходит уже НАШ объект.
|
|
1413
|
+
*/
|
|
1414
|
+
type Decision = {
|
|
1415
|
+
what: 'nothing';
|
|
1416
|
+
} | {
|
|
1417
|
+
what: 'shell_too_old';
|
|
1418
|
+
manifest: EngineManifest;
|
|
1419
|
+
} | {
|
|
1420
|
+
what: 'newer_available';
|
|
1421
|
+
manifest: EngineManifest;
|
|
1422
|
+
};
|
|
1423
|
+
|
|
1424
|
+
/** Спросить указатель начинки. Механика — общая, `manifest.ts`, `askPointer`. */
|
|
1425
|
+
declare function fetchManifest(url?: string, timeoutMs?: number, key?: string): Promise<ManifestOutcome>;
|
|
1426
|
+
/** Что делать с тем, что приехало в манифесте. */
|
|
1427
|
+
declare function decide(installed: string | null, manifest: EngineManifest | null, shellVersion?: string): Decision;
|
|
1428
|
+
declare function timeToCheck(engine: UpdateDeps, now?: number): boolean;
|
|
1429
|
+
declare function markChecked(engine: UpdateDeps, now?: number): void;
|
|
1430
|
+
/** Какая версия была рабочей до последнего обновления. */
|
|
1431
|
+
declare function previousVersion(engine: UpdateDeps): string | null;
|
|
1432
|
+
/** Сделать версию рабочей. */
|
|
1433
|
+
declare function markCurrent(engine: UpdateDeps, version: string): void;
|
|
1434
|
+
/** Убрать всё, кроме текущей и предыдущей; улику `broken-*` бережём. */
|
|
1435
|
+
declare function tidyOld(engine: UpdateDeps): string[];
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* Что обновлению нужно от движка — интерфейсом, а не классом.
|
|
1439
|
+
*
|
|
1440
|
+
* ⚠ ЭТО ШОВ ДЛЯ ПРОВЕРКИ, А НЕ УКРАШЕНИЕ. Настоящий `Engine` на каждый шаг
|
|
1441
|
+
* поднимает питон; живой прогон против настоящего движка лежит рядом —
|
|
1442
|
+
* `update.test.ts`, «дымовой прогон против настоящего движка». А решения, ради
|
|
1443
|
+
* которых обновление и написано, — переключать или нет, что сохранить при
|
|
1444
|
+
* откате, что снести, — проверяются подставным прогоном там же, и проверяются
|
|
1445
|
+
* ПОЛНОСТЬЮ, включая ветку «скачалось, но не запускается», которую живым
|
|
1446
|
+
* прогоном воспроизвести нечем.
|
|
1447
|
+
*
|
|
1448
|
+
* ⚠ Раньше здесь стояла ссылка на `mcp-editor/test-update.ts` — папки такой в
|
|
1449
|
+
* репозитории нет. Обещание «живое проверено где-то там» и было единственным,
|
|
1450
|
+
* что стояло между нами и полугодовым молчанием этой границы.
|
|
1451
|
+
*/
|
|
1452
|
+
interface UpdateDeps {
|
|
1453
|
+
place(): EnginePlace | null;
|
|
1454
|
+
whyNoEngine(): string;
|
|
1455
|
+
versionsDir(): string;
|
|
1456
|
+
freshVersion(): string | null;
|
|
1457
|
+
command(args: string[], opts?: {
|
|
1458
|
+
timeout?: number;
|
|
1459
|
+
key?: string;
|
|
1460
|
+
}): Promise<CommandOutcome>;
|
|
1461
|
+
enginePython(): string;
|
|
1462
|
+
pythonEnv(): NodeJS.ProcessEnv;
|
|
1463
|
+
}
|
|
1464
|
+
interface DeployTestHooks {
|
|
1465
|
+
afterPublishRename?(): void;
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Скачать, разложить, проверить запуском и переключиться.
|
|
1469
|
+
*
|
|
1470
|
+
* ⚠ КАЧАЕТ ТЕКУЩАЯ УСТАНОВЛЕННАЯ ВЕРСИЯ. Это не хитрость, а единственный
|
|
1471
|
+
* способ не заводить второй загрузчик: докачка с обрыва, разбор 416 и сверка
|
|
1472
|
+
* суммы уже написаны в движке и покрыты тестами. Движка нет вовсе — это не
|
|
1473
|
+
* обновление, а первая установка, и она идёт через `install_engine`.
|
|
1474
|
+
*/
|
|
1475
|
+
declare function deploy(engine: UpdateDeps, manifest: EngineManifest, trial?: Trial,
|
|
1476
|
+
/** Ключ для закрытой раздачи движка: без него скачивание получит 403. */
|
|
1477
|
+
key?: string, hooks?: DeployTestHooks): Promise<InstallOutcome>;
|
|
1478
|
+
/**
|
|
1479
|
+
* Дымовой прогон новой сборки: запускается ли она вообще и та ли это версия.
|
|
1480
|
+
* Возвращает текст беды или null, если всё в порядке.
|
|
1481
|
+
*/
|
|
1482
|
+
type Trial = (engine: UpdateDeps, consolePy: string) => Promise<{
|
|
1483
|
+
stdout: string;
|
|
1484
|
+
}>;
|
|
1485
|
+
/** Запуск `console.py версия` новой сборки — тем же питоном, что и всё прочее. */
|
|
1486
|
+
declare function probe(engine: UpdateDeps, consolePy: string): Promise<{
|
|
1487
|
+
stdout: string;
|
|
1488
|
+
}>;
|
|
1489
|
+
/** Вернуться на предыдущую версию. */
|
|
1490
|
+
declare function rollback(engine: UpdateDeps): InstallOutcome;
|
|
1491
|
+
|
|
1492
|
+
/** Имя сервера в конфиге Codex. Оно же — имя блока. */
|
|
1493
|
+
declare const SERVER_NAME = "naparnik";
|
|
1494
|
+
/**
|
|
1495
|
+
* Пометка владения. Стоит СТРОКОЙ ВЫШЕ заголовка блока: по ней мы отличаем
|
|
1496
|
+
* «наше, можно переписывать» от «человек написал сам, руки прочь».
|
|
1497
|
+
*/
|
|
1498
|
+
declare const MARKER = "# \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E \u041D\u0430\u043F\u0430\u0440\u043D\u0438\u043A\u043E\u043C, \u043E\u0431\u043E\u043B\u043E\u0447\u043A\u0430";
|
|
1499
|
+
/**
|
|
1500
|
+
* Сколько ждать старта сервера и ответа инструмента.
|
|
1501
|
+
*
|
|
1502
|
+
* Умолчания Codex — 10 и 60 секунд. Старт мы укладываем и в 10 (паспорт пака
|
|
1503
|
+
* читается с потолком в 8), но запас нужен на холодной машине, где питон ещё не
|
|
1504
|
+
* в кэше файловой системы. А вот 60 секунд на инструмент мало: `inspect_video`
|
|
1505
|
+
* на длинном ролике делает холодный проход ffprobe, ebur128 и silencedetect.
|
|
1506
|
+
*
|
|
1507
|
+
* ⚠ ЧИСЛА — ПРЕДПОЛОЖЕНИЕ. Проверить их можно только на живом Codex, которого у
|
|
1508
|
+
* нас нет; здесь они записаны явно, чтобы правились одним местом, а не
|
|
1509
|
+
* подбирались заново.
|
|
1510
|
+
*/
|
|
1511
|
+
declare const START_SEC = 20;
|
|
1512
|
+
declare const TOOL_SEC = 120;
|
|
1513
|
+
/**
|
|
1514
|
+
* Что установщик сделал с конфигом.
|
|
1515
|
+
*
|
|
1516
|
+
* ⚠ ЗНАЧЕНИЯ ЛАТИНИЦЕЙ, ХОТЯ НАРУЖУ ОНИ НЕ ЕЗДЯТ. Это код, а код у нас
|
|
1517
|
+
* английский (правило №0): человеку показывается `words` рядом, и вот он
|
|
1518
|
+
* русский. Волна латиницы 2026-09-09 забрала их вместе с остальным проводом —
|
|
1519
|
+
* ни один сторож эти значения не видит, значит сами они не почистились бы
|
|
1520
|
+
* никогда.
|
|
1521
|
+
*/
|
|
1522
|
+
type Action = 'created' | 'added' | 'updated' | 'unchanged' | 'removed' | 'nothing_to_remove';
|
|
1523
|
+
interface Outcome {
|
|
1524
|
+
file: string;
|
|
1525
|
+
action: Action;
|
|
1526
|
+
/** Куда положили копию прежнего файла. */
|
|
1527
|
+
copy?: string;
|
|
1528
|
+
words: string;
|
|
1529
|
+
}
|
|
1530
|
+
/** Чужой блок с нашим именем: не трогаем, показываем человеку. */
|
|
1531
|
+
declare class ForeignBlockError extends Error {
|
|
1532
|
+
readonly block: string;
|
|
1533
|
+
constructor(block: string, file: string);
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Живём ли мы во временной распаковке `npx`.
|
|
1537
|
+
*
|
|
1538
|
+
* ⚠ ПРОПИСАТЬ ЭТОТ ПУТЬ В КОНФИГ — ЗНАЧИТ ОДНАЖДЫ ЕГО СЛОМАТЬ. `npx` кладёт
|
|
1539
|
+
* пакет в `~/.npm/_npx/<хеш>/` — неуправляемый кэш, который сносят `npm cache
|
|
1540
|
+
* clean`, чистильщики диска и корпоративные политики на `%LOCALAPPDATA%`
|
|
1541
|
+
* (на этой машине там уже полсотни таких каталогов). После уборки Codex вечно
|
|
1542
|
+
* рапортует «сервер не запустился», а в конфиге стоит путь, которого нет.
|
|
1543
|
+
* Отказываемся ДО записи, а не разбираемся после пропажи.
|
|
1544
|
+
*/
|
|
1545
|
+
declare function fromNpxCache(path: string): boolean;
|
|
1546
|
+
/** Путь к общему конфигу Codex. Переменная — для тестов и нестандартных домов. */
|
|
1547
|
+
declare function configPath(env?: NodeJS.ProcessEnv): string;
|
|
1548
|
+
/** Наш блок целиком, включая пометку владения. */
|
|
1549
|
+
declare function buildBlock(entryFile: string, key?: string): string;
|
|
1550
|
+
interface FoundBlock {
|
|
1551
|
+
start: number;
|
|
1552
|
+
end: number;
|
|
1553
|
+
text: string;
|
|
1554
|
+
ours: boolean;
|
|
1555
|
+
}
|
|
1556
|
+
/**
|
|
1557
|
+
* Где в файле лежит наш блок.
|
|
1558
|
+
*
|
|
1559
|
+
* ⚠ ДОЧЕРНИЕ ТАБЛИЦЫ ВХОДЯТ В БЛОК. `[mcp_servers.naparnik.env]` — это
|
|
1560
|
+
* заголовок в первой колонке, и наивное «до следующего заголовка» обрезало бы
|
|
1561
|
+
* блок перед ним. Тогда при обновлении старая таблица env оставалась бы в
|
|
1562
|
+
* файле сиротой, а в ней лежит ключ: человек менял ключ, а работал старый.
|
|
1563
|
+
*/
|
|
1564
|
+
declare function findBlock(text: string): FoundBlock | null;
|
|
1565
|
+
/**
|
|
1566
|
+
* Заведён ли наш сервер ВНУТРИ таблицы `[mcp_servers]`, а не своим заголовком.
|
|
1567
|
+
*
|
|
1568
|
+
* ⚠ ЭТО НЕ ЭКЗОТИКА, А ВТОРАЯ ЗАКОННАЯ ФОРМА ЗАПИСИ TOML. Человек мог написать
|
|
1569
|
+
*
|
|
1570
|
+
* [mcp_servers]
|
|
1571
|
+
* naparnik = { command = "node", args = [...] }
|
|
1572
|
+
*
|
|
1573
|
+
* Наш поиск ищет только заголовок `[mcp_servers.naparnik]`, не находит его и
|
|
1574
|
+
* дописывает блок — а TOML получает ДВА определения одного ключа и перестаёт
|
|
1575
|
+
* разбираться целиком. Тогда у человека отваливаются ВСЕ серверы, а не только
|
|
1576
|
+
* наш. Отказываемся так же, как при чужом блоке: показать и не трогать.
|
|
1577
|
+
*/
|
|
1578
|
+
declare function keyInTable(text: string): string | null;
|
|
1579
|
+
/** Сколько копий прежнего конфига держим рядом. */
|
|
1580
|
+
declare const KEEP_COPIES = 3;
|
|
1581
|
+
interface Settings {
|
|
1582
|
+
/** Полный путь к `cli.js`, который будет запускать Codex. */
|
|
1583
|
+
entryFile: string;
|
|
1584
|
+
key?: string;
|
|
1585
|
+
env?: NodeJS.ProcessEnv;
|
|
1586
|
+
}
|
|
1587
|
+
/** Прописать сервер в конфиг Codex. */
|
|
1588
|
+
declare function install(n: Settings): Outcome;
|
|
1589
|
+
/** Убрать наш блок и ничего кроме него. */
|
|
1590
|
+
declare function remove(env?: NodeJS.ProcessEnv): Outcome;
|
|
1591
|
+
|
|
1592
|
+
export { type ArgsSchema, type CheckedTool, type CommandOutcome, DEFAULT_HOME, type Decision, DeliveryError, Engine, type EngineManifest, type EnginePlace, ForeignBlockError, INSIDE_PYTHON, INSTALL_STEPS, type InstallOutcome, KEEP_COPIES, KEEP_VERSIONS, LONG_STEPS, MANIFEST_TIMEOUT_MS, MANIFEST_URL, MARKER, MIN_ENGINE, type ManifestOutcome, NaparnikApiClient, NaparnikAuthError, NaparnikClientError, NaparnikConfigError, NaparnikError, NaparnikInsufficientBalanceError, type NaparnikMcpConfig, NaparnikScopeError, NaparnikServerError, NaparnikTimeoutError, type Outcome, PAUSE_HOURS, type PackPassport, SERVER_NAME, SOURCES_FILE, START_SEC, type Settings, TOOL_SEC, type Trial, USER_AGENT, type UpdateDeps, VERSION, buildAll, buildBlock, buildResources, compare, configPath, decide, defineTool, deploy, downloadFile, fetchManifest, fileOrRefusal, findBlock, fromNpxCache, inside, install, installPayload, installPython, jsonSchemaOf, keyInTable, lastLine, loadConfig, markChecked, markCurrent, parseEngineManifest, parseFailure, path, platformKey, previousVersion, probe, pythonSource, refusalFor, remove, rollback, tarEntries, tidyOld, timeToCheck, unpackArchive, withArgs };
|