zames_pro 1.0.3 → 1.1.1

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/src/index.js CHANGED
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import path from 'path'
3
3
  import fs from 'fs/promises'
4
- import chalk from 'chalk'
5
4
  import { fileURLToPath } from 'url'
6
- import * as readlinePromises from 'readline/promises'
5
+ import { theme } from './theme.js'
7
6
 
8
7
  import { DeepSeekBrowser } from './browser.js'
9
8
  import { createTools } from './tools.js'
@@ -61,8 +60,10 @@ const chatIdArg = getArg('--chat', null)
61
60
  // заставляет дослать его заново — например, если промпт обновился.
62
61
  const resendPrompt = hasFlag('--resend-prompt')
63
62
  // --new-chat: намеренно начать с чистого чата, игнорируя сохранённый
64
- // last-chat (полезно в watch-режиме, где по умолчанию чат восстанавливается).
65
- const newChatFlag = hasFlag('--new-chat')
63
+ // --resume-last: при старте вернуться в последний сохранённый чат.
64
+ // По умолчанию НЕ восстанавливаем — открывается новый чат.
65
+ // (Флаг --new-chat сохранён для совместимости и ничего не меняет.)
66
+ const resumeLastFlag = hasFlag('--resume-last')
66
67
 
67
68
  // ---------- last chat persistence ----------
68
69
  // Запоминаем последний открытый chat id, чтобы после перезапуска процесса
@@ -171,9 +172,9 @@ async function autoReload() {
171
172
  const { errors } = await reloadModules()
172
173
  if (errors.length) {
173
174
  console.error(
174
- chalk.yellow('⚠ авто-reload: часть модулей не загрузилась, работаю на прежней версии:'),
175
+ theme.warn('⚠ авто-reload: часть модулей не загрузилась, работаю на прежней версии:'),
175
176
  )
176
- for (const e of errors) console.error(chalk.yellow(' ' + e))
177
+ for (const e of errors) console.error(theme.warn(' ' + e))
177
178
  }
178
179
  }
179
180
 
