zames_pro 2.5.1 → 2.7.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/dist/input.js CHANGED
@@ -1,32 +1,34 @@
1
1
  import { theme } from './theme.js';
2
2
  import { renderMarkdown } from './markdown.js';
3
3
  import { randomThinkingPhrase, stripEllipsis } from './spinner.js';
4
- // Постоянная строка ввода внизу терминала + область статуса/вывода над ней.
4
+ // A permanent input line at the bottom of the terminal + a status/output area above it.
5
5
  //
6
- // Зачем свой редактор:
7
- // * строка ввода видна ВСЕГДА и не затирается выводом агента;
8
- // * спиннер/статус и ответы печатаются ВЫШЕ строки ввода (printAbove);
9
- // * перерисовка учитывает перенос по ширине терминала, поэтому нет
10
- // дублирования строк при многострочном вводе;
11
- // * многострочный ввод: Enter отправляет, Ctrl+J (и Shift+Enter в
12
- // терминалах с расширенным протоколом) вставляют перевод строки;
13
- // * перенос длинных строк по словам (слово не рвётся посередине);
14
- // * перемещение по словам: Ctrl+←/→ (и Alt+B/Alt+F), удаление слова
15
- // Ctrl+W; Ctrl+K — удалить до конца строки;
16
- // * история введённых сообщений: ↑/↓ на краях ввода (внутри многострочного
17
- // ввода стрелки двигают по строкам, как в обычном терминале);
18
- // * подсказки slash-команд: при вводе «/» под строкой показываются
19
- // подходящие команды с описанием, Tab — дополнить.
6
+ // Why a custom editor:
7
+ // * the input line is ALWAYS visible and is not overwritten by the agent's output;
8
+ // * the spinner/status and answers are printed ABOVE the input line (printAbove);
9
+ // * redraw accounts for wrapping by terminal width, so there is no
10
+ // line duplication on multiline input;
11
+ // * multiline input: Enter sends, but if the cursor is right after a «\»,
12
+ // Enter removes the «\» and breaks the line (like in Claude Code).
13
+ // Ctrl+J, Ctrl+Enter and Shift+Enter (in terminals with the extended
14
+ // protocol) always insert a newline;
15
+ // * long lines wrap by words (a word is not split in the middle);
16
+ // * word movement: Ctrl+←/→ (and Alt+B/Alt+F), word deletion
17
+ // Ctrl+W; Ctrl+K — delete to end of line;
18
+ // * input history: ↑/↓ at the edges of the input (inside multiline
19
+ // input the arrows move by lines, like in a normal terminal);
20
+ // * slash-command hints: when you type «/», matching commands with
21
+ // descriptions are shown below the line, Tab completes.
20
22
  //
21
- // Все управляющие символы собираются из кодов, чтобы в файле не было
22
- // «сырых» ESC/CR/LF в строковых литералах (см. AGENTS.md).
23
+ // All control characters are assembled from codes so the file has no
24
+ // "raw" ESC/CR/LF in string literals (see AGENTS.md).
23
25
  const NL = String.fromCharCode(10);
24
26
  const CR = String.fromCharCode(13);
25
27
  const ESC = String.fromCharCode(27);
26
28
  const PASTE_START = ESC + '[200~';
27
29
  const PASTE_END = ESC + '[201~';
28
- // Анимация точек: старт с пустой строки (0 точек), затем рост.
29
- // Ширину выравниваем по максимуму (3), чтобы подсказка не смещалась.
30
+ // Dot animation: start from an empty string (0 dots), then grow.
31
+ // We align the width to the maximum (3) so the hint doesn't shift.
30
32
  const DOTS = ['', '.', '..', '...'];
31
33
  const DOTS_PAD = ' ';
32
34
  const HINT = theme.dim(' · Esc — стоп');
@@ -38,15 +40,15 @@ function safeJson(v) {
38
40
  return String(v);
39
41
  }
40
42
  }
