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/index.js CHANGED
@@ -41,12 +41,12 @@ function getPositional() {
41
41
  return positional;
42
42
  }
43
43
  const config = loadConfig();
44
- // Текущий язык интерфейса/агента. Меняется командой /config lang <ru|en>.
44
+ // Current interface/agent language. Changed by the /config lang <ru|en> command.
45
45
  let currentLocale = isLocale(config.ui?.locale)
46
46
  ? config.ui.locale
47
47
  : 'ru';
48
- // Перевод: читает currentLocale в момент вызова, поэтому смена языка
49
- // действует сразу, без перезапуска (для уже напечатанных строк).
48
+ // Translation: reads currentLocale at call time, so a language change takes
49
+ // effect immediately, without a restart (for lines printed afterwards).
50
50
  const t = (key, params) => translate(currentLocale)(key, params);
51
51
  const headless = hasFlag('--headless') || config.headless;
52
52
  const debug = hasFlag('--debug') || config.debug;
@@ -56,21 +56,21 @@ const maxIter = Number(getArg('--max-iter', String(config.maxIterations))) ||
56
56
  const positional = getPositional();
57
57
  const task = getArg('--task', positional.join(' ').trim() || null);
58
58
  const chatIdArg = getArg('--chat', null);
59
- // При возобновлении существующего чата system-prompt по умолчанию НЕ
60
- // переотправляется (он уже есть в начале чата). Флаг --resend-prompt
61
- // заставляет дослать его заново — например, если промпт обновился.
59
+ // When resuming an existing chat, the system-prompt is by default NOT
60
+ // resent (it is already at the start of the chat). The --resend-prompt flag
61
+ // forces resending it — for example, if the prompt was updated.
62
62
  const resendPrompt = hasFlag('--resend-prompt');
63
- // По умолчанию при старте начинается НОВЫЙ чат (контекст не тянется).
64
- // --resume-last: вернуться в последнюю сессию для рабочей директории.
65
- // --new-chat: оставлен для совместимости — это и так поведение по умолчанию.
63
+ // By default a NEW chat is started on launch (context is not carried over).
64
+ // --resume-last: return to the last session for the working directory.
65
+ // --new-chat: kept for compatibility — this is the default behavior anyway.
66
66
  const newChatFlag = hasFlag('--new-chat');
67
67
  const resumeLastFlag = hasFlag('--resume-last');
68
68
  // ---------- session persistence ----------
