tensorgrid-ui 1.4.1 → 1.10.0
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/LICENSE +15 -0
- package/brand/tensorgrid.ico +0 -0
- package/env/snapshot.json +916 -0
- package/install.ps1 +457 -0
- package/lib/cli.mjs +67 -0
- package/lib/client.js +316 -35
- package/lib/http-runtime.js +104 -4
- package/lib/index.js +75 -1
- package/lib/review-log.js +538 -0
- package/lib/review-settings.js +246 -22
- package/lib/review.js +212 -39
- package/lib/reviewers.js +235 -17
- package/package.json +12 -2
- package/preset/agent.cordis.yml +304 -0
- package/preset/preset.yml +5 -0
- package/profile/cordis.patch.yml +65 -0
- package/profile/package.json +14 -0
- package/tools/round-ico-corners.mjs +113 -0
package/lib/reviewers.js
CHANGED
|
@@ -44,26 +44,136 @@ const STDERR_CAP = 64 * 1024
|
|
|
44
44
|
* список устарел бы при первом же выпуске нового имени модели.
|
|
45
45
|
*/
|
|
46
46
|
export const REVIEWERS = {
|
|
47
|
+
// ── Наши ревизоры ────────────────────────────────────────────────────
|
|
48
|
+
//
|
|
49
|
+
// Работают не отдельными продуктами, а нашими подагентами на нашем же
|
|
50
|
+
// маршруте моделей. Их ДВА, и это не прихоть: два разных СЕМЕЙСТВА
|
|
51
|
+
// моделей и есть то разнообразие, ради которого раньше звали чужое.
|
|
52
|
+
//
|
|
53
|
+
// Чистый опыт показал, что обвязка почти ничего не решает: одна модель в
|
|
54
|
+
// Claude Code и у нас дала совпадение 13 находок из 14. Разное находят
|
|
55
|
+
// разные МОДЕЛИ, а не разные обвязки. А обе подписки — Anthropic и
|
|
56
|
+
// OpenAI — доступны прямо здесь, со своими списками моделей и без
|
|
57
|
+
// поштучной оплаты.
|
|
58
|
+
//
|
|
59
|
+
// Список моделей пуст намеренно: он приходит живым из маршрута. Внешние
|
|
60
|
+
// продукты понимают только свои имена, а здесь доступно всё подключённое,
|
|
61
|
+
// включая сотни моделей через OpenRouter.
|
|
62
|
+
internal: {
|
|
63
|
+
label: 'Наш ревизор',
|
|
64
|
+
kind: 'internal',
|
|
65
|
+
models: [],
|
|
66
|
+
efforts: ['low', 'medium', 'high', 'max'],
|
|
67
|
+
defaultProvider: 'anthropic',
|
|
68
|
+
defaultModel: 'claude-opus-5',
|
|
69
|
+
},
|
|
70
|
+
'internal-alt': {
|
|
71
|
+
label: 'Наш ревизор, вторая модель',
|
|
72
|
+
kind: 'internal',
|
|
73
|
+
models: [],
|
|
74
|
+
efforts: ['low', 'medium', 'high', 'max'],
|
|
75
|
+
// Намеренно ДРУГОЕ семейство: два прогона одной модели дадут почти
|
|
76
|
+
// одинаковые находки, и второй ревизор не окупит своего времени.
|
|
77
|
+
defaultProvider: 'openai-codex',
|
|
78
|
+
defaultModel: 'gpt-6-astra',
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
// ── Внешние продукты ─────────────────────────────────────────────────
|
|
82
|
+
//
|
|
83
|
+
// Оставлены, но НЕ требуются: в умолчания не входят, в установку не
|
|
84
|
+
// включены, и без них всё работает.
|
|
85
|
+
//
|
|
86
|
+
// Причина не в том, что они плохи, а в том, что не добавляют. По деньгам
|
|
87
|
+
// — подписка работает и у нас. По находкам — совпадение 13 из 14. А
|
|
88
|
+
// стоят они двух лишних установок сотруднику и почти всех дефектов,
|
|
89
|
+
// которые пришлось чинить: аргументы, поток ввода, забор ответа,
|
|
90
|
+
// обёртки Windows, ключи авторизации, отмена, тайм-аут.
|
|
91
|
+
//
|
|
92
|
+
// Кому они уже установлены — пусть включает и пользуется.
|
|
47
93
|
'claude-code': {
|
|
48
94
|
label: 'Claude Code',
|
|
95
|
+
kind: 'external',
|
|
96
|
+
optional: true,
|
|
49
97
|
command: 'claude',
|
|
98
|
+
// Список — подсказка, а НЕ ограничение: интерфейс даёт ввести и своё имя.
|
|
99
|
+
// Жёсткий перечень устарел бы при первом же выпуске новой модели, а
|
|
100
|
+
// выяснить у продукта его собственный список нечем.
|
|
50
101
|
models: ['claude-opus-5', 'claude-opus-4-8', 'claude-sonnet-4-6'],
|
|
51
102
|
efforts: ['low', 'medium', 'high'],
|
|
52
|
-
|
|
53
|
-
|
|
103
|
+
// Имена, которыми продукт авторизуется. См. `authEnvFor` ниже.
|
|
104
|
+
authEnv: ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'CLAUDE_CODE_OAUTH_TOKEN', 'ANTHROPIC_BASE_URL'],
|
|
54
105
|
},
|
|
55
106
|
codex: {
|
|
56
107
|
label: 'Codex',
|
|
108
|
+
kind: 'external',
|
|
109
|
+
optional: true,
|
|
57
110
|
command: 'codex',
|
|
58
111
|
models: ['gpt-6-astra', 'gpt-5.6-sol'],
|
|
59
112
|
efforts: ['low', 'medium', 'high', 'xhigh'],
|
|
60
|
-
|
|
61
|
-
defaultEffort: null,
|
|
113
|
+
authEnv: ['OPENAI_API_KEY', 'CODEX_HOME'],
|
|
62
114
|
},
|
|
63
115
|
}
|
|
64
116
|
|
|
65
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* ЕДИНСТВЕННЫЙ источник списка ревизоров.
|
|
119
|
+
*
|
|
120
|
+
* Список жил в трёх файлах, и ревизор указал, чем это кончится: добавление
|
|
121
|
+
* продукта в один файл из трёх делает запись выбора неопределённой и роняет
|
|
122
|
+
* весь раздел настроек. Я тогда починил половину и отчитался за целое —
|
|
123
|
+
* следующий обзор это и поймал.
|
|
124
|
+
*/
|
|
125
|
+
export const KNOWN = Object.keys(REVIEWERS)
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Что допустимо в модели и усилии.
|
|
129
|
+
*
|
|
130
|
+
* Строгий вариант — для ВНЕШНИХ ревизоров: значение уходит в аргументы
|
|
131
|
+
* командной строки, и косая черта там ни в одном имени не нужна, а вот
|
|
132
|
+
* ведущий дефис превратил бы значение в ключ команды.
|
|
133
|
+
*
|
|
134
|
+
* Для НАШЕГО ревизора черта обязательна: маршрут называет модели вида
|
|
135
|
+
* `anthropic/claude-opus-5`, а через OpenRouter — `x-ai/grok-4.6`. Туда
|
|
136
|
+
* значение уходит не в командную строку, а в опции вызова, поэтому
|
|
137
|
+
* послабление безопасно. Одна общая проверка отвергала бы половину
|
|
138
|
+
* подключённых моделей — что и обнаружилось при первой же сверке.
|
|
139
|
+
*/
|
|
66
140
|
const SAFE_VALUE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/
|
|
141
|
+
const SAFE_ROUTE_VALUE = /^[A-Za-z0-9][A-Za-z0-9._\-/]{0,95}$/
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Что разрешено НАШЕМУ ревизору.
|
|
145
|
+
*
|
|
146
|
+
* Список РАЗРЕШЕНИЙ, а не запретов, и это принципиально. Запрет пришлось бы
|
|
147
|
+
* дополнять при каждом новом инструменте харнесса: забыли один раз — и
|
|
148
|
+
* ревизор снова умеет писать. Разрешение ошибается в безопасную сторону:
|
|
149
|
+
* незнакомый инструмент по умолчанию недоступен.
|
|
150
|
+
*
|
|
151
|
+
* Те же три возможности, что у Claude Code: читать файл, искать по именам,
|
|
152
|
+
* искать по содержимому. Больше обзору не нужно ничего.
|
|
153
|
+
*/
|
|
154
|
+
const REVIEW_TOOLS = { allow: ['Read', 'Glob', 'Grep'] }
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Вернуть ревизору его собственные ключи авторизации.
|
|
158
|
+
*
|
|
159
|
+
* Служба запуска вычищает из окружения ребёнка всё, что похоже на секрет —
|
|
160
|
+
* `KEY`, `TOKEN`, `SECRET`, `PASSWORD`. Для чужого процесса это правильно:
|
|
161
|
+
* наши ключи ему незачем. Но ревизор авторизуется как раз такими именами, и
|
|
162
|
+
* у пользователя, который входит ключом, а не подпиской, обзор молча не
|
|
163
|
+
* работал бы — при том что та же команда из консоли отвечает.
|
|
164
|
+
*
|
|
165
|
+
* Поэтому возвращаем ТОЧЕЧНО: только имена этого продукта и только если они
|
|
166
|
+
* заданы. Ничего чужого, никаких шаблонов.
|
|
167
|
+
*/
|
|
168
|
+
function authEnvFor(kind) {
|
|
169
|
+
const names = REVIEWERS[kind]?.authEnv ?? []
|
|
170
|
+
const env = {}
|
|
171
|
+
for (const name of names) {
|
|
172
|
+
const value = process.env[name]
|
|
173
|
+
if (typeof value === 'string' && value !== '') env[name] = value
|
|
174
|
+
}
|
|
175
|
+
return env
|
|
176
|
+
}
|
|
67
177
|
|
|
68
178
|
/**
|
|
69
179
|
* Собрать аргументы запуска.
|
|
@@ -114,22 +224,99 @@ export function buildArgs(kind, { model, effort, answerFile }) {
|
|
|
114
224
|
throw new Error(`неизвестный ревизор: ${kind}`)
|
|
115
225
|
}
|
|
116
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Позвать нашего собственного ревизора — подагента на нашем маршруте.
|
|
229
|
+
*
|
|
230
|
+
* Отличается от внешних принципиально: не отдельный продукт, а агент в этом
|
|
231
|
+
* же процессе. Отсюда и возможности, которых нет у остальных: любая модель
|
|
232
|
+
* из подключённых, включая сотни через OpenRouter, и усилие рассуждения из
|
|
233
|
+
* тех же настроек, что у основного агента.
|
|
234
|
+
*
|
|
235
|
+
* Рабочую папку он берёт у родителя сам — она и есть папка чата, что
|
|
236
|
+
* совпадает с ограничением доступа для внешних.
|
|
237
|
+
*/
|
|
238
|
+
async function runInternal(ctx, kind, { prompt, provider, model, effort, signal }) {
|
|
239
|
+
const subagents = ctx.get === undefined ? ctx.subagents : ctx.get('subagents')
|
|
240
|
+
if (subagents === undefined) {
|
|
241
|
+
return { kind, failed: 'реестр подагентов не смонтирован', answer: null }
|
|
242
|
+
}
|
|
243
|
+
if (typeof subagents.list === 'function' && !subagents.list().includes('spawn')) {
|
|
244
|
+
return { kind, failed: 'провайдер spawn недоступен', answer: null }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const agentOptions = {}
|
|
248
|
+
// Провайдер и модель приходят ПОРОЗНЬ и порознь же передаются.
|
|
249
|
+
//
|
|
250
|
+
// Соблазнительно вывести провайдера из имени модели по первой косой
|
|
251
|
+
// черте — и это неверно. У OpenRouter `x-ai/grok-4.6` целиком является
|
|
252
|
+
// именем модели, а провайдер называется `openrouter`. Деление по черте
|
|
253
|
+
// дало бы несуществующего провайдера `x-ai`, и маршрут отказал бы.
|
|
254
|
+
if (provider) agentOptions.provider = String(provider)
|
|
255
|
+
if (model) agentOptions.model = String(model)
|
|
256
|
+
if (effort) agentOptions.reasoningEffort = String(effort)
|
|
257
|
+
|
|
258
|
+
let run = null
|
|
259
|
+
try {
|
|
260
|
+
run = await subagents.start('spawn', {
|
|
261
|
+
label: 'обзор: наш ревизор',
|
|
262
|
+
prompt: [{ type: 'text', text: prompt }],
|
|
263
|
+
parent: ctx.reviewParent,
|
|
264
|
+
signal,
|
|
265
|
+
// ТОЛЬКО ЧТЕНИЕ — так же, как у внешних ревизоров.
|
|
266
|
+
//
|
|
267
|
+
// Без этого ревизор наследовал бы весь набор инструментов родителя,
|
|
268
|
+
// включая запись файлов и запуск команд. Ровно та дыра, которую оба
|
|
269
|
+
// внешних ревизора назвали критичной у Claude Code: я убрал её там и
|
|
270
|
+
// немедленно завёл здесь, просто не передав ограничения.
|
|
271
|
+
//
|
|
272
|
+
// Асимметрия недопустима: обзор, который «ничего не меняет», не должен
|
|
273
|
+
// иметь права изменить всё — тем более что ревизор по заданию читает
|
|
274
|
+
// чужой непроверенный код и указание внутри этого кода исполнилось бы
|
|
275
|
+
// без подтверждения.
|
|
276
|
+
toolFilter: REVIEW_TOOLS,
|
|
277
|
+
...(Object.keys(agentOptions).length > 0 ? { agentOptions } : {}),
|
|
278
|
+
})
|
|
279
|
+
const result = await run.result
|
|
280
|
+
const answer = (result?.output ?? [])
|
|
281
|
+
.filter((block) => block?.type === 'text')
|
|
282
|
+
.map((block) => String(block.text ?? ''))
|
|
283
|
+
.join('')
|
|
284
|
+
|
|
285
|
+
if (signal?.aborted === true) return { kind, failed: 'отменено', cancelled: true, answer: null }
|
|
286
|
+
if (answer.trim() === '') return { kind, failed: 'пустой ответ', answer: null }
|
|
287
|
+
return { kind, failed: null, cancelled: false, truncated: false, answer, model: model ?? null, effort: effort ?? null }
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (signal?.aborted === true) return { kind, failed: 'отменено', cancelled: true, answer: null }
|
|
290
|
+
return { kind, failed: String(error?.message ?? error), answer: null }
|
|
291
|
+
} finally {
|
|
292
|
+
if (run !== null) {
|
|
293
|
+
try {
|
|
294
|
+
await run.dispose()
|
|
295
|
+
} catch {
|
|
296
|
+
// Уборка не должна подменять результат обзора.
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
117
302
|
/**
|
|
118
303
|
* Запустить одного ревизора и вернуть его ответ.
|
|
119
304
|
*
|
|
120
305
|
* Отказ не бросается наружу: обзор двумя ревизорами не должен пропадать
|
|
121
306
|
* целиком из-за одного неответившего. Возвращается либо ответ, либо причина.
|
|
122
307
|
*
|
|
123
|
-
* @param ctx - контекст со службой `subprocess
|
|
308
|
+
* @param ctx - контекст со службой `subprocess`; для нашего ревизора ещё и
|
|
309
|
+
* `subagents`, а в `reviewParent` — агент, от имени которого идёт вызов.
|
|
124
310
|
* @param kind - имя ревизора.
|
|
125
311
|
* @param options - задание, рабочая папка, модель, усилие, сигнал отмены.
|
|
126
312
|
*/
|
|
127
|
-
export async function runReviewer(ctx, kind, { prompt, cwd, model, effort, signal, timeoutMs }) {
|
|
313
|
+
export async function runReviewer(ctx, kind, { prompt, cwd, provider, model, effort, signal, timeoutMs }) {
|
|
128
314
|
const spec = REVIEWERS[kind]
|
|
129
315
|
if (spec === undefined) return { kind, failed: `неизвестный ревизор: ${kind}`, answer: null }
|
|
130
316
|
|
|
131
|
-
|
|
132
|
-
|
|
317
|
+
const allowed = spec.kind === 'internal' ? SAFE_ROUTE_VALUE : SAFE_VALUE
|
|
318
|
+
for (const [label, value] of [['провайдер', provider], ['модель', model], ['усилие', effort]]) {
|
|
319
|
+
if (value !== undefined && value !== null && value !== '' && !allowed.test(String(value))) {
|
|
133
320
|
return { kind, failed: `недопустимое значение (${label}): ${String(value)}`, answer: null }
|
|
134
321
|
}
|
|
135
322
|
}
|
|
@@ -144,8 +331,15 @@ export async function runReviewer(ctx, kind, { prompt, cwd, model, effort, signa
|
|
|
144
331
|
return { kind, failed: 'отменено', cancelled: true, answer: null }
|
|
145
332
|
}
|
|
146
333
|
|
|
334
|
+
// Наш ревизор — не отдельный продукт, а подагент на нашем маршруте.
|
|
335
|
+
// Ни команды, ни аргументов, ни временных файлов ему не нужно.
|
|
336
|
+
if (spec.kind === 'internal') {
|
|
337
|
+
return runInternal(ctx, kind, { prompt, provider, model, effort, signal })
|
|
338
|
+
}
|
|
339
|
+
|
|
147
340
|
let workDir = null
|
|
148
341
|
let handle = null
|
|
342
|
+
let truncated = false
|
|
149
343
|
// Своя отмена поверх пользовательской: ревизор без ограничения по времени
|
|
150
344
|
// держал бы обзор бесконечно.
|
|
151
345
|
const deadline = new AbortController()
|
|
@@ -175,6 +369,9 @@ export async function runReviewer(ctx, kind, { prompt, cwd, model, effort, signa
|
|
|
175
369
|
},
|
|
176
370
|
graceMs: 5000,
|
|
177
371
|
signal: deadline.signal,
|
|
372
|
+
// Точечный возврат собственных ключей продукта: служба вычищает всё
|
|
373
|
+
// похожее на секрет, а ревизор именно ими и авторизуется.
|
|
374
|
+
env: authEnvFor(kind),
|
|
178
375
|
})
|
|
179
376
|
|
|
180
377
|
const outcome = await handle.done
|
|
@@ -221,14 +418,24 @@ export async function runReviewer(ctx, kind, { prompt, cwd, model, effort, signa
|
|
|
221
418
|
return { kind, failed: `Codex не оставил итогового ответа${tail === '' ? '' : `: ${tail}`}`, answer: null }
|
|
222
419
|
}
|
|
223
420
|
} else {
|
|
224
|
-
|
|
421
|
+
// Признак потери читается, а не игнорируется.
|
|
422
|
+
//
|
|
423
|
+
// Собранный вывод хранит только ХВОСТ потока: длинный ответ обрезается
|
|
424
|
+
// с НАЧАЛА, а список ревизор начинает с самого опасного. Молча отдать
|
|
425
|
+
// такой обрывок как полный обзор — значит потерять именно то, ради
|
|
426
|
+
// чего обзор затевался.
|
|
427
|
+
const read = await handle.collected.stdout.readFrom(0)
|
|
428
|
+
answer = read.text
|
|
429
|
+
if (read.lossy === true) {
|
|
430
|
+
truncated = true
|
|
431
|
+
}
|
|
225
432
|
}
|
|
226
433
|
|
|
227
434
|
if (answer.trim() === '') {
|
|
228
435
|
return { kind, failed: `пустой ответ${tail === '' ? '' : `: ${tail}`}`, answer: null }
|
|
229
436
|
}
|
|
230
437
|
|
|
231
|
-
return { kind, failed: null, cancelled: false, answer, model: model ?? null, effort: effort ?? null }
|
|
438
|
+
return { kind, failed: null, cancelled: false, truncated, answer, model: model ?? null, effort: effort ?? null }
|
|
232
439
|
} catch (error) {
|
|
233
440
|
if (signal?.aborted === true) return { kind, failed: 'отменено', cancelled: true, answer: null }
|
|
234
441
|
const reason = deadline.signal.aborted ? 'превышено время ожидания' : String(error?.message ?? error)
|
|
@@ -239,12 +446,23 @@ export async function runReviewer(ctx, kind, { prompt, cwd, model, effort, signa
|
|
|
239
446
|
if (handle !== null) {
|
|
240
447
|
try {
|
|
241
448
|
handle.terminate()
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
// это
|
|
246
|
-
// копятся после каждой отмены.
|
|
247
|
-
|
|
449
|
+
// Ждём смерти процесса, но НЕ бесконечно.
|
|
450
|
+
//
|
|
451
|
+
// Ждать нужно: без ожидания уборка идёт при живом процессе с открытым
|
|
452
|
+
// файлом итога, на Windows это отказ, `force` его проглатывает, и
|
|
453
|
+
// временные каталоги копятся после каждой отмены.
|
|
454
|
+
//
|
|
455
|
+
// Ограничивать тоже нужно: `waitForExit` ждёт опустошения всего
|
|
456
|
+
// управляемого диапазона, а не только самой команды. Потомок,
|
|
457
|
+
// который не умирает, держал бы вызов после тайм-аута — в
|
|
458
|
+
// неотменяемом `finally`, где «стоп» уже не поможет.
|
|
459
|
+
const cleanup = new AbortController()
|
|
460
|
+
const cleanupTimer = setTimeout(() => cleanup.abort(), 10000)
|
|
461
|
+
try {
|
|
462
|
+
await handle.waitForExit(cleanup.signal)
|
|
463
|
+
} finally {
|
|
464
|
+
clearTimeout(cleanupTimer)
|
|
465
|
+
}
|
|
248
466
|
} catch {
|
|
249
467
|
// Уборка не должна подменять результат.
|
|
250
468
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tensorgrid-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "TENSOR GRID — фирменный интерфейс поверх DeepSeek Harness: палитра, живой ambient-слой, айдентика и русский язык-пакет",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -16,8 +16,18 @@
|
|
|
16
16
|
},
|
|
17
17
|
"./package.json": "./package.json"
|
|
18
18
|
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"tensorgrid-ui": "lib/cli.mjs"
|
|
21
|
+
},
|
|
19
22
|
"files": [
|
|
20
|
-
"lib"
|
|
23
|
+
"lib",
|
|
24
|
+
"install.ps1",
|
|
25
|
+
"preset",
|
|
26
|
+
"profile",
|
|
27
|
+
"brand/tensorgrid.ico",
|
|
28
|
+
"tools/round-ico-corners.mjs",
|
|
29
|
+
"env/snapshot.json",
|
|
30
|
+
"LICENSE"
|
|
21
31
|
],
|
|
22
32
|
"peerDependencies": {
|
|
23
33
|
"@deepseek-ai/dsh-tools": "*"
|