zames_pro 2.6.0 → 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,34 +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 отправляет, но если курсор стоит сразу
12
- // после «\», Enter удаляет «\» и переносит строку (как в Claude Code).
13
- // Ctrl+J, Ctrl+Enter и Shift+Enter (в терминалах с расширенным
14
- // протоколом) всегда вставляют перевод строки;
15
- // * перенос длинных строк по словам (слово не рвётся посередине);
16
- // * перемещение по словам: Ctrl+←/→ (и Alt+B/Alt+F), удаление слова
17
- // Ctrl+W; Ctrl+K — удалить до конца строки;
18
- // * история введённых сообщений: ↑/↓ на краях ввода (внутри многострочного
19
- // ввода стрелки двигают по строкам, как в обычном терминале);
20
- // * подсказки slash-команд: при вводе «/» под строкой показываются
21
- // подходящие команды с описанием, 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.
22
22
  //
23
- // Все управляющие символы собираются из кодов, чтобы в файле не было
24
- // «сырых» 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).
25
25
  const NL = String.fromCharCode(10);
26
26
  const CR = String.fromCharCode(13);
27
27
  const ESC = String.fromCharCode(27);
28
28
  const PASTE_START = ESC + '[200~';
29
29
  const PASTE_END = ESC + '[201~';
30
- // Анимация точек: старт с пустой строки (0 точек), затем рост.
31
- // Ширину выравниваем по максимуму (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.
32
32
  const DOTS = ['', '.', '..', '...'];
33
33
  const DOTS_PAD = ' ';
34
34
  const HINT = theme.dim(' · Esc — стоп');
@@ -40,15 +40,15 @@ function safeJson(v) {
40
40
  return String(v);
41
41
  }
42
42
  }
43
- // Сколько визуальных строк занимает текст при данной ширине терминала
44
- // (с учётом переноса). Нужно, чтобы курсор/стирание блока не сбивались,
45
- // когда статус длиннее ширины терминала и переносится на 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.
46
46
  export function visRows(s, cols) {
47
47
  const width = Math.max(1, Number(cols) || 80);
48
48
  const len = visLen(s);
49
49
  return Math.max(1, Math.ceil(len / width));
50
50
  }