69
- // Сессии (чаты DeepSeek) храним в ~/.zames/.sessions, чтобы они переживали
70
- // перезапуск и были доступны для явного восстановления (--resume-last,
71
- // --chat <id>, /resume <n>). Автоматически при старте они НЕ поднимаются.
72
- // Раньше здесь был один файл last-chat.json, который терялся при смене
73
- // проекта и не давал списка сессий для восстановления.
69
+ // We store sessions (DeepSeek chats) in ~/.zames/.sessions so they survive
70
+ // a restart and are available for explicit restore (--resume-last,
71
+ // --chat <id>, /resume <n>). They are NOT brought up automatically on launch.
72
+ // Previously there was a single last-chat.json file that got lost when the
73
+ // project changed and gave no list of sessions to restore.
74
74
  function saveLastChat(id, workdir = '', title = '') {
75
75
  if (!id)
76
76
  return;
@@ -81,13 +81,13 @@ function loadLastChat(workdir = '') {
81
81
  return s ? s.id : null;
82
82
  }
83
83
  // ---------- hot reload ----------
84
- // Держим ссылки на модули логики в объекте mod. Команда /reload перечитывает
85
- // их через динамический import с timestamp-query — Node кэширует ESM по URL,
86
- // поэтому такой import вернёт свежую версию модуля. Браузер, чат и текущее
87
- // состояние НЕ перезапускаются: обновляется только логика.
88
- // В dev-режиме tsx грузит .ts-исходники из src/, а в собранном dist — .js.
89
- // Динамический import с query ?t= идёт мимо переписывания расширений tsx,
90
- // поэтому расширение подбираем сами — по фактическому файлу текущего модуля.
84
+ // We keep references to the logic modules in the mod object. The /reload
85
+ // command re-reads them via a dynamic import with a timestamp query — Node
86
+ // caches ESM by URL, so such an import returns a fresh version of the module.
87
+ // The browser, chat and current state are NOT restarted: only the logic is updated.
88
+ // In dev mode tsx loads the .ts sources from src/, and in the built dist — .js.
89
+ // A dynamic import with the ?t= query bypasses tsx's extension rewriting,
90
+ // so we pick the extension ourselves — from the actual file of the current module.
91
91
  const SRC_EXT = /[.]ts$/.test(new URL(import.meta.url).pathname) ? '.ts' : '.js';
92
92
  const RELOADABLE = [
93
93
  'tools',
@@ -151,13 +151,13 @@ async function reloadModules() {
151
151
  mod.createSpinner = pick('spinner', 'createSpinner');
152
152
  return { count: loaded.size, errors };
153
153
  }
154
- // Первичная загрузка, чтобы mod.buildSystemPrompt и остальные были заполнены.
154
+ // Initial load so that mod.buildSystemPrompt and the rest are populated.
155
155
  await reloadModules();
156
- // Dev-режим: авто-перечитывание модулей логики перед каждой задачей.
157
- // Включается флагом --dev (его ставит `npm run dev`) или config.hotReload === true.
158
- // В обычном режиме (npm start, глобальный zames) авто-reload выключен.
156
+ // Dev mode: auto-reload of the logic modules before each task.
157
+ // Enabled by the --dev flag (`npm run dev` sets it) or config.hotReload === true.
158
+ // In normal mode (npm start, global zames) auto-reload is off.
159
159
  const devMode = hasFlag('--dev') || config.hotReload === true;
160
- // Безопасный авто-reload: при ошибке загрузки оставляем прошлые рабочие модули.
160
+ // Safe auto-reload: on a load error we keep the previous working modules.
161
161
  async function autoReload() {
162
162
  if (!devMode)
163
163
  return;
@@ -239,8 +239,8 @@ ${theme.bold(t('help.files'))}
239
239
  function dirLabel(p) {
240
240
  return path.basename(p) || p;
241
241
  }
242
- // Список slash-команд для автодополнения при вводе «/» (Tab — дополнить).
243
- // Описание локализуется по ключу help.cmd.* в момент отображения — см.
242
+ // List of slash commands for completion when you type «/» (Tab — complete).
243
+ // The description is localized by the help.cmd.* key at display time — see
244
244
  // buildSlashCommands().
245
245
  const SLASH_COMMANDS = [
246
246
  { name: '/help', key: 'help.cmd.help' },
@@ -269,15 +269,15 @@ const SLASH_COMMANDS = [
269
269
  { name: '/exit', key: 'help.cmd.exit' },
270
270
  { name: '/quit', key: 'help.cmd.exit' },
271
271
  ];
272
- // Описания slash-команд на текущем языке (для подсказок LineEditor).
272
+ // Slash-command descriptions in the current language (for LineEditor hints).
273
273
  function buildSlashCommands() {
274
274
  return SLASH_COMMANDS.map((c) => ({
275
275
  name: c.name,
276
276
  description: t(c.key),
277
277
  }));
278
278
  }
279
- // Временные файлы агента (одноразовые скрипты и т.п.) складываем в
280
- // <проект>/tmp — эта папка в .gitignore и очищается при каждом запуске.
279
+ // We put the agent's temporary files (one-off scripts, etc.) in
280
+ // <project>/tmp — this folder is in .gitignore and is cleaned on every launch.
281
281
  const TMP_DIR = path.join(__dirname, '..', 'tmp');
282
282
  async function cleanTmpDir() {
283
283
  try {
@@ -289,23 +289,22 @@ async function cleanTmpDir() {
289
289
  console.error('tmp: не удалось очистить:', e.message);
290
290
  }
291
291
  }
292
- // Ввод строки в терминале с корректной обработкой вставки (Shift+Insert,
293
- // Ctrl+Shift+V, правая кнопка мыши и т.п.).
292
+ // Terminal line input with correct paste handling (Shift+Insert,
293
+ // Ctrl+Shift+V, right mouse button, etc.).
294
294
  //
295
- // Зачем свой ридер вместо readline:
296
- // 1) readline отправляет строку на ПЕРВОМ переводе строки. При вставке
297
- // многострочного текста это приводило к немедленной отправке и к тому,
298
- // что в чат уходила только первая строка. Здесь переводы строк внутри
299
- // вставки заменяются на пробелы, а отправка происходит только по
300
- // одиночному нажатию Enter.
301
- // 2) Включаем bracketed paste mode (\x1b[?2004h): терминал оборачивает
302
- // вставленный текст в маркеры \x1b[200~ … \x1b[201~, поэтому мы точно
303
- // знаем, что это вставка, а не набор с клавиатуры, и Enter внутри неё
304
- // не считается отправкой.
295
+ // Why a custom reader instead of readline:
296
+ // 1) readline submits the line on the FIRST newline. When pasting
297
+ // multiline text this caused immediate submission and only the first
298
+ // line went to the chat. Here newlines inside a paste are replaced with
299
+ // spaces, and submission happens only on a single Enter press.
300
+ // 2) We enable bracketed paste mode (\x1b[?2004h): the terminal wraps the
301
+ // pasted text in the markers \x1b[200~ … \x1b[201~, so we know for sure
302
+ // that this is a paste, not keyboard typing, and Enter inside it is not
303
+ // treated as submission.
305
304
  async function promptOnce(question) {
306
305
  const stdin = process.stdin;
307
306
  const stdout = process.stdout;
308
- // Не-TTY (пайп, редирект): читаем всё до EOF одной строкой.
307
+ // Non-TTY (pipe, redirect): read everything up to EOF as a single string.
309
308
  if (!stdin.isTTY || !stdin.setRawMode) {
310
309
  const chunks = [];
311
310
  return await new Promise((resolve) => {
@@ -323,7 +322,7 @@ async function promptOnce(question) {
323
322
  const wasRaw = stdin.isRaw;
324
323
  stdin.setRawMode(true);
325
324
  stdin.resume();
326
- // Bracketed paste включаем/выключаем парно.
325
+ // We enable/disable bracketed paste in pairs.
327
326
  stdout.write('\x1b[?2004h');
328
327
  stdout.write(question);
329
328
  let line = '';
@@ -332,11 +331,11 @@ async function promptOnce(question) {
332
331
  const PASTE_START = '\x1b[200~';
333
332
  const PASTE_END = '\x1b[201~';
334
333
  const redraw = () => {
335
- // Возвращаемся в начало строки, стираем и печатаем заново.
334
+ // Return to the start of the line, erase and print again.
336
335
  stdout.write(String.fromCharCode(13));
337
336
  stdout.write('\x1b[K');
338
337
  stdout.write(question + line);
339
- // Ставим курсор в нужную позицию.
338
+ // Put the cursor in the right position.
340
339
  const back = line.length - cursor;
341
340
  if (back > 0)
342
341
  stdout.write('\x1b[' + back + 'D');
@@ -352,9 +351,9 @@ async function promptOnce(question) {
352
351
  resolve(value);
353
352
  };
354
353
  const insertText = (text) => {
355
- // Нормализуем переводы строк: они приходят от многострочной вставки,
356
- // но означают «отправить». Внутри сообщения заменяем на пробел, чтобы
357
- // вся вставка ушла ОДНИМ сообщением.
354
+ // Normalize newlines: they come from a multiline paste but mean
355
+ // "submit". Inside the message we replace them with a space so the
356
+ // whole paste goes as ONE message.
358
357
  const clean = text
359
358
  .replace(/\r\n/g, ' ')
360
359
  .replace(/\r/g, ' ')
@@ -364,11 +363,11 @@ async function promptOnce(question) {
364
363
  };
365
364
  const onData = (buf) => {
366
365
  let s = buf.toString('utf-8');
367
- // Fallback для терминалов без bracketed paste: если весь чанк — это
368
- // «голый» перевод строки (один байт), значит нажали Enter → отправляем.
369
- // Если переводы строк пришли ВМЕСТЕ с другим текстом в одном чанке —
370
- // это вставка; такие переводы строк не отправляют сообщение, а
371
- // заменяются на пробелы (см. insertText).
366
+ // Fallback for terminals without bracketed paste: if the whole chunk is
367
+ // a "bare" newline (one byte), then Enter was pressed → submit.
368
+ // If newlines came TOGETHER with other text in one chunk — that's a
369
+ // paste; such newlines don't submit the message but are replaced with
370
+ // spaces (see insertText).
372
371
  if (!inPaste && (s === '\r' || s === '\n')) {
373
372
  return finish(line, true);
374
373
  }
@@ -389,14 +388,14 @@ async function promptOnce(question) {
389
388
  }
390
389
  const start = s.indexOf(PASTE_START);
391
390
  if (start !== -1) {
392
- // Всё до маркера обрабатываем как обычный ввод.
391
+ // Everything before the marker is processed as regular input.
393
392
  const before = s.slice(0, start);
394
393
  s = s.slice(start + PASTE_START.length);
395
394
  inPaste = true;
396
395
  if (before) {
397
396
  for (const ch of before) {
398
397
  if (ch === '\r' || ch === '\n') {
399
- /* внутри вставки — пропускаем */
398
+ /* inside a paste — skip */
400
399
  }
401
400
  else
402
401
  insertText(ch);
@@ -409,22 +408,22 @@ async function promptOnce(question) {
409
408
  const code = s.charCodeAt(0);
410
409
  s = s.slice(1);
411
410
  if (ch === '\r' || ch === '\n') {
412
- // Перевод строки внутри чанка с другим текстом (вставка без
413
- // bracketed paste): не отправляем, а вставляем пробел.
411
+ // A newline inside a chunk with other text (paste without
412
+ // bracketed paste): don't submit, insert a space instead.
414
413
  insertText(' ');
415
414
  redraw();
416
415
  continue;
417
416
  }
418
417
  if (code === 3) {
419
- // Ctrl+C — прерываем ввод.
418
+ // Ctrl+C — abort input.
420
419
  return finish('', true);
421
420
  }
422
421
  if (code === 4) {
423
- // Ctrl+D — как отправка пустой строки.
422
+ // Ctrl+D — like submitting an empty line.
424
423
  return finish(line, true);
425
424
  }
426
425
  if (code === 21) {
427
- // Ctrl+U — стереть строку.
426
+ // Ctrl+U — erase the line.
428
427
  line = '';
429
428
  cursor = 0;
430
429
  redraw();
@@ -440,7 +439,7 @@ async function promptOnce(question) {
440
439
  continue;
441
440
  }
442
441
  if (ch === '\x1b') {
443
- // Escape-последовательности (стрелки, Home/End, Delete…).
442
+ // Escape sequences (arrows, Home/End, Delete…).
444
443
  const rest = s;
445
444
  if (rest.startsWith('[D')) {
446
445
  if (cursor > 0)
@@ -477,14 +476,14 @@ async function promptOnce(question) {
477
476
  s = s.slice(3);
478
477
  continue;
479
478
  }
480
- // Прочие ESC-последовательности пропускаем до буквы/тильды.
479
+ // Other ESC sequences are skipped up to a letter/tilde.
481
480
  const m = s.match(/^\[[0-9;]*[A-Za-z~]/);
482
481
  if (m)
483
482
  s = s.slice(m[0].length);
484
483
  continue;
485
484
  }
486
485
  if (code < 32)
487
- continue; // прочие управляющие символы игнорируем
486
+ continue; // other control characters are ignored
488
487
  insertText(ch);
489
488
  redraw();
490
489
  }
@@ -492,24 +491,24 @@ async function promptOnce(question) {
492
491
  stdin.on('data', onData);
493
492
  });
494
493
  }
495
- // Слежение за клавиатурой во время работы агента.
494
+ // Keyboard monitoring while the agent works.
496
495
  //
497
- // Терминал остаётся живым, пока агент думает:
498
- // - Esc (или Ctrl+C) — прервать текущую генерацию (клик Stop в браузере);
499
- // - набор текста + Enter — поставить сообщение в очередь; оно уйдёт
500
- // агенту сразу после того, как текущая задача завершится
501
- // (как «отправить во время генерации» в веб-версии DeepSeek).
496
+ // The terminal stays live while the agent thinks:
497
+ // - Esc (or Ctrl+C) — abort the current generation (a Stop click in the browser);
498
+ // - typing + Enter — queue a message; it goes to the agent right after
499
+ // the current task finishes (like "send during generation" in the
500
+ // DeepSeek web version).
502
501
  //
503
- // Ввод буферизуется без построчного редактора: набранный текст отображается
504
- // в строке спиннера через onChange -> ui.setPending(). Enter отправляет буфер
505
- // в onQueue, пустой Enter игнорируется. Поддержаны Backspace, Ctrl+U, Esc-
506
- // последовательности (стрелки/Home/End/Delete игнорируются) и bracketed paste.
502
+ // Input is buffered without a line editor: the typed text is shown in the
503
+ // spinner line via onChange -> ui.setPending(). Enter sends the buffer to
504
+ // onQueue, an empty Enter is ignored. Backspace, Ctrl+U, Esc sequences
505
+ // (arrows/Home/End/Delete are ignored) and bracketed paste are supported.
507
506
  function watchInput({ onEscape, onChange, onQueue, } = {}) {
508
507
  const stdin = process.stdin;
509
508
  if (!stdin.isTTY || !stdin.setRawMode)
510
509
  return () => { };
511
- // Управляющие символы собираем из кодов: в этом файле нельзя писать
512
- // «сырые» ESC/CR/LF в строковых литералах (см. AGENTS.md).
510
+ // Control characters are assembled from codes: this file must not contain
511
+ // "raw" ESC/CR/LF in string literals (see AGENTS.md).
513
512
  const ESC = String.fromCharCode(27);
514
513
  const CSI = String.fromCharCode(91);
515
514
  const CR = String.fromCharCode(13);
@@ -527,8 +526,8 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
527
526
  if (onChange)
528
527
  onChange(buf);
529
528
  };
530
- // Вставленный текст: переводы строк внутри многострочной вставки
531
- // трактуем как пробелы — сообщение уходит одной строкой.
529
+ // Pasted text: newlines inside a multiline paste are treated as spaces —
530
+ // the message goes as a single line.
532
531
  const insert = (text) => {
533
532
  buf += text
534
533
  .split(CR + LF)
@@ -540,8 +539,8 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
540
539
  };
541
540
  function onData(data) {
542
541
  let s = data.toString('utf-8');
543
- // Одиночный Esc — прервать генерацию. Стрелки приходят целым чанком
544
- // и сюда не попадают.
542
+ // A single Esc — abort generation. Arrows come as a whole chunk and
543
+ // don't reach here.
545
544
  if (!inPaste && s === ESC) {
546
545
  if (onEscape)
547
546
  onEscape();
@@ -585,13 +584,13 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
585
584
  continue;
586
585
  }
587
586
  if (code === 3) {
588
- // Ctrl+C — как Esc: прервать генерацию.
587
+ // Ctrl+C — like Esc: abort generation.
589
588
  if (onEscape)
590
589
  onEscape();
591
590
  continue;
592
591
  }
593
592
  if (code === 21) {
594
- // Ctrl+U — очистить набранное.
593
+ // Ctrl+U — clear what was typed.
595
594
  buf = '';
596
595
  emitChange();
597
596
  continue;
@@ -605,14 +604,14 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
605
604
  continue;
606
605
  }
607
606
  if (ch === ESC) {
608
- // Escape-последовательность (стрелки, Home/End, Delete) — пропускаем.
607
+ // Escape sequence (arrows, Home/End, Delete) — skip.
609
608
  const m = s.match(CSI_RE);
610
609
  if (m)
611
610
  s = s.slice(m[0].length);
612
611
  continue;
613
612
  }
614
613
  if (code < 32)
615
- continue; // прочие управляющие — игнорируем
614
+ continue; // other control characters — ignore
616
615
  buf += ch;
617
616
  emitChange();
618
617
  }
@@ -627,8 +626,8 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
627
626
  };
628
627
  }
629
628
  // ---------- workdir resolution ----------
630
- // Агент работает в директории, из которой его запустили (process.cwd()).
631
- // Это корень sandbox: инструменты не могут выходить выше него.
629
+ // The agent works in the directory it was launched from (process.cwd()).
630
+ // This is the sandbox root: tools cannot go above it.
632
631
  async function resolveWorkdir() {
633
632
  const explicitDir = getArg('--dir', null);
634
633
  const dir = explicitDir ? path.resolve(explicitDir) : process.cwd();
@@ -641,10 +640,10 @@ async function resolveWorkdir() {
641
640
  // ---------- task runner ----------
642
641
  async function runTask(browser, tools, taskText, workdir, opts) {
643
642
  const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, } = opts;
644
- // В TTY-режиме UI — это LineEditor: он владеет вводом (очередь, Esc,
645
- // Ctrl+C) и рисует статус НАД постоянной строкой ввода. В не-TTY режиме
646
- // (пайпы) — обычный спиннер + watchInput.
647
- // Новая задача с промпта — сбрасываем «стоп» от прошлого прерывания.
643
+ // In TTY mode the UI is a LineEditor: it owns the input (queue, Esc,
644
+ // Ctrl+C) and draws the status ABOVE the permanent input line. In non-TTY
645
+ // mode (pipes) — a regular spinner + watchInput.
646
+ // A new task from the prompt — we reset the "stop" from the previous abort.
648
647
  browser._stopped = false;
649
648
  browser._abort = false;
650
649
  const ui = editor || mod.createSpinner(currentLocale);
@@ -668,8 +667,8 @@ async function runTask(browser, tools, taskText, workdir, opts) {
668
667
  });
669
668
  try {
670
669
  let next = { task: taskText, freshChat, sendSystemPrompt };
671
- // Выполняем задачу, затем — всё, что пользователь успел напечатать за
672
- // время работы. Очередь может пополняться прямо во время дренажа.
670
+ // Execute the task, then everything the user managed to type while it
671
+ // ran. The queue may be replenished right during draining.
673
672
  while (true) {
674
673
  ui.thinking();
675
674
  await mod.runAgentLoop({
@@ -685,12 +684,13 @@ async function runTask(browser, tools, taskText, workdir, opts) {
685
684
  onToolCall: (name, toolArgs) => ui.toolCall(name, toolArgs),
686
685
  onToolResult: (result) => ui.toolResult(result),
687
686
  onAssistantMessage: (msg) => ui.assistant(msg),
687
+ onWarning: (msg) => ui.warning(msg),
688
688
  onChatReady,
689
689
  debugLog: debug,
690
690
  locale: currentLocale,
691
691
  });
692
- // Прервали (Esc/Ctrl+C) — не запускаем следующие задачи из очереди
693
- // и очищаем её, чтобы «стоп» действительно останавливал всё.
692
+ // Aborted (Esc/Ctrl+C) — we don't start the next tasks from the queue
693
+ // and clear it, so "stop" really stops everything.
694
694
  if (browser._stopped) {
695
695
  queue.length = 0;
696
696
  break;
@@ -740,7 +740,7 @@ async function main() {
740
740
  let sandboxRoot;
741
741
  try {
742
742
  currentWorkdir = await resolveWorkdir();
743
- // Корень sandbox: агент не может выходить выше директории запуска.
743
+ // Sandbox root: the agent cannot go above the launch directory.
744
744
  sandboxRoot = currentWorkdir;
745
745
  }
746
746
  catch (e) {
@@ -779,13 +779,13 @@ async function main() {
779
779
  transcript.close();
780
780
  process.exit(1);
781
781
  }
782
- // Разовый режим
782
+ // One-shot mode
783
783
  if (task) {
784
784
  const tools = mod.createTools(currentWorkdir, { undo });
785
785
  let freshChat = true;
786
786
  let sendSystemPrompt = true;
787
- // По умолчанию начинаем новый чат. Продолжить прошлую сессию —
788
- // явно: --chat <id> или --resume-last.
787
+ // By default we start a new chat. To continue a previous session —
788
+ // explicitly: --chat <id> or --resume-last.
789
789
  let resumeId = chatIdArg;
790
790
  if (!resumeId && resumeLastFlag && !newChatFlag) {
791
791
  const last = loadLastSession(currentWorkdir);
@@ -828,29 +828,30 @@ async function main() {
828
828
  let lastChats = [];
829
829
  let currentChatId = null;
830
830
  let running = true;
831
- // Сообщения, набранные пользователем, пока агент работал. runTask
832
- // забирает их по одному после завершения текущей задачи.
831
+ // Messages the user typed while the agent worked. runTask takes them one
832
+ // by one after the current task finishes.
833
833
  const pendingQueue = [];
834
834
  // ---------- review mode state ----------
835
- // null — обычный режим.
836
- // { snapDir, snapName, originalWorkdir } — мы внутри снапшота, чат уже
837
- // инициализирован review-промптом, юзер может просто писать «исправь...».
835
+ // null — normal mode.
836
+ // { snapDir, snapName, originalWorkdir } — we're inside a snapshot, the chat
837
+ // is already initialized with the review prompt, the user can just type "fix...".
838
838
  let reviewMode = null;
839
839
  process.on('SIGINT', async () => {
840
840
  running = false;
841
841
  await browser.close().catch(() => { });
842
- // Закрываем ленивый headless-браузер из web.js, иначе он останется
843
- // висеть отдельным процессом после выхода агента.
842
+ // Close the lazy headless browser from web.js, otherwise it would stay
843
+ // as a separate process after the agent exits.
844
844
  await mod.closeWeb().catch(() => { });
845
845
  transcript.close();
846
846
  console.log(theme.system(String.fromCharCode(10) + t('msg.bye')));
847
847
  process.exit(0);
848
848
  });
849
- // По умолчанию при старте начинаем НОВЫЙ чат: контекст прошлой сессии
850
- // не тянется автоматически. Продолжить прошлую сессию можно явно:
851
- // --chat <id> открыть конкретный чат
852
- // --resume-last вернуться в последний чат для этой рабочей директории
853
- // --new-chat оставлен для совместимости (это и так поведение по умолчанию)
849
+ // By default we start a NEW chat on launch: the context of a previous
850
+ // session is not carried over automatically. To continue a previous
851
+ // session explicitly:
852
+ // --chat <id> open a specific chat
853
+ // --resume-last return to the last chat for this working directory
854
+ // --new-chat kept for compatibility (this is the default behavior)
854
855
  let resumeId = chatIdArg;
855
856
  if (!resumeId && resumeLastFlag && !newChatFlag) {
856
857
  const last = loadLastSession(currentWorkdir);
@@ -873,11 +874,11 @@ async function main() {
873
874
  console.error(theme.error(t('msg.open_chat_error', { v: e.message })));
874
875
  }
875
876
  }
876
- // ---------- ввод: постоянная строка внизу + статус сверху ----------
877
- // В TTY используем LineEditor: он владеет вводом всё время, показывает
878
- // статус над строкой ввода и печатает ответы агента ВЫШЕ неё, поэтому
879
- // набранный текст никогда не затирается выводом. В не-TTY (пайп) —
880
- // старый promptOnce.
877
+ // ---------- input: permanent line below + status above ----------
878
+ // In TTY we use LineEditor: it owns the input all the time, shows the
879
+ // status above the input line and prints the agent's answers ABOVE it, so
880
+ // the typed text is never overwritten by output. In non-TTY (pipe) —
881
+ // the old promptOnce.
881
882
  let editor = null;
882
883
  let waiter = null;
883
884
  const takeInput = () => {
@@ -924,7 +925,7 @@ async function main() {
924
925
  browser.stopGeneration().catch(() => { });
925
926
  }
926
927
  else {
927
- // Не заняты — выходим. Будим takeInput(), чтобы цикл завершился.
928
+ // Not busy — exit. We wake takeInput() so the loop finishes.
928
929
  running = false;
929
930
  if (waiter) {
930
931
  const r = waiter;
@@ -934,9 +935,9 @@ async function main() {
934
935
  }
935
936
  };
936
937
  ed.start();
937
- // Весь вывод команд (console.log/error) должен идти ВЫШЕ строки ввода,
938
- // иначе он затирает набираемый текст. Пока редактор активен, заворачиваем
939
- // оба потока в ed.printAbove.
938
+ // All command output (console.log/error) must go ABOVE the input line,
939
+ // otherwise it overwrites the text being typed. While the editor is
940
+ // active, we wrap both streams in ed.printAbove.
940
941
  const origLog = console.log.bind(console);
941
942
  const origErr = console.error.bind(console);
942
943
  const fmt = (a) => typeof a === 'string'
@@ -951,21 +952,21 @@ async function main() {
951
952
  })();
952
953
  console.log = (...a) => ed.printAbove(a.map(fmt).join(' '));
953
954
  console.error = (...a) => ed.printAbove(a.map(fmt).join(' '));
954
- // Сохраняем на случай отладки.
955
+ // Save for debugging.
955
956
  const edAny = ed;
956
957
  edAny._origLog = origLog;
957
958
  edAny._origErr = origErr;
958
959
  }
959
960
  // ---------- /config ----------
960
- // Просмотр и правка настроек без перезапуска. Значения валидируются по
961
- // CONFIG_SCHEMA (src/config.ts) и пишутся в проектный .zamesrc.json.
962
- // Язык (ui.locale) применяется сразу: меняем currentLocale, пересобираем
963
- // подсказки редактора и обновляем config.ui.locale (его читает agent-loop
964
- // при следующей задаче).
961
+ // View and edit settings without a restart. Values are validated against
962
+ // CONFIG_SCHEMA (src/config.ts) and written to the project .zamesrc.json.
963
+ // The language (ui.locale) is applied immediately: we change currentLocale,
964
+ // rebuild the editor hints and update config.ui.locale (agent-loop reads it
965
+ // on the next task).
965
966
  //
966
- // Без аргументов в интерактивном терминале открывается меню (стрелки,
967
- // Enter — изменить, d — сбросить, q — выйти). Есть и текстовые подкоманды
968
- // (list/get/set/reset/lang/path) — для скриптов и не-TTY.
967
+ // With no arguments in an interactive terminal a menu opens (arrows,
968
+ // Enter — edit, d — reset, q — quit). There are also text subcommands
969
+ // (list/get/set/reset/lang/path) — for scripts and non-TTY.
969
970
  function setConfigRuntime(path, value) {
970
971
  const segs = path.split('.');
971
972
  let obj = config;
@@ -989,7 +990,7 @@ async function main() {
989
990
  }
990
991
  function configResetField(field) {
991
992
  resetConfigValue('project', field.path);
992
- // Возвращаем рантайм-значение к дефолту.
993
+ // Reset the runtime value to the default.
993
994
  const def = getByPath(DEFAULTS, field.path);
994
995
  setConfigRuntime(field.path, def);
995
996
  }
@@ -1028,9 +1029,9 @@ async function main() {
1028
1029
  configShowList();
1029
1030
  return;
1030
1031
  }
1031
- // Меню рисует напрямую в stdout и само читает клавиши. Чтобы его вывод
1032
- // не накладывался на постоянную строку ввода LineEditor, на время меню
1033
- // «ставим редактор на паузу», а после — возвращаем.
1032
+ // The menu draws directly to stdout and reads keys itself. So that its
1033
+ // output doesn't overlap the LineEditor's permanent input line, we "pause"
1034
+ // the editor for the duration of the menu and restore it afterwards.
1034
1035
  if (editor)
1035
1036
  editor.pause();
1036
1037
  try {
@@ -1053,7 +1054,7 @@ async function main() {
1053
1054
  async function handleConfigCommand(input) {
1054
1055
  const parts = input.trim().split(/\s+/);
1055
1056
  const sub = (parts[1] || '').toLowerCase();
1056
- // Без подкоманды — меню (или текстовый список в не-TTY).
1057
+ // With no subcommand — the menu (or a text list in non-TTY).
1057
1058
  if (!sub || sub === 'menu' || sub === 'ui') {
1058
1059
  await configOpenMenu();
1059
1060
  return;
@@ -1069,7 +1070,7 @@ async function main() {
1069
1070
  })));
1070
1071
  return;
1071
1072
  }
1072
- // /config lang <ru|en> — быстрый доступ к ui.locale
1073
+ // /config lang <ru|en> — quick access to ui.locale
1073
1074
  if (sub === 'lang' || sub === 'language' || sub === 'язык') {
1074
1075
  const val = parts[2];
1075
1076
  if (!val) {
@@ -1152,7 +1153,7 @@ async function main() {
1152
1153
  console.log(theme.assistant(t('cfg.reset', { v: key })));
1153
1154
  return;
1154
1155
  }
1155
- // Неизвестная подкоманда — показываем список.
1156
+ // Unknown subcommand — show the list.
1156
1157
  console.error(theme.error(t('cfg.unknown_key', { v: sub })));
1157
1158
  configShowList();
1158
1159
  }
@@ -1206,10 +1207,10 @@ async function main() {
1206
1207
  }
1207
1208
  continue;
1208
1209
  }
1209
- // ---------- Самообзор ----------
1210
+ // ---------- Self-review ----------
1210
1211
  if (lower === '/self-review' || lower.startsWith('/self-review ')) {
1211
1212
  const focus = trimmed.slice('/self-review'.length).trim();
1212
- // Запоминаем, куда вернуться
1213
+ // Remember where to return
1213
1214
  const originalWorkdir = reviewMode
1214
1215
  ? reviewMode.originalWorkdir
1215
1216
  : currentWorkdir;
@@ -1220,10 +1221,10 @@ async function main() {
1220
1221
  focus: focus || undefined,
1221
1222
  transcript,
1222
1223
  });
1223
- // Переходим в review-режим:
1224
- // - рабочая директория = снапшот
1225
- // - чат НЕ сбрасываем — внутри selfReview уже создан свежий чат
1226
- // и отправлен review-промпт, продолжим в нём
1224
+ // Switch to review mode:
1225
+ // - working directory = snapshot
1226
+ // - we do NOT reset the chat — inside selfReview a fresh chat was
1227
+ // already created and the review prompt sent, we continue in it
1227
1228
  reviewMode = {
1228
1229
  snapDir: result.snapDir,
1229
1230
  snapName: path.basename(result.snapDir),
@@ -1261,7 +1262,7 @@ async function main() {
1261
1262
  const originalWorkdir = reviewMode
1262
1263
  ? reviewMode.originalWorkdir
1263
1264
  : currentWorkdir;
1264
- // Свежий чат + review-промпт на этот снапшот
1265
+ // Fresh chat + review prompt for this snapshot
1265
1266
  try {
1266
1267
  const { runAgentLoop: ral } = await import('./agent-loop.js');
1267
1268
  const tools = mod.createTools(snapRoot, { undo: null });
@@ -1291,6 +1292,8 @@ async function main() {
1291
1292
  onAssistantMessage: (m) => {
1292
1293
  ui.assistant(m);
1293
1294
  },
1295
+ onWarning: (m) => ui.warning(m),
1296
+ locale: currentLocale,
1294
1297
  });
1295
1298
  }
1296
1299
  reviewMode = {
@@ -1318,7 +1321,7 @@ async function main() {
1318
1321
  const back = reviewMode.originalWorkdir;
1319
1322
  reviewMode = null;
1320
1323
  currentWorkdir = back;
1321
- // Раз чат занят review-контекстом, для обычной работы создадим новый
1324
+ // Since the chat is busy with the review context, we'll create a new one for regular work
1322
1325
  freshChatNext = true;
1323
1326
  sendSystemPromptNext = true;
1324
1327
  console.log(theme.system(t('self.done_hint', { v: back })));
@@ -1361,7 +1364,7 @@ async function main() {
1361
1364
  }
1362
1365
  continue;
1363
1366
  }
1364
- // ---------- Обычные команды ----------
1367
+ // ---------- Regular commands ----------
1365
1368
  if (lower === '/chats') {
1366
1369
  const spin = editor || mod.createSpinner(currentLocale);
1367
1370
  spin.thinking();
@@ -1616,8 +1619,8 @@ async function main() {
1616
1619
  console.error(theme.error(t('msg.unknown_cmd', { v: trimmed })));
1617
1620
  continue;
1618
1621
  }
1619
- // ---- Обычная задача (в том числе в review-режиме) ----
1620
- // Dev-режим: подхватываем свежие модули логики перед задачей.
1622
+ // ---- Regular task (including in review mode) ----
1623
+ // Dev mode: pick up fresh logic modules before the task.
1621
1624
  await autoReload();
1622
1625
  transcript.log('user_task', { task: trimmed, workdir: currentWorkdir });
1623
1626
  const tools = mod.createTools(currentWorkdir, { undo });
@@ -1631,9 +1634,9 @@ async function main() {
1631
1634
  queue: pendingQueue,
1632
1635
  ui: editor || null,
1633
1636
  onChatReady: (chatId) => {
1634
- // Сохраняем сессию сразу при начале диалога, не дожидаясь конца
1635
- // задачи. Иначе при долгой/прерванной задаче чат не попадал в
1636
- // ~/.zames/.sessions и терялся после перезапуска.
1637
+ // Save the session right at the start of the dialog, without waiting
1638
+ // for the task to finish. Otherwise a long/aborted task would not
1639
+ // get the chat into ~/.zames/.sessions and it would be lost after a restart.
1637
1640
  if (chatId) {
1638
1641
  currentChatId = chatId;
1639
1642
  saveLastChat(chatId, currentWorkdir);