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/index.js CHANGED
@@ -8,7 +8,9 @@ import { createTools } from './tools.js';
8
8
  import { runAgentLoop } from './agent-loop.js';
9
9
  import { createSpinner } from './spinner.js';
10
10
  import { LineEditor } from './input.js';
11
- import { loadConfig, CONFIG_PATHS, ZAMES_HOME } from './config.js';
11
+ import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
12
+ import { runConfigMenu } from './config-menu.js';
13
+ import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n.js';
12
14
  import { Transcript } from './transcript.js';
13
15
  import { UndoStore } from './undo.js';
14
16
  import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
@@ -39,6 +41,13 @@ function getPositional() {
39
41
  return positional;
40
42
  }
41
43
  const config = loadConfig();
44
+ // Current interface/agent language. Changed by the /config lang <ru|en> command.
45
+ let currentLocale = isLocale(config.ui?.locale)
46
+ ? config.ui.locale
47
+ : 'ru';
48
+ // Translation: reads currentLocale at call time, so a language change takes
49
+ // effect immediately, without a restart (for lines printed afterwards).
50
+ const t = (key, params) => translate(currentLocale)(key, params);
42
51
  const headless = hasFlag('--headless') || config.headless;
43
52
  const debug = hasFlag('--debug') || config.debug;
44
53
  const calibrate = hasFlag('--calibrate');
@@ -47,21 +56,21 @@ const maxIter = Number(getArg('--max-iter', String(config.maxIterations))) ||
47
56
  const positional = getPositional();
48
57
  const task = getArg('--task', positional.join(' ').trim() || null);
49
58
  const chatIdArg = getArg('--chat', null);
50
- // При возобновлении существующего чата system-prompt по умолчанию НЕ
51
- // переотправляется (он уже есть в начале чата). Флаг --resend-prompt
52
- // заставляет дослать его заново — например, если промпт обновился.
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.
53
62
  const resendPrompt = hasFlag('--resend-prompt');
54
- // По умолчанию при старте начинается НОВЫЙ чат (контекст не тянется).
55
- // --resume-last: вернуться в последнюю сессию для рабочей директории.
56
- // --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.
57
66
  const newChatFlag = hasFlag('--new-chat');
58
67
  const resumeLastFlag = hasFlag('--resume-last');
59
68
  // ---------- session persistence ----------
