zames_pro 2.5.0 → 2.6.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
+ // Текущий язык интерфейса/агента. Меняется командой /config lang <ru|en>.
45
+ let currentLocale = isLocale(config.ui?.locale)
46
+ ? config.ui.locale
47
+ : 'ru';
48
+ // Перевод: читает currentLocale в момент вызова, поэтому смена языка
49
+ // действует сразу, без перезапуска (для уже напечатанных строк).
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');
@@ -154,7 +163,7 @@ async function autoReload() {
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,68 +171,68 @@ 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
  }
@@ -231,33 +240,42 @@ function dirLabel(p) {
231
240
  return path.basename(p) || p;
232
241
  }
233
242
  // Список slash-команд для автодополнения при вводе «/» (Tab — дополнить).
243
+ // Описание локализуется по ключу help.cmd.* в момент отображения — см.
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
  ];
272
+ // Описания slash-команд на текущем языке (для подсказок LineEditor).
273
+ function buildSlashCommands() {
274
+ return SLASH_COMMANDS.map((c) => ({
275
+ name: c.name,
276
+ description: t(c.key),
277
+ }));
278
+ }
261
279
  // Временные файлы агента (одноразовые скрипты и т.п.) складываем в
262
280
  // <проект>/tmp — эта папка в .gitignore и очищается при каждом запуске.
263
281
  const TMP_DIR = path.join(__dirname, '..', 'tmp');