51
- // Видимая длина строки без ANSI-последовательностей.
51
+ // Visible length of a line without ANSI sequences.
52
52
  export function visLen(s) {
53
53
  s = String(s);
54
54
  let n = 0;
@@ -82,13 +82,13 @@ export function layoutInput(promptStr, buf, cursor, cols) {
82
82
  const prefix = first ? promptStr : pad;
83
83
  const avail = Math.max(1, width - promptW);
84
84
  const start = i;
85
- // Собираем очередную визуальную строку. Сначала набираем столько
86
- // символов, сколько влезает (count < avail). Если упёрлись в ширину
87
- // и следующий символ — не конец строки, откатываемся к последнему
88
- // пробелу, чтобы не разрывать слово посередине (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).
89
89
  let text = '';
90
90
  let count = 0;
91
- let lastSpace = -1; // индекс пробела в text (по Array.from)
91
+ let lastSpace = -1; // index of the space in text (by Array.from)
92
92
  while (i < chars.length && chars[i] !== NL && count < avail) {
93
93
  if (chars[i] === ' ')
94
94
  lastSpace = count;
@@ -98,7 +98,7 @@ export function layoutInput(promptStr, buf, cursor, cols) {
98
98
  }
99
99
  const atLineEnd = i >= chars.length || chars[i] === NL;
100
100
  if (!atLineEnd && count >= avail && lastSpace > 0) {
101
- // Переносим «хвост» строки на следующую визуальную строку.
101
+ // Move the "tail" of the line to the next visual line.
102
102
  const textChars = Array.from(text);
103
103
  const tailLen = textChars.length - lastSpace;
104
104
  i -= tailLen;
@@ -149,11 +149,11 @@ export class LineEditor {
149
149
  _thinkBase;
150
150
  _wasRaw;
151
151
  _onData;
152
- // История отправленных сообщений (для стрелок вверх/вниз).
152
+ // History of sent messages (for the up/down arrows).
153
153
  history;
154
154
  _histIndex;
155
155
  _histDraft;
156
- // Подсказки slash-команд (показываются при вводе «/»).
156
+ // Slash-command hints (shown when you type «/»).
157
157
  slashCommands;
158
158
  _suggestCount;
159
159
  constructor({ prompt = '> ', commands = [] } = {}) {
@@ -180,9 +180,9 @@ export class LineEditor {
180
180
  this.slashCommands = commands;
181
181
  this._suggestCount = 0;
182
182
  }
183
- // Список подсказок для текущего ввода. Показываем, только когда строка
184
- // начинается с «/» и это одно слово (без пробелов/переводов строк) — как
185
- // автодополнение команд в 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.
186
186
  _suggestions() {
187
187
  const b = this.buf;
188
188
  if (!b.startsWith('/'))
@@ -192,8 +192,8 @@ export class LineEditor {
192
192
  const q = b.toLowerCase();
193
193
  return this.slashCommands.filter((c) => c.name.toLowerCase().startsWith(q));
194
194
  }
195
- // Tab: дополнить команду до общего префикса; если совпадение одно —
196
- // подставить его целиком и добавить пробел.
195
+ // Tab: complete the command up to the common prefix; if there is a single
196
+ // match — insert it whole and add a space.
197
197
  _completeCommand() {
198
198
  const sugg = this._suggestions();
199
199
  if (!sugg.length)
@@ -236,9 +236,9 @@ export class LineEditor {
236
236
  if (stdin.setRawMode)
237
237
  stdin.setRawMode(this._wasRaw || false);
238
238
  }
239
- // Временно отдать терминал внешнему UI (например, меню /config):
240
- // снимаем свой обработчик ввода и стираем блок ввода, чтобы чужой вывод
241
- // не накладывался на строку ввода. Парно с resume().
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
242
  pause() {
243
243
  const stdin = process.stdin;
244
244
  this._stopDots();
@@ -263,7 +263,7 @@ export class LineEditor {
263
263
  this.promptStr = str;
264
264
  this._render();
265
265
  }
266
- // Обновить описания slash-команд (смена языка интерфейса).
266
+ // Update slash-command descriptions (interface language change).
267
267
  setCommands(commands) {
268
268
  this.slashCommands = commands;
269
269
  this._render();
@@ -273,7 +273,7 @@ export class LineEditor {
273
273
  this.cursor = 0;
274
274
  this._render();
275
275
  }
276
- // ---------- отрисовка ----------
276
+ // ---------- rendering ----------
277
277
  _eraseBlock() {
278
278
  if (!this.rendered)
279
279
  return;
@@ -289,14 +289,14 @@ export class LineEditor {
289
289
  let top = 0;
290
290
  if (this.statusText) {
291
291
  out += this.statusText + NL;
292
- // Статус может переноситься на несколько строк — учитываем это,
293
- // иначе стирание блока промахивается и статусы «стопкой» копятся.
292
+ // The status may wrap onto several lines — we account for this,
293
+ // otherwise the block erase misses and statuses pile up.
294
294
  top = visRows(this.statusText, cols);
295
295
  }
296
296
  const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols);
297
297
  out += lay.rows.map((r) => r.prefix + r.text).join(NL);
298
- // Подсказки slash-команд рисуем НИЖЕ строки ввода. Курсор потом
299
- // возвращаем вверх, на строку ввода, поэтому 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.
300
300
  const sugg = this._suggestions();
301
301
  const shown = sugg.slice(0, 8);
302
302
  this._suggestCount = shown.length;
@@ -314,8 +314,8 @@ export class LineEditor {
314
314
  }
315
315
  process.stdout.write(out);
316
316
  const lastRow = lay.rows.length - 1;
317
- // Курсор вверх: сначала на строку ввода внутри lay, затем ещё на
318
- // строки подсказок (если они есть) — курсор должен стоять на вводе.
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.
319
319
  const linesBelow = shown.length ? shown.length + (sugg.length > shown.length ? 1 : 0) : 0;
320
320
  const up = lastRow - lay.cursorRow + linesBelow;
321
321
  if (up > 0)
@@ -330,13 +330,13 @@ export class LineEditor {
330
330
  this._eraseBlock();
331
331
  this._writeBlock();
332
332
  }
333
- // Напечатать блок ВЫШЕ строки ввода и вернуть строку ввода на место.
333
+ // Print a block ABOVE the input line and put the input line back.
334
334
  printAbove(text) {
335
335
  this._eraseBlock();
336
336
  process.stdout.write(String(text) + NL);
337
337
  this._writeBlock();
338
338
  }
339
- // ---------- интерфейс, совместимый со спиннером ----------
339
+ // ---------- spinner-compatible interface ----------
340
340
  setStatus(text) {
341
341
  this.statusText = text || '';
342
342
  this._render();
@@ -348,8 +348,8 @@ export class LineEditor {
348
348
  }
349
349
  }
350
350
  _dots(n) {
351
- // Точки того же цвета, что и база, и фиксированной ширины — иначе
352
- // подсказка справа «прыгает» при смене фазы анимации.
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.
353
353
  return theme.brown(DOTS[n] + DOTS_PAD.slice(DOTS[n].length));
354
354
  }
355
355
  _startThinking() {
@@ -399,14 +399,19 @@ export class LineEditor {
399
399
  const rendered = renderMarkdown(msg);
400
400
  this.printAbove(NL + theme.assistant('● Ответ') + NL + rendered + NL + theme.dim('─'.repeat(60)));
401
401
  }
402
- // ---------- ввод ----------
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 ----------
403
408
  _submit() {
404
409
  const text = this.buf;
405
410
  if (!text.trim()) {
406
411
  this._render();
407
412
  return;
408
413
  }
409
- // В историю — только непустые и не дублирующие прошлое сообщение.
414
+ // Add to history only non-empty messages that don't duplicate the previous one.
410
415
  if (text.trim() && this.history[this.history.length - 1] !== text) {
411
416
  this.history.push(text);
412
417
  }
@@ -428,9 +433,9 @@ export class LineEditor {
428
433
  this.buf = next.join('');
429
434
  this.cursor += ins.length;
430
435
  }
431
- // Вставить перевод строки. Если курсор стоит сразу после символа «\»
432
- // (как в Claude Code), символ удаляется — чтобы «\» + Enter давали
433
- // обычный перенос без лишней обратной косой черты в тексте.
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.
434
439
  _insertNewline() {
435
440
  const chars = Array.from(this.buf);
436
441
  if (this.cursor > 0 && chars[this.cursor - 1] === '\\') {
@@ -477,9 +482,9 @@ export class LineEditor {
477
482
  i++;
478
483
  this.cursor = i;
479
484
  }
480
- // Курсор на первой/последней ВИЗУАЛЬНОЙ строке (с учётом переноса).
481
- // Нужно, чтобы Up/Down работали как история на краях ввода, а внутри —
482
- // как перемещение по строкам, как в обычном терминале.
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.
483
488
  _onFirstVisualLine() {
484
489
  const cols = process.stdout.columns || 80;
485
490
  const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols);
@@ -521,8 +526,8 @@ export class LineEditor {
521
526
  nextEnd++;
522
527
  this.cursor = nextStart + Math.min(col, nextEnd - nextStart);
523
528
  }
524
- // Перемещение на слово назад (Ctrl+Left / Alt+B): пропускаем пробелы
525
- // слева, затем идём до начала слова.
529
+ // Move one word back (Ctrl+Left / Alt+B): skip spaces
530
+ // on the left, then go to the start of the word.
526
531
  _wordLeft() {
527
532
  const chars = Array.from(this.buf);
528
533
  let i = this.cursor;
@@ -532,7 +537,7 @@ export class LineEditor {
532
537
  i--;
533
538
  this.cursor = i;
534
539
  }
535
- // Перемещение на слово вперёд (Ctrl+Right / Alt+F).
540
+ // Move one word forward (Ctrl+Right / Alt+F).
536
541
  _wordRight() {
537
542
  const chars = Array.from(this.buf);
538
543
  let i = this.cursor;
@@ -542,7 +547,7 @@ export class LineEditor {
542
547
  i++;
543
548
  this.cursor = i;
544
549
  }
545
- // Удаление слова слева (Ctrl+W / Alt+Backspace).
550
+ // Delete the word on the left (Ctrl+W / Alt+Backspace).
546
551
  _deleteWordLeft() {
547
552
  const chars = Array.from(this.buf);
548
553
  let i = this.cursor;
@@ -554,7 +559,7 @@ export class LineEditor {
554
559
  this.buf = chars.join('');
555
560
  this.cursor = i;
556
561
  }
557
- // ---------- история ----------
562
+ // ---------- history ----------
558
563
  _historyUp() {
559
564
  if (!this.history.length)
560
565
  return;
@@ -612,8 +617,8 @@ export class LineEditor {
612
617
  }
613
618
  continue;
614
619
  }
615
- // Shift+Enter, Ctrl+Enter в терминалах с расширенным протоколом
616
- // и Alt+Enter — вставка перевода строки.
620
+ // Shift+Enter, Ctrl+Enter in terminals with the extended protocol
621
+ // and Alt+Enter — insert a newline.
617
622
  if (s.startsWith(ESC + '[13;2u')) {
618
623
  this._insertNewline();
619
624
  s = s.slice(7);
@@ -647,10 +652,10 @@ export class LineEditor {
647
652
  const ch = s[0];
648
653
  const code = s.charCodeAt(0);
649
654
  s = s.slice(1);
650
- // Enter: если предыдущий символ — «\», Claude-Code-стиль: удаляем
651
- // «\» и переносим строку; иначе отправляем сообщение.
652
- // Ctrl+J (code 10) всегда вставляет перевод строки; Ctrl+Enter
653
- // (ESC[13;5u) и Shift+Enter тоже.
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.
654
659
  if (ch === CR) {
655
660
  const before = this.cursor > 0 ? Array.from(this.buf)[this.cursor - 1] : '';
656
661
  if (before === '\\') {
@@ -703,7 +708,7 @@ export class LineEditor {
703
708
  continue;
704
709
  }
705
710
  if (code === 11) {
706
- // Ctrl+K — удалить от курсора до конца строки.
711
+ // Ctrl+K — delete from the cursor to the end of the line.
707
712
  const arr = Array.from(this.buf);
708
713
  let e = this.cursor;
709
714
  while (e < arr.length && arr[e] !== NL)
@@ -720,7 +725,7 @@ export class LineEditor {
720
725
  }
721
726
  if (code === 27) {
722
727
  // Ctrl+Left / Ctrl+Right (xterm: ESC [1;5D / ESC [1;5C;
723
- // некоторые терминалы: ESC [5D / ESC [5C).
728
+ // some terminals: ESC [5D / ESC [5C).
724
729
  if (s.startsWith('[1;5D') || s.startsWith('[5D')) {
725
730
  this._wordLeft();
726
731
  s = s.slice(s.startsWith('[1;5D') ? 5 : 3);
@@ -746,7 +751,7 @@ export class LineEditor {
746
751
  continue;
747
752
  }
748
753
  if (s.startsWith('[A')) {
749
- // Вверх: на первой визуальной строке — история, иначе — строка выше.
754
+ // Up: on the first visual line — history, otherwise — the line above.
750
755
  if (this._onFirstVisualLine())
751
756
  this._historyUp();
752
757
  else
@@ -782,7 +787,7 @@ export class LineEditor {
782
787
  this._render();
783
788
  continue;
784
789
  }
785
- // Alt+B / Alt+F — перемещение по словам.
790
+ // Alt+B / Alt+F — word movement.
786
791
  if (s.startsWith('b') || s.startsWith('B')) {
787
792
  this._wordLeft();
788
793
  s = s.slice(1);
@@ -808,7 +813,7 @@ export class LineEditor {
808
813
  }
809
814
  }
810
815
  }
811
- // Проверка раскладки: node src/input.js --selftest
816
+ // Layout check: node src/input.js --selftest
812
817
  export function selftest() {
813
818
  const cases = [
814
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.`;
package/dist/sessions.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
- // Сессии (чаты DeepSeek) храним в отдельной папке, чтобы они не терялись
5
- // после перезапуска процесса. Каждая сессия — отдельный JSON-файл
6
- // <id>.json в ~/.zames/.sessions. last.json указывает на последнюю
7
- // открытую сессию для конкретной рабочей директории.
4
+ // We store sessions (DeepSeek chats) in a separate folder so they aren't lost
5
+ // after a process restart. Each session is a separate JSON file
6
+ // <id>.json in ~/.zames/.sessions. last.json points to the last
7
+ // opened session for a specific working directory.
8
8
  const SESSIONS_DIR = path.join(os.homedir(), '.zames', '.sessions');
9
9
  const INDEX_FILE = path.join(SESSIONS_DIR, 'last.json');
10
10
  function ensureDir() {
@@ -13,8 +13,8 @@ function ensureDir() {
13
13
  function safeName(id) {
14
14
  return String(id).replace(/[^a-zA-Z0-9_.-]/g, '_');
15
15
  }
16
- // Сохраняем/обновляем сессию. workdir нужен, чтобы при запуске из того же
17
- // проекта восстанавливать именно его последний чат.
16
+ // Save/update a session. workdir is needed so that when launched from the same
17
+ // project we restore exactly its last chat.
18
18
  export function saveSession({ id, title = '', workdir = '', }) {
19
19
  if (!id)
20
20
  return null;
@@ -63,16 +63,16 @@ function writeIndex({ id, workdir }) {
63
63
  }
64
64
  catch { }
65
65
  }
66
- // Последняя сессия для рабочей директории. Если для неё ничего нет —
67
- // возвращаем последнюю сессию вообще (полезно при запуске из нового места).
66
+ // The last session for the working directory. If there is none —
67
+ // we return the last session overall (useful when launched from a new place).
68
68
  export function loadLastSession(workdir = '') {
69
69
  try {
70
70
  const index = JSON.parse(fs.readFileSync(INDEX_FILE, 'utf-8'));
71
71
  const byWorkdir = index && index.byWorkdir ? index.byWorkdir : {};
72
72
  const candidates = [byWorkdir[workdir], index.last].filter((x) => Boolean(x));
73
73
  for (const id of candidates) {
74
- // Файл сессии мог быть удалён — индекс тогда устарел, и сессию
75
- // восстанавливать нельзя. Пробуем следующий кандидат.
74
+ // The session file may have been deleted — the index is then stale and
75
+ // the session cannot be restored. We try the next candidate.
76
76
  const s = readSession(id);
77
77
  if (s)
78
78
  return s;
@@ -94,7 +94,7 @@ export function readSession(id) {
94
94
  return null;
95
95
  }
96
96
  }
97
- // Список всех сессий, свежие — первыми.
97
+ // List all sessions, newest first.
98
98
  export function listSessions() {
99
99
  try {
100
100
  ensureDir();