60
- // Сессии (чаты DeepSeek) храним в ~/.zames/.sessions, чтобы они переживали
61
- // перезапуск и были доступны для явного восстановления (--resume-last,
62
- // --chat <id>, /resume <n>). Автоматически при старте они НЕ поднимаются.
63
- // Раньше здесь был один файл last-chat.json, который терялся при смене
64
- // проекта и не давал списка сессий для восстановления.
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.
65
74
  function saveLastChat(id, workdir = '', title = '') {
66
75
  if (!id)
67
76
  return;
@@ -72,13 +81,13 @@ function loadLastChat(workdir = '') {
72
81
  return s ? s.id : null;
73
82
  }
74
83
  // ---------- hot reload ----------
75
- // Держим ссылки на модули логики в объекте mod. Команда /reload перечитывает
76
- // их через динамический import с timestamp-query — Node кэширует ESM по URL,
77
- // поэтому такой import вернёт свежую версию модуля. Браузер, чат и текущее
78
- // состояние НЕ перезапускаются: обновляется только логика.
79
- // В dev-режиме tsx грузит .ts-исходники из src/, а в собранном dist — .js.
80
- // Динамический import с query ?t= идёт мимо переписывания расширений tsx,
81
- // поэтому расширение подбираем сами — по фактическому файлу текущего модуля.
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.
82
91
  const SRC_EXT = /[.]ts$/.test(new URL(import.meta.url).pathname) ? '.ts' : '.js';
83
92
  const RELOADABLE = [
84
93
  'tools',
@@ -142,19 +151,19 @@ async function reloadModules() {
142
151
  mod.createSpinner = pick('spinner', 'createSpinner');
143
152
  return { count: loaded.size, errors };
144
153
  }
145
- // Первичная загрузка, чтобы mod.buildSystemPrompt и остальные были заполнены.
154
+ // Initial load so that mod.buildSystemPrompt and the rest are populated.
146
155
  await reloadModules();
147
- // Dev-режим: авто-перечитывание модулей логики перед каждой задачей.
148
- // Включается флагом --dev (его ставит `npm run dev`) или config.hotReload === true.
149
- // В обычном режиме (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.
150
159
  const devMode = hasFlag('--dev') || config.hotReload === true;
151
- // Безопасный авто-reload: при ошибке загрузки оставляем прошлые рабочие модули.
160
+ // Safe auto-reload: on a load error we keep the previous working modules.
152
161
  async function autoReload() {
153
162
  if (!devMode)
154
163
  return;
155
164
  const { errors } = await reloadModules();
156
165
  if (errors.length) {
157
- console.error(theme.warn('⚠ авто-reload: часть модулей не загрузилась, работаю на прежней версии:'));
166
+ console.error(theme.warn(t('reload.auto_partial')));
158
167
  for (const e of errors)
159
168
  console.error(theme.warn(' ' + e));
160
169
  }
@@ -162,104 +171,113 @@ async function autoReload() {
162
171
  // ---------- helpers ----------
163
172
  function printHelp() {
164
173
  console.log(`
165
- ${theme.bold('zames')} — агент поверх chat.deepseek.com через Playwright
174
+ ${theme.bold('zames')} — ${t('app.tagline')}
166
175
 
167
- ${theme.bold('Опции CLI:')}
168
- --dir <path> рабочая директория агента
169
- --task <text> задача одной строкой
170
- --chat <id> продолжить существующий чат по id
171
- --resume-last вернуться в последний сохранённый чат
172
- --new-chat начать новый чат (поведение по умолчанию)
173
- --resend-prompt дослать system-prompt в существующий чат
174
- --max-iter <n> лимит итераций (по умолчанию ${config.maxIterations})
175
- --headless браузер без UI
176
- --debug подробный лог
177
- --calibrate режим калибровки селекторов
178
- --dev режим разработки: авто-перечитывание модулей
179
- --version, -v показать версию
180
- --help, -h эта справка
176
+ ${theme.bold(t('help.options'))}
177
+ --dir <path> ${t('help.opt.dir')}
178
+ --task <text> ${t('help.opt.task')}
179
+ --chat <id> ${t('help.opt.chat')}
180
+ --resume-last ${t('help.opt.resume_last')}
181
+ --new-chat ${t('help.opt.new_chat')}
182
+ --resend-prompt ${t('help.opt.resend_prompt')}
183
+ --max-iter <n> ${t('help.opt.max_iter', { n: config.maxIterations })}
184
+ --headless ${t('help.opt.headless')}
185
+ --debug ${t('help.opt.debug')}
186
+ --calibrate ${t('help.opt.calibrate')}
187
+ --dev ${t('help.opt.dev')}
188
+ --version, -v ${t('help.opt.version')}
189
+ --help, -h ${t('help.opt.help')}
181
190
 
182
- ${theme.bold('Пока агент работает:')}
183
- печать + Enter поставить сообщение в очередь (уйдёт после текущей задачи)
184
- ↑ / ↓ история введённых сообщений
185
- Ctrl+← / Ctrl+→ перемещение по словам
186
- / + Tab подсказка и автодополнение slash-команд
187
- Ctrl+J новая строка (Shift+Enter в терминалах с поддержкой)
188
- Esc, Ctrl+C прервать текущую генерацию
191
+ ${theme.bold(t('help.while_working'))}
192
+ ${t('help.key.queue')}
193
+ ${t('help.key.history')}
194
+ ${t('help.key.words')}
195
+ ${t('help.key.slash')}
196
+ ${t('help.key.newline')}
197
+ ${t('help.key.backslash')}
198
+ ${t('help.key.esc')}
189
199
 
190
- ${theme.bold('Обычные команды:')}
191
- /new, /clear новый чат (сброс контекста)
192
- /sessions список сохранённых сессий (~/.zames/.sessions)
193
- /resume-id <id> восстановить сессию по полному id
194
- /chats список последних чатов DeepSeek
195
- /resume <n> открыть чат №n из /chats
196
- /chat показать текущий chat id
197
- /cd <path> сменить рабочую директорию
198
- /pwd текущая директория
199
- /status состояние сессии
200
- /reload перечитать модули логики без перезапуска
201
- /undo откатить последнюю запись/правку
202
- /undo-list список того, что можно откатить
203
- /transcript путь к файлу транскрипта
204
- /config показать текущий конфиг
205
- /debug-dom сохранить HTML страницы (для отладки)
206
- /help, help справка
207
- /exit, /quit, exit выход
200
+ ${theme.bold(t('help.commands'))}
201
+ ${t('help.cmd.new')}
202
+ ${t('help.cmd.sessions')}
203
+ ${t('help.cmd.resume_id')}
204
+ ${t('help.cmd.chats')}
205
+ ${t('help.cmd.resume')}
206
+ ${t('help.cmd.chat')}
207
+ ${t('help.cmd.cd')}
208
+ ${t('help.cmd.pwd')}
209
+ ${t('help.cmd.status')}
210
+ ${t('help.cmd.reload')}
211
+ ${t('help.cmd.undo')}
212
+ ${t('help.cmd.undo_list')}
213
+ ${t('help.cmd.transcript')}
214
+ ${t('help.cmd.config')}
215
+ ${t('help.cmd.lang')}
216
+ ${t('help.cmd.debug_dom')}
217
+ ${t('help.cmd.help')}
218
+ ${t('help.cmd.exit')}
208
219
 
209
- ${theme.bold('Самообзор (отладка агента):')}
210
- /self-review [фокус] снять снапшот src/ и запустить ревью
211
- после этой команды ты остаёшься В СНАПШОТЕ
212
- и можешь писать «исправь ошибки» и т.п.
213
- /self-fix <name> [фокус] вернуться в существующий снапшот и продолжить
214
- /self-done выйти из режима ревью (вернуться в свою папку)
215
- /self-list список снапшотов
216
- /self-diff <name> различия между текущим src/ и снапшотом
217
- /self-apply <name> применить снапшот к src/ (с бэкапом)
220
+ ${theme.bold(t('help.self_review'))}
221
+ ${t('help.self.review')}
222
+ ${t('help.self.fix')}
223
+ ${t('help.self.done')}
224
+ ${t('help.self.list')}
225
+ ${t('help.self.diff')}
226
+ ${t('help.self.apply')}
218
227
 
219
- ${theme.bold('Файлы:')}
220
- Логи: ${config.transcript.dir}
221
- Undo: ~/.zames/undo
222
- Сессии: ${sessionsDir()}
223
- Профиль: ~/.zames/profile
224
- Снапшоты: ~/.zames/snapshots
225
- Временные: <проект>/tmp (в .gitignore, чистится при запуске)
226
- Конфиг: ${CONFIG_PATHS.HOME_CONFIG}
228
+ ${theme.bold(t('help.files'))}
229
+ ${t('help.files.logs')} ${config.transcript.dir}
230
+ ${t('help.files.undo')} ~/.zames/undo
231
+ ${t('help.files.sessions')} ${sessionsDir()}
232
+ ${t('help.files.profile')} ~/.zames/profile
233
+ ${t('help.files.snapshots')} ~/.zames/snapshots
234
+ ${t('help.files.tmp')} <project>/tmp (.gitignore, cleaned on start)
235
+ ${t('help.files.config')} ${CONFIG_PATHS.HOME_CONFIG}
227
236
  ${CONFIG_PATHS.PROJECT_CONFIG}
228
237
  `);
229
238
  }
230
239
  function dirLabel(p) {
231
240
  return path.basename(p) || p;
232
241
  }
233
- // Список slash-команд для автодополнения при вводе «/» (Tab — дополнить).
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
+ // buildSlashCommands().
234
245
  const SLASH_COMMANDS = [
235
- { name: '/help', description: 'справка по командам и опциям' },
236
- { name: '/new', description: 'новый чат (сброс контекста)' },
237
- { name: '/clear', description: 'то же, что /new' },
238
- { name: '/sessions', description: 'список сохранённых сессий' },
239
- { name: '/chats', description: 'последние чаты DeepSeek' },
240
- { name: '/resume', description: 'открыть чат №n из /chats' },
241
- { name: '/resume-id', description: 'восстановить сессию по полному id' },
242
- { name: '/chat', description: 'показать текущий chat id' },
243
- { name: '/cd', description: 'сменить рабочую директорию' },
244
- { name: '/pwd', description: 'текущая рабочая директория' },
245
- { name: '/status', description: 'состояние сессии' },
246
- { name: '/reload', description: 'перечитать модули логики без перезапуска' },
247
- { name: '/undo', description: 'откатить последнюю запись/правку' },
248
- { name: '/undo-list', description: 'список того, что можно откатить' },
249
- { name: '/transcript', description: 'путь к файлу транскрипта' },
250
- { name: '/config', description: 'показать текущий конфиг' },
251
- { name: '/debug-dom', description: 'сохранить HTML страницы (отладка)' },
252
- { name: '/self-review', description: 'снапшот src/ и запуск ревью' },
253
- { name: '/self-fix', description: 'продолжить в существующем снапшоте' },
254
- { name: '/self-done', description: 'выйти из режима ревью' },
255
- { name: '/self-list', description: 'список снапшотов' },
256
- { name: '/self-diff', description: 'различия текущего src/ и снапшота' },
257
- { name: '/self-apply', description: 'применить снапшот к src/ (с бэкапом)' },
258
- { name: '/exit', description: 'выход' },
259
- { name: '/quit', description: 'выход' },
246
+ { name: '/help', key: 'help.cmd.help' },
247
+ { name: '/new', key: 'help.cmd.new' },
248
+ { name: '/clear', key: 'help.cmd.new' },
249
+ { name: '/sessions', key: 'help.cmd.sessions' },
250
+ { name: '/chats', key: 'help.cmd.chats' },
251
+ { name: '/resume', key: 'help.cmd.resume' },
252
+ { name: '/resume-id', key: 'help.cmd.resume_id' },
253
+ { name: '/chat', key: 'help.cmd.chat' },
254
+ { name: '/cd', key: 'help.cmd.cd' },
255
+ { name: '/pwd', key: 'help.cmd.pwd' },
256
+ { name: '/status', key: 'help.cmd.status' },
257
+ { name: '/reload', key: 'help.cmd.reload' },
258
+ { name: '/undo', key: 'help.cmd.undo' },
259
+ { name: '/undo-list', key: 'help.cmd.undo_list' },
260
+ { name: '/transcript', key: 'help.cmd.transcript' },
261
+ { name: '/config', key: 'help.cmd.config' },
262
+ { name: '/debug-dom', key: 'help.cmd.debug_dom' },
263
+ { name: '/self-review', key: 'help.self.review' },
264
+ { name: '/self-fix', key: 'help.self.fix' },
265
+ { name: '/self-done', key: 'help.self.done' },
266
+ { name: '/self-list', key: 'help.self.list' },
267
+ { name: '/self-diff', key: 'help.self.diff' },
268
+ { name: '/self-apply', key: 'help.self.apply' },
269
+ { name: '/exit', key: 'help.cmd.exit' },
270
+ { name: '/quit', key: 'help.cmd.exit' },
260
271
  ];
261
- // Временные файлы агента (одноразовые скрипты и т.п.) складываем в
262
- // <проект>/tmp — эта папка в .gitignore и очищается при каждом запуске.
272
+ // Slash-command descriptions in the current language (for LineEditor hints).
273
+ function buildSlashCommands() {
274
+ return SLASH_COMMANDS.map((c) => ({
275
+ name: c.name,
276
+ description: t(c.key),
277
+ }));
278
+ }
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.
263
281
  const TMP_DIR = path.join(__dirname, '..', 'tmp');
264
282
  async function cleanTmpDir() {
265
283
  try {
@@ -271,23 +289,22 @@ async function cleanTmpDir() {
271
289
  console.error('tmp: не удалось очистить:', e.message);
272
290
  }
273
291
  }
274
- // Ввод строки в терминале с корректной обработкой вставки (Shift+Insert,
275
- // Ctrl+Shift+V, правая кнопка мыши и т.п.).
292
+ // Terminal line input with correct paste handling (Shift+Insert,
293
+ // Ctrl+Shift+V, right mouse button, etc.).
276
294
  //
277
- // Зачем свой ридер вместо readline:
278
- // 1) readline отправляет строку на ПЕРВОМ переводе строки. При вставке
279
- // многострочного текста это приводило к немедленной отправке и к тому,
280
- // что в чат уходила только первая строка. Здесь переводы строк внутри
281
- // вставки заменяются на пробелы, а отправка происходит только по
282
- // одиночному нажатию Enter.
283
- // 2) Включаем bracketed paste mode (\x1b[?2004h): терминал оборачивает
284
- // вставленный текст в маркеры \x1b[200~ … \x1b[201~, поэтому мы точно
285
- // знаем, что это вставка, а не набор с клавиатуры, и Enter внутри неё
286
- // не считается отправкой.
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.
287
304
  async function promptOnce(question) {
288
305
  const stdin = process.stdin;
289
306
  const stdout = process.stdout;
290
- // Не-TTY (пайп, редирект): читаем всё до EOF одной строкой.
307
+ // Non-TTY (pipe, redirect): read everything up to EOF as a single string.
291
308
  if (!stdin.isTTY || !stdin.setRawMode) {
292
309
  const chunks = [];
293
310
  return await new Promise((resolve) => {
@@ -305,7 +322,7 @@ async function promptOnce(question) {
305
322
  const wasRaw = stdin.isRaw;
306
323
  stdin.setRawMode(true);
307
324
  stdin.resume();
308
- // Bracketed paste включаем/выключаем парно.
325
+ // We enable/disable bracketed paste in pairs.
309
326
  stdout.write('\x1b[?2004h');
310
327
  stdout.write(question);
311
328
  let line = '';
@@ -314,11 +331,11 @@ async function promptOnce(question) {
314
331
  const PASTE_START = '\x1b[200~';
315
332
  const PASTE_END = '\x1b[201~';
316
333
  const redraw = () => {
317
- // Возвращаемся в начало строки, стираем и печатаем заново.
334
+ // Return to the start of the line, erase and print again.
318
335
  stdout.write(String.fromCharCode(13));
319
336
  stdout.write('\x1b[K');
320
337
  stdout.write(question + line);
321
- // Ставим курсор в нужную позицию.
338
+ // Put the cursor in the right position.
322
339
  const back = line.length - cursor;
323
340
  if (back > 0)
324
341
  stdout.write('\x1b[' + back + 'D');
@@ -334,9 +351,9 @@ async function promptOnce(question) {
334
351
  resolve(value);
335
352
  };
336
353
  const insertText = (text) => {
337
- // Нормализуем переводы строк: они приходят от многострочной вставки,
338
- // но означают «отправить». Внутри сообщения заменяем на пробел, чтобы
339
- // вся вставка ушла ОДНИМ сообщением.
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.
340
357
  const clean = text
341
358
  .replace(/\r\n/g, ' ')
342
359
  .replace(/\r/g, ' ')
@@ -346,11 +363,11 @@ async function promptOnce(question) {
346
363
  };
347
364
  const onData = (buf) => {
348
365
  let s = buf.toString('utf-8');
349
- // Fallback для терминалов без bracketed paste: если весь чанк — это
350
- // «голый» перевод строки (один байт), значит нажали Enter → отправляем.
351
- // Если переводы строк пришли ВМЕСТЕ с другим текстом в одном чанке —
352
- // это вставка; такие переводы строк не отправляют сообщение, а
353
- // заменяются на пробелы (см. 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).
354
371
  if (!inPaste && (s === '\r' || s === '\n')) {
355
372
  return finish(line, true);
356
373
  }
@@ -371,14 +388,14 @@ async function promptOnce(question) {
371
388
  }
372
389
  const start = s.indexOf(PASTE_START);
373
390
  if (start !== -1) {
374
- // Всё до маркера обрабатываем как обычный ввод.
391
+ // Everything before the marker is processed as regular input.
375
392
  const before = s.slice(0, start);
376
393
  s = s.slice(start + PASTE_START.length);
377
394
  inPaste = true;
378
395
  if (before) {
379
396
  for (const ch of before) {
380
397
  if (ch === '\r' || ch === '\n') {
381
- /* внутри вставки — пропускаем */
398
+ /* inside a paste — skip */
382
399
  }
383
400
  else
384
401
  insertText(ch);
@@ -391,22 +408,22 @@ async function promptOnce(question) {
391
408
  const code = s.charCodeAt(0);
392
409
  s = s.slice(1);
393
410
  if (ch === '\r' || ch === '\n') {
394
- // Перевод строки внутри чанка с другим текстом (вставка без
395
- // bracketed paste): не отправляем, а вставляем пробел.
411
+ // A newline inside a chunk with other text (paste without
412
+ // bracketed paste): don't submit, insert a space instead.
396
413
  insertText(' ');
397
414
  redraw();
398
415
  continue;
399
416
  }
400
417
  if (code === 3) {
401
- // Ctrl+C — прерываем ввод.
418
+ // Ctrl+C — abort input.
402
419
  return finish('', true);
403
420
  }
404
421
  if (code === 4) {
405
- // Ctrl+D — как отправка пустой строки.
422
+ // Ctrl+D — like submitting an empty line.
406
423
  return finish(line, true);
407
424
  }
408
425
  if (code === 21) {
409
- // Ctrl+U — стереть строку.
426
+ // Ctrl+U — erase the line.
410
427
  line = '';
411
428
  cursor = 0;
412
429
  redraw();
@@ -422,7 +439,7 @@ async function promptOnce(question) {
422
439
  continue;
423
440
  }
424
441
  if (ch === '\x1b') {
425
- // Escape-последовательности (стрелки, Home/End, Delete…).
442
+ // Escape sequences (arrows, Home/End, Delete…).
426
443
  const rest = s;
427
444
  if (rest.startsWith('[D')) {
428
445
  if (cursor > 0)
@@ -459,14 +476,14 @@ async function promptOnce(question) {
459
476
  s = s.slice(3);
460
477
  continue;
461
478
  }
462
- // Прочие ESC-последовательности пропускаем до буквы/тильды.
479
+ // Other ESC sequences are skipped up to a letter/tilde.
463
480
  const m = s.match(/^\[[0-9;]*[A-Za-z~]/);
464
481
  if (m)
465
482
  s = s.slice(m[0].length);
466
483
  continue;
467
484
  }
468
485
  if (code < 32)
469
- continue; // прочие управляющие символы игнорируем
486
+ continue; // other control characters are ignored
470
487
  insertText(ch);
471
488
  redraw();
472
489
  }
@@ -474,24 +491,24 @@ async function promptOnce(question) {
474
491
  stdin.on('data', onData);
475
492
  });
476
493
  }
477
- // Слежение за клавиатурой во время работы агента.
494
+ // Keyboard monitoring while the agent works.
478
495
  //
479
- // Терминал остаётся живым, пока агент думает:
480
- // - Esc (или Ctrl+C) — прервать текущую генерацию (клик Stop в браузере);
481
- // - набор текста + Enter — поставить сообщение в очередь; оно уйдёт
482
- // агенту сразу после того, как текущая задача завершится
483
- // (как «отправить во время генерации» в веб-версии 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).
484
501
  //
485
- // Ввод буферизуется без построчного редактора: набранный текст отображается
486
- // в строке спиннера через onChange -> ui.setPending(). Enter отправляет буфер
487
- // в onQueue, пустой Enter игнорируется. Поддержаны Backspace, Ctrl+U, Esc-
488
- // последовательности (стрелки/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.
489
506
  function watchInput({ onEscape, onChange, onQueue, } = {}) {
490
507
  const stdin = process.stdin;
491
508
  if (!stdin.isTTY || !stdin.setRawMode)
492
509
  return () => { };
493
- // Управляющие символы собираем из кодов: в этом файле нельзя писать
494
- // «сырые» 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).
495
512
  const ESC = String.fromCharCode(27);
496
513
  const CSI = String.fromCharCode(91);
497
514
  const CR = String.fromCharCode(13);
@@ -509,8 +526,8 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
509
526
  if (onChange)
510
527
  onChange(buf);
511
528
  };
512
- // Вставленный текст: переводы строк внутри многострочной вставки
513
- // трактуем как пробелы — сообщение уходит одной строкой.
529
+ // Pasted text: newlines inside a multiline paste are treated as spaces —
530
+ // the message goes as a single line.
514
531
  const insert = (text) => {
515
532
  buf += text
516
533
  .split(CR + LF)
@@ -522,8 +539,8 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
522
539
  };
523
540
  function onData(data) {
524
541
  let s = data.toString('utf-8');
525
- // Одиночный Esc — прервать генерацию. Стрелки приходят целым чанком
526
- // и сюда не попадают.
542
+ // A single Esc — abort generation. Arrows come as a whole chunk and
543
+ // don't reach here.
527
544
  if (!inPaste && s === ESC) {
528
545
  if (onEscape)
529
546
  onEscape();
@@ -567,13 +584,13 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
567
584
  continue;
568
585
  }
569
586
  if (code === 3) {
570
- // Ctrl+C — как Esc: прервать генерацию.
587
+ // Ctrl+C — like Esc: abort generation.
571
588
  if (onEscape)
572
589
  onEscape();
573
590
  continue;
574
591
  }
575
592
  if (code === 21) {
576
- // Ctrl+U — очистить набранное.
593
+ // Ctrl+U — clear what was typed.
577
594
  buf = '';
578
595
  emitChange();
579
596
  continue;
@@ -587,14 +604,14 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
587
604
  continue;
588
605
  }
589
606
  if (ch === ESC) {
590
- // Escape-последовательность (стрелки, Home/End, Delete) — пропускаем.
607
+ // Escape sequence (arrows, Home/End, Delete) — skip.
591
608
  const m = s.match(CSI_RE);
592
609
  if (m)
593
610
  s = s.slice(m[0].length);
594
611
  continue;
595
612
  }
596
613
  if (code < 32)
597
- continue; // прочие управляющие — игнорируем
614
+ continue; // other control characters — ignore
598
615
  buf += ch;
599
616
  emitChange();
600
617
  }
@@ -609,8 +626,8 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
609
626
  };
610
627
  }
611
628
  // ---------- workdir resolution ----------
612
- // Агент работает в директории, из которой его запустили (process.cwd()).
613
- // Это корень 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.
614
631
  async function resolveWorkdir() {
615
632
  const explicitDir = getArg('--dir', null);
616
633
  const dir = explicitDir ? path.resolve(explicitDir) : process.cwd();
@@ -623,19 +640,19 @@ async function resolveWorkdir() {
623
640
  // ---------- task runner ----------
624
641
  async function runTask(browser, tools, taskText, workdir, opts) {
625
642
  const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, } = opts;
626
- // В TTY-режиме UI — это LineEditor: он владеет вводом (очередь, Esc,
627
- // Ctrl+C) и рисует статус НАД постоянной строкой ввода. В не-TTY режиме
628
- // (пайпы) — обычный спиннер + watchInput.
629
- // Новая задача с промпта — сбрасываем «стоп» от прошлого прерывания.
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.
630
647
  browser._stopped = false;
631
648
  browser._abort = false;
632
- const ui = editor || mod.createSpinner();
649
+ const ui = editor || mod.createSpinner(currentLocale);
633
650
  const stopWatching = editor
634
651
  ? () => { }
635
652
  : watchInput({
636
653
  onEscape: () => {
637
654
  ui.stop();
638
- console.error(theme.warn('⏹ Esc — прерываю генерацию...'));
655
+ console.error(theme.warn(t('msg.abort_gen_short')));
639
656
  browser.stopGeneration().catch(() => { });
640
657
  },
641
658
  onChange: (text) => ui.setPending(text),
@@ -643,15 +660,15 @@ async function runTask(browser, tools, taskText, workdir, opts) {
643
660
  queue.push(text);
644
661
  ui.setPending(null);
645
662
  ui.stop();
646
- console.log(theme.user('📨 В очередь (' + queue.length + '): ') +
663
+ console.log(theme.user(t('msg.queued', { n: queue.length })) +
647
664
  theme.assistant(text));
648
665
  ui.thinking();
649
666
  },
650
667
  });
651
668
  try {
652
669
  let next = { task: taskText, freshChat, sendSystemPrompt };
653
- // Выполняем задачу, затем — всё, что пользователь успел напечатать за
654
- // время работы. Очередь может пополняться прямо во время дренажа.
670
+ // Execute the task, then everything the user managed to type while it
671
+ // ran. The queue may be replenished right during draining.
655
672
  while (true) {
656
673
  ui.thinking();
657
674
  await mod.runAgentLoop({
@@ -667,11 +684,13 @@ async function runTask(browser, tools, taskText, workdir, opts) {
667
684
  onToolCall: (name, toolArgs) => ui.toolCall(name, toolArgs),
668
685
  onToolResult: (result) => ui.toolResult(result),
669
686
  onAssistantMessage: (msg) => ui.assistant(msg),
687
+ onWarning: (msg) => ui.warning(msg),
670
688
  onChatReady,
671
689
  debugLog: debug,
690
+ locale: currentLocale,
672
691
  });
673
- // Прервали (Esc/Ctrl+C) — не запускаем следующие задачи из очереди
674
- // и очищаем её, чтобы «стоп» действительно останавливал всё.
692
+ // Aborted (Esc/Ctrl+C) — we don't start the next tasks from the queue
693
+ // and clear it, so "stop" really stops everything.
675
694
  if (browser._stopped) {
676
695
  queue.length = 0;
677
696
  break;
@@ -681,10 +700,10 @@ async function runTask(browser, tools, taskText, workdir, opts) {
681
700
  const queued = queue.shift() ?? '';
682
701
  ui.stop();
683
702
  if (editor) {
684
- editor.printAbove(theme.user('▶ Из очереди: ') + theme.assistant(queued));
703
+ editor.printAbove(theme.user(t('msg.from_queue')) + theme.assistant(queued));
685
704
  }
686
705
  else {
687
- console.log(theme.user('▶ Из очереди: ') + theme.assistant(queued));
706
+ console.log(theme.user(t('msg.from_queue')) + theme.assistant(queued));
688
707
  }
689
708
  transcript?.log('queued_task', { task: queued });
690
709
  next = { task: queued, freshChat: false, sendSystemPrompt: false };
@@ -692,7 +711,7 @@ async function runTask(browser, tools, taskText, workdir, opts) {
692
711
  }
693
712
  catch (e) {
694
713
  ui.stop();
695
- console.error(theme.error(String.fromCharCode(10) + '✖ Ошибка агента:'), e.message);
714
+ console.error(theme.error(String.fromCharCode(10) + t('msg.agent_error')), e.message);
696
715
  if (debug)
697
716
  console.error(e.stack);
698
717
  transcript?.log('agent_error', { error: e.message });
@@ -714,28 +733,28 @@ async function main() {
714
733
  return;
715
734
  }
716
735
  if (calibrate) {
717
- console.log(theme.warn('\n🔧 Режим калибровки селекторов\n'));
736
+ console.log(theme.warn(t('calibrate')));
718
737
  }
719
738
  await cleanTmpDir();
720
739
  let currentWorkdir;
721
740
  let sandboxRoot;
722
741
  try {
723
742
  currentWorkdir = await resolveWorkdir();
724
- // Корень sandbox: агент не может выходить выше директории запуска.
743
+ // Sandbox root: the agent cannot go above the launch directory.
725
744
  sandboxRoot = currentWorkdir;
726
745
  }
727
746
  catch (e) {
728
- console.error(theme.error('Не удалось определить рабочую директорию:'), e.message);
747
+ console.error(theme.error(t('msg.workdir_error')), e.message);
729
748
  process.exit(1);
730
749
  }
731
- console.log(theme.system(`Рабочая директория: ${currentWorkdir}`));
750
+ console.log(theme.system(t('msg.working_dir', { v: currentWorkdir })));
732
751
  const transcript = new Transcript({
733
752
  dir: config.transcript.dir,
734
753
  enabled: config.transcript.enabled,
735
754
  sessionName: dirLabel(currentWorkdir),
736
755
  });
737
756
  if (transcript.file) {
738
- console.log(theme.system(`Транскрипт: ${transcript.file}`));
757
+ console.log(theme.system(t('msg.transcript', { v: transcript.file })));
739
758
  }
740
759
  const undo = new UndoStore(config.undo);
741
760
  const browser = new DeepSeekBrowser({
@@ -744,7 +763,7 @@ async function main() {
744
763
  channel: config.browserChannel,
745
764
  ...config.browser,
746
765
  });
747
- const bootSpinner = mod.createSpinner();
766
+ const bootSpinner = mod.createSpinner(currentLocale);
748
767
  bootSpinner.thinking();
749
768
  try {
750
769
  await browser.launch();
@@ -753,38 +772,38 @@ async function main() {
753
772
  }
754
773
  catch (e) {
755
774
  bootSpinner.stop();
756
- console.error(theme.error('Не удалось запустить браузер:'), e.message);
775
+ console.error(theme.error(t('msg.browser_error')), e.message);
757
776
  if (debug)
758
777
  console.error(e.stack);
759
778
  await browser.close().catch(() => { });
760
779
  transcript.close();
761
780
  process.exit(1);
762
781
  }
763
- // Разовый режим
782
+ // One-shot mode
764
783
  if (task) {
765
784
  const tools = mod.createTools(currentWorkdir, { undo });
766
785
  let freshChat = true;
767
786
  let sendSystemPrompt = true;
768
- // По умолчанию начинаем новый чат. Продолжить прошлую сессию —
769
- // явно: --chat <id> или --resume-last.
787
+ // By default we start a new chat. To continue a previous session —
788
+ // explicitly: --chat <id> or --resume-last.
770
789
  let resumeId = chatIdArg;
771
790
  if (!resumeId && resumeLastFlag && !newChatFlag) {
772
791
  const last = loadLastSession(currentWorkdir);
773
792
  if (last && last.id) {
774
793
  resumeId = last.id;
775
- console.log(theme.system(`Восстанавливаю сессию ${resumeId}...`));
794
+ console.log(theme.system(t('msg.resuming', { id: resumeId })));
776
795
  }
777
796
  }
778
797
  if (resumeId) {
779
798
  try {
780
- console.log(theme.system(`Открываю чат ${resumeId}...`));
799
+ console.log(theme.system(t('msg.opening_chat', { id: resumeId })));
781
800
  await browser.openChat(resumeId);
782
801
  freshChat = false;
783
802
  sendSystemPrompt = resendPrompt;
784
803
  saveLastChat(resumeId, currentWorkdir);
785
804
  }
786
805
  catch (e) {
787
- console.error(theme.error(`Не удалось открыть чат: ${e.message}`));
806
+ console.error(theme.error(t('msg.open_chat_error', { v: e.message })));
788
807
  }
789
808
  }
790
809
  await autoReload();
@@ -802,36 +821,37 @@ async function main() {
802
821
  transcript.close();
803
822
  return;
804
823
  }
805
- console.log(theme.system('Интерактивный режим. Введите задачу. Команды — /help. Выход — /exit.'));
806
- console.log(theme.system('Пока агент работает, можно печатать следующее сообщение — оно уйдёт в очередь (Enter — отправить, Esc — прервать).'));
824
+ console.log(theme.system(t('msg.interactive')));
825
+ console.log(theme.system(t('msg.queue_hint')));
807
826
  let freshChatNext = true;
808
827
  let sendSystemPromptNext = true;
809
828
  let lastChats = [];
810
829
  let currentChatId = null;
811
830
  let running = true;
812
- // Сообщения, набранные пользователем, пока агент работал. runTask
813
- // забирает их по одному после завершения текущей задачи.
831
+ // Messages the user typed while the agent worked. runTask takes them one
832
+ // by one after the current task finishes.
814
833
  const pendingQueue = [];
815
834
  // ---------- review mode state ----------
816
- // null — обычный режим.
817
- // { snapDir, snapName, originalWorkdir } — мы внутри снапшота, чат уже
818
- // инициализирован 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...".
819
838
  let reviewMode = null;
820
839
  process.on('SIGINT', async () => {
821
840
  running = false;
822
841
  await browser.close().catch(() => { });
823
- // Закрываем ленивый headless-браузер из web.js, иначе он останется
824
- // висеть отдельным процессом после выхода агента.
842
+ // Close the lazy headless browser from web.js, otherwise it would stay
843
+ // as a separate process after the agent exits.
825
844
  await mod.closeWeb().catch(() => { });
826
845
  transcript.close();
827
- console.log(theme.system('\nВыход.'));
846
+ console.log(theme.system(String.fromCharCode(10) + t('msg.bye')));
828
847
  process.exit(0);
829
848
  });
830
- // По умолчанию при старте начинаем НОВЫЙ чат: контекст прошлой сессии
831
- // не тянется автоматически. Продолжить прошлую сессию можно явно:
832
- // --chat <id> открыть конкретный чат
833
- // --resume-last вернуться в последний чат для этой рабочей директории
834
- // --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)
835
855
  let resumeId = chatIdArg;
836
856
  if (!resumeId && resumeLastFlag && !newChatFlag) {
837
857
  const last = loadLastSession(currentWorkdir);
@@ -842,23 +862,23 @@ async function main() {
842
862
  }
843
863
  if (resumeId) {
844
864
  try {
845
- console.log(theme.system(`Открываю чат ${resumeId}...`));
865
+ console.log(theme.system(t('msg.opening_chat', { id: resumeId })));
846
866
  await browser.openChat(resumeId);
847
867
  currentChatId = resumeId;
848
868
  freshChatNext = false;
849
869
  sendSystemPromptNext = resendPrompt;
850
870
  saveLastChat(resumeId, currentWorkdir);
851
- console.log(theme.system(`Чат открыт: ${resumeId}\n`));
871
+ console.log(theme.system(t('msg.chat_opened') + ' ' + resumeId + String.fromCharCode(10)));
852
872
  }
853
873
  catch (e) {
854
- console.error(theme.error(`Не удалось открыть чат: ${e.message}`));
874
+ console.error(theme.error(t('msg.open_chat_error', { v: e.message })));
855
875
  }
856
876
  }
857
- // ---------- ввод: постоянная строка внизу + статус сверху ----------
858
- // В TTY используем LineEditor: он владеет вводом всё время, показывает
859
- // статус над строкой ввода и печатает ответы агента ВЫШЕ неё, поэтому
860
- // набранный текст никогда не затирается выводом. В не-TTY (пайп) —
861
- // старый 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.
862
882
  let editor = null;
863
883
  let waiter = null;
864
884
  const takeInput = () => {
@@ -872,7 +892,7 @@ async function main() {
872
892
  let tail;
873
893
  if (reviewMode) {
874
894
  tail =
875
- theme.warn('REVIEW') + theme.dim(':') + theme.dir(reviewMode.snapName);
895
+ theme.warn(t('prompt.review')) + theme.dim(':') + theme.dir(reviewMode.snapName);
876
896
  }
877
897
  else {
878
898
  tail = theme.dir(dirLabel(currentWorkdir));
@@ -882,7 +902,7 @@ async function main() {
882
902
  if (process.stdin.isTTY && process.stdout.isTTY) {
883
903
  const ed = new LineEditor({
884
904
  prompt: buildPrompt(),
885
- commands: SLASH_COMMANDS,
905
+ commands: buildSlashCommands(),
886
906
  });
887
907
  editor = ed;
888
908
  ed.onSubmit = (text) => {
@@ -895,17 +915,17 @@ async function main() {
895
915
  };
896
916
  ed.onEscape = () => {
897
917
  if (ed.busy) {
898
- ed.printAbove(theme.warn('⏹ Esc — прерываю генерацию...'));
918
+ ed.printAbove(theme.warn(t('msg.abort_gen_short')));
899
919
  browser.stopGeneration().catch(() => { });
900
920
  }
901
921
  };
902
922
  ed.onCtrlC = () => {
903
923
  if (ed.busy) {
904
- ed.printAbove(theme.warn('⏹ Ctrl+C — прерываю генерацию...'));
924
+ ed.printAbove(theme.warn(t('msg.abort_ctrlc_short')));
905
925
  browser.stopGeneration().catch(() => { });
906
926
  }
907
927
  else {
908
- // Не заняты — выходим. Будим takeInput(), чтобы цикл завершился.
928
+ // Not busy — exit. We wake takeInput() so the loop finishes.
909
929
  running = false;
910
930
  if (waiter) {
911
931
  const r = waiter;
@@ -915,9 +935,9 @@ async function main() {
915
935
  }
916
936
  };
917
937
  ed.start();
918
- // Весь вывод команд (console.log/error) должен идти ВЫШЕ строки ввода,
919
- // иначе он затирает набираемый текст. Пока редактор активен, заворачиваем
920
- // оба потока в 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.
921
941
  const origLog = console.log.bind(console);
922
942
  const origErr = console.error.bind(console);
923
943
  const fmt = (a) => typeof a === 'string'
@@ -932,11 +952,211 @@ async function main() {
932
952
  })();
933
953
  console.log = (...a) => ed.printAbove(a.map(fmt).join(' '));
934
954
  console.error = (...a) => ed.printAbove(a.map(fmt).join(' '));
935
- // Сохраняем на случай отладки.
955
+ // Save for debugging.
936
956
  const edAny = ed;
937
957
  edAny._origLog = origLog;
938
958
  edAny._origErr = origErr;
939
959
  }
960
+ // ---------- /config ----------
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).
966
+ //
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.
970
+ function setConfigRuntime(path, value) {
971
+ const segs = path.split('.');
972
+ let obj = config;
973
+ for (let i = 0; i < segs.length - 1; i++) {
974
+ obj = obj[segs[i]];
975
+ }
976
+ obj[segs[segs.length - 1]] = value;
977
+ if (path === 'ui.locale' && isLocale(value)) {
978
+ currentLocale = value;
979
+ config.ui.locale = value;
980
+ if (editor) {
981
+ editor.setCommands(buildSlashCommands());
982
+ editor.setPrompt(buildPrompt());
983
+ }
984
+ }
985
+ }
986
+ function configSetRaw(field, raw) {
987
+ const value = validateConfigValue(field, raw);
988
+ writeConfigValue('project', field.path, raw);
989
+ setConfigRuntime(field.path, value);
990
+ }
991
+ function configResetField(field) {
992
+ resetConfigValue('project', field.path);
993
+ // Reset the runtime value to the default.
994
+ const def = getByPath(DEFAULTS, field.path);
995
+ setConfigRuntime(field.path, def);
996
+ }
997
+ function configLabel(field) {
998
+ return t(field.labelKey);
999
+ }
1000
+ function configShowList() {
1001
+ console.log(theme.system(t('cfg.title')));
1002
+ let lastGroup = '';
1003
+ for (const f of CONFIG_SCHEMA) {
1004
+ const g = t(f.groupKey);
1005
+ if (g !== lastGroup) {
1006
+ console.log(theme.system('\n ' + g));
1007
+ lastGroup = g;
1008
+ }
1009
+ const cur = getByPath(config, f.path);
1010
+ const shown = cur === undefined
1011
+ ? t('cfg.menu.default')
1012
+ : typeof cur === 'boolean'
1013
+ ? cur
1014
+ ? t('common.on')
1015
+ : t('common.off')
1016
+ : JSON.stringify(cur);
1017
+ const extra = f.values ? ' [' + f.values.join('|') + ']' : '';
1018
+ console.log(' ' +
1019
+ theme.user(f.path) +
1020
+ theme.dim(' = ') +
1021
+ theme.assistant(shown) +
1022
+ theme.dim(extra) +
1023
+ theme.dim(' # ' + configLabel(f)));
1024
+ }
1025
+ console.log(theme.dim('\n' + t('cfg.usage')));
1026
+ }
1027
+ async function configOpenMenu() {
1028
+ if (!(process.stdin.isTTY && process.stdout.isTTY)) {
1029
+ configShowList();
1030
+ return;
1031
+ }
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.
1035
+ if (editor)
1036
+ editor.pause();
1037
+ try {
1038
+ await runConfigMenu({
1039
+ fields: CONFIG_SCHEMA,
1040
+ t,
1041
+ get: (path) => getByPath(config, path),
1042
+ set: (field, raw) => configSetRaw(field, raw),
1043
+ reset: (field) => configResetField(field),
1044
+ });
1045
+ }
1046
+ catch (e) {
1047
+ console.error(theme.error(String(e.message || e)));
1048
+ }
1049
+ finally {
1050
+ if (editor)
1051
+ editor.resume();
1052
+ }
1053
+ }
1054
+ async function handleConfigCommand(input) {
1055
+ const parts = input.trim().split(/\s+/);
1056
+ const sub = (parts[1] || '').toLowerCase();
1057
+ // With no subcommand — the menu (or a text list in non-TTY).
1058
+ if (!sub || sub === 'menu' || sub === 'ui') {
1059
+ await configOpenMenu();
1060
+ return;
1061
+ }
1062
+ if (sub === 'list' || sub === 'show' || sub === 'ls') {
1063
+ configShowList();
1064
+ return;
1065
+ }
1066
+ if (sub === 'path' || sub === 'paths') {
1067
+ console.log(theme.system(t('cfg.paths', {
1068
+ global: CONFIG_PATHS.HOME_CONFIG,
1069
+ project: CONFIG_PATHS.PROJECT_CONFIG,
1070
+ })));
1071
+ return;
1072
+ }
1073
+ // /config lang <ru|en> — quick access to ui.locale
1074
+ if (sub === 'lang' || sub === 'language' || sub === 'язык') {
1075
+ const val = parts[2];
1076
+ if (!val) {
1077
+ console.log(theme.system(t('status.locale', { v: localeDisplayName(currentLocale) })));
1078
+ console.log(theme.dim(t('cfg.lang_usage')));
1079
+ return;
1080
+ }
1081
+ const field = CONFIG_SCHEMA.find((f) => f.path === 'ui.locale');
1082
+ const loc = normalizeLocale(val);
1083
+ try {
1084
+ configSetRaw(field, loc);
1085
+ }
1086
+ catch (e) {
1087
+ console.error(theme.error(t('cfg.write_error', { v: e.message })));
1088
+ return;
1089
+ }
1090
+ console.log(theme.assistant(t('cfg.lang_set', { v: localeDisplayName(loc) })));
1091
+ return;
1092
+ }
1093
+ if (sub === 'get') {
1094
+ const key = parts[2];
1095
+ if (!key) {
1096
+ console.log(theme.dim(t('cfg.usage')));
1097
+ return;
1098
+ }
1099
+ const field = CONFIG_SCHEMA.find((f) => f.path === key);
1100
+ if (!field) {
1101
+ console.error(theme.error(t('cfg.unknown_key', { v: key })));
1102
+ return;
1103
+ }
1104
+ const cur = getByPath(config, key);
1105
+ console.log(theme.system(t('cfg.value', {
1106
+ v: key,
1107
+ value: cur === undefined ? t('cfg.menu.default') : JSON.stringify(cur),
1108
+ })));
1109
+ return;
1110
+ }
1111
+ if (sub === 'set') {
1112
+ const key = parts[2];
1113
+ const raw = parts.slice(3).join(' ');
1114
+ const field = CONFIG_SCHEMA.find((f) => f.path === key);
1115
+ if (!field) {
1116
+ console.error(theme.error(t('cfg.unknown_key', { v: key || '' })));
1117
+ return;
1118
+ }
1119
+ if (!raw) {
1120
+ console.log(theme.dim(t('cfg.usage')));
1121
+ return;
1122
+ }
1123
+ try {
1124
+ configSetRaw(field, raw);
1125
+ }
1126
+ catch {
1127
+ console.error(theme.error(t('cfg.bad_value', {
1128
+ v: key,
1129
+ type: field.values ? field.values.join('|') : field.type,
1130
+ })));
1131
+ return;
1132
+ }
1133
+ console.log(theme.assistant(t('cfg.saved', {
1134
+ v: key,
1135
+ value: JSON.stringify(getByPath(config, key)),
1136
+ })));
1137
+ return;
1138
+ }
1139
+ if (sub === 'reset' || sub === 'unset') {
1140
+ const key = parts[2];
1141
+ const field = CONFIG_SCHEMA.find((f) => f.path === key);
1142
+ if (!field) {
1143
+ console.error(theme.error(t('cfg.unknown_key', { v: key || '' })));
1144
+ return;
1145
+ }
1146
+ try {
1147
+ configResetField(field);
1148
+ }
1149
+ catch (e) {
1150
+ console.error(theme.error(t('cfg.write_error', { v: e.message })));
1151
+ return;
1152
+ }
1153
+ console.log(theme.assistant(t('cfg.reset', { v: key })));
1154
+ return;
1155
+ }
1156
+ // Unknown subcommand — show the list.
1157
+ console.error(theme.error(t('cfg.unknown_key', { v: sub })));
1158
+ configShowList();
1159
+ }
940
1160
  while (running) {
941
1161
  let input;
942
1162
  try {
@@ -948,7 +1168,7 @@ async function main() {
948
1168
  let tail;
949
1169
  if (reviewMode) {
950
1170
  tail =
951
- theme.warn('REVIEW') +
1171
+ theme.warn(t('prompt.review')) +
952
1172
  theme.dim(':') +
953
1173
  theme.dir(reviewMode.snapName);
954
1174
  }
@@ -972,7 +1192,7 @@ async function main() {
972
1192
  continue;
973
1193
  }
974
1194
  if (['/new', '/clear', 'new'].includes(lower)) {
975
- console.log(theme.system('Создаю новый чат...'));
1195
+ console.log(theme.system(t('msg.new_chat')));
976
1196
  try {
977
1197
  await browser.newChat();
978
1198
  freshChatNext = false;
@@ -980,17 +1200,17 @@ async function main() {
980
1200
  currentChatId = await browser.getCurrentChatId();
981
1201
  saveLastChat(currentChatId, currentWorkdir);
982
1202
  transcript.log('new_chat');
983
- console.log(theme.system('Новый чат.\n'));
1203
+ console.log(theme.system(t('msg.new_chat_ok') + String.fromCharCode(10)));
984
1204
  }
985
1205
  catch (e) {
986
- console.error(theme.error('Не удалось создать новый чат:'), e.message);
1206
+ console.error(theme.error(t('msg.new_chat_error', { v: e.message })), e.message);
987
1207
  }
988
1208
  continue;
989
1209
  }
990
- // ---------- Самообзор ----------
1210
+ // ---------- Self-review ----------
991
1211
  if (lower === '/self-review' || lower.startsWith('/self-review ')) {
992
1212
  const focus = trimmed.slice('/self-review'.length).trim();
993
- // Запоминаем, куда вернуться
1213
+ // Remember where to return
994
1214
  const originalWorkdir = reviewMode
995
1215
  ? reviewMode.originalWorkdir
996
1216
  : currentWorkdir;
@@ -1001,10 +1221,10 @@ async function main() {
1001
1221
  focus: focus || undefined,
1002
1222
  transcript,
1003
1223
  });
1004
- // Переходим в review-режим:
1005
- // - рабочая директория = снапшот
1006
- // - чат НЕ сбрасываем — внутри selfReview уже создан свежий чат
1007
- // и отправлен 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
1008
1228
  reviewMode = {
1009
1229
  snapDir: result.snapDir,
1010
1230
  snapName: path.basename(result.snapDir),
@@ -1015,16 +1235,10 @@ async function main() {
1015
1235
  sendSystemPromptNext = false;
1016
1236
  currentChatId = await browser.getCurrentChatId();
1017
1237
  saveLastChat(currentChatId, currentWorkdir);
1018
- console.log(theme.user('\n💡 Теперь ты в режиме ревью. Просто пиши агенту, например:\n' +
1019
- ' «исправь ошибки»\n' +
1020
- ' «доработай обработку ошибок в ask()»\n' +
1021
- ' «покажи, что не так с undo»\n' +
1022
- 'Выйти: /self-done. Применить: /self-apply ' +
1023
- reviewMode.snapName +
1024
- '\n'));
1238
+ console.log(theme.user(t('self.review_hint', { name: reviewMode.snapName })));
1025
1239
  }
1026
1240
  catch (e) {
1027
- console.error(theme.error('Самообзор провалился:'), e.message);
1241
+ console.error(theme.error(t('self.review_failed')), e.message);
1028
1242
  if (debug)
1029
1243
  console.error(e.stack);
1030
1244
  }
@@ -1033,7 +1247,7 @@ async function main() {
1033
1247
  if (lower === '/self-fix' || lower.startsWith('/self-fix ')) {
1034
1248
  const rest = trimmed.slice('/self-fix'.length).trim();
1035
1249
  if (!rest) {
1036
- console.error(theme.error('Использование: /self-fix <name> [фокус]'));
1250
+ console.error(theme.error(t('self.fix_usage')));
1037
1251
  continue;
1038
1252
  }
1039
1253
  const sp = rest.indexOf(' ');
@@ -1042,26 +1256,26 @@ async function main() {
1042
1256
  const snapRoot = path.join(ZAMES_HOME, 'snapshots', name);
1043
1257
  const stat = await fs.stat(snapRoot).catch(() => null);
1044
1258
  if (!stat || !stat.isDirectory()) {
1045
- console.error(theme.error(`Снапшот не найден: ${snapRoot}`));
1259
+ console.error(theme.error(t('self.snapshot_not_found', { v: snapRoot })));
1046
1260
  continue;
1047
1261
  }
1048
1262
  const originalWorkdir = reviewMode
1049
1263
  ? reviewMode.originalWorkdir
1050
1264
  : currentWorkdir;
1051
- // Свежий чат + review-промпт на этот снапшот
1265
+ // Fresh chat + review prompt for this snapshot
1052
1266
  try {
1053
1267
  const { runAgentLoop: ral } = await import('./agent-loop.js');
1054
- const { buildSystemPrompt } = await import('./system-prompt.js');
1055
1268
  const tools = mod.createTools(snapRoot, { undo: null });
1056
1269
  await browser.newChat();
1057
1270
  const sysPrompt = mod.buildSystemPrompt({
1058
1271
  workdir: snapRoot,
1059
1272
  tools,
1273
+ locale: currentLocale,
1060
1274
  });
1061
- console.log(theme.system('Инициализирую review-чат для снапшота...'));
1275
+ console.log(theme.system(t('self.init_review_chat')));
1062
1276
  await browser.ask(sysPrompt, { timeout: 60_000 });
1063
1277
  if (focus) {
1064
- const ui = mod.createSpinner();
1278
+ const ui = mod.createSpinner(currentLocale);
1065
1279
  ui.thinking();
1066
1280
  await ral({
1067
1281
  browser,
@@ -1078,6 +1292,8 @@ async function main() {
1078
1292
  onAssistantMessage: (m) => {
1079
1293
  ui.assistant(m);
1080
1294
  },
1295
+ onWarning: (m) => ui.warning(m),
1296
+ locale: currentLocale,
1081
1297
  });
1082
1298
  }
1083
1299
  reviewMode = {
@@ -1090,25 +1306,25 @@ async function main() {
1090
1306
  sendSystemPromptNext = false;
1091
1307
  currentChatId = await browser.getCurrentChatId();
1092
1308
  saveLastChat(currentChatId, currentWorkdir);
1093
- console.log(theme.user(`\n💡 Режим ревью по снапшоту ${name}. Пиши агенту задачу или /self-done.\n`));
1309
+ console.log(theme.user(t('self.fix_hint', { name })));
1094
1310
  }
1095
1311
  catch (e) {
1096
- console.error(theme.error('Не удалось войти в снапшот:'), e.message);
1312
+ console.error(theme.error(t('self.enter_failed')), e.message);
1097
1313
  }
1098
1314
  continue;
1099
1315
  }
1100
1316
  if (lower === '/self-done') {
1101
1317
  if (!reviewMode) {
1102
- console.log(theme.system('Ты и так не в режиме ревью.'));
1318
+ console.log(theme.system(t('self.not_in_review')));
1103
1319
  continue;
1104
1320
  }
1105
1321
  const back = reviewMode.originalWorkdir;
1106
1322
  reviewMode = null;
1107
1323
  currentWorkdir = back;
1108
- // Раз чат занят review-контекстом, для обычной работы создадим новый
1324
+ // Since the chat is busy with the review context, we'll create a new one for regular work
1109
1325
  freshChatNext = true;
1110
1326
  sendSystemPromptNext = true;
1111
- console.log(theme.system(`Вернулся в ${back}. Следующая задача начнёт новый чат.\n`));
1327
+ console.log(theme.system(t('self.done_hint', { v: back })));
1112
1328
  continue;
1113
1329
  }
1114
1330
  if (lower === '/self-list') {
@@ -1123,7 +1339,7 @@ async function main() {
1123
1339
  if (lower === '/self-diff' || lower.startsWith('/self-diff ')) {
1124
1340
  const name = trimmed.slice('/self-diff'.length).trim();
1125
1341
  if (!name) {
1126
- console.error(theme.error('Использование: /self-diff <name>'));
1342
+ console.error(theme.error(t('self.diff_usage')));
1127
1343
  continue;
1128
1344
  }
1129
1345
  try {
@@ -1137,7 +1353,7 @@ async function main() {
1137
1353
  if (lower === '/self-apply' || lower.startsWith('/self-apply ')) {
1138
1354
  const name = trimmed.slice('/self-apply'.length).trim();
1139
1355
  if (!name) {
1140
- console.error(theme.error('Использование: /self-apply <name>'));
1356
+ console.error(theme.error(t('self.apply_usage')));
1141
1357
  continue;
1142
1358
  }
1143
1359
  try {
@@ -1148,9 +1364,9 @@ async function main() {
1148
1364
  }
1149
1365
  continue;
1150
1366
  }
1151
- // ---------- Обычные команды ----------
1367
+ // ---------- Regular commands ----------
1152
1368
  if (lower === '/chats') {
1153
- const spin = editor || mod.createSpinner();
1369
+ const spin = editor || mod.createSpinner(currentLocale);
1154
1370
  spin.thinking();
1155
1371
  try {
1156
1372
  lastChats = await browser.listChats(30);
@@ -1159,41 +1375,41 @@ async function main() {
1159
1375
  console.log(theme.system('Чатов не найдено. Возможно, сайдбар свёрнут или селекторы устарели.'));
1160
1376
  }
1161
1377
  else {
1162
- console.log(theme.system('Последние чаты DeepSeek:'));
1378
+ console.log(theme.system(t('chats.recent')));
1163
1379
  lastChats.forEach((c, i) => {
1164
1380
  const n = String(i + 1).padStart(2, ' ');
1165
1381
  console.log(` ${theme.user(n)}. ${c.title} ${theme.system('(' + c.id.slice(0, 8) + '…)')}`);
1166
1382
  });
1167
- console.log(theme.system('\nИспользуй /resume <n> для продолжения.\n'));
1383
+ console.log(theme.system(t('chats.use_resume')));
1168
1384
  }
1169
1385
  }
1170
1386
  catch (e) {
1171
1387
  spin.stop();
1172
- console.error(theme.error('Не удалось получить список:'), e.message);
1388
+ console.error(theme.error(t('chats.fetch_error')), e.message);
1173
1389
  }
1174
1390
  continue;
1175
1391
  }
1176
1392
  if (lower === '/resume' || lower.startsWith('/resume ')) {
1177
1393
  const arg = trimmed.slice(7).trim();
1178
1394
  if (!arg) {
1179
- console.error(theme.error('Использование: /resume <n> (или /chats для списка)'));
1395
+ console.error(theme.error(t('chats.resume_usage')));
1180
1396
  continue;
1181
1397
  }
1182
1398
  const n = Number(arg);
1183
1399
  if (!Number.isFinite(n) || n < 1) {
1184
- console.error(theme.error('Нужен номер из /chats.'));
1400
+ console.error(theme.error(t('chats.need_number')));
1185
1401
  continue;
1186
1402
  }
1187
1403
  if (!lastChats.length) {
1188
- console.error(theme.error('Сначала выполни /chats.'));
1404
+ console.error(theme.error(t('chats.run_chats_first')));
1189
1405
  continue;
1190
1406
  }
1191
1407
  const pick = lastChats[n - 1];
1192
1408
  if (!pick) {
1193
- console.error(theme.error(`Нет чата №${n}. Всего: ${lastChats.length}.`));
1409
+ console.error(theme.error(t('chats.no_n', { n, total: lastChats.length })));
1194
1410
  continue;
1195
1411
  }
1196
- console.log(theme.system(`Открываю: ${pick.title}`));
1412
+ console.log(theme.system(t('chats.opening', { v: pick.title })));
1197
1413
  try {
1198
1414
  await browser.openChat(pick.id);
1199
1415
  currentChatId = pick.id;
@@ -1201,32 +1417,32 @@ async function main() {
1201
1417
  sendSystemPromptNext = resendPrompt;
1202
1418
  saveLastChat(pick.id, currentWorkdir, pick.title);
1203
1419
  transcript.log('resume_chat', { id: pick.id, title: pick.title });
1204
- console.log(theme.assistant(`Чат открыт.`) +
1420
+ console.log(theme.assistant(t('msg.chat_opened')) +
1205
1421
  theme.system(resendPrompt
1206
1422
  ? ' Системный промпт будет переслан на следующей задаче.\n'
1207
1423
  : ' Контекст чата сохранён. Системный промпт не пересылается (--resend-prompt чтобы дослать).\n'));
1208
1424
  }
1209
1425
  catch (e) {
1210
- console.error(theme.error('Не удалось открыть чат:'), e.message);
1426
+ console.error(theme.error(t('msg.open_chat_error', { v: '' })), e.message);
1211
1427
  }
1212
1428
  continue;
1213
1429
  }
1214
1430
  if (lower === '/chat') {
1215
1431
  if (currentChatId) {
1216
- console.log(theme.system(`Текущий chat id: ${currentChatId}`));
1432
+ console.log(theme.system(t('chats.current_id', { v: currentChatId })));
1217
1433
  console.log(theme.system(`URL: https://chat.deepseek.com/a/chat/s/${currentChatId}`));
1218
1434
  }
1219
1435
  else {
1220
1436
  const id = await browser.getCurrentChatId();
1221
- console.log(theme.system(id ? `Текущий chat id: ${id}` : 'Чат ещё не создан.'));
1437
+ console.log(theme.system(id ? t('chats.current_id', { v: id }) : t('chats.not_created')));
1222
1438
  }
1223
1439
  continue;
1224
1440
  }
1225
1441
  if (lower === '/sessions' || lower === '/session') {
1226
1442
  const all = listSessions();
1227
- console.log(theme.system(`Папка сессий: ${sessionsDir()}`));
1443
+ console.log(theme.system(t('sessions.dir', { v: sessionsDir() })));
1228
1444
  if (!all.length) {
1229
- console.log(theme.system('Сохранённых сессий нет. Они появятся после первой задачи/чата.'));
1445
+ console.log(theme.system(t('sessions.none')));
1230
1446
  }
1231
1447
  else {
1232
1448
  all.forEach((s, i) => {
@@ -1236,18 +1452,18 @@ async function main() {
1236
1452
  const wd = s.workdir ? theme.dim(` [${dirLabel(s.workdir)}]`) : '';
1237
1453
  console.log(` ${theme.user(n)}. ${s.id.slice(0, 8)}…${title}${wd}${mark}`);
1238
1454
  });
1239
- console.log(theme.system('Восстановить: /resume-id <id> (полный id) или /resume <n> после /chats.'));
1455
+ console.log(theme.system(t('sessions.restore_hint')));
1240
1456
  }
1241
1457
  continue;
1242
1458
  }
1243
1459
  if (lower.startsWith('/resume-id ')) {
1244
1460
  const id = trimmed.slice('/resume-id'.length).trim();
1245
1461
  if (!id) {
1246
- console.error(theme.error('Использование: /resume-id <chat id>'));
1462
+ console.error(theme.error(t('sessions.resume_id_usage')));
1247
1463
  continue;
1248
1464
  }
1249
1465
  try {
1250
- console.log(theme.system(`Открываю чат ${id}...`));
1466
+ console.log(theme.system(t('msg.opening_chat', { id })));
1251
1467
  await browser.openChat(id);
1252
1468
  currentChatId = id;
1253
1469
  freshChatNext = false;
@@ -1258,7 +1474,7 @@ async function main() {
1258
1474
  theme.system(String.fromCharCode(10)));
1259
1475
  }
1260
1476
  catch (e) {
1261
- console.error(theme.error('Не удалось открыть чат:'), e.message);
1477
+ console.error(theme.error(t('msg.open_chat_error', { v: '' })), e.message);
1262
1478
  }
1263
1479
  continue;
1264
1480
  }
@@ -1267,11 +1483,11 @@ async function main() {
1267
1483
  continue;
1268
1484
  }
1269
1485
  if (lower === '/reload') {
1270
- console.log(theme.system('Перечитываю модули логики...'));
1486
+ console.log(theme.system(t('msg.reload_start')));
1271
1487
  try {
1272
1488
  const { count, errors } = await reloadModules();
1273
1489
  if (errors.length) {
1274
- console.error(theme.error('Часть модулей не перезагрузилась:'));
1490
+ console.error(theme.error(t('msg.reload_partial')));
1275
1491
  for (const e of errors)
1276
1492
  console.error(theme.error(' ' + e));
1277
1493
  }
@@ -1280,59 +1496,72 @@ async function main() {
1280
1496
  }
1281
1497
  }
1282
1498
  catch (e) {
1283
- console.error(theme.error('Ошибка reload:'), e.message);
1499
+ console.error(theme.error(t('msg.reload_error')), e.message);
1284
1500
  }
1285
1501
  continue;
1286
1502
  }
1287
1503
  if (lower === '/status') {
1288
- console.log(theme.system(`Рабочая директория: ${currentWorkdir}`));
1289
- console.log(theme.system(`Режим ревью: ${reviewMode ? reviewMode.snapName : 'нет'}`));
1504
+ const yes = t('common.yes');
1505
+ const no = t('common.no');
1506
+ console.log(theme.system(t('status.workdir', { v: currentWorkdir })));
1507
+ console.log(theme.system(t('status.review_mode', {
1508
+ v: reviewMode ? reviewMode.snapName : t('status.review_none'),
1509
+ })));
1290
1510
  if (reviewMode) {
1291
- console.log(theme.system(`Исходная директория: ${reviewMode.originalWorkdir}`));
1292
- }
1293
- console.log(theme.system(`Текущий чат: ${currentChatId || '(нет)'}`));
1294
- console.log(theme.system(`Fresh chat на след. задаче: ${freshChatNext ? 'да' : 'нет'}`));
1295
- console.log(theme.system(`System prompt на след. задаче: ${sendSystemPromptNext ? 'да' : 'нет'}`));
1296
- console.log(theme.system(`Resend prompt (--resend-prompt): ${resendPrompt ? 'да' : 'нет'}`));
1297
- console.log(theme.system(`Last chat: ${loadLastChat(currentWorkdir) || 'нет'}`));
1298
- console.log(theme.system(`Сессии: ${sessionsDir()}`));
1299
- console.log(theme.system(`Dev mode (auto-reload): ${devMode ? 'вкл' : 'выкл'}`));
1300
- console.log(theme.system(`Лимит итераций: ${maxIter}`));
1301
- console.log(theme.system(`Headless: ${headless ? 'да' : 'нет'}`));
1302
- console.log(theme.system(`Debug: ${debug ? 'да' : 'нет'}`));
1303
- console.log(theme.system(`Undo: ${config.undo.enabled ? 'вкл' : 'выкл'}`));
1304
- console.log(theme.system(`Транскрипт: ${transcript.file || 'выкл'}`));
1511
+ console.log(theme.system(t('status.orig_dir', { v: reviewMode.originalWorkdir })));
1512
+ }
1513
+ console.log(theme.system(t('status.chat', { v: currentChatId || t('common.none') })));
1514
+ console.log(theme.system(t('status.fresh_next', { v: freshChatNext ? yes : no })));
1515
+ console.log(theme.system(t('status.prompt_next', { v: sendSystemPromptNext ? yes : no })));
1516
+ console.log(theme.system(t('status.resend', { v: resendPrompt ? yes : no })));
1517
+ console.log(theme.system(t('status.last_chat', {
1518
+ v: loadLastChat(currentWorkdir) || t('common.none'),
1519
+ })));
1520
+ console.log(theme.system(t('status.sessions', { v: sessionsDir() })));
1521
+ console.log(theme.system(t('status.dev', {
1522
+ v: devMode ? t('common.on') : t('common.off'),
1523
+ })));
1524
+ console.log(theme.system(t('status.max_iter', { v: maxIter })));
1525
+ console.log(theme.system(t('status.headless', { v: headless ? yes : no })));
1526
+ console.log(theme.system(t('status.debug', { v: debug ? yes : no })));
1527
+ console.log(theme.system(t('status.undo', {
1528
+ v: config.undo.enabled ? t('common.on') : t('common.off'),
1529
+ })));
1530
+ console.log(theme.system(t('status.transcript', {
1531
+ v: transcript.file || t('common.off'),
1532
+ })));
1533
+ console.log(theme.system(t('status.locale', { v: localeDisplayName(currentLocale) })));
1305
1534
  continue;
1306
1535
  }
1307
- if (lower === '/config') {
1308
- console.log(JSON.stringify(config, null, 2));
1536
+ if (lower === '/config' || lower.startsWith('/config ')) {
1537
+ await handleConfigCommand(trimmed);
1309
1538
  continue;
1310
1539
  }
1311
1540
  if (lower === '/transcript') {
1312
- console.log(theme.system(transcript.file || '(выключен)'));
1541
+ console.log(theme.system(transcript.file || t('common.off')));
1313
1542
  continue;
1314
1543
  }
1315
1544
  if (lower === '/undo') {
1316
1545
  const result = await undo.undoLast();
1317
1546
  if (result.ok && result.record) {
1318
- console.log(theme.assistant(`↶ Откатили: ${result.record.originalPath}`) +
1319
- theme.system(result.record.existed ? ' (восстановлено)' : ' (удалено)'));
1547
+ console.log(theme.assistant(t('undo.reverted', { v: result.record.originalPath })) +
1548
+ theme.system(result.record.existed ? t('undo.restored') : t('undo.deleted')));
1320
1549
  transcript.log('undo', { path: result.record.originalPath });
1321
1550
  }
1322
1551
  else {
1323
- console.error(theme.error(`Не удалось откатить: ${result.reason}`));
1552
+ console.error(theme.error(t('undo.failed', { v: String(result.reason ?? '') })));
1324
1553
  }
1325
1554
  continue;
1326
1555
  }
1327
1556
  if (lower === '/undo-list' || lower === '/history') {
1328
1557
  const list = await undo.list(10);
1329
1558
  if (!list.length) {
1330
- console.log(theme.system('История пуста.'));
1559
+ console.log(theme.system(t('undo.empty')));
1331
1560
  }
1332
1561
  else {
1333
1562
  for (const r of list) {
1334
1563
  const stamp = new Date(r.stamp).toLocaleString();
1335
- const flag = r.existed ? 'изменён' : 'создан';
1564
+ const flag = r.existed ? t('undo.changed') : t('undo.created');
1336
1565
  console.log(theme.system(`${stamp} [${flag}] ${r.originalPath}`));
1337
1566
  }
1338
1567
  }
@@ -1343,12 +1572,12 @@ async function main() {
1343
1572
  const file = path.join(config.transcript.dir, `dom-${stamp}.html`);
1344
1573
  try {
1345
1574
  const result = await browser.dumpDom(file);
1346
- console.log(theme.assistant(`HTML сохранён: ${result.file}`));
1347
- console.log(theme.system('Селекторы:'));
1575
+ console.log(theme.assistant(t('dom.saved', { v: result.file })));
1576
+ console.log(theme.system(t('dom.selectors')));
1348
1577
  console.log(JSON.stringify(result.selectors, null, 2));
1349
1578
  }
1350
1579
  catch (e) {
1351
- console.error(theme.error('Не удалось сохранить DOM:'), e.message);
1580
+ console.error(theme.error(t('dom.save_error')), e.message);
1352
1581
  }
1353
1582
  continue;
1354
1583
  }
@@ -1360,38 +1589,38 @@ async function main() {
1360
1589
  : sandboxRoot;
1361
1590
  const rel = path.relative(sandboxRoot, newDir);
1362
1591
  if (rel.startsWith('..') || path.isAbsolute(rel)) {
1363
- console.error(theme.error('Нельзя выйти за пределы: ' + sandboxRoot));
1592
+ console.error(theme.error(t('cd.outside', { v: sandboxRoot })));
1364
1593
  continue;
1365
1594
  }
1366
1595
  const stat = await fs.stat(newDir).catch(() => null);
1367
1596
  if (!stat || !stat.isDirectory()) {
1368
- console.error(theme.error('Не директория: ' + newDir));
1597
+ console.error(theme.error(t('cd.not_dir', { v: newDir })));
1369
1598
  continue;
1370
1599
  }
1371
1600
  if (newDir === currentWorkdir) {
1372
- console.log(theme.system('Уже здесь.'));
1601
+ console.log(theme.system(t('cd.already')));
1373
1602
  continue;
1374
1603
  }
1375
1604
  if (reviewMode) {
1376
- console.log(theme.system('Вышел из режима ревью (/cd).'));
1605
+ console.log(theme.system(t('cd.left_review')));
1377
1606
  reviewMode = null;
1378
1607
  }
1379
1608
  currentWorkdir = newDir;
1380
1609
  freshChatNext = true;
1381
1610
  sendSystemPromptNext = true;
1382
- console.log(theme.system('Рабочая директория: ' + newDir));
1611
+ console.log(theme.system(t('cd.changed', { v: newDir })));
1383
1612
  }
1384
1613
  catch (e) {
1385
- console.error(theme.error('Не удалось перейти: ' + e.message));
1614
+ console.error(theme.error(t('cd.failed', { v: e.message })));
1386
1615
  }
1387
1616
  continue;
1388
1617
  }
1389
1618
  if (lower.startsWith('/')) {
1390
- console.error(theme.error(`Неизвестная команда: ${trimmed}. Набери /help.`));
1619
+ console.error(theme.error(t('msg.unknown_cmd', { v: trimmed })));
1391
1620
  continue;
1392
1621
  }
1393
- // ---- Обычная задача (в том числе в review-режиме) ----
1394
- // Dev-режим: подхватываем свежие модули логики перед задачей.
1622
+ // ---- Regular task (including in review mode) ----
1623
+ // Dev mode: pick up fresh logic modules before the task.
1395
1624
  await autoReload();
1396
1625
  transcript.log('user_task', { task: trimmed, workdir: currentWorkdir });
1397
1626
  const tools = mod.createTools(currentWorkdir, { undo });
@@ -1405,9 +1634,9 @@ async function main() {
1405
1634
  queue: pendingQueue,
1406
1635
  ui: editor || null,
1407
1636
  onChatReady: (chatId) => {
1408
- // Сохраняем сессию сразу при начале диалога, не дожидаясь конца
1409
- // задачи. Иначе при долгой/прерванной задаче чат не попадал в
1410
- // ~/.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.
1411
1640
  if (chatId) {
1412
1641
  currentChatId = chatId;
1413
1642
  saveLastChat(chatId, currentWorkdir);
@@ -1433,7 +1662,7 @@ async function main() {
1433
1662
  transcript.close();
1434
1663
  }
1435
1664
  main().catch((e) => {
1436
- console.error(theme.error('Критическая ошибка:'), e.message);
1665
+ console.error(theme.error(t('msg.critical')), e.message);
1437
1666
  if (debug)
1438
1667
  console.error(e.stack);
1439
1668
  process.exit(1);