@@ -629,13 +647,13 @@ async function runTask(browser, tools, taskText, workdir, opts) {
629
647
  // Новая задача с промпта — сбрасываем «стоп» от прошлого прерывания.
630
648
  browser._stopped = false;
631
649
  browser._abort = false;
632
- const ui = editor || mod.createSpinner();
650
+ const ui = editor || mod.createSpinner(currentLocale);
633
651
  const stopWatching = editor
634
652
  ? () => { }
635
653
  : watchInput({
636
654
  onEscape: () => {
637
655
  ui.stop();
638
- console.error(theme.warn('⏹ Esc — прерываю генерацию...'));
656
+ console.error(theme.warn(t('msg.abort_gen_short')));
639
657
  browser.stopGeneration().catch(() => { });
640
658
  },
641
659
  onChange: (text) => ui.setPending(text),
@@ -643,7 +661,7 @@ async function runTask(browser, tools, taskText, workdir, opts) {
643
661
  queue.push(text);
644
662
  ui.setPending(null);
645
663
  ui.stop();
646
- console.log(theme.user('📨 В очередь (' + queue.length + '): ') +
664
+ console.log(theme.user(t('msg.queued', { n: queue.length })) +
647
665
  theme.assistant(text));
648
666
  ui.thinking();
649
667
  },
@@ -669,6 +687,7 @@ async function runTask(browser, tools, taskText, workdir, opts) {
669
687
  onAssistantMessage: (msg) => ui.assistant(msg),
670
688
  onChatReady,
671
689
  debugLog: debug,
690
+ locale: currentLocale,
672
691
  });
673
692
  // Прервали (Esc/Ctrl+C) — не запускаем следующие задачи из очереди
674
693
  // и очищаем её, чтобы «стоп» действительно останавливал всё.
@@ -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,7 +733,7 @@ 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;
@@ -725,17 +744,17 @@ async function main() {
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,7 +772,7 @@ 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(() => { });
@@ -772,19 +791,19 @@ async function main() {
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,8 +821,8 @@ 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 = [];
@@ -824,7 +843,7 @@ async function main() {
824
843
  // висеть отдельным процессом после выхода агента.
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
849
  // По умолчанию при старте начинаем НОВЫЙ чат: контекст прошлой сессии
@@ -842,16 +861,16 @@ async function main() {
842
861
  }
843
862
  if (resumeId) {
844
863
  try {
845
- console.log(theme.system(`Открываю чат ${resumeId}...`));
864
+ console.log(theme.system(t('msg.opening_chat', { id: resumeId })));
846
865
  await browser.openChat(resumeId);
847
866
  currentChatId = resumeId;
848
867
  freshChatNext = false;
849
868
  sendSystemPromptNext = resendPrompt;
850
869
  saveLastChat(resumeId, currentWorkdir);
851
- console.log(theme.system(`Чат открыт: ${resumeId}\n`));
870
+ console.log(theme.system(t('msg.chat_opened') + ' ' + resumeId + String.fromCharCode(10)));
852
871
  }
853
872
  catch (e) {
854
- console.error(theme.error(`Не удалось открыть чат: ${e.message}`));
873
+ console.error(theme.error(t('msg.open_chat_error', { v: e.message })));
855
874
  }
856
875
  }
857
876
  // ---------- ввод: постоянная строка внизу + статус сверху ----------
@@ -872,7 +891,7 @@ async function main() {
872
891
  let tail;
873
892
  if (reviewMode) {
874
893
  tail =
875
- theme.warn('REVIEW') + theme.dim(':') + theme.dir(reviewMode.snapName);
894
+ theme.warn(t('prompt.review')) + theme.dim(':') + theme.dir(reviewMode.snapName);
876
895
  }
877
896
  else {
878
897
  tail = theme.dir(dirLabel(currentWorkdir));
@@ -882,7 +901,7 @@ async function main() {
882
901
  if (process.stdin.isTTY && process.stdout.isTTY) {
883
902
  const ed = new LineEditor({
884
903
  prompt: buildPrompt(),
885
- commands: SLASH_COMMANDS,
904
+ commands: buildSlashCommands(),
886
905
  });
887
906
  editor = ed;
888
907
  ed.onSubmit = (text) => {
@@ -895,13 +914,13 @@ async function main() {
895
914
  };
896
915
  ed.onEscape = () => {
897
916
  if (ed.busy) {
898
- ed.printAbove(theme.warn('⏹ Esc — прерываю генерацию...'));
917
+ ed.printAbove(theme.warn(t('msg.abort_gen_short')));
899
918
  browser.stopGeneration().catch(() => { });
900
919
  }
901
920
  };
902
921
  ed.onCtrlC = () => {
903
922
  if (ed.busy) {
904
- ed.printAbove(theme.warn('⏹ Ctrl+C — прерываю генерацию...'));
923
+ ed.printAbove(theme.warn(t('msg.abort_ctrlc_short')));
905
924
  browser.stopGeneration().catch(() => { });
906
925
  }
907
926
  else {
@@ -937,6 +956,206 @@ async function main() {
937
956
  edAny._origLog = origLog;
938
957
  edAny._origErr = origErr;
939
958
  }
959
+ // ---------- /config ----------
960
+ // Просмотр и правка настроек без перезапуска. Значения валидируются по
961
+ // CONFIG_SCHEMA (src/config.ts) и пишутся в проектный .zamesrc.json.
962
+ // Язык (ui.locale) применяется сразу: меняем currentLocale, пересобираем
963
+ // подсказки редактора и обновляем config.ui.locale (его читает agent-loop
964
+ // при следующей задаче).
965
+ //
966
+ // Без аргументов в интерактивном терминале открывается меню (стрелки,
967
+ // Enter — изменить, d — сбросить, q — выйти). Есть и текстовые подкоманды
968
+ // (list/get/set/reset/lang/path) — для скриптов и не-TTY.
969
+ function setConfigRuntime(path, value) {
970
+ const segs = path.split('.');
971
+ let obj = config;
972
+ for (let i = 0; i < segs.length - 1; i++) {
973
+ obj = obj[segs[i]];
974
+ }
975
+ obj[segs[segs.length - 1]] = value;
976
+ if (path === 'ui.locale' && isLocale(value)) {
977
+ currentLocale = value;
978
+ config.ui.locale = value;
979
+ if (editor) {
980
+ editor.setCommands(buildSlashCommands());
981
+ editor.setPrompt(buildPrompt());
982
+ }
983
+ }
984
+ }
985
+ function configSetRaw(field, raw) {
986
+ const value = validateConfigValue(field, raw);
987
+ writeConfigValue('project', field.path, raw);
988
+ setConfigRuntime(field.path, value);
989
+ }
990
+ function configResetField(field) {
991
+ resetConfigValue('project', field.path);
992
+ // Возвращаем рантайм-значение к дефолту.
993
+ const def = getByPath(DEFAULTS, field.path);
994
+ setConfigRuntime(field.path, def);
995
+ }
996
+ function configLabel(field) {
997
+ return t(field.labelKey);
998
+ }
999
+ function configShowList() {
1000
+ console.log(theme.system(t('cfg.title')));
1001
+ let lastGroup = '';
1002
+ for (const f of CONFIG_SCHEMA) {
1003
+ const g = t(f.groupKey);
1004
+ if (g !== lastGroup) {
1005
+ console.log(theme.system('\n ' + g));
1006
+ lastGroup = g;
1007
+ }
1008
+ const cur = getByPath(config, f.path);
1009
+ const shown = cur === undefined
1010
+ ? t('cfg.menu.default')
1011
+ : typeof cur === 'boolean'
1012
+ ? cur
1013
+ ? t('common.on')
1014
+ : t('common.off')
1015
+ : JSON.stringify(cur);
1016
+ const extra = f.values ? ' [' + f.values.join('|') + ']' : '';
1017
+ console.log(' ' +
1018
+ theme.user(f.path) +
1019
+ theme.dim(' = ') +
1020
+ theme.assistant(shown) +
1021
+ theme.dim(extra) +
1022
+ theme.dim(' # ' + configLabel(f)));
1023
+ }
1024
+ console.log(theme.dim('\n' + t('cfg.usage')));
1025
+ }
1026
+ async function configOpenMenu() {
1027
+ if (!(process.stdin.isTTY && process.stdout.isTTY)) {
1028
+ configShowList();
1029
+ return;
1030
+ }
1031
+ // Меню рисует напрямую в stdout и само читает клавиши. Чтобы его вывод
1032
+ // не накладывался на постоянную строку ввода LineEditor, на время меню
1033
+ // «ставим редактор на паузу», а после — возвращаем.
1034
+ if (editor)
1035
+ editor.pause();
1036
+ try {
1037
+ await runConfigMenu({
1038
+ fields: CONFIG_SCHEMA,
1039
+ t,
1040
+ get: (path) => getByPath(config, path),
1041
+ set: (field, raw) => configSetRaw(field, raw),
1042
+ reset: (field) => configResetField(field),
1043
+ });
1044
+ }
1045
+ catch (e) {
1046
+ console.error(theme.error(String(e.message || e)));
1047
+ }
1048
+ finally {
1049
+ if (editor)
1050
+ editor.resume();
1051
+ }
1052
+ }
1053
+ async function handleConfigCommand(input) {
1054
+ const parts = input.trim().split(/\s+/);
1055
+ const sub = (parts[1] || '').toLowerCase();
1056
+ // Без подкоманды — меню (или текстовый список в не-TTY).
1057
+ if (!sub || sub === 'menu' || sub === 'ui') {
1058
+ await configOpenMenu();
1059
+ return;
1060
+ }
1061
+ if (sub === 'list' || sub === 'show' || sub === 'ls') {
1062
+ configShowList();
1063
+ return;
1064
+ }
1065
+ if (sub === 'path' || sub === 'paths') {
1066
+ console.log(theme.system(t('cfg.paths', {
1067
+ global: CONFIG_PATHS.HOME_CONFIG,
1068
+ project: CONFIG_PATHS.PROJECT_CONFIG,
1069
+ })));
1070
+ return;
1071
+ }
1072
+ // /config lang <ru|en> — быстрый доступ к ui.locale
1073
+ if (sub === 'lang' || sub === 'language' || sub === 'язык') {
1074
+ const val = parts[2];
1075
+ if (!val) {
1076
+ console.log(theme.system(t('status.locale', { v: localeDisplayName(currentLocale) })));
1077
+ console.log(theme.dim(t('cfg.lang_usage')));
1078
+ return;
1079
+ }
1080
+ const field = CONFIG_SCHEMA.find((f) => f.path === 'ui.locale');
1081
+ const loc = normalizeLocale(val);
1082
+ try {
1083
+ configSetRaw(field, loc);
1084
+ }
1085
+ catch (e) {
1086
+ console.error(theme.error(t('cfg.write_error', { v: e.message })));
1087
+ return;
1088
+ }
1089
+ console.log(theme.assistant(t('cfg.lang_set', { v: localeDisplayName(loc) })));
1090
+ return;
1091
+ }
1092
+ if (sub === 'get') {
1093
+ const key = parts[2];
1094
+ if (!key) {
1095
+ console.log(theme.dim(t('cfg.usage')));
1096
+ return;
1097
+ }
1098
+ const field = CONFIG_SCHEMA.find((f) => f.path === key);
1099
+ if (!field) {
1100
+ console.error(theme.error(t('cfg.unknown_key', { v: key })));
1101
+ return;
1102
+ }
1103
+ const cur = getByPath(config, key);
1104
+ console.log(theme.system(t('cfg.value', {
1105
+ v: key,
1106
+ value: cur === undefined ? t('cfg.menu.default') : JSON.stringify(cur),
1107
+ })));
1108
+ return;
1109
+ }
1110
+ if (sub === 'set') {
1111
+ const key = parts[2];
1112
+ const raw = parts.slice(3).join(' ');
1113
+ const field = CONFIG_SCHEMA.find((f) => f.path === key);
1114
+ if (!field) {
1115
+ console.error(theme.error(t('cfg.unknown_key', { v: key || '' })));
1116
+ return;
1117
+ }
1118
+ if (!raw) {
1119
+ console.log(theme.dim(t('cfg.usage')));
1120
+ return;
1121
+ }
1122
+ try {
1123
+ configSetRaw(field, raw);
1124
+ }
1125
+ catch {
1126
+ console.error(theme.error(t('cfg.bad_value', {
1127
+ v: key,
1128
+ type: field.values ? field.values.join('|') : field.type,
1129
+ })));
1130
+ return;
1131
+ }
1132
+ console.log(theme.assistant(t('cfg.saved', {
1133
+ v: key,
1134
+ value: JSON.stringify(getByPath(config, key)),
1135
+ })));
1136
+ return;
1137
+ }
1138
+ if (sub === 'reset' || sub === 'unset') {
1139
+ const key = parts[2];
1140
+ const field = CONFIG_SCHEMA.find((f) => f.path === key);
1141
+ if (!field) {
1142
+ console.error(theme.error(t('cfg.unknown_key', { v: key || '' })));
1143
+ return;
1144
+ }
1145
+ try {
1146
+ configResetField(field);
1147
+ }
1148
+ catch (e) {
1149
+ console.error(theme.error(t('cfg.write_error', { v: e.message })));
1150
+ return;
1151
+ }
1152
+ console.log(theme.assistant(t('cfg.reset', { v: key })));
1153
+ return;
1154
+ }
1155
+ // Неизвестная подкоманда — показываем список.
1156
+ console.error(theme.error(t('cfg.unknown_key', { v: sub })));
1157
+ configShowList();
1158
+ }
940
1159
  while (running) {
941
1160
  let input;
942
1161
  try {
@@ -948,7 +1167,7 @@ async function main() {
948
1167
  let tail;
949
1168
  if (reviewMode) {
950
1169
  tail =
951
- theme.warn('REVIEW') +
1170
+ theme.warn(t('prompt.review')) +
952
1171
  theme.dim(':') +
953
1172
  theme.dir(reviewMode.snapName);
954
1173
  }
@@ -972,7 +1191,7 @@ async function main() {
972
1191
  continue;
973
1192
  }
974
1193
  if (['/new', '/clear', 'new'].includes(lower)) {
975
- console.log(theme.system('Создаю новый чат...'));
1194
+ console.log(theme.system(t('msg.new_chat')));
976
1195
  try {
977
1196
  await browser.newChat();
978
1197
  freshChatNext = false;
@@ -980,10 +1199,10 @@ async function main() {
980
1199
  currentChatId = await browser.getCurrentChatId();
981
1200
  saveLastChat(currentChatId, currentWorkdir);
982
1201
  transcript.log('new_chat');
983
- console.log(theme.system('Новый чат.\n'));
1202
+ console.log(theme.system(t('msg.new_chat_ok') + String.fromCharCode(10)));
984
1203
  }
985
1204
  catch (e) {
986
- console.error(theme.error('Не удалось создать новый чат:'), e.message);
1205
+ console.error(theme.error(t('msg.new_chat_error', { v: e.message })), e.message);
987
1206
  }
988
1207
  continue;
989
1208
  }
@@ -1015,16 +1234,10 @@ async function main() {
1015
1234
  sendSystemPromptNext = false;
1016
1235
  currentChatId = await browser.getCurrentChatId();
1017
1236
  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'));
1237
+ console.log(theme.user(t('self.review_hint', { name: reviewMode.snapName })));
1025
1238
  }
1026
1239
  catch (e) {
1027
- console.error(theme.error('Самообзор провалился:'), e.message);
1240
+ console.error(theme.error(t('self.review_failed')), e.message);
1028
1241
  if (debug)
1029
1242
  console.error(e.stack);
1030
1243
  }
@@ -1033,7 +1246,7 @@ async function main() {
1033
1246
  if (lower === '/self-fix' || lower.startsWith('/self-fix ')) {
1034
1247
  const rest = trimmed.slice('/self-fix'.length).trim();
1035
1248
  if (!rest) {
1036
- console.error(theme.error('Использование: /self-fix <name> [фокус]'));
1249
+ console.error(theme.error(t('self.fix_usage')));
1037
1250
  continue;
1038
1251
  }
1039
1252
  const sp = rest.indexOf(' ');
@@ -1042,7 +1255,7 @@ async function main() {
1042
1255
  const snapRoot = path.join(ZAMES_HOME, 'snapshots', name);
1043
1256
  const stat = await fs.stat(snapRoot).catch(() => null);
1044
1257
  if (!stat || !stat.isDirectory()) {
1045
- console.error(theme.error(`Снапшот не найден: ${snapRoot}`));
1258
+ console.error(theme.error(t('self.snapshot_not_found', { v: snapRoot })));
1046
1259
  continue;
1047
1260
  }
1048
1261
  const originalWorkdir = reviewMode
@@ -1051,17 +1264,17 @@ async function main() {
1051
1264
  // Свежий чат + review-промпт на этот снапшот
1052
1265
  try {
1053
1266
  const { runAgentLoop: ral } = await import('./agent-loop.js');
1054
- const { buildSystemPrompt } = await import('./system-prompt.js');
1055
1267
  const tools = mod.createTools(snapRoot, { undo: null });
1056
1268
  await browser.newChat();
1057
1269
  const sysPrompt = mod.buildSystemPrompt({
1058
1270
  workdir: snapRoot,
1059
1271
  tools,
1272
+ locale: currentLocale,
1060
1273
  });
1061
- console.log(theme.system('Инициализирую review-чат для снапшота...'));
1274
+ console.log(theme.system(t('self.init_review_chat')));
1062
1275
  await browser.ask(sysPrompt, { timeout: 60_000 });
1063
1276
  if (focus) {
1064
- const ui = mod.createSpinner();
1277
+ const ui = mod.createSpinner(currentLocale);
1065
1278
  ui.thinking();
1066
1279
  await ral({
1067
1280
  browser,
@@ -1090,16 +1303,16 @@ async function main() {
1090
1303
  sendSystemPromptNext = false;
1091
1304
  currentChatId = await browser.getCurrentChatId();
1092
1305
  saveLastChat(currentChatId, currentWorkdir);
1093
- console.log(theme.user(`\n💡 Режим ревью по снапшоту ${name}. Пиши агенту задачу или /self-done.\n`));
1306
+ console.log(theme.user(t('self.fix_hint', { name })));
1094
1307
  }
1095
1308
  catch (e) {
1096
- console.error(theme.error('Не удалось войти в снапшот:'), e.message);
1309
+ console.error(theme.error(t('self.enter_failed')), e.message);
1097
1310
  }
1098
1311
  continue;
1099
1312
  }
1100
1313
  if (lower === '/self-done') {
1101
1314
  if (!reviewMode) {
1102
- console.log(theme.system('Ты и так не в режиме ревью.'));
1315
+ console.log(theme.system(t('self.not_in_review')));
1103
1316
  continue;
1104
1317
  }
1105
1318
  const back = reviewMode.originalWorkdir;
@@ -1108,7 +1321,7 @@ async function main() {
1108
1321
  // Раз чат занят review-контекстом, для обычной работы создадим новый
1109
1322
  freshChatNext = true;
1110
1323
  sendSystemPromptNext = true;
1111
- console.log(theme.system(`Вернулся в ${back}. Следующая задача начнёт новый чат.\n`));
1324
+ console.log(theme.system(t('self.done_hint', { v: back })));
1112
1325
  continue;
1113
1326
  }
1114
1327
  if (lower === '/self-list') {
@@ -1123,7 +1336,7 @@ async function main() {
1123
1336
  if (lower === '/self-diff' || lower.startsWith('/self-diff ')) {
1124
1337
  const name = trimmed.slice('/self-diff'.length).trim();
1125
1338
  if (!name) {
1126
- console.error(theme.error('Использование: /self-diff <name>'));
1339
+ console.error(theme.error(t('self.diff_usage')));
1127
1340
  continue;
1128
1341
  }
1129
1342
  try {
@@ -1137,7 +1350,7 @@ async function main() {
1137
1350
  if (lower === '/self-apply' || lower.startsWith('/self-apply ')) {
1138
1351
  const name = trimmed.slice('/self-apply'.length).trim();
1139
1352
  if (!name) {
1140
- console.error(theme.error('Использование: /self-apply <name>'));
1353
+ console.error(theme.error(t('self.apply_usage')));
1141
1354
  continue;
1142
1355
  }
1143
1356
  try {
@@ -1150,7 +1363,7 @@ async function main() {
1150
1363
  }
1151
1364
  // ---------- Обычные команды ----------
1152
1365
  if (lower === '/chats') {
1153
- const spin = editor || mod.createSpinner();
1366
+ const spin = editor || mod.createSpinner(currentLocale);
1154
1367
  spin.thinking();
1155
1368
  try {
1156
1369
  lastChats = await browser.listChats(30);
@@ -1159,41 +1372,41 @@ async function main() {
1159
1372
  console.log(theme.system('Чатов не найдено. Возможно, сайдбар свёрнут или селекторы устарели.'));
1160
1373
  }
1161
1374
  else {
1162
- console.log(theme.system('Последние чаты DeepSeek:'));
1375
+ console.log(theme.system(t('chats.recent')));
1163
1376
  lastChats.forEach((c, i) => {
1164
1377
  const n = String(i + 1).padStart(2, ' ');
1165
1378
  console.log(` ${theme.user(n)}. ${c.title} ${theme.system('(' + c.id.slice(0, 8) + '…)')}`);
1166
1379
  });
1167
- console.log(theme.system('\nИспользуй /resume <n> для продолжения.\n'));
1380
+ console.log(theme.system(t('chats.use_resume')));
1168
1381
  }
1169
1382
  }
1170
1383
  catch (e) {
1171
1384
  spin.stop();
1172
- console.error(theme.error('Не удалось получить список:'), e.message);
1385
+ console.error(theme.error(t('chats.fetch_error')), e.message);
1173
1386
  }
1174
1387
  continue;
1175
1388
  }
1176
1389
  if (lower === '/resume' || lower.startsWith('/resume ')) {
1177
1390
  const arg = trimmed.slice(7).trim();
1178
1391
  if (!arg) {
1179
- console.error(theme.error('Использование: /resume <n> (или /chats для списка)'));
1392
+ console.error(theme.error(t('chats.resume_usage')));
1180
1393
  continue;
1181
1394
  }
1182
1395
  const n = Number(arg);
1183
1396
  if (!Number.isFinite(n) || n < 1) {
1184
- console.error(theme.error('Нужен номер из /chats.'));
1397
+ console.error(theme.error(t('chats.need_number')));
1185
1398
  continue;
1186
1399
  }
1187
1400
  if (!lastChats.length) {
1188
- console.error(theme.error('Сначала выполни /chats.'));
1401
+ console.error(theme.error(t('chats.run_chats_first')));
1189
1402
  continue;
1190
1403
  }
1191
1404
  const pick = lastChats[n - 1];
1192
1405
  if (!pick) {
1193
- console.error(theme.error(`Нет чата №${n}. Всего: ${lastChats.length}.`));
1406
+ console.error(theme.error(t('chats.no_n', { n, total: lastChats.length })));
1194
1407
  continue;
1195
1408
  }
1196
- console.log(theme.system(`Открываю: ${pick.title}`));
1409
+ console.log(theme.system(t('chats.opening', { v: pick.title })));
1197
1410
  try {
1198
1411
  await browser.openChat(pick.id);
1199
1412
  currentChatId = pick.id;
@@ -1201,32 +1414,32 @@ async function main() {
1201
1414
  sendSystemPromptNext = resendPrompt;
1202
1415
  saveLastChat(pick.id, currentWorkdir, pick.title);
1203
1416
  transcript.log('resume_chat', { id: pick.id, title: pick.title });
1204
- console.log(theme.assistant(`Чат открыт.`) +
1417
+ console.log(theme.assistant(t('msg.chat_opened')) +
1205
1418
  theme.system(resendPrompt
1206
1419
  ? ' Системный промпт будет переслан на следующей задаче.\n'
1207
1420
  : ' Контекст чата сохранён. Системный промпт не пересылается (--resend-prompt чтобы дослать).\n'));
1208
1421
  }
1209
1422
  catch (e) {
1210
- console.error(theme.error('Не удалось открыть чат:'), e.message);
1423
+ console.error(theme.error(t('msg.open_chat_error', { v: '' })), e.message);
1211
1424
  }
1212
1425
  continue;
1213
1426
  }
1214
1427
  if (lower === '/chat') {
1215
1428
  if (currentChatId) {
1216
- console.log(theme.system(`Текущий chat id: ${currentChatId}`));
1429
+ console.log(theme.system(t('chats.current_id', { v: currentChatId })));
1217
1430
  console.log(theme.system(`URL: https://chat.deepseek.com/a/chat/s/${currentChatId}`));
1218
1431
  }
1219
1432
  else {
1220
1433
  const id = await browser.getCurrentChatId();
1221
- console.log(theme.system(id ? `Текущий chat id: ${id}` : 'Чат ещё не создан.'));
1434
+ console.log(theme.system(id ? t('chats.current_id', { v: id }) : t('chats.not_created')));
1222
1435
  }
1223
1436
  continue;
1224
1437
  }
1225
1438
  if (lower === '/sessions' || lower === '/session') {
1226
1439
  const all = listSessions();
1227
- console.log(theme.system(`Папка сессий: ${sessionsDir()}`));
1440
+ console.log(theme.system(t('sessions.dir', { v: sessionsDir() })));
1228
1441
  if (!all.length) {
1229
- console.log(theme.system('Сохранённых сессий нет. Они появятся после первой задачи/чата.'));
1442
+ console.log(theme.system(t('sessions.none')));
1230
1443
  }
1231
1444
  else {
1232
1445
  all.forEach((s, i) => {
@@ -1236,18 +1449,18 @@ async function main() {
1236
1449
  const wd = s.workdir ? theme.dim(` [${dirLabel(s.workdir)}]`) : '';
1237
1450
  console.log(` ${theme.user(n)}. ${s.id.slice(0, 8)}…${title}${wd}${mark}`);
1238
1451
  });
1239
- console.log(theme.system('Восстановить: /resume-id <id> (полный id) или /resume <n> после /chats.'));
1452
+ console.log(theme.system(t('sessions.restore_hint')));
1240
1453
  }
1241
1454
  continue;
1242
1455
  }
1243
1456
  if (lower.startsWith('/resume-id ')) {
1244
1457
  const id = trimmed.slice('/resume-id'.length).trim();
1245
1458
  if (!id) {
1246
- console.error(theme.error('Использование: /resume-id <chat id>'));
1459
+ console.error(theme.error(t('sessions.resume_id_usage')));
1247
1460
  continue;
1248
1461
  }
1249
1462
  try {
1250
- console.log(theme.system(`Открываю чат ${id}...`));
1463
+ console.log(theme.system(t('msg.opening_chat', { id })));
1251
1464
  await browser.openChat(id);
1252
1465
  currentChatId = id;
1253
1466
  freshChatNext = false;
@@ -1258,7 +1471,7 @@ async function main() {
1258
1471
  theme.system(String.fromCharCode(10)));
1259
1472
  }
1260
1473
  catch (e) {
1261
- console.error(theme.error('Не удалось открыть чат:'), e.message);
1474
+ console.error(theme.error(t('msg.open_chat_error', { v: '' })), e.message);
1262
1475
  }
1263
1476
  continue;
1264
1477
  }
@@ -1267,11 +1480,11 @@ async function main() {
1267
1480
  continue;
1268
1481
  }
1269
1482
  if (lower === '/reload') {
1270
- console.log(theme.system('Перечитываю модули логики...'));
1483
+ console.log(theme.system(t('msg.reload_start')));
1271
1484
  try {
1272
1485
  const { count, errors } = await reloadModules();
1273
1486
  if (errors.length) {
1274
- console.error(theme.error('Часть модулей не перезагрузилась:'));
1487
+ console.error(theme.error(t('msg.reload_partial')));
1275
1488
  for (const e of errors)
1276
1489
  console.error(theme.error(' ' + e));
1277
1490
  }
@@ -1280,59 +1493,72 @@ async function main() {
1280
1493
  }
1281
1494
  }
1282
1495
  catch (e) {
1283
- console.error(theme.error('Ошибка reload:'), e.message);
1496
+ console.error(theme.error(t('msg.reload_error')), e.message);
1284
1497
  }
1285
1498
  continue;
1286
1499
  }
1287
1500
  if (lower === '/status') {
1288
- console.log(theme.system(`Рабочая директория: ${currentWorkdir}`));
1289
- console.log(theme.system(`Режим ревью: ${reviewMode ? reviewMode.snapName : 'нет'}`));
1501
+ const yes = t('common.yes');
1502
+ const no = t('common.no');
1503
+ console.log(theme.system(t('status.workdir', { v: currentWorkdir })));
1504
+ console.log(theme.system(t('status.review_mode', {
1505
+ v: reviewMode ? reviewMode.snapName : t('status.review_none'),
1506
+ })));
1290
1507
  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 || 'выкл'}`));
1508
+ console.log(theme.system(t('status.orig_dir', { v: reviewMode.originalWorkdir })));
1509
+ }
1510
+ console.log(theme.system(t('status.chat', { v: currentChatId || t('common.none') })));
1511
+ console.log(theme.system(t('status.fresh_next', { v: freshChatNext ? yes : no })));
1512
+ console.log(theme.system(t('status.prompt_next', { v: sendSystemPromptNext ? yes : no })));
1513
+ console.log(theme.system(t('status.resend', { v: resendPrompt ? yes : no })));
1514
+ console.log(theme.system(t('status.last_chat', {
1515
+ v: loadLastChat(currentWorkdir) || t('common.none'),
1516
+ })));
1517
+ console.log(theme.system(t('status.sessions', { v: sessionsDir() })));
1518
+ console.log(theme.system(t('status.dev', {
1519
+ v: devMode ? t('common.on') : t('common.off'),
1520
+ })));
1521
+ console.log(theme.system(t('status.max_iter', { v: maxIter })));
1522
+ console.log(theme.system(t('status.headless', { v: headless ? yes : no })));
1523
+ console.log(theme.system(t('status.debug', { v: debug ? yes : no })));
1524
+ console.log(theme.system(t('status.undo', {
1525
+ v: config.undo.enabled ? t('common.on') : t('common.off'),
1526
+ })));
1527
+ console.log(theme.system(t('status.transcript', {
1528
+ v: transcript.file || t('common.off'),
1529
+ })));
1530
+ console.log(theme.system(t('status.locale', { v: localeDisplayName(currentLocale) })));
1305
1531
  continue;
1306
1532
  }
1307
- if (lower === '/config') {
1308
- console.log(JSON.stringify(config, null, 2));
1533
+ if (lower === '/config' || lower.startsWith('/config ')) {
1534
+ await handleConfigCommand(trimmed);
1309
1535
  continue;
1310
1536
  }
1311
1537
  if (lower === '/transcript') {
1312
- console.log(theme.system(transcript.file || '(выключен)'));
1538
+ console.log(theme.system(transcript.file || t('common.off')));
1313
1539
  continue;
1314
1540
  }
1315
1541
  if (lower === '/undo') {
1316
1542
  const result = await undo.undoLast();
1317
1543
  if (result.ok && result.record) {
1318
- console.log(theme.assistant(`↶ Откатили: ${result.record.originalPath}`) +
1319
- theme.system(result.record.existed ? ' (восстановлено)' : ' (удалено)'));
1544
+ console.log(theme.assistant(t('undo.reverted', { v: result.record.originalPath })) +
1545
+ theme.system(result.record.existed ? t('undo.restored') : t('undo.deleted')));
1320
1546
  transcript.log('undo', { path: result.record.originalPath });
1321
1547
  }
1322
1548
  else {
1323
- console.error(theme.error(`Не удалось откатить: ${result.reason}`));
1549
+ console.error(theme.error(t('undo.failed', { v: String(result.reason ?? '') })));
1324
1550
  }
1325
1551
  continue;
1326
1552
  }
1327
1553
  if (lower === '/undo-list' || lower === '/history') {
1328
1554
  const list = await undo.list(10);
1329
1555
  if (!list.length) {
1330
- console.log(theme.system('История пуста.'));
1556
+ console.log(theme.system(t('undo.empty')));
1331
1557
  }
1332
1558
  else {
1333
1559
  for (const r of list) {
1334
1560
  const stamp = new Date(r.stamp).toLocaleString();
1335
- const flag = r.existed ? 'изменён' : 'создан';
1561
+ const flag = r.existed ? t('undo.changed') : t('undo.created');
1336
1562
  console.log(theme.system(`${stamp} [${flag}] ${r.originalPath}`));
1337
1563
  }
1338
1564
  }
@@ -1343,12 +1569,12 @@ async function main() {
1343
1569
  const file = path.join(config.transcript.dir, `dom-${stamp}.html`);
1344
1570
  try {
1345
1571
  const result = await browser.dumpDom(file);
1346
- console.log(theme.assistant(`HTML сохранён: ${result.file}`));
1347
- console.log(theme.system('Селекторы:'));
1572
+ console.log(theme.assistant(t('dom.saved', { v: result.file })));
1573
+ console.log(theme.system(t('dom.selectors')));
1348
1574
  console.log(JSON.stringify(result.selectors, null, 2));
1349
1575
  }
1350
1576
  catch (e) {
1351
- console.error(theme.error('Не удалось сохранить DOM:'), e.message);
1577
+ console.error(theme.error(t('dom.save_error')), e.message);
1352
1578
  }
1353
1579
  continue;
1354
1580
  }
@@ -1360,34 +1586,34 @@ async function main() {
1360
1586
  : sandboxRoot;
1361
1587
  const rel = path.relative(sandboxRoot, newDir);
1362
1588
  if (rel.startsWith('..') || path.isAbsolute(rel)) {
1363
- console.error(theme.error('Нельзя выйти за пределы: ' + sandboxRoot));
1589
+ console.error(theme.error(t('cd.outside', { v: sandboxRoot })));
1364
1590
  continue;
1365
1591
  }
1366
1592
  const stat = await fs.stat(newDir).catch(() => null);
1367
1593
  if (!stat || !stat.isDirectory()) {
1368
- console.error(theme.error('Не директория: ' + newDir));
1594
+ console.error(theme.error(t('cd.not_dir', { v: newDir })));
1369
1595
  continue;
1370
1596
  }
1371
1597
  if (newDir === currentWorkdir) {
1372
- console.log(theme.system('Уже здесь.'));
1598
+ console.log(theme.system(t('cd.already')));
1373
1599
  continue;
1374
1600
  }
1375
1601
  if (reviewMode) {
1376
- console.log(theme.system('Вышел из режима ревью (/cd).'));
1602
+ console.log(theme.system(t('cd.left_review')));
1377
1603
  reviewMode = null;
1378
1604
  }
1379
1605
  currentWorkdir = newDir;
1380
1606
  freshChatNext = true;
1381
1607
  sendSystemPromptNext = true;
1382
- console.log(theme.system('Рабочая директория: ' + newDir));
1608
+ console.log(theme.system(t('cd.changed', { v: newDir })));
1383
1609
  }
1384
1610
  catch (e) {
1385
- console.error(theme.error('Не удалось перейти: ' + e.message));
1611
+ console.error(theme.error(t('cd.failed', { v: e.message })));
1386
1612
  }
1387
1613
  continue;
1388
1614
  }
1389
1615
  if (lower.startsWith('/')) {
1390
- console.error(theme.error(`Неизвестная команда: ${trimmed}. Набери /help.`));
1616
+ console.error(theme.error(t('msg.unknown_cmd', { v: trimmed })));
1391
1617
  continue;
1392
1618
  }
1393
1619
  // ---- Обычная задача (в том числе в review-режиме) ----
@@ -1433,7 +1659,7 @@ async function main() {
1433
1659
  transcript.close();
1434
1660
  }
1435
1661
  main().catch((e) => {
1436
- console.error(theme.error('Критическая ошибка:'), e.message);
1662
+ console.error(theme.error(t('msg.critical')), e.message);
1437
1663
  if (debug)
1438
1664
  console.error(e.stack);
1439
1665
  process.exit(1);