@@ -182,13 +183,14 @@ async function autoReload() {
182
183
 
183
184
  function printHelp() {
184
185
  console.log(`
185
- ${chalk.bold('zames')} — агент поверх chat.deepseek.com через Playwright
186
+ ${theme.bold('zames')} — агент поверх chat.deepseek.com через Playwright
186
187
 
187
- ${chalk.bold('Опции CLI:')}
188
+ ${theme.bold('Опции CLI:')}
188
189
  --dir <path> рабочая директория агента
189
190
  --task <text> задача одной строкой
190
191
  --chat <id> продолжить существующий чат по id
191
- --new-chat начать новый чат (игнорировать сохранённый)
192
+ --resume-last вернуться в последний сохранённый чат
193
+ --new-chat начать новый чат (по умолчанию и так новый)
192
194
  --resend-prompt дослать system-prompt в существующий чат
193
195
  --max-iter <n> лимит итераций (по умолчанию ${config.maxIterations})
194
196
  --headless браузер без UI
@@ -198,7 +200,7 @@ ${chalk.bold('Опции CLI:')}
198
200
  --version, -v показать версию
199
201
  --help, -h эта справка
200
202
 
201
- ${chalk.bold('Обычные команды:')}
203
+ ${theme.bold('Обычные команды:')}
202
204
  /new, /clear новый чат (сброс контекста)
203
205
  /chats список последних чатов DeepSeek
204
206
  /resume <n> открыть чат №n из /chats
@@ -215,7 +217,7 @@ ${chalk.bold('Обычные команды:')}
215
217
  /help, help справка
216
218
  /exit, /quit, exit выход
217
219
 
218
- ${chalk.bold('Самообзор (отладка агента):')}
220
+ ${theme.bold('Самообзор (отладка агента):')}
219
221
  /self-review [фокус] снять снапшот src/ и запустить ревью
220
222
  после этой команды ты остаёшься В СНАПШОТЕ
221
223
  и можешь писать «исправь ошибки» и т.п.
@@ -225,11 +227,12 @@ ${chalk.bold('Самообзор (отладка агента):')}
225
227
  /self-diff <name> различия между текущим src/ и снапшотом
226
228
  /self-apply <name> применить снапшот к src/ (с бэкапом)
227
229
 
228
- ${chalk.bold('Файлы:')}
230
+ ${theme.bold('Файлы:')}
229
231
  Логи: ${config.transcript.dir}
230
232
  Undo: ~/.zames/undo
231
233
  Профиль: ~/.zames/profile
232
234
  Снапшоты: ~/.zames/snapshots
235
+ Временные: <проект>/tmp (в .gitignore, чистится при запуске)
233
236
  Конфиг: ${CONFIG_PATHS.HOME_CONFIG}
234
237
  ${CONFIG_PATHS.PROJECT_CONFIG}
235
238
  `)
@@ -239,19 +242,244 @@ function dirLabel(p) {
239
242
  return path.basename(p) || p
240
243
  }
241
244
 
245
+ // Временные файлы агента (одноразовые скрипты и т.п.) складываем в
246
+ // <проект>/tmp — эта папка в .gitignore и очищается при каждом запуске.
247
+ const TMP_DIR = path.join(__dirname, '..', 'tmp')
248
+
249
+ async function cleanTmpDir() {
250
+ try {
251
+ await fs.rm(TMP_DIR, { recursive: true, force: true })
252
+ await fs.mkdir(TMP_DIR, { recursive: true })
253
+ } catch (e) {
254
+ if (debug) console.error('tmp: не удалось очистить:', e.message)
255
+ }
256
+ }
257
+
258
+ // Ввод строки в терминале с корректной обработкой вставки (Shift+Insert,
259
+ // Ctrl+Shift+V, правая кнопка мыши и т.п.).
260
+ //
261
+ // Зачем свой ридер вместо readline:
262
+ // 1) readline отправляет строку на ПЕРВОМ переводе строки. При вставке
263
+ // многострочного текста это приводило к немедленной отправке и к тому,
264
+ // что в чат уходила только первая строка. Здесь переводы строк внутри
265
+ // вставки заменяются на пробелы, а отправка происходит только по
266
+ // одиночному нажатию Enter.
267
+ // 2) Включаем bracketed paste mode (\x1b[?2004h): терминал оборачивает
268
+ // вставленный текст в маркеры \x1b[200~ … \x1b[201~, поэтому мы точно
269
+ // знаем, что это вставка, а не набор с клавиатуры, и Enter внутри неё
270
+ // не считается отправкой.
242
271
  async function promptOnce(question) {
243
- process.stdin.resume()
244
- if (process.stdin.isTTY && process.stdin.setRawMode) {
245
- process.stdin.setRawMode(false)
272
+ const stdin = process.stdin
273
+ const stdout = process.stdout
274
+
275
+ // Не-TTY (пайп, редирект): читаем всё до EOF одной строкой.
276
+ if (!stdin.isTTY || !stdin.setRawMode) {
277
+ const chunks = []
278
+ return await new Promise((resolve) => {
279
+ const onData = (b) => chunks.push(b)
280
+ const onEnd = () => {
281
+ stdin.removeListener('data', onData)
282
+ stdin.removeListener('end', onEnd)
283
+ resolve(Buffer.concat(chunks).toString('utf-8'))
284
+ }
285
+ stdin.on('data', onData)
286
+ stdin.on('end', onEnd)
287
+ stdin.resume()
288
+ })
289
+ }
290
+
291
+ const wasRaw = stdin.isRaw
292
+ stdin.setRawMode(true)
293
+ stdin.resume()
294
+
295
+ // Bracketed paste включаем/выключаем парно.
296
+ stdout.write('\x1b[?2004h')
297
+ stdout.write(question)
298
+
299
+ let line = ''
300
+ let cursor = 0
301
+ let inPaste = false
302
+ const PASTE_START = '\x1b[200~'
303
+ const PASTE_END = '\x1b[201~'
304
+
305
+ const redraw = () => {
306
+ // Возвращаемся в начало строки, стираем и печатаем заново.
307
+ stdout.write(String.fromCharCode(13))
308
+ stdout.write('\x1b[K')
309
+ stdout.write(question + line)
310
+ // Ставим курсор в нужную позицию.
311
+ const back = line.length - cursor
312
+ if (back > 0) stdout.write('\x1b[' + back + 'D')
246
313
  }
247
- const rl = readlinePromises.createInterface({
248
- input: process.stdin,
249
- output: process.stdout,
314
+
315
+ return await new Promise((resolve) => {
316
+ const finish = (value, submit) => {
317
+ stdin.removeListener('data', onData)
318
+ stdout.write('\x1b[?2004l')
319
+ if (stdin.setRawMode) stdin.setRawMode(wasRaw || false)
320
+ if (submit) stdout.write(String.fromCharCode(10))
321
+ resolve(value)
322
+ }
323
+
324
+ const insertText = (text) => {
325
+ // Нормализуем переводы строк: они приходят от многострочной вставки,
326
+ // но означают «отправить». Внутри сообщения заменяем на пробел, чтобы
327
+ // вся вставка ушла ОДНИМ сообщением.
328
+ const clean = text
329
+ .replace(/\r\n/g, ' ')
330
+ .replace(/\r/g, ' ')
331
+ .replace(/\n/g, ' ')
332
+ line = line.slice(0, cursor) + clean + line.slice(cursor)
333
+ cursor += clean.length
334
+ }
335
+
336
+ const onData = (buf) => {
337
+ let s = buf.toString('utf-8')
338
+
339
+ // Fallback для терминалов без bracketed paste: если весь чанк — это
340
+ // «голый» перевод строки (один байт), значит нажали Enter → отправляем.
341
+ // Если переводы строк пришли ВМЕСТЕ с другим текстом в одном чанке —
342
+ // это вставка; такие переводы строк не отправляют сообщение, а
343
+ // заменяются на пробелы (см. insertText).
344
+ if (!inPaste && (s === '\r' || s === '\n')) {
345
+ return finish(line, true)
346
+ }
347
+
348
+ while (s.length) {
349
+ if (inPaste) {
350
+ const end = s.indexOf(PASTE_END)
351
+ if (end === -1) {
352
+ insertText(s)
353
+ s = ''
354
+ } else {
355
+ insertText(s.slice(0, end))
356
+ s = s.slice(end + PASTE_END.length)
357
+ inPaste = false
358
+ }
359
+ redraw()
360
+ continue
361
+ }
362
+
363
+ const start = s.indexOf(PASTE_START)
364
+ if (start !== -1) {
365
+ // Всё до маркера обрабатываем как обычный ввод.
366
+ const before = s.slice(0, start)
367
+ s = s.slice(start + PASTE_START.length)
368
+ inPaste = true
369
+ if (before) {
370
+ for (const ch of before) {
371
+ if (ch === '\r' || ch === '\n') { /* внутри вставки — пропускаем */ }
372
+ else insertText(ch)
373
+ }
374
+ redraw()
375
+ }
376
+ continue
377
+ }
378
+
379
+ const ch = s[0]
380
+ const code = s.charCodeAt(0)
381
+ s = s.slice(1)
382
+
383
+ if (ch === '\r' || ch === '\n') {
384
+ // Перевод строки внутри чанка с другим текстом (вставка без
385
+ // bracketed paste): не отправляем, а вставляем пробел.
386
+ insertText(' ')
387
+ redraw()
388
+ continue
389
+ }
390
+ if (code === 3) {
391
+ // Ctrl+C — прерываем ввод.
392
+ return finish('', true)
393
+ }
394
+ if (code === 4) {
395
+ // Ctrl+D — как отправка пустой строки.
396
+ return finish(line, true)
397
+ }
398
+ if (code === 21) {
399
+ // Ctrl+U — стереть строку.
400
+ line = ''
401
+ cursor = 0
402
+ redraw()
403
+ continue
404
+ }
405
+ if (code === 127 || code === 8) {
406
+ // Backspace.
407
+ if (cursor > 0) {
408
+ line = line.slice(0, cursor - 1) + line.slice(cursor)
409
+ cursor--
410
+ redraw()
411
+ }
412
+ continue
413
+ }
414
+ if (ch === '\x1b') {
415
+ // Escape-последовательности (стрелки, Home/End, Delete…).
416
+ const rest = s
417
+ if (rest.startsWith('[D')) {
418
+ if (cursor > 0) cursor--
419
+ s = s.slice(2)
420
+ redraw()
421
+ continue
422
+ }
423
+ if (rest.startsWith('[C')) {
424
+ if (cursor < line.length) cursor++
425
+ s = s.slice(2)
426
+ redraw()
427
+ continue
428
+ }
429
+ if (rest.startsWith('[H') || rest.startsWith('[1~')) {
430
+ cursor = 0
431
+ s = s.slice(rest.startsWith('[1~') ? 3 : 2)
432
+ redraw()
433
+ continue
434
+ }
435
+ if (rest.startsWith('[F') || rest.startsWith('[4~')) {
436
+ cursor = line.length
437
+ s = s.slice(rest.startsWith('[4~') ? 3 : 2)
438
+ redraw()
439
+ continue
440
+ }
441
+ if (rest.startsWith('[3~')) {
442
+ // Delete.
443
+ if (cursor < line.length) {
444
+ line = line.slice(0, cursor) + line.slice(cursor + 1)
445
+ redraw()
446
+ }
447
+ s = s.slice(3)
448
+ continue
449
+ }
450
+ // Прочие ESC-последовательности пропускаем до буквы/тильды.
451
+ const m = s.match(/^\[[0-9;]*[A-Za-z~]/)
452
+ if (m) s = s.slice(m[0].length)
453
+ continue
454
+ }
455
+ if (code < 32) continue // прочие управляющие символы игнорируем
456
+
457
+ insertText(ch)
458
+ redraw()
459
+ }
460
+ }
461
+
462
+ stdin.on('data', onData)
250
463
  })
251
- try {
252
- return await rl.question(question)
253
- } finally {
254
- rl.close()
464
+ }
465
+
466
+ // Слежение за клавиатурой во время работы агента.
467
+ // Esc — прервать текущую генерацию (клик Stop в браузере).
468
+ function watchEscape(onEscape) {
469
+ const stdin = process.stdin
470
+ if (!stdin.isTTY) return () => {}
471
+ const wasRaw = stdin.isRaw
472
+ if (stdin.setRawMode) stdin.setRawMode(true)
473
+ stdin.resume()
474
+ const handler = (buf) => {
475
+ if (buf.length === 1 && buf[0] === 27) {
476
+ onEscape()
477
+ }
478
+ }
479
+ stdin.on('data', handler)
480
+ return () => {
481
+ stdin.removeListener('data', handler)
482
+ if (stdin.setRawMode) stdin.setRawMode(wasRaw || false)
255
483
  }
256
484
  }
257
485
 
@@ -277,9 +505,15 @@ async function runTask(browser, tools, taskText, workdir, opts) {
277
505
  const { transcript, freshChat, sendSystemPrompt } = opts
278
506
 
279
507
  const ui = mod.createSpinner()
280
- ui.taskHeader(taskText)
281
508
  ui.thinking()
282
509
 
510
+ // Esc во время работы — прервать генерацию.
511
+ const stopWatching = watchEscape(() => {
512
+ ui.stop()
513
+ console.error(theme.warn('⏹ Esc — прерываю генерацию...'))
514
+ browser.stopGeneration().catch(() => {})
515
+ })
516
+
283
517
  let finished = false
284
518
 
285
519
  try {
@@ -299,13 +533,15 @@ async function runTask(browser, tools, taskText, workdir, opts) {
299
533
  ui.assistant(msg)
300
534
  finished = true
301
535
  },
536
+ debugLog: debug,
302
537
  })
303
538
  } catch (e) {
304
539
  ui.stop()
305
- console.error(chalk.red('\n✖ Ошибка агента:'), e.message)
540
+ console.error(theme.error('\n✖ Ошибка агента:'), e.message)
306
541
  if (debug) console.error(e.stack)
307
542
  transcript?.log('agent_error', { error: e.message })
308
543
  } finally {
544
+ stopWatching()
309
545
  if (!finished) ui.stop()
310
546
  }
311
547
  }
@@ -327,9 +563,11 @@ async function main() {
327
563
  }
328
564
 
329
565
  if (calibrate) {
330
- console.log(chalk.yellow('\n🔧 Режим калибровки селекторов\n'))
566
+ console.log(theme.warn('\n🔧 Режим калибровки селекторов\n'))
331
567
  }
332
568
 
569
+ await cleanTmpDir()
570
+
333
571
  let currentWorkdir
334
572
  try {
335
573
  currentWorkdir = await resolveWorkdir()
@@ -337,13 +575,13 @@ async function main() {
337
575
  const sandboxRoot = currentWorkdir
338
576
  } catch (e) {
339
577
  console.error(
340
- chalk.red('Не удалось определить рабочую директорию:'),
578
+ theme.error('Не удалось определить рабочую директорию:'),
341
579
  e.message,
342
580
  )
343
581
  process.exit(1)
344
582
  }
345
583
 
346
- console.log(chalk.gray(`Рабочая директория: ${currentWorkdir}`))
584
+ console.log(theme.system(`Рабочая директория: ${currentWorkdir}`))
347
585
 
348
586
  const transcript = new Transcript({
349
587
  dir: config.transcript.dir,
@@ -351,7 +589,7 @@ async function main() {
351
589
  sessionName: dirLabel(currentWorkdir),
352
590
  })
353
591
  if (transcript.file) {
354
- console.log(chalk.gray(`Транскрипт: ${transcript.file}`))
592
+ console.log(theme.system(`Транскрипт: ${transcript.file}`))
355
593
  }
356
594
 
357
595
  const undo = new UndoStore(config.undo)
@@ -372,7 +610,7 @@ async function main() {
372
610
  await browser.waitForLogin()
373
611
  } catch (e) {
374
612
  bootSpinner.stop()
375
- console.error(chalk.red('Не удалось запустить браузер:'), e.message)
613
+ console.error(theme.error('Не удалось запустить браузер:'), e.message)
376
614
  if (debug) console.error(e.stack)
377
615
  await browser.close().catch(() => {})
378
616
  transcript.close()
@@ -386,21 +624,23 @@ async function main() {
386
624
  let freshChat = true
387
625
  let sendSystemPrompt = true
388
626
 
389
- // Приоритет: явный --chat, иначе сохранённый last-chat (если не --new-chat).
627
+ // По умолчанию — новый чат. Продолжить прошлый можно явно:
628
+ // --chat <id> открыть конкретный чат
629
+ // --resume-last вернуться в последний сохранённый чат
390
630
  let resumeId = chatIdArg
391
- if (!resumeId && !newChatFlag) {
631
+ if (!resumeId && resumeLastFlag) {
392
632
  resumeId = await loadLastChat()
393
- if (resumeId) console.log(chalk.gray(`Восстанавливаю чат ${resumeId}...`))
633
+ if (resumeId) console.log(theme.system(`Восстанавливаю чат ${resumeId}...`))
394
634
  }
395
635
 
396
636
  if (resumeId) {
397
637
  try {
398
- console.log(chalk.gray(`Открываю чат ${resumeId}...`))
638
+ console.log(theme.system(`Открываю чат ${resumeId}...`))
399
639
  await browser.openChat(resumeId)
400
640
  freshChat = false
401
641
  sendSystemPrompt = resendPrompt
402
642
  } catch (e) {
403
- console.error(chalk.red(`Не удалось открыть чат: ${e.message}`))
643
+ console.error(theme.error(`Не удалось открыть чат: ${e.message}`))
404
644
  }
405
645
  }
406
646
 
@@ -418,7 +658,7 @@ async function main() {
418
658
  }
419
659
 
420
660
  console.log(
421
- chalk.gray(
661
+ theme.system(
422
662
  'Интерактивный режим. Введите задачу. Команды — /help. Выход — /exit.\n',
423
663
  ),
424
664
  )
@@ -442,42 +682,45 @@ async function main() {
442
682
  // висеть отдельным процессом после выхода агента.
443
683
  await mod.closeWeb().catch(() => {})
444
684
  transcript.close()
445
- console.log(chalk.gray('\nВыход.'))
685
+ console.log(theme.system('\nВыход.'))
446
686
  process.exit(0)
447
687
  })
448
688
 
449
- // Приоритет: явный --chat, иначе сохранённый last-chat (если не --new-chat).
689
+ // По умолчанию — новый чат. Продолжить прошлый можно явно:
690
+ // --chat <id> открыть конкретный чат
691
+ // --resume-last вернуться в последний сохранённый чат
450
692
  let resumeId = chatIdArg
451
- if (!resumeId && !newChatFlag) {
693
+ if (!resumeId && resumeLastFlag) {
452
694
  resumeId = await loadLastChat()
695
+ if (resumeId) console.log(theme.system(`Восстанавливаю чат ${resumeId}...`))
453
696
  }
454
697
 
455
698
  if (resumeId) {
456
699
  try {
457
- console.log(chalk.gray(`Открываю чат ${resumeId}...`))
700
+ console.log(theme.system(`Открываю чат ${resumeId}...`))
458
701
  await browser.openChat(resumeId)
459
702
  currentChatId = resumeId
460
703
  freshChatNext = false
461
704
  sendSystemPromptNext = resendPrompt
462
705
  await saveLastChat(resumeId)
463
- console.log(chalk.gray(`Чат открыт: ${resumeId}\n`))
706
+ console.log(theme.system(`Чат открыт: ${resumeId}\n`))
464
707
  } catch (e) {
465
- console.error(chalk.red(`Не удалось открыть чат: ${e.message}`))
708
+ console.error(theme.error(`Не удалось открыть чат: ${e.message}`))
466
709
  }
467
710
  }
468
711
 
469
712
  while (running) {
470
713
  let input
471
714
  try {
472
- let label
715
+ // Компактное приглашение: золотая стрелка + светло-голубая директория.
716
+ let tail
473
717
  if (reviewMode) {
474
- label = `zames[REVIEW:${reviewMode.snapName}]> `
475
- } else if (currentChatId) {
476
- label = `zames[${dirLabel(currentWorkdir)}|${currentChatId.slice(0, 6)}]> `
718
+ tail =
719
+ theme.warn('REVIEW') + theme.dim(':') + theme.dir(reviewMode.snapName)
477
720
  } else {
478
- label = `zames[${dirLabel(currentWorkdir)}]> `
721
+ tail = theme.dir(dirLabel(currentWorkdir))
479
722
  }
480
- input = await promptOnce(chalk.cyan(label))
723
+ input = await promptOnce(theme.prompt('❯ ') + tail + theme.dim(' › '))
481
724
  } catch {
482
725
  break
483
726
  }
@@ -495,7 +738,7 @@ async function main() {
495
738
  }
496
739
 
497
740
  if (['/new', '/clear', 'new'].includes(lower)) {
498
- console.log(chalk.gray('Создаю новый чат...'))
741
+ console.log(theme.system('Создаю новый чат...'))
499
742
  try {
500
743
  await browser.newChat()
501
744
  freshChatNext = false
@@ -503,9 +746,9 @@ async function main() {
503
746
  currentChatId = await browser.getCurrentChatId()
504
747
  await saveLastChat(currentChatId)
505
748
  transcript.log('new_chat')
506
- console.log(chalk.gray('Новый чат.\n'))
749
+ console.log(theme.system('Новый чат.\n'))
507
750
  } catch (e) {
508
- console.error(chalk.red('Не удалось создать новый чат:'), e.message)
751
+ console.error(theme.error('Не удалось создать новый чат:'), e.message)
509
752
  }
510
753
  continue
511
754
  }
@@ -544,7 +787,7 @@ async function main() {
544
787
  await saveLastChat(currentChatId)
545
788
 
546
789
  console.log(
547
- chalk.cyan(
790
+ theme.user(
548
791
  '\n💡 Теперь ты в режиме ревью. Просто пиши агенту, например:\n' +
549
792
  ' «исправь ошибки»\n' +
550
793
  ' «доработай обработку ошибок в ask()»\n' +
@@ -555,7 +798,7 @@ async function main() {
555
798
  ),
556
799
  )
557
800
  } catch (e) {
558
- console.error(chalk.red('Самообзор провалился:'), e.message)
801
+ console.error(theme.error('Самообзор провалился:'), e.message)
559
802
  if (debug) console.error(e.stack)
560
803
  }
561
804
  continue
@@ -564,7 +807,7 @@ async function main() {
564
807
  if (lower === '/self-fix' || lower.startsWith('/self-fix ')) {
565
808
  const rest = trimmed.slice('/self-fix'.length).trim()
566
809
  if (!rest) {
567
- console.error(chalk.red('Использование: /self-fix <name> [фокус]'))
810
+ console.error(theme.error('Использование: /self-fix <name> [фокус]'))
568
811
  continue
569
812
  }
570
813
  const sp = rest.indexOf(' ')
@@ -574,7 +817,7 @@ async function main() {
574
817
  const snapRoot = path.join(ZAMES_HOME, 'snapshots', name)
575
818
  const stat = await fs.stat(snapRoot).catch(() => null)
576
819
  if (!stat || !stat.isDirectory()) {
577
- console.error(chalk.red(`Снапшот не найден: ${snapRoot}`))
820
+ console.error(theme.error(`Снапшот не найден: ${snapRoot}`))
578
821
  continue
579
822
  }
580
823
 
@@ -593,7 +836,7 @@ async function main() {
593
836
  workdir: snapRoot,
594
837
  tools,
595
838
  })
596
- console.log(chalk.gray('Инициализирую review-чат для снапшота...'))
839
+ console.log(theme.system('Инициализирую review-чат для снапшота...'))
597
840
  await browser.ask(sysPrompt, { timeout: 60_000 })
598
841
 
599
842
  if (focus) {
@@ -611,7 +854,7 @@ async function main() {
611
854
  onThinking: () => ui.thinking(),
612
855
  onAssistantThought: (text) => {
613
856
  ui.stop()
614
- console.log(chalk.gray('\n💭 ' + text.slice(0, 1200) + '\n'))
857
+ console.log(theme.system('\n💭 ' + text.slice(0, 1200) + '\n'))
615
858
  },
616
859
  onToolCall: (name, toolArgs) => ui.toolCall(name, toolArgs),
617
860
  onToolResult: (r) => ui.toolResult(r),
@@ -633,19 +876,19 @@ async function main() {
633
876
  await saveLastChat(currentChatId)
634
877
 
635
878
  console.log(
636
- chalk.cyan(
879
+ theme.user(
637
880
  `\n💡 Режим ревью по снапшоту ${name}. Пиши агенту задачу или /self-done.\n`,
638
881
  ),
639
882
  )
640
883
  } catch (e) {
641
- console.error(chalk.red('Не удалось войти в снапшот:'), e.message)
884
+ console.error(theme.error('Не удалось войти в снапшот:'), e.message)
642
885
  }
643
886
  continue
644
887
  }
645
888
 
646
889
  if (lower === '/self-done') {
647
890
  if (!reviewMode) {
648
- console.log(chalk.gray('Ты и так не в режиме ревью.'))
891
+ console.log(theme.system('Ты и так не в режиме ревью.'))
649
892
  continue
650
893
  }
651
894
  const back = reviewMode.originalWorkdir
@@ -655,7 +898,7 @@ async function main() {
655
898
  freshChatNext = true
656
899
  sendSystemPromptNext = true
657
900
  console.log(
658
- chalk.gray(`Вернулся в ${back}. Следующая задача начнёт новый чат.\n`),
901
+ theme.system(`Вернулся в ${back}. Следующая задача начнёт новый чат.\n`),
659
902
  )
660
903
  continue
661
904
  }
@@ -664,7 +907,7 @@ async function main() {
664
907
  try {
665
908
  await mod.selfList({ config })
666
909
  } catch (e) {
667
- console.error(chalk.red('Ошибка:'), e.message)
910
+ console.error(theme.error('Ошибка:'), e.message)
668
911
  }
669
912
  continue
670
913
  }
@@ -672,13 +915,13 @@ async function main() {
672
915
  if (lower === '/self-diff' || lower.startsWith('/self-diff ')) {
673
916
  const name = trimmed.slice('/self-diff'.length).trim()
674
917
  if (!name) {
675
- console.error(chalk.red('Использование: /self-diff <name>'))
918
+ console.error(theme.error('Использование: /self-diff <name>'))
676
919
  continue
677
920
  }
678
921
  try {
679
922
  await mod.selfDiff({ config, name })
680
923
  } catch (e) {
681
- console.error(chalk.red('Ошибка:'), e.message)
924
+ console.error(theme.error('Ошибка:'), e.message)
682
925
  }
683
926
  continue
684
927
  }
@@ -686,13 +929,13 @@ async function main() {
686
929
  if (lower === '/self-apply' || lower.startsWith('/self-apply ')) {
687
930
  const name = trimmed.slice('/self-apply'.length).trim()
688
931
  if (!name) {
689
- console.error(chalk.red('Использование: /self-apply <name>'))
932
+ console.error(theme.error('Использование: /self-apply <name>'))
690
933
  continue
691
934
  }
692
935
  try {
693
936
  await mod.selfApply({ config, name })
694
937
  } catch (e) {
695
- console.error(chalk.red('Ошибка:'), e.message)
938
+ console.error(theme.error('Ошибка:'), e.message)
696
939
  }
697
940
  continue
698
941
  }
@@ -707,23 +950,23 @@ async function main() {
707
950
  spin.stop()
708
951
  if (!lastChats.length) {
709
952
  console.log(
710
- chalk.gray(
953
+ theme.system(
711
954
  'Чатов не найдено. Возможно, сайдбар свёрнут или селекторы устарели.',
712
955
  ),
713
956
  )
714
957
  } else {
715
- console.log(chalk.gray('Последние чаты DeepSeek:'))
958
+ console.log(theme.system('Последние чаты DeepSeek:'))
716
959
  lastChats.forEach((c, i) => {
717
960
  const n = String(i + 1).padStart(2, ' ')
718
961
  console.log(
719
- ` ${chalk.cyan(n)}. ${c.title} ${chalk.gray('(' + c.id.slice(0, 8) + '…)')}`,
962
+ ` ${theme.user(n)}. ${c.title} ${theme.system('(' + c.id.slice(0, 8) + '…)')}`,
720
963
  )
721
964
  })
722
- console.log(chalk.gray('\nИспользуй /resume <n> для продолжения.\n'))
965
+ console.log(theme.system('\nИспользуй /resume <n> для продолжения.\n'))
723
966
  }
724
967
  } catch (e) {
725
968
  spin.stop()
726
- console.error(chalk.red('Не удалось получить список:'), e.message)
969
+ console.error(theme.error('Не удалось получить список:'), e.message)
727
970
  }
728
971
  continue
729
972
  }
@@ -732,26 +975,26 @@ async function main() {
732
975
  const arg = trimmed.slice(7).trim()
733
976
  if (!arg) {
734
977
  console.error(
735
- chalk.red('Использование: /resume <n> (или /chats для списка)'),
978
+ theme.error('Использование: /resume <n> (или /chats для списка)'),
736
979
  )
737
980
  continue
738
981
  }
739
982
  const n = Number(arg)
740
983
  if (!Number.isFinite(n) || n < 1) {
741
- console.error(chalk.red('Нужен номер из /chats.'))
984
+ console.error(theme.error('Нужен номер из /chats.'))
742
985
  continue
743
986
  }
744
987
  if (!lastChats.length) {
745
- console.error(chalk.red('Сначала выполни /chats.'))
988
+ console.error(theme.error('Сначала выполни /chats.'))
746
989
  continue
747
990
  }
748
991
  const pick = lastChats[n - 1]
749
992
  if (!pick) {
750
- console.error(chalk.red(`Нет чата №${n}. Всего: ${lastChats.length}.`))
993
+ console.error(theme.error(`Нет чата №${n}. Всего: ${lastChats.length}.`))
751
994
  continue
752
995
  }
753
996
 
754
- console.log(chalk.gray(`Открываю: ${pick.title}`))
997
+ console.log(theme.system(`Открываю: ${pick.title}`))
755
998
  try {
756
999
  await browser.openChat(pick.id)
757
1000
  currentChatId = pick.id
@@ -760,94 +1003,94 @@ async function main() {
760
1003
  await saveLastChat(pick.id)
761
1004
  transcript.log('resume_chat', { id: pick.id, title: pick.title })
762
1005
  console.log(
763
- chalk.green(`Чат открыт.`) +
764
- chalk.gray(
1006
+ theme.assistant(`Чат открыт.`) +
1007
+ theme.system(
765
1008
  resendPrompt ? ' Системный промпт будет переслан на следующей задаче.\n' : ' Контекст чата сохранён. Системный промпт не пересылается (--resend-prompt чтобы дослать).\n',
766
1009
  ),
767
1010
  )
768
1011
  } catch (e) {
769
- console.error(chalk.red('Не удалось открыть чат:'), e.message)
1012
+ console.error(theme.error('Не удалось открыть чат:'), e.message)
770
1013
  }
771
1014
  continue
772
1015
  }
773
1016
 
774
1017
  if (lower === '/chat') {
775
1018
  if (currentChatId) {
776
- console.log(chalk.gray(`Текущий chat id: ${currentChatId}`))
1019
+ console.log(theme.system(`Текущий chat id: ${currentChatId}`))
777
1020
  console.log(
778
- chalk.gray(
1021
+ theme.system(
779
1022
  `URL: https://chat.deepseek.com/a/chat/s/${currentChatId}`,
780
1023
  ),
781
1024
  )
782
1025
  } else {
783
1026
  const id = await browser.getCurrentChatId()
784
1027
  console.log(
785
- chalk.gray(id ? `Текущий chat id: ${id}` : 'Чат ещё не создан.'),
1028
+ theme.system(id ? `Текущий chat id: ${id}` : 'Чат ещё не создан.'),
786
1029
  )
787
1030
  }
788
1031
  continue
789
1032
  }
790
1033
 
791
1034
  if (lower === '/pwd') {
792
- console.log(chalk.gray(currentWorkdir))
1035
+ console.log(theme.system(currentWorkdir))
793
1036
  continue
794
1037
  }
795
1038
 
796
1039
  if (lower === '/reload') {
797
- console.log(chalk.gray('Перечитываю модули логики...'))
1040
+ console.log(theme.system('Перечитываю модули логики...'))
798
1041
  try {
799
1042
  const { count, errors } = await reloadModules()
800
1043
  if (errors.length) {
801
- console.error(chalk.red('Часть модулей не перезагрузилась:'))
802
- for (const e of errors) console.error(chalk.red(' ' + e))
1044
+ console.error(theme.error('Часть модулей не перезагрузилась:'))
1045
+ for (const e of errors) console.error(theme.error(' ' + e))
803
1046
  } else {
804
1047
  console.log(
805
- chalk.green(
1048
+ theme.assistant(
806
1049
  `Перезагружено модулей: ${count}. Браузер и чат не тронуты.`,
807
1050
  ),
808
1051
  )
809
1052
  }
810
1053
  } catch (e) {
811
- console.error(chalk.red('Ошибка reload:'), e.message)
1054
+ console.error(theme.error('Ошибка reload:'), e.message)
812
1055
  }
813
1056
  continue
814
1057
  }
815
1058
 
816
1059
  if (lower === '/status') {
817
- console.log(chalk.gray(`Рабочая директория: ${currentWorkdir}`))
1060
+ console.log(theme.system(`Рабочая директория: ${currentWorkdir}`))
818
1061
  console.log(
819
- chalk.gray(`Режим ревью: ${reviewMode ? reviewMode.snapName : 'нет'}`),
1062
+ theme.system(`Режим ревью: ${reviewMode ? reviewMode.snapName : 'нет'}`),
820
1063
  )
821
1064
  if (reviewMode) {
822
1065
  console.log(
823
- chalk.gray(`Исходная директория: ${reviewMode.originalWorkdir}`),
1066
+ theme.system(`Исходная директория: ${reviewMode.originalWorkdir}`),
824
1067
  )
825
1068
  }
826
- console.log(chalk.gray(`Текущий чат: ${currentChatId || '(нет)'}`))
1069
+ console.log(theme.system(`Текущий чат: ${currentChatId || '(нет)'}`))
827
1070
  console.log(
828
- chalk.gray(
1071
+ theme.system(
829
1072
  `Fresh chat на след. задаче: ${freshChatNext ? 'да' : 'нет'}`,
830
1073
  ),
831
1074
  )
832
1075
  console.log(
833
- chalk.gray(
1076
+ theme.system(
834
1077
  `System prompt на след. задаче: ${sendSystemPromptNext ? 'да' : 'нет'}`,
835
1078
  ),
836
1079
  )
837
1080
  console.log(
838
- chalk.gray(`Resend prompt (--resend-prompt): ${resendPrompt ? 'да' : 'нет'}`),
1081
+ theme.system(`Resend prompt (--resend-prompt): ${resendPrompt ? 'да' : 'нет'}`),
839
1082
  )
840
1083
  console.log(
841
- chalk.gray(`Last chat: ${(await loadLastChat()) || 'нет'}`),
1084
+ theme.system(`Last chat: ${(await loadLastChat()) || 'нет'}`),
842
1085
  )
843
1086
  console.log(
844
- chalk.gray(`Dev mode (auto-reload): ${devMode ? 'вкл' : 'выкл'}`),
1087
+ theme.system(`Dev mode (auto-reload): ${devMode ? 'вкл' : 'выкл'}`),
845
1088
  )
846
- console.log(chalk.gray(`Лимит итераций: ${maxIter}`))
847
- console.log(chalk.gray(`Headless: ${headless ? 'да' : 'нет'}`))
848
- console.log(chalk.gray(`Debug: ${debug ? 'да' : 'нет'}`))
849
- console.log(chalk.gray(`Undo: ${config.undo.enabled ? 'вкл' : 'выкл'}`))
850
- console.log(chalk.gray(`Транскрипт: ${transcript.file || 'выкл'}`))
1089
+ console.log(theme.system(`Лимит итераций: ${maxIter}`))
1090
+ console.log(theme.system(`Headless: ${headless ? 'да' : 'нет'}`))
1091
+ console.log(theme.system(`Debug: ${debug ? 'да' : 'нет'}`))
1092
+ console.log(theme.system(`Undo: ${config.undo.enabled ? 'вкл' : 'выкл'}`))
1093
+ console.log(theme.system(`Транскрипт: ${transcript.file || 'выкл'}`))
851
1094
  continue
852
1095
  }
853
1096
 
@@ -857,7 +1100,7 @@ async function main() {
857
1100
  }
858
1101
 
859
1102
  if (lower === '/transcript') {
860
- console.log(chalk.gray(transcript.file || '(выключен)'))
1103
+ console.log(theme.system(transcript.file || '(выключен)'))
861
1104
  continue
862
1105
  }
863
1106
 
@@ -865,14 +1108,14 @@ async function main() {
865
1108
  const result = await undo.undoLast()
866
1109
  if (result.ok) {
867
1110
  console.log(
868
- chalk.green(`↶ Откатили: ${result.record.originalPath}`) +
869
- chalk.gray(
1111
+ theme.assistant(`↶ Откатили: ${result.record.originalPath}`) +
1112
+ theme.system(
870
1113
  result.record.existed ? ' (восстановлено)' : ' (удалено)',
871
1114
  ),
872
1115
  )
873
1116
  transcript.log('undo', { path: result.record.originalPath })
874
1117
  } else {
875
- console.error(chalk.red(`Не удалось откатить: ${result.reason}`))
1118
+ console.error(theme.error(`Не удалось откатить: ${result.reason}`))
876
1119
  }
877
1120
  continue
878
1121
  }
@@ -880,12 +1123,12 @@ async function main() {
880
1123
  if (lower === '/undo-list' || lower === '/history') {
881
1124
  const list = await undo.list(10)
882
1125
  if (!list.length) {
883
- console.log(chalk.gray('История пуста.'))
1126
+ console.log(theme.system('История пуста.'))
884
1127
  } else {
885
1128
  for (const r of list) {
886
1129
  const stamp = new Date(r.stamp).toLocaleString()
887
1130
  const flag = r.existed ? 'изменён' : 'создан'
888
- console.log(chalk.gray(`${stamp} [${flag}] ${r.originalPath}`))
1131
+ console.log(theme.system(`${stamp} [${flag}] ${r.originalPath}`))
889
1132
  }
890
1133
  }
891
1134
  continue
@@ -896,11 +1139,11 @@ async function main() {
896
1139
  const file = path.join(config.transcript.dir, `dom-${stamp}.html`)
897
1140
  try {
898
1141
  const result = await browser.dumpDom(file)
899
- console.log(chalk.green(`HTML сохранён: ${result.file}`))
900
- console.log(chalk.gray('Селекторы:'))
1142
+ console.log(theme.assistant(`HTML сохранён: ${result.file}`))
1143
+ console.log(theme.system('Селекторы:'))
901
1144
  console.log(JSON.stringify(result.selectors, null, 2))
902
1145
  } catch (e) {
903
- console.error(chalk.red('Не удалось сохранить DOM:'), e.message)
1146
+ console.error(theme.error('Не удалось сохранить DOM:'), e.message)
904
1147
  }
905
1148
  continue
906
1149
  }
@@ -913,34 +1156,34 @@ async function main() {
913
1156
  : sandboxRoot
914
1157
  const rel = path.relative(sandboxRoot, newDir)
915
1158
  if (rel.startsWith('..') || path.isAbsolute(rel)) {
916
- console.error(chalk.red('Нельзя выйти за пределы: ' + sandboxRoot))
1159
+ console.error(theme.error('Нельзя выйти за пределы: ' + sandboxRoot))
917
1160
  continue
918
1161
  }
919
1162
  const stat = await fs.stat(newDir).catch(() => null)
920
1163
  if (!stat || !stat.isDirectory()) {
921
- console.error(chalk.red('Не директория: ' + newDir))
1164
+ console.error(theme.error('Не директория: ' + newDir))
922
1165
  continue
923
1166
  }
924
1167
  if (newDir === currentWorkdir) {
925
- console.log(chalk.gray('Уже здесь.'))
1168
+ console.log(theme.system('Уже здесь.'))
926
1169
  continue
927
1170
  }
928
1171
  if (reviewMode) {
929
- console.log(chalk.gray('Вышел из режима ревью (/cd).'))
1172
+ console.log(theme.system('Вышел из режима ревью (/cd).'))
930
1173
  reviewMode = null
931
1174
  }
932
1175
  currentWorkdir = newDir
933
1176
  freshChatNext = true
934
1177
  sendSystemPromptNext = true
935
- console.log(chalk.gray('Рабочая директория: ' + newDir))
1178
+ console.log(theme.system('Рабочая директория: ' + newDir))
936
1179
  } catch (e) {
937
- console.error(chalk.red('Не удалось перейти: ' + e.message))
1180
+ console.error(theme.error('Не удалось перейти: ' + e.message))
938
1181
  }
939
1182
  continue
940
1183
  }
941
1184
 
942
1185
  if (lower.startsWith('/')) {
943
- console.error(chalk.red(`Неизвестная команда: ${trimmed}. Набери /help.`))
1186
+ console.error(theme.error(`Неизвестная команда: ${trimmed}. Набери /help.`))
944
1187
  continue
945
1188
  }
946
1189
 
@@ -972,7 +1215,7 @@ async function main() {
972
1215
  }
973
1216
 
974
1217
  main().catch((e) => {
975
- console.error(chalk.red('Критическая ошибка:'), e.message)
1218
+ console.error(theme.error('Критическая ошибка:'), e.message)
976
1219
  if (debug) console.error(e.stack)
977
1220
  process.exit(1)
978
1221
  })