41
- // Сколько визуальных строк занимает текст при данной ширине терминала
42
- // (с учётом переноса). Нужно, чтобы курсор/стирание блока не сбивались,
43
- // когда статус длиннее ширины терминала и переносится на 2+ строки.
43
+ // How many visual lines the text occupies at the given terminal width
44
+ // (accounting for wrapping). Needed so the cursor/block erase don't drift
45
+ // when the status is longer than the terminal width and wraps onto 2+ lines.
44
46
  export function visRows(s, cols) {
45
47
  const width = Math.max(1, Number(cols) || 80);
46
48
  const len = visLen(s);
47
49
  return Math.max(1, Math.ceil(len / width));
48
50
  }
49
- // Видимая длина строки без ANSI-последовательностей.
51
+ // Visible length of a line without ANSI sequences.
50
52
  export function visLen(s) {
51
53
  s = String(s);
52
54
  let n = 0;
@@ -80,13 +82,13 @@ export function layoutInput(promptStr, buf, cursor, cols) {
80
82
  const prefix = first ? promptStr : pad;
81
83
  const avail = Math.max(1, width - promptW);
82
84
  const start = i;
83
- // Собираем очередную визуальную строку. Сначала набираем столько
84
- // символов, сколько влезает (count < avail). Если упёрлись в ширину
85
- // и следующий символ — не конец строки, откатываемся к последнему
86
- // пробелу, чтобы не разрывать слово посередине (word-wrap).
85
+ // Build the next visual line. First we take as many
86
+ // characters as fit (count < avail). If we hit the width
87
+ // and the next character is not the end of the line, we roll back to the
88
+ // last space so we don't split a word in the middle (word-wrap).
87
89
  let text = '';
88
90
  let count = 0;
89
- let lastSpace = -1; // индекс пробела в text (по Array.from)
91
+ let lastSpace = -1; // index of the space in text (by Array.from)
90
92
  while (i < chars.length && chars[i] !== NL && count < avail) {
91
93
  if (chars[i] === ' ')
92
94
  lastSpace = count;
@@ -96,7 +98,7 @@ export function layoutInput(promptStr, buf, cursor, cols) {
96
98
  }
97
99
  const atLineEnd = i >= chars.length || chars[i] === NL;
98
100
  if (!atLineEnd && count >= avail && lastSpace > 0) {
99
- // Переносим «хвост» строки на следующую визуальную строку.
101
+ // Move the "tail" of the line to the next visual line.
100
102
  const textChars = Array.from(text);
101
103
  const tailLen = textChars.length - lastSpace;
102
104
  i -= tailLen;
@@ -147,11 +149,11 @@ export class LineEditor {
147
149
  _thinkBase;
148
150
  _wasRaw;
149
151
  _onData;
150
- // История отправленных сообщений (для стрелок вверх/вниз).
152
+ // History of sent messages (for the up/down arrows).
151
153
  history;
152
154
  _histIndex;
153
155
  _histDraft;
154
- // Подсказки slash-команд (показываются при вводе «/»).
156
+ // Slash-command hints (shown when you type «/»).
155
157
  slashCommands;
156
158
  _suggestCount;
157
159
  constructor({ prompt = '> ', commands = [] } = {}) {
@@ -178,9 +180,9 @@ export class LineEditor {
178
180
  this.slashCommands = commands;
179
181
  this._suggestCount = 0;
180
182
  }
181
- // Список подсказок для текущего ввода. Показываем, только когда строка
182
- // начинается с «/» и это одно слово (без пробелов/переводов строк) — как
183
- // автодополнение команд в Claude Code.
183
+ // List of hints for the current input. We show them only when the line
184
+ // starts with «/» and is a single word (no spaces/newlines) — like
185
+ // command completion in Claude Code.
184
186
  _suggestions() {
185
187
  const b = this.buf;
186
188
  if (!b.startsWith('/'))
@@ -190,8 +192,8 @@ export class LineEditor {
190
192
  const q = b.toLowerCase();
191
193
  return this.slashCommands.filter((c) => c.name.toLowerCase().startsWith(q));
192
194
  }
193
- // Tab: дополнить команду до общего префикса; если совпадение одно —
194
- // подставить его целиком и добавить пробел.
195
+ // Tab: complete the command up to the common prefix; if there is a single
196
+ // match — insert it whole and add a space.
195
197
  _completeCommand() {
196
198
  const sugg = this._suggestions();
197
199
  if (!sugg.length)
@@ -234,16 +236,44 @@ export class LineEditor {
234
236
  if (stdin.setRawMode)
235
237
  stdin.setRawMode(this._wasRaw || false);
236
238
  }
239
+ // Temporarily hand the terminal to an external UI (e.g. the /config menu):
240
+ // we detach our input handler and erase the input block so foreign output
241
+ // doesn't overlap the input line. Paired with resume().
242
+ pause() {
243
+ const stdin = process.stdin;
244
+ this._stopDots();
245
+ if (this.rendered)
246
+ this._eraseBlock();
247
+ stdin.removeListener('data', this._onData);
248
+ process.stdout.write(ESC + '[?2004l');
249
+ }
250
+ resume() {
251
+ const stdin = process.stdin;
252
+ if (!stdin.isTTY || !stdin.setRawMode)
253
+ return;
254
+ this._wasRaw = stdin.isRaw;
255
+ stdin.setRawMode(true);
256
+ stdin.resume();
257
+ process.stdout.write(ESC + '[?2004h');
258
+ stdin.on('data', this._onData);
259
+ this._writeBlock();
260
+ this._render();
261
+ }
237
262
  setPrompt(str) {
238
263
  this.promptStr = str;
239
264
  this._render();
240
265
  }
266
+ // Update slash-command descriptions (interface language change).
267
+ setCommands(commands) {
268
+ this.slashCommands = commands;
269
+ this._render();
270
+ }
241
271
  clear() {
242
272
  this.buf = '';
243
273
  this.cursor = 0;
244
274
  this._render();
245
275
  }
246
- // ---------- отрисовка ----------
276
+ // ---------- rendering ----------
247
277
  _eraseBlock() {
248
278
  if (!this.rendered)
249
279
  return;
@@ -259,14 +289,14 @@ export class LineEditor {
259
289
  let top = 0;
260
290
  if (this.statusText) {
261
291
  out += this.statusText + NL;
262
- // Статус может переноситься на несколько строк — учитываем это,
263
- // иначе стирание блока промахивается и статусы «стопкой» копятся.
292
+ // The status may wrap onto several lines — we account for this,
293
+ // otherwise the block erase misses and statuses pile up.
264
294
  top = visRows(this.statusText, cols);
265
295
  }
266
296
  const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols);
267
297
  out += lay.rows.map((r) => r.prefix + r.text).join(NL);
268
- // Подсказки slash-команд рисуем НИЖЕ строки ввода. Курсор потом
269
- // возвращаем вверх, на строку ввода, поэтому cursorRowFromTop не меняется.
298
+ // We draw the slash-command hints BELOW the input line. We then move the
299
+ // cursor back up to the input line, so cursorRowFromTop doesn't change.
270
300
  const sugg = this._suggestions();
271
301
  const shown = sugg.slice(0, 8);
272
302
  this._suggestCount = shown.length;
@@ -284,8 +314,8 @@ export class LineEditor {
284
314
  }
285
315
  process.stdout.write(out);
286
316
  const lastRow = lay.rows.length - 1;
287
- // Курсор вверх: сначала на строку ввода внутри lay, затем ещё на
288
- // строки подсказок (если они есть) — курсор должен стоять на вводе.
317
+ // Move the cursor up: first to the input line within lay, then further by
318
+ // the hint lines (if any) — the cursor must sit on the input.
289
319
  const linesBelow = shown.length ? shown.length + (sugg.length > shown.length ? 1 : 0) : 0;
290
320
  const up = lastRow - lay.cursorRow + linesBelow;
291
321
  if (up > 0)
@@ -300,13 +330,13 @@ export class LineEditor {
300
330
  this._eraseBlock();
301
331
  this._writeBlock();
302
332
  }
303
- // Напечатать блок ВЫШЕ строки ввода и вернуть строку ввода на место.
333
+ // Print a block ABOVE the input line and put the input line back.
304
334
  printAbove(text) {
305
335
  this._eraseBlock();
306
336
  process.stdout.write(String(text) + NL);
307
337
  this._writeBlock();
308
338
  }
309
- // ---------- интерфейс, совместимый со спиннером ----------
339
+ // ---------- spinner-compatible interface ----------
310
340
  setStatus(text) {
311
341
  this.statusText = text || '';
312
342
  this._render();
@@ -318,8 +348,8 @@ export class LineEditor {
318
348
  }
319
349
  }
320
350
  _dots(n) {
321
- // Точки того же цвета, что и база, и фиксированной ширины — иначе
322
- // подсказка справа «прыгает» при смене фазы анимации.
351
+ // The dots are the same color as the base and of fixed width — otherwise
352
+ // the hint on the right "jumps" when the animation phase changes.
323
353
  return theme.brown(DOTS[n] + DOTS_PAD.slice(DOTS[n].length));
324
354
  }
325
355
  _startThinking() {
@@ -369,14 +399,19 @@ export class LineEditor {
369
399
  const rendered = renderMarkdown(msg);
370
400
  this.printAbove(NL + theme.assistant('● Ответ') + NL + rendered + NL + theme.dim('─'.repeat(60)));
371
401
  }
372
- // ---------- ввод ----------
402
+ // Warning to the operator (e.g. the agent stopped suspiciously).
403
+ // Printed above the input line without overwriting it.
404
+ warning(msg) {
405
+ this.printAbove(NL + theme.warn('⚠ ' + msg));
406
+ }
407
+ // ---------- input ----------
373
408
  _submit() {
374
409
  const text = this.buf;
375
410
  if (!text.trim()) {
376
411
  this._render();
377
412
  return;
378
413
  }
379
- // В историю — только непустые и не дублирующие прошлое сообщение.
414
+ // Add to history only non-empty messages that don't duplicate the previous one.
380
415
  if (text.trim() && this.history[this.history.length - 1] !== text) {
381
416
  this.history.push(text);
382
417
  }
@@ -398,6 +433,18 @@ export class LineEditor {
398
433
  this.buf = next.join('');
399
434
  this.cursor += ins.length;
400
435
  }
436
+ // Insert a newline. If the cursor is right after a «\»
437
+ // (like in Claude Code), the character is removed — so «\» + Enter gives
438
+ // an ordinary break without a stray backslash in the text.
439
+ _insertNewline() {
440
+ const chars = Array.from(this.buf);
441
+ if (this.cursor > 0 && chars[this.cursor - 1] === '\\') {
442
+ chars.splice(this.cursor - 1, 1);
443
+ this.buf = chars.join('');
444
+ this.cursor--;
445
+ }
446
+ this._insert(NL);
447
+ }
401
448
  _backspace() {
402
449
  if (this.cursor <= 0)
403
450
  return;
@@ -435,9 +482,9 @@ export class LineEditor {
435
482
  i++;
436
483
  this.cursor = i;
437
484
  }
438
- // Курсор на первой/последней ВИЗУАЛЬНОЙ строке (с учётом переноса).
439
- // Нужно, чтобы Up/Down работали как история на краях ввода, а внутри —
440
- // как перемещение по строкам, как в обычном терминале.
485
+ // Is the cursor on the first/last VISUAL line (accounting for wrapping).
486
+ // Needed so Up/Down act as history at the edges of the input, and inside —
487
+ // as line movement, like in a normal terminal.
441
488
  _onFirstVisualLine() {
442
489
  const cols = process.stdout.columns || 80;
443
490
  const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols);
@@ -479,8 +526,8 @@ export class LineEditor {
479
526
  nextEnd++;
480
527
  this.cursor = nextStart + Math.min(col, nextEnd - nextStart);
481
528
  }
482
- // Перемещение на слово назад (Ctrl+Left / Alt+B): пропускаем пробелы
483
- // слева, затем идём до начала слова.
529
+ // Move one word back (Ctrl+Left / Alt+B): skip spaces
530
+ // on the left, then go to the start of the word.
484
531
  _wordLeft() {
485
532
  const chars = Array.from(this.buf);
486
533
  let i = this.cursor;
@@ -490,7 +537,7 @@ export class LineEditor {
490
537
  i--;
491
538
  this.cursor = i;
492
539
  }
493
- // Перемещение на слово вперёд (Ctrl+Right / Alt+F).
540
+ // Move one word forward (Ctrl+Right / Alt+F).
494
541
  _wordRight() {
495
542
  const chars = Array.from(this.buf);
496
543
  let i = this.cursor;
@@ -500,7 +547,7 @@ export class LineEditor {
500
547
  i++;
501
548
  this.cursor = i;
502
549
  }
503
- // Удаление слова слева (Ctrl+W / Alt+Backspace).
550
+ // Delete the word on the left (Ctrl+W / Alt+Backspace).
504
551
  _deleteWordLeft() {
505
552
  const chars = Array.from(this.buf);
506
553
  let i = this.cursor;
@@ -512,7 +559,7 @@ export class LineEditor {
512
559
  this.buf = chars.join('');
513
560
  this.cursor = i;
514
561
  }
515
- // ---------- история ----------
562
+ // ---------- history ----------
516
563
  _historyUp() {
517
564
  if (!this.history.length)
518
565
  return;
@@ -570,21 +617,34 @@ export class LineEditor {
570
617
  }
571
618
  continue;
572
619
  }
573
- // Shift+Enter в терминалах с расширенным протоколом и Alt+Enter.
620
+ // Shift+Enter, Ctrl+Enter in terminals with the extended protocol
621
+ // and Alt+Enter — insert a newline.
574
622
  if (s.startsWith(ESC + '[13;2u')) {
575
- this._insert(NL);
623
+ this._insertNewline();
624
+ s = s.slice(7);
625
+ this._render();
626
+ continue;
627
+ }
628
+ if (s.startsWith(ESC + '[13;5u')) {
629
+ this._insertNewline();
576
630
  s = s.slice(7);
577
631
  this._render();
578
632
  continue;
579
633
  }
580
634
  if (s.startsWith(ESC + '[27;2;13~')) {
581
- this._insert(NL);
635
+ this._insertNewline();
636
+ s = s.slice(10);
637
+ this._render();
638
+ continue;
639
+ }
640
+ if (s.startsWith(ESC + '[27;5;13~')) {
641
+ this._insertNewline();
582
642
  s = s.slice(10);
583
643
  this._render();
584
644
  continue;
585
645
  }
586
646
  if (s.startsWith(ESC + NL)) {
587
- this._insert(NL);
647
+ this._insertNewline();
588
648
  s = s.slice(2);
589
649
  this._render();
590
650
  continue;
@@ -592,12 +652,22 @@ export class LineEditor {
592
652
  const ch = s[0];
593
653
  const code = s.charCodeAt(0);
594
654
  s = s.slice(1);
655
+ // Enter: if the previous character is «\», Claude-Code style: remove
656
+ // the «\» and break the line; otherwise send the message.
657
+ // Ctrl+J (code 10) always inserts a newline; Ctrl+Enter
658
+ // (ESC[13;5u) and Shift+Enter too.
595
659
  if (ch === CR) {
660
+ const before = this.cursor > 0 ? Array.from(this.buf)[this.cursor - 1] : '';
661
+ if (before === '\\') {
662
+ this._insertNewline();
663
+ this._render();
664
+ continue;
665
+ }
596
666
  this._submit();
597
667
  continue;
598
668
  }
599
669
  if (code === 10) {
600
- this._insert(NL);
670
+ this._insertNewline();
601
671
  this._render();
602
672
  continue;
603
673
  }
@@ -638,7 +708,7 @@ export class LineEditor {
638
708
  continue;
639
709
  }
640
710
  if (code === 11) {
641
- // Ctrl+K — удалить от курсора до конца строки.
711
+ // Ctrl+K — delete from the cursor to the end of the line.
642
712
  const arr = Array.from(this.buf);
643
713
  let e = this.cursor;
644
714
  while (e < arr.length && arr[e] !== NL)
@@ -655,7 +725,7 @@ export class LineEditor {
655
725
  }
656
726
  if (code === 27) {
657
727
  // Ctrl+Left / Ctrl+Right (xterm: ESC [1;5D / ESC [1;5C;
658
- // некоторые терминалы: ESC [5D / ESC [5C).
728
+ // some terminals: ESC [5D / ESC [5C).
659
729
  if (s.startsWith('[1;5D') || s.startsWith('[5D')) {
660
730
  this._wordLeft();
661
731
  s = s.slice(s.startsWith('[1;5D') ? 5 : 3);
@@ -681,7 +751,7 @@ export class LineEditor {
681
751
  continue;
682
752
  }
683
753
  if (s.startsWith('[A')) {
684
- // Вверх: на первой визуальной строке — история, иначе — строка выше.
754
+ // Up: on the first visual line — history, otherwise — the line above.
685
755
  if (this._onFirstVisualLine())
686
756
  this._historyUp();
687
757
  else
@@ -717,7 +787,7 @@ export class LineEditor {
717
787
  this._render();
718
788
  continue;
719
789
  }
720
- // Alt+B / Alt+F — перемещение по словам.
790
+ // Alt+B / Alt+F — word movement.
721
791
  if (s.startsWith('b') || s.startsWith('B')) {
722
792
  this._wordLeft();
723
793
  s = s.slice(1);
@@ -743,7 +813,7 @@ export class LineEditor {
743
813
  }
744
814
  }
745
815
  }
746
- // Проверка раскладки: node src/input.js --selftest
816
+ // Layout check: node src/input.js --selftest
747
817
  export function selftest() {
748
818
  const cases = [
749
819
  { p: '> ', b: 'hello', c: 5, w: 80 },
package/dist/markdown.js CHANGED
@@ -14,7 +14,7 @@ const theme = {
14
14
  tableHeader: { color: '#9fb8d8', bold: true },
15
15
  tableCell: { color: '#d0d0d0' },
16
16
  };
17
- // Подсветка кода через cli-highlight (транзитивно уже есть у нас).
17
+ // Code highlighting via cli-highlight (already present transitively).
18
18
  function highlighter(code, lang) {
19
19
  try {
20
20
  return highlight(code, {
@@ -1,14 +1,14 @@
1
1
  import fs from 'fs/promises';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
- // Извлечение СЫРОГО текста ответа модели из сетевых данных DeepSeek.
4
+ // Extraction of the RAW model answer text from DeepSeek's network data.
5
5
  //
6
- // Зачем: чтение ответа идёт из ОТРЕНДЕРЕННОГО DOM, а рендер DeepSeek
7
- // искажает ответ — превращает доллар-формулы в LaTeX (символ доллара
8
- // теряется), нормализует переводы строк, делает автолинки. Из-за этого
9
- // tool-call с шаблонными строками и экранированными переводами строк в
10
- // аргументах доходил до инструментов искажённым. Здесь мы достаём исходный
11
- // текст из тела сетевого ответа.
6
+ // Why: the answer is read from the RENDERED DOM, and DeepSeek's rendering
7
+ // distorts the answer — it turns dollar formulas into LaTeX (the dollar sign
8
+ // is lost), normalizes newlines, and auto-links. Because of this a tool-call
9
+ // with template strings and escaped newlines in its arguments reached the
10
+ // tools distorted. Here we extract the original text from the network
11
+ // response body.
12
12
  //
13
13
  // Format DeepSeek (SSE, chat.deepseek.com/api/v0/chat/completion):
14
14
  // * startovy fragment otveta lezhit v data-chanke s uzlom v.response
@@ -36,11 +36,11 @@ function parseDataLines(body) {
36
36
  }
37
37
  return out;
38
38
  }
39
- // Собирает текст ответа из SSE-потока. Поддерживает два формата:
40
- // * OpenAI-совместимый: choices[].delta.content (reasoning_content
41
- // игнорируется — это размышления, а не ответ);
42
- // * DeepSeek chat.deepseek.com: стартовый v.response.fragments[] и
43
- // инкрементальные APPEND-чанки response/fragments/-1/content.
39
+ // Assembles the answer text from the SSE stream. Supports two formats:
40
+ // * OpenAI-compatible: choices[].delta.content (reasoning_content
41
+ // is ignored — that's reasoning, not the answer);
42
+ // * DeepSeek chat.deepseek.com: the initial v.response.fragments[] and
43
+ // incremental APPEND chunks response/fragments/-1/content.
44
44
  export function extractFromSse(body) {
45
45
  let out = '';
46
46
  for (const obj of parseDataLines(body)) {
@@ -65,8 +65,8 @@ export function extractFromSse(body) {
65
65
  }
66
66
  continue;
67
67
  }
68
- // Инкрементальный APPEND: p='response/fragments/-1/content' -> v=строка;
69
- // последующие чанки идут с одним полем v.
68
+ // Incremental APPEND: p='response/fragments/-1/content' -> v=string;
69
+ // subsequent chunks come with a single v field.
70
70
  if (o.p === 'response/fragments/-1/content' && typeof o.v === 'string') {
71
71
  out += o.v;
72
72
  continue;
@@ -87,9 +87,9 @@ export function extractFromJson(body) {
87
87
  }
88
88
  return pickAnswerText(obj);
89
89
  }
90
- // Достает финальный текст ответа из узла, НЕ смешивая его с reasoning.
91
- // Порядок: choices/messages -> delta/message -> content; reasoning_content
92
- // намеренно не берем — это размышления модели, а не ответ.
90
+ // Extracts the final answer text from a node, WITHOUT mixing it with reasoning.
91
+ // Order: choices/messages -> delta/message -> content; reasoning_content is
92
+ // deliberately not taken — that's the model's reasoning, not the answer.
93
93
  function pickAnswerText(node) {
94
94
  if (node == null)
95
95
  return '';
@@ -124,15 +124,15 @@ function pickAnswerText(node) {
124
124
  }
125
125
  return '';
126
126
  }
127
- // Универсальная попытка: сначала SSE, затем обычный JSON.
127
+ // A universal attempt: first SSE, then regular JSON.
128
128
  export function extractAnswer(body) {
129
129
  const sse = extractFromSse(body);
130
130
  if (sse)
131
131
  return sse;
132
132
  return extractFromJson(body);
133
133
  }
134
- // Сохраняет тело сетевого ответа DeepSeek на диск для разбора постфактум.
135
- // Файлы лежат в ~/.zames/net-log — po nim видно реальный формат ответа.
134
+ // Saves the DeepSeek network response body to disk for post-mortem analysis.
135
+ // The files live in ~/.zames/net-log — from them the real answer format is visible.
136
136
  export async function dumpNetBody(url, body) {
137
137
  try {
138
138
  const dir = path.join(os.homedir(), '.zames', 'net-log');
@@ -4,10 +4,10 @@ import { theme } from './theme.js';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { ZAMES_HOME } from './config.js';
6
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
- // Исходники для самообзора — это .ts файлы. После сборки (tsc → dist/)
8
- // __dirname указывает на dist, где лежат .js. Поэтому ищем каталог с
9
- // исходниками: либо рядом (запуск из src/ через tsx), либо в ../src
10
- // (запуск собранного dist/ из корня репозитория).
7
+ // The sources for self-review are .ts files. After the build (tsc → dist/)
8
+ // __dirname points to dist, where the .js files live. So we look for the
9
+ // source directory: either nearby (running from src/ via tsx), or in ../src
10
+ // (running the built dist/ from the repo root).
11
11
  function resolveSrcDir() {
12
12
  const candidates = [__dirname, path.join(__dirname, '..', 'src')];
13
13
  for (const dir of candidates) {
@@ -22,9 +22,9 @@ function resolveSrcDir() {
22
22
  }
23
23
  const SRC_DIR = resolveSrcDir();
24
24
  const SNAP_ROOT = path.join(ZAMES_HOME, 'snapshots');
25
- // Динамический импорт логики с timestamp — чтобы при /reload (или авто-reload)
26
- // self-review использовал СВЕЖИЕ tools/agent-loop, а не закэшированные при
27
- // первой загрузке. Иначе reload не доходил бы до зависимостей self-review.
25
+ // Dynamic import of the logic with a timestamp — so that on /reload (or
26
+ // auto-reload) self-review uses FRESH tools/agent-loop, not the ones cached on
27
+ // first load. Otherwise reload wouldn't reach self-review's dependencies.
28
28
  async function loadFresh() {
29
29
  const stamp = Date.now();
30
30
  const toolsUrl = new URL('./tools.js', import.meta.url);
@@ -66,7 +66,7 @@ export async function selfReview({ browser, focus, transcript }) {
66
66
  `Проверь, что src/ не пуст и ты запускаешь агента из корня проекта.\n`));
67
67
  throw new Error('SRC_DIR пуст — нечего ревьюить');
68
68
  }
69
- // Копируем package.json для контекста — чтобы агент видел зависимости
69
+ // Copy package.json for context — so the agent sees the dependencies
70
70
  try {
71
71
  const pkg = await fs.readFile(path.resolve(SRC_DIR, '..', 'package.json'), 'utf-8');
72
72
  await fs.writeFile(path.join(snapDir, 'package.json'), pkg, 'utf-8');
@@ -104,10 +104,10 @@ export async function selfReview({ browser, focus, transcript }) {
104
104
  console.log();
105
105
  },
106
106
  });
107
- // Сохраняем отчёт
107
+ // Save the report
108
108
  const reportPath = path.join(snapDir, '_report.md');
109
109
  await fs.writeFile(reportPath, finalMessage || '(пусто)', 'utf-8');
110
- // Считаем, что изменилось
110
+ // Compute what changed
111
111
  const changed = await diffFiles(SRC_DIR, snapDir);
112
112
  console.log(theme.system('─'.repeat(60)));
113
113
  console.log(theme.system(`Отчёт: ${reportPath}`));
@@ -159,7 +159,7 @@ export async function selfApply({ name }) {
159
159
  const snapStat = await fs.stat(snapDir).catch(() => null);
160
160
  if (!snapStat)
161
161
  throw new Error(`Снапшот не найден: ${snapDir}`);
162
- // Бэкап текущего src перед перезаписью
162
+ // Back up the current src before overwriting
163
163
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
164
164
  const backupDir = path.join(SNAP_ROOT, `backup-before-apply-${stamp}`);
165
165
  await copyDirJsFiles(SRC_DIR, backupDir);
@@ -264,16 +264,16 @@ ${focusLine}
264
264
 
265
265
  When done, respond with a markdown report as the message:
266
266
 
267
- ## Найдено
267
+ ## Found
268
268
  - (bullet list of issues you found, with file:line if possible)
269
269
 
270
- ## Исправлено
271
- - (bullet list: file — что именно изменил)
270
+ ## Fixed
271
+ - (bullet list: file — what exactly you changed)
272
272
 
273
- ## Не исправлено (осознанно)
273
+ ## Not fixed (deliberately)
274
274
  - (bullet list: what you left alone and why)
275
275
 
276
- ## Проверка
276
+ ## Verification
277
277
  - (bullet list: what you verified, e.g. \`node --check\` results)
278
278
 
279
279
  Be honest and specific. If the code is fine in some area, say so.`;