zames_pro 1.0.2 → 1.1.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/src/index.js CHANGED
@@ -1,9 +1,9 @@
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
5
  import * as readlinePromises from 'readline/promises'
6
+ import { theme } from './theme.js'
7
7
 
8
8
  import { DeepSeekBrowser } from './browser.js'
9
9
  import { createTools } from './tools.js'
@@ -56,25 +56,152 @@ const maxIter =
56
56
  const positional = getPositional()
57
57
  const task = getArg('--task', positional.join(' ').trim() || null)
58
58
  const chatIdArg = getArg('--chat', null)
59
+ // При возобновлении существующего чата system-prompt по умолчанию НЕ
60
+ // переотправляется (он уже есть в начале чата). Флаг --resend-prompt
61
+ // заставляет дослать его заново — например, если промпт обновился.
62
+ const resendPrompt = hasFlag('--resend-prompt')
63
+ // --new-chat: намеренно начать с чистого чата, игнорируя сохранённый
64
+ // --resume-last: при старте вернуться в последний сохранённый чат.
65
+ // По умолчанию НЕ восстанавливаем — открывается новый чат.
66
+ // (Флаг --new-chat сохранён для совместимости и ничего не меняет.)
67
+ const resumeLastFlag = hasFlag('--resume-last')
68
+
69
+ // ---------- last chat persistence ----------
70
+ // Запоминаем последний открытый chat id, чтобы после перезапуска процесса
71
+ // (в т.ч. автоперезапуска через `node --watch`) автоматически вернуться
72
+ // в ту же сессию, а не создавать новый чат.
73
+ const LAST_CHAT_FILE = path.join(ZAMES_HOME, 'last-chat.json')
74
+
75
+ async function saveLastChat(id) {
76
+ if (!id) return
77
+ try {
78
+ await fs.mkdir(ZAMES_HOME, { recursive: true })
79
+ await fs.writeFile(
80
+ LAST_CHAT_FILE,
81
+ JSON.stringify({ id, updatedAt: new Date().toISOString() }, null, 2),
82
+ 'utf-8',
83
+ )
84
+ } catch (e) {
85
+ if (debug) console.error('last-chat: не удалось сохранить:', e.message)
86
+ }
87
+ }
88
+
89
+ async function loadLastChat() {
90
+ try {
91
+ const raw = await fs.readFile(LAST_CHAT_FILE, 'utf-8')
92
+ const data = JSON.parse(raw)
93
+ return data && typeof data.id === 'string' ? data.id : null
94
+ } catch {
95
+ return null
96
+ }
97
+ }
98
+
99
+
100
+ // ---------- hot reload ----------
101
+ // Держим ссылки на модули логики в объекте mod. Команда /reload перечитывает
102
+ // их через динамический import с timestamp-query — Node кэширует ESM по URL,
103
+ // поэтому такой import вернёт свежую версию модуля. Браузер, чат и текущее
104
+ // состояние НЕ перезапускаются: обновляется только логика.
105
+ const RELOADABLE = [
106
+ './tools.js',
107
+ './agent-loop.js',
108
+ './system-prompt.js',
109
+ './gitTools.js',
110
+ './web.js',
111
+ './self-review.js',
112
+ './diff.js',
113
+ './undo.js',
114
+ './confirm.js',
115
+ './transcript.js',
116
+ './spinner.js',
117
+ './config.js',
118
+ ]
119
+
120
+ const mod = {
121
+ createTools,
122
+ runAgentLoop,
123
+ buildSystemPrompt: null, // подгрузим ниже
124
+ selfReview,
125
+ selfDiff,
126
+ selfApply,
127
+ selfList,
128
+ closeWeb,
129
+ createSpinner,
130
+ }
131
+
132
+ async function reloadModules() {
133
+ const stamp = Date.now()
134
+ const loaded = new Map()
135
+ const errors = []
136
+ for (const rel of RELOADABLE) {
137
+ try {
138
+ const url = new URL(rel, import.meta.url)
139
+ url.searchParams.set('t', String(stamp))
140
+ const m = await import(url.href)
141
+ loaded.set(rel, m)
142
+ } catch (e) {
143
+ errors.push(`${rel}: ${e.message}`)
144
+ }
145
+ }
146
+
147
+ const pick = (rel, name) => loaded.get(rel)?.[name]
148
+
149
+ if (pick('./tools.js', 'createTools')) mod.createTools = pick('./tools.js', 'createTools')
150
+ if (pick('./agent-loop.js', 'runAgentLoop')) mod.runAgentLoop = pick('./agent-loop.js', 'runAgentLoop')
151
+ if (pick('./system-prompt.js', 'buildSystemPrompt')) mod.buildSystemPrompt = pick('./system-prompt.js', 'buildSystemPrompt')
152
+ if (pick('./self-review.js', 'selfReview')) mod.selfReview = pick('./self-review.js', 'selfReview')
153
+ if (pick('./self-review.js', 'selfDiff')) mod.selfDiff = pick('./self-review.js', 'selfDiff')
154
+ if (pick('./self-review.js', 'selfApply')) mod.selfApply = pick('./self-review.js', 'selfApply')
155
+ if (pick('./self-review.js', 'selfList')) mod.selfList = pick('./self-review.js', 'selfList')
156
+ if (pick('./web.js', 'closeWeb')) mod.closeWeb = pick('./web.js', 'closeWeb')
157
+ if (pick('./spinner.js', 'createSpinner')) mod.createSpinner = pick('./spinner.js', 'createSpinner')
158
+
159
+ return { count: loaded.size, errors }
160
+ }
161
+
162
+ // Первичная загрузка, чтобы mod.buildSystemPrompt и остальные были заполнены.
163
+ await reloadModules()
164
+
165
+ // Dev-режим: авто-перечитывание модулей логики перед каждой задачей.
166
+ // Включается флагом --dev (его ставит `npm run dev`) или config.hotReload === true.
167
+ // В обычном режиме (npm start, глобальный zames) авто-reload выключен.
168
+ const devMode = hasFlag('--dev') || config.hotReload === true
169
+
170
+ // Безопасный авто-reload: при ошибке загрузки оставляем прошлые рабочие модули.
171
+ async function autoReload() {
172
+ if (!devMode) return
173
+ const { errors } = await reloadModules()
174
+ if (errors.length) {
175
+ console.error(
176
+ theme.warn('⚠ авто-reload: часть модулей не загрузилась, работаю на прежней версии:'),
177
+ )
178
+ for (const e of errors) console.error(theme.warn(' ' + e))
179
+ }
180
+ }
59
181
 
60
182
 
61
183
  // ---------- helpers ----------
62
184
 
63
185
  function printHelp() {
64
186
  console.log(`
65
- ${chalk.bold('zames')} — агент поверх chat.deepseek.com через Playwright
187
+ ${theme.bold('zames')} — агент поверх chat.deepseek.com через Playwright
66
188
 
67
- ${chalk.bold('Опции CLI:')}
189
+ ${theme.bold('Опции CLI:')}
68
190
  --dir <path> рабочая директория агента
69
191
  --task <text> задача одной строкой
70
192
  --chat <id> продолжить существующий чат по id
193
+ --resume-last вернуться в последний сохранённый чат
194
+ --new-chat начать новый чат (по умолчанию и так новый)
195
+ --resend-prompt дослать system-prompt в существующий чат
71
196
  --max-iter <n> лимит итераций (по умолчанию ${config.maxIterations})
72
197
  --headless браузер без UI
73
198
  --debug подробный лог
74
199
  --calibrate режим калибровки селекторов
200
+ --dev режим разработки: авто-перечитывание модулей
201
+ --version, -v показать версию
75
202
  --help, -h эта справка
76
203
 
77
- ${chalk.bold('Обычные команды:')}
204
+ ${theme.bold('Обычные команды:')}
78
205
  /new, /clear новый чат (сброс контекста)
79
206
  /chats список последних чатов DeepSeek
80
207
  /resume <n> открыть чат №n из /chats
@@ -82,6 +209,7 @@ ${chalk.bold('Обычные команды:')}
82
209
  /cd <path> сменить рабочую директорию
83
210
  /pwd текущая директория
84
211
  /status состояние сессии
212
+ /reload перечитать модули логики без перезапуска
85
213
  /undo откатить последнюю запись/правку
86
214
  /undo-list список того, что можно откатить
87
215
  /transcript путь к файлу транскрипта
@@ -90,7 +218,7 @@ ${chalk.bold('Обычные команды:')}
90
218
  /help, help справка
91
219
  /exit, /quit, exit выход
92
220
 
93
- ${chalk.bold('Самообзор (отладка агента):')}
221
+ ${theme.bold('Самообзор (отладка агента):')}
94
222
  /self-review [фокус] снять снапшот src/ и запустить ревью
95
223
  после этой команды ты остаёшься В СНАПШОТЕ
96
224
  и можешь писать «исправь ошибки» и т.п.
@@ -100,11 +228,12 @@ ${chalk.bold('Самообзор (отладка агента):')}
100
228
  /self-diff <name> различия между текущим src/ и снапшотом
101
229
  /self-apply <name> применить снапшот к src/ (с бэкапом)
102
230
 
103
- ${chalk.bold('Файлы:')}
231
+ ${theme.bold('Файлы:')}
104
232
  Логи: ${config.transcript.dir}
105
233
  Undo: ~/.zames/undo
106
234
  Профиль: ~/.zames/profile
107
235
  Снапшоты: ~/.zames/snapshots
236
+ Временные: <проект>/tmp (в .gitignore, чистится при запуске)
108
237
  Конфиг: ${CONFIG_PATHS.HOME_CONFIG}
109
238
  ${CONFIG_PATHS.PROJECT_CONFIG}
110
239
  `)
@@ -114,6 +243,19 @@ function dirLabel(p) {
114
243
  return path.basename(p) || p
115
244
  }
116
245
 
246
+ // Временные файлы агента (одноразовые скрипты и т.п.) складываем в
247
+ // <проект>/tmp — эта папка в .gitignore и очищается при каждом запуске.
248
+ const TMP_DIR = path.join(__dirname, '..', 'tmp')
249
+
250
+ async function cleanTmpDir() {
251
+ try {
252
+ await fs.rm(TMP_DIR, { recursive: true, force: true })
253
+ await fs.mkdir(TMP_DIR, { recursive: true })
254
+ } catch (e) {
255
+ if (debug) console.error('tmp: не удалось очистить:', e.message)
256
+ }
257
+ }
258
+
117
259
  async function promptOnce(question) {
118
260
  process.stdin.resume()
119
261
  if (process.stdin.isTTY && process.stdin.setRawMode) {
@@ -130,6 +272,26 @@ async function promptOnce(question) {
130
272
  }
131
273
  }
132
274
 
275
+ // Слежение за клавиатурой во время работы агента.
276
+ // Esc — прервать текущую генерацию (клик Stop в браузере).
277
+ function watchEscape(onEscape) {
278
+ const stdin = process.stdin
279
+ if (!stdin.isTTY) return () => {}
280
+ const wasRaw = stdin.isRaw
281
+ if (stdin.setRawMode) stdin.setRawMode(true)
282
+ stdin.resume()
283
+ const handler = (buf) => {
284
+ if (buf.length === 1 && buf[0] === 27) {
285
+ onEscape()
286
+ }
287
+ }
288
+ stdin.on('data', handler)
289
+ return () => {
290
+ stdin.removeListener('data', handler)
291
+ if (stdin.setRawMode) stdin.setRawMode(wasRaw || false)
292
+ }
293
+ }
294
+
133
295
  // ---------- workdir resolution ----------
134
296
 
135
297
  // ---------- workdir resolution ----------
@@ -151,14 +313,20 @@ async function resolveWorkdir() {
151
313
  async function runTask(browser, tools, taskText, workdir, opts) {
152
314
  const { transcript, freshChat, sendSystemPrompt } = opts
153
315
 
154
- const ui = createSpinner()
155
- ui.taskHeader(taskText)
316
+ const ui = mod.createSpinner()
156
317
  ui.thinking()
157
318
 
319
+ // Esc во время работы — прервать генерацию.
320
+ const stopWatching = watchEscape(() => {
321
+ ui.stop()
322
+ console.error(theme.warn('⏹ Esc — прерываю генерацию...'))
323
+ browser.stopGeneration().catch(() => {})
324
+ })
325
+
158
326
  let finished = false
159
327
 
160
328
  try {
161
- await runAgentLoop({
329
+ await mod.runAgentLoop({
162
330
  browser,
163
331
  tools,
164
332
  task: taskText,
@@ -174,13 +342,15 @@ async function runTask(browser, tools, taskText, workdir, opts) {
174
342
  ui.assistant(msg)
175
343
  finished = true
176
344
  },
345
+ debugLog: debug,
177
346
  })
178
347
  } catch (e) {
179
348
  ui.stop()
180
- console.error(chalk.red('\n✖ Ошибка агента:'), e.message)
349
+ console.error(theme.error('\n✖ Ошибка агента:'), e.message)
181
350
  if (debug) console.error(e.stack)
182
351
  transcript?.log('agent_error', { error: e.message })
183
352
  } finally {
353
+ stopWatching()
184
354
  if (!finished) ui.stop()
185
355
  }
186
356
  }
@@ -188,15 +358,25 @@ async function runTask(browser, tools, taskText, workdir, opts) {
188
358
  // ---------- main ----------
189
359
 
190
360
  async function main() {
361
+ if (hasFlag('--version') || hasFlag('-v')) {
362
+ const pkg = JSON.parse(
363
+ await fs.readFile(path.join(__dirname, '..', 'package.json'), 'utf-8'),
364
+ )
365
+ console.log(pkg.version)
366
+ return
367
+ }
368
+
191
369
  if (hasFlag('--help') || hasFlag('-h')) {
192
370
  printHelp()
193
371
  return
194
372
  }
195
373
 
196
374
  if (calibrate) {
197
- console.log(chalk.yellow('\n🔧 Режим калибровки селекторов\n'))
375
+ console.log(theme.warn('\n🔧 Режим калибровки селекторов\n'))
198
376
  }
199
377
 
378
+ await cleanTmpDir()
379
+
200
380
  let currentWorkdir
201
381
  try {
202
382
  currentWorkdir = await resolveWorkdir()
@@ -204,13 +384,13 @@ async function main() {
204
384
  const sandboxRoot = currentWorkdir
205
385
  } catch (e) {
206
386
  console.error(
207
- chalk.red('Не удалось определить рабочую директорию:'),
387
+ theme.error('Не удалось определить рабочую директорию:'),
208
388
  e.message,
209
389
  )
210
390
  process.exit(1)
211
391
  }
212
392
 
213
- console.log(chalk.gray(`Рабочая директория: ${currentWorkdir}`))
393
+ console.log(theme.system(`Рабочая директория: ${currentWorkdir}`))
214
394
 
215
395
  const transcript = new Transcript({
216
396
  dir: config.transcript.dir,
@@ -218,7 +398,7 @@ async function main() {
218
398
  sessionName: dirLabel(currentWorkdir),
219
399
  })
220
400
  if (transcript.file) {
221
- console.log(chalk.gray(`Транскрипт: ${transcript.file}`))
401
+ console.log(theme.system(`Транскрипт: ${transcript.file}`))
222
402
  }
223
403
 
224
404
  const undo = new UndoStore(config.undo)
@@ -230,7 +410,7 @@ async function main() {
230
410
  ...config.browser,
231
411
  })
232
412
 
233
- const bootSpinner = createSpinner()
413
+ const bootSpinner = mod.createSpinner()
234
414
  bootSpinner.thinking()
235
415
 
236
416
  try {
@@ -239,7 +419,7 @@ async function main() {
239
419
  await browser.waitForLogin()
240
420
  } catch (e) {
241
421
  bootSpinner.stop()
242
- console.error(chalk.red('Не удалось запустить браузер:'), e.message)
422
+ console.error(theme.error('Не удалось запустить браузер:'), e.message)
243
423
  if (debug) console.error(e.stack)
244
424
  await browser.close().catch(() => {})
245
425
  transcript.close()
@@ -248,35 +428,46 @@ async function main() {
248
428
 
249
429
  // Разовый режим
250
430
  if (task) {
251
- const tools = createTools(currentWorkdir, { undo })
431
+ const tools = mod.createTools(currentWorkdir, { undo })
252
432
 
253
433
  let freshChat = true
254
434
  let sendSystemPrompt = true
255
435
 
256
- if (chatIdArg) {
436
+ // По умолчанию — новый чат. Продолжить прошлый можно явно:
437
+ // --chat <id> открыть конкретный чат
438
+ // --resume-last вернуться в последний сохранённый чат
439
+ let resumeId = chatIdArg
440
+ if (!resumeId && resumeLastFlag) {
441
+ resumeId = await loadLastChat()
442
+ if (resumeId) console.log(theme.system(`Восстанавливаю чат ${resumeId}...`))
443
+ }
444
+
445
+ if (resumeId) {
257
446
  try {
258
- console.log(chalk.gray(`Открываю чат ${chatIdArg}...`))
259
- await browser.openChat(chatIdArg)
447
+ console.log(theme.system(`Открываю чат ${resumeId}...`))
448
+ await browser.openChat(resumeId)
260
449
  freshChat = false
261
- sendSystemPrompt = true
450
+ sendSystemPrompt = resendPrompt
262
451
  } catch (e) {
263
- console.error(chalk.red(`Не удалось открыть чат: ${e.message}`))
452
+ console.error(theme.error(`Не удалось открыть чат: ${e.message}`))
264
453
  }
265
454
  }
266
455
 
456
+ await autoReload()
457
+
267
458
  await runTask(browser, tools, task, currentWorkdir, {
268
459
  transcript,
269
460
  freshChat,
270
461
  sendSystemPrompt,
271
462
  })
272
463
  await browser.close()
273
- await closeWeb().catch(() => {})
464
+ await mod.closeWeb().catch(() => {})
274
465
  transcript.close()
275
466
  return
276
467
  }
277
468
 
278
469
  console.log(
279
- chalk.gray(
470
+ theme.system(
280
471
  'Интерактивный режим. Введите задачу. Команды — /help. Выход — /exit.\n',
281
472
  ),
282
473
  )
@@ -298,37 +489,47 @@ async function main() {
298
489
  await browser.close().catch(() => {})
299
490
  // Закрываем ленивый headless-браузер из web.js, иначе он останется
300
491
  // висеть отдельным процессом после выхода агента.
301
- await closeWeb().catch(() => {})
492
+ await mod.closeWeb().catch(() => {})
302
493
  transcript.close()
303
- console.log(chalk.gray('\nВыход.'))
494
+ console.log(theme.system('\nВыход.'))
304
495
  process.exit(0)
305
496
  })
306
497
 
307
- if (chatIdArg) {
498
+ // По умолчанию — новый чат. Продолжить прошлый можно явно:
499
+ // --chat <id> открыть конкретный чат
500
+ // --resume-last вернуться в последний сохранённый чат
501
+ let resumeId = chatIdArg
502
+ if (!resumeId && resumeLastFlag) {
503
+ resumeId = await loadLastChat()
504
+ if (resumeId) console.log(theme.system(`Восстанавливаю чат ${resumeId}...`))
505
+ }
506
+
507
+ if (resumeId) {
308
508
  try {
309
- console.log(chalk.gray(`Открываю чат ${chatIdArg}...`))
310
- await browser.openChat(chatIdArg)
311
- currentChatId = chatIdArg
509
+ console.log(theme.system(`Открываю чат ${resumeId}...`))
510
+ await browser.openChat(resumeId)
511
+ currentChatId = resumeId
312
512
  freshChatNext = false
313
- sendSystemPromptNext = true
314
- console.log(chalk.gray(`Чат открыт: ${chatIdArg}\n`))
513
+ sendSystemPromptNext = resendPrompt
514
+ await saveLastChat(resumeId)
515
+ console.log(theme.system(`Чат открыт: ${resumeId}\n`))
315
516
  } catch (e) {
316
- console.error(chalk.red(`Не удалось открыть чат: ${e.message}`))
517
+ console.error(theme.error(`Не удалось открыть чат: ${e.message}`))
317
518
  }
318
519
  }
319
520
 
320
521
  while (running) {
321
522
  let input
322
523
  try {
323
- let label
524
+ // Компактное приглашение: золотая стрелка + светло-голубая директория.
525
+ let tail
324
526
  if (reviewMode) {
325
- label = `zames[REVIEW:${reviewMode.snapName}]> `
326
- } else if (currentChatId) {
327
- label = `zames[${dirLabel(currentWorkdir)}|${currentChatId.slice(0, 6)}]> `
527
+ tail =
528
+ theme.warn('REVIEW') + theme.dim(':') + theme.dir(reviewMode.snapName)
328
529
  } else {
329
- label = `zames[${dirLabel(currentWorkdir)}]> `
530
+ tail = theme.dir(dirLabel(currentWorkdir))
330
531
  }
331
- input = await promptOnce(chalk.cyan(label))
532
+ input = await promptOnce(theme.prompt('❯ ') + tail + theme.dim(' › '))
332
533
  } catch {
333
534
  break
334
535
  }
@@ -346,16 +547,17 @@ async function main() {
346
547
  }
347
548
 
348
549
  if (['/new', '/clear', 'new'].includes(lower)) {
349
- console.log(chalk.gray('Создаю новый чат...'))
550
+ console.log(theme.system('Создаю новый чат...'))
350
551
  try {
351
552
  await browser.newChat()
352
553
  freshChatNext = false
353
554
  sendSystemPromptNext = true
354
555
  currentChatId = await browser.getCurrentChatId()
556
+ await saveLastChat(currentChatId)
355
557
  transcript.log('new_chat')
356
- console.log(chalk.gray('Новый чат.\n'))
558
+ console.log(theme.system('Новый чат.\n'))
357
559
  } catch (e) {
358
- console.error(chalk.red('Не удалось создать новый чат:'), e.message)
560
+ console.error(theme.error('Не удалось создать новый чат:'), e.message)
359
561
  }
360
562
  continue
361
563
  }
@@ -371,7 +573,7 @@ async function main() {
371
573
  : currentWorkdir
372
574
 
373
575
  try {
374
- const result = await selfReview({
576
+ const result = await mod.selfReview({
375
577
  browser,
376
578
  config,
377
579
  focus: focus || null,
@@ -391,9 +593,10 @@ async function main() {
391
593
  freshChatNext = false
392
594
  sendSystemPromptNext = false
393
595
  currentChatId = await browser.getCurrentChatId()
596
+ await saveLastChat(currentChatId)
394
597
 
395
598
  console.log(
396
- chalk.cyan(
599
+ theme.user(
397
600
  '\n💡 Теперь ты в режиме ревью. Просто пиши агенту, например:\n' +
398
601
  ' «исправь ошибки»\n' +
399
602
  ' «доработай обработку ошибок в ask()»\n' +
@@ -404,7 +607,7 @@ async function main() {
404
607
  ),
405
608
  )
406
609
  } catch (e) {
407
- console.error(chalk.red('Самообзор провалился:'), e.message)
610
+ console.error(theme.error('Самообзор провалился:'), e.message)
408
611
  if (debug) console.error(e.stack)
409
612
  }
410
613
  continue
@@ -413,7 +616,7 @@ async function main() {
413
616
  if (lower === '/self-fix' || lower.startsWith('/self-fix ')) {
414
617
  const rest = trimmed.slice('/self-fix'.length).trim()
415
618
  if (!rest) {
416
- console.error(chalk.red('Использование: /self-fix <name> [фокус]'))
619
+ console.error(theme.error('Использование: /self-fix <name> [фокус]'))
417
620
  continue
418
621
  }
419
622
  const sp = rest.indexOf(' ')
@@ -423,7 +626,7 @@ async function main() {
423
626
  const snapRoot = path.join(ZAMES_HOME, 'snapshots', name)
424
627
  const stat = await fs.stat(snapRoot).catch(() => null)
425
628
  if (!stat || !stat.isDirectory()) {
426
- console.error(chalk.red(`Снапшот не найден: ${snapRoot}`))
629
+ console.error(theme.error(`Снапшот не найден: ${snapRoot}`))
427
630
  continue
428
631
  }
429
632
 
@@ -435,18 +638,18 @@ async function main() {
435
638
  try {
436
639
  const { runAgentLoop: ral } = await import('./agent-loop.js')
437
640
  const { buildSystemPrompt } = await import('./system-prompt.js')
438
- const tools = createTools(snapRoot, { undo: null })
641
+ const tools = mod.createTools(snapRoot, { undo: null })
439
642
 
440
643
  await browser.newChat()
441
- const sysPrompt = buildSystemPrompt({
644
+ const sysPrompt = mod.buildSystemPrompt({
442
645
  workdir: snapRoot,
443
646
  tools,
444
647
  })
445
- console.log(chalk.gray('Инициализирую review-чат для снапшота...'))
648
+ console.log(theme.system('Инициализирую review-чат для снапшота...'))
446
649
  await browser.ask(sysPrompt, { timeout: 60_000 })
447
650
 
448
651
  if (focus) {
449
- const ui = createSpinner()
652
+ const ui = mod.createSpinner()
450
653
  ui.thinking()
451
654
  await ral({
452
655
  browser,
@@ -460,7 +663,7 @@ async function main() {
460
663
  onThinking: () => ui.thinking(),
461
664
  onAssistantThought: (text) => {
462
665
  ui.stop()
463
- console.log(chalk.gray('\n💭 ' + text.slice(0, 1200) + '\n'))
666
+ console.log(theme.system('\n💭 ' + text.slice(0, 1200) + '\n'))
464
667
  },
465
668
  onToolCall: (name, toolArgs) => ui.toolCall(name, toolArgs),
466
669
  onToolResult: (r) => ui.toolResult(r),
@@ -479,21 +682,22 @@ async function main() {
479
682
  freshChatNext = false
480
683
  sendSystemPromptNext = false
481
684
  currentChatId = await browser.getCurrentChatId()
685
+ await saveLastChat(currentChatId)
482
686
 
483
687
  console.log(
484
- chalk.cyan(
688
+ theme.user(
485
689
  `\n💡 Режим ревью по снапшоту ${name}. Пиши агенту задачу или /self-done.\n`,
486
690
  ),
487
691
  )
488
692
  } catch (e) {
489
- console.error(chalk.red('Не удалось войти в снапшот:'), e.message)
693
+ console.error(theme.error('Не удалось войти в снапшот:'), e.message)
490
694
  }
491
695
  continue
492
696
  }
493
697
 
494
698
  if (lower === '/self-done') {
495
699
  if (!reviewMode) {
496
- console.log(chalk.gray('Ты и так не в режиме ревью.'))
700
+ console.log(theme.system('Ты и так не в режиме ревью.'))
497
701
  continue
498
702
  }
499
703
  const back = reviewMode.originalWorkdir
@@ -503,16 +707,16 @@ async function main() {
503
707
  freshChatNext = true
504
708
  sendSystemPromptNext = true
505
709
  console.log(
506
- chalk.gray(`Вернулся в ${back}. Следующая задача начнёт новый чат.\n`),
710
+ theme.system(`Вернулся в ${back}. Следующая задача начнёт новый чат.\n`),
507
711
  )
508
712
  continue
509
713
  }
510
714
 
511
715
  if (lower === '/self-list') {
512
716
  try {
513
- await selfList({ config })
717
+ await mod.selfList({ config })
514
718
  } catch (e) {
515
- console.error(chalk.red('Ошибка:'), e.message)
719
+ console.error(theme.error('Ошибка:'), e.message)
516
720
  }
517
721
  continue
518
722
  }
@@ -520,13 +724,13 @@ async function main() {
520
724
  if (lower === '/self-diff' || lower.startsWith('/self-diff ')) {
521
725
  const name = trimmed.slice('/self-diff'.length).trim()
522
726
  if (!name) {
523
- console.error(chalk.red('Использование: /self-diff <name>'))
727
+ console.error(theme.error('Использование: /self-diff <name>'))
524
728
  continue
525
729
  }
526
730
  try {
527
- await selfDiff({ config, name })
731
+ await mod.selfDiff({ config, name })
528
732
  } catch (e) {
529
- console.error(chalk.red('Ошибка:'), e.message)
733
+ console.error(theme.error('Ошибка:'), e.message)
530
734
  }
531
735
  continue
532
736
  }
@@ -534,13 +738,13 @@ async function main() {
534
738
  if (lower === '/self-apply' || lower.startsWith('/self-apply ')) {
535
739
  const name = trimmed.slice('/self-apply'.length).trim()
536
740
  if (!name) {
537
- console.error(chalk.red('Использование: /self-apply <name>'))
741
+ console.error(theme.error('Использование: /self-apply <name>'))
538
742
  continue
539
743
  }
540
744
  try {
541
- await selfApply({ config, name })
745
+ await mod.selfApply({ config, name })
542
746
  } catch (e) {
543
- console.error(chalk.red('Ошибка:'), e.message)
747
+ console.error(theme.error('Ошибка:'), e.message)
544
748
  }
545
749
  continue
546
750
  }
@@ -548,30 +752,30 @@ async function main() {
548
752
  // ---------- Обычные команды ----------
549
753
 
550
754
  if (lower === '/chats') {
551
- const spin = createSpinner()
755
+ const spin = mod.createSpinner()
552
756
  spin.thinking()
553
757
  try {
554
758
  lastChats = await browser.listChats(30)
555
759
  spin.stop()
556
760
  if (!lastChats.length) {
557
761
  console.log(
558
- chalk.gray(
762
+ theme.system(
559
763
  'Чатов не найдено. Возможно, сайдбар свёрнут или селекторы устарели.',
560
764
  ),
561
765
  )
562
766
  } else {
563
- console.log(chalk.gray('Последние чаты DeepSeek:'))
767
+ console.log(theme.system('Последние чаты DeepSeek:'))
564
768
  lastChats.forEach((c, i) => {
565
769
  const n = String(i + 1).padStart(2, ' ')
566
770
  console.log(
567
- ` ${chalk.cyan(n)}. ${c.title} ${chalk.gray('(' + c.id.slice(0, 8) + '…)')}`,
771
+ ` ${theme.user(n)}. ${c.title} ${theme.system('(' + c.id.slice(0, 8) + '…)')}`,
568
772
  )
569
773
  })
570
- console.log(chalk.gray('\nИспользуй /resume <n> для продолжения.\n'))
774
+ console.log(theme.system('\nИспользуй /resume <n> для продолжения.\n'))
571
775
  }
572
776
  } catch (e) {
573
777
  spin.stop()
574
- console.error(chalk.red('Не удалось получить список:'), e.message)
778
+ console.error(theme.error('Не удалось получить список:'), e.message)
575
779
  }
576
780
  continue
577
781
  }
@@ -580,92 +784,122 @@ async function main() {
580
784
  const arg = trimmed.slice(7).trim()
581
785
  if (!arg) {
582
786
  console.error(
583
- chalk.red('Использование: /resume <n> (или /chats для списка)'),
787
+ theme.error('Использование: /resume <n> (или /chats для списка)'),
584
788
  )
585
789
  continue
586
790
  }
587
791
  const n = Number(arg)
588
792
  if (!Number.isFinite(n) || n < 1) {
589
- console.error(chalk.red('Нужен номер из /chats.'))
793
+ console.error(theme.error('Нужен номер из /chats.'))
590
794
  continue
591
795
  }
592
796
  if (!lastChats.length) {
593
- console.error(chalk.red('Сначала выполни /chats.'))
797
+ console.error(theme.error('Сначала выполни /chats.'))
594
798
  continue
595
799
  }
596
800
  const pick = lastChats[n - 1]
597
801
  if (!pick) {
598
- console.error(chalk.red(`Нет чата №${n}. Всего: ${lastChats.length}.`))
802
+ console.error(theme.error(`Нет чата №${n}. Всего: ${lastChats.length}.`))
599
803
  continue
600
804
  }
601
805
 
602
- console.log(chalk.gray(`Открываю: ${pick.title}`))
806
+ console.log(theme.system(`Открываю: ${pick.title}`))
603
807
  try {
604
808
  await browser.openChat(pick.id)
605
809
  currentChatId = pick.id
606
810
  freshChatNext = false
607
- sendSystemPromptNext = true
811
+ sendSystemPromptNext = resendPrompt
812
+ await saveLastChat(pick.id)
608
813
  transcript.log('resume_chat', { id: pick.id, title: pick.title })
609
814
  console.log(
610
- chalk.green(`Чат открыт.`) +
611
- chalk.gray(
612
- ' Системный промпт будет переслан на следующей задаче.\n',
815
+ theme.assistant(`Чат открыт.`) +
816
+ theme.system(
817
+ resendPrompt ? ' Системный промпт будет переслан на следующей задаче.\n' : ' Контекст чата сохранён. Системный промпт не пересылается (--resend-prompt чтобы дослать).\n',
613
818
  ),
614
819
  )
615
820
  } catch (e) {
616
- console.error(chalk.red('Не удалось открыть чат:'), e.message)
821
+ console.error(theme.error('Не удалось открыть чат:'), e.message)
617
822
  }
618
823
  continue
619
824
  }
620
825
 
621
826
  if (lower === '/chat') {
622
827
  if (currentChatId) {
623
- console.log(chalk.gray(`Текущий chat id: ${currentChatId}`))
828
+ console.log(theme.system(`Текущий chat id: ${currentChatId}`))
624
829
  console.log(
625
- chalk.gray(
830
+ theme.system(
626
831
  `URL: https://chat.deepseek.com/a/chat/s/${currentChatId}`,
627
832
  ),
628
833
  )
629
834
  } else {
630
835
  const id = await browser.getCurrentChatId()
631
836
  console.log(
632
- chalk.gray(id ? `Текущий chat id: ${id}` : 'Чат ещё не создан.'),
837
+ theme.system(id ? `Текущий chat id: ${id}` : 'Чат ещё не создан.'),
633
838
  )
634
839
  }
635
840
  continue
636
841
  }
637
842
 
638
843
  if (lower === '/pwd') {
639
- console.log(chalk.gray(currentWorkdir))
844
+ console.log(theme.system(currentWorkdir))
845
+ continue
846
+ }
847
+
848
+ if (lower === '/reload') {
849
+ console.log(theme.system('Перечитываю модули логики...'))
850
+ try {
851
+ const { count, errors } = await reloadModules()
852
+ if (errors.length) {
853
+ console.error(theme.error('Часть модулей не перезагрузилась:'))
854
+ for (const e of errors) console.error(theme.error(' ' + e))
855
+ } else {
856
+ console.log(
857
+ theme.assistant(
858
+ `Перезагружено модулей: ${count}. Браузер и чат не тронуты.`,
859
+ ),
860
+ )
861
+ }
862
+ } catch (e) {
863
+ console.error(theme.error('Ошибка reload:'), e.message)
864
+ }
640
865
  continue
641
866
  }
642
867
 
643
868
  if (lower === '/status') {
644
- console.log(chalk.gray(`Рабочая директория: ${currentWorkdir}`))
869
+ console.log(theme.system(`Рабочая директория: ${currentWorkdir}`))
645
870
  console.log(
646
- chalk.gray(`Режим ревью: ${reviewMode ? reviewMode.snapName : 'нет'}`),
871
+ theme.system(`Режим ревью: ${reviewMode ? reviewMode.snapName : 'нет'}`),
647
872
  )
648
873
  if (reviewMode) {
649
874
  console.log(
650
- chalk.gray(`Исходная директория: ${reviewMode.originalWorkdir}`),
875
+ theme.system(`Исходная директория: ${reviewMode.originalWorkdir}`),
651
876
  )
652
877
  }
653
- console.log(chalk.gray(`Текущий чат: ${currentChatId || '(нет)'}`))
878
+ console.log(theme.system(`Текущий чат: ${currentChatId || '(нет)'}`))
654
879
  console.log(
655
- chalk.gray(
880
+ theme.system(
656
881
  `Fresh chat на след. задаче: ${freshChatNext ? 'да' : 'нет'}`,
657
882
  ),
658
883
  )
659
884
  console.log(
660
- chalk.gray(
885
+ theme.system(
661
886
  `System prompt на след. задаче: ${sendSystemPromptNext ? 'да' : 'нет'}`,
662
887
  ),
663
888
  )
664
- console.log(chalk.gray(`Лимит итераций: ${maxIter}`))
665
- console.log(chalk.gray(`Headless: ${headless ? 'да' : 'нет'}`))
666
- console.log(chalk.gray(`Debug: ${debug ? 'да' : 'нет'}`))
667
- console.log(chalk.gray(`Undo: ${config.undo.enabled ? 'вкл' : 'выкл'}`))
668
- console.log(chalk.gray(`Транскрипт: ${transcript.file || 'выкл'}`))
889
+ console.log(
890
+ theme.system(`Resend prompt (--resend-prompt): ${resendPrompt ? 'да' : 'нет'}`),
891
+ )
892
+ console.log(
893
+ theme.system(`Last chat: ${(await loadLastChat()) || 'нет'}`),
894
+ )
895
+ console.log(
896
+ theme.system(`Dev mode (auto-reload): ${devMode ? 'вкл' : 'выкл'}`),
897
+ )
898
+ console.log(theme.system(`Лимит итераций: ${maxIter}`))
899
+ console.log(theme.system(`Headless: ${headless ? 'да' : 'нет'}`))
900
+ console.log(theme.system(`Debug: ${debug ? 'да' : 'нет'}`))
901
+ console.log(theme.system(`Undo: ${config.undo.enabled ? 'вкл' : 'выкл'}`))
902
+ console.log(theme.system(`Транскрипт: ${transcript.file || 'выкл'}`))
669
903
  continue
670
904
  }
671
905
 
@@ -675,7 +909,7 @@ async function main() {
675
909
  }
676
910
 
677
911
  if (lower === '/transcript') {
678
- console.log(chalk.gray(transcript.file || '(выключен)'))
912
+ console.log(theme.system(transcript.file || '(выключен)'))
679
913
  continue
680
914
  }
681
915
 
@@ -683,14 +917,14 @@ async function main() {
683
917
  const result = await undo.undoLast()
684
918
  if (result.ok) {
685
919
  console.log(
686
- chalk.green(`↶ Откатили: ${result.record.originalPath}`) +
687
- chalk.gray(
920
+ theme.assistant(`↶ Откатили: ${result.record.originalPath}`) +
921
+ theme.system(
688
922
  result.record.existed ? ' (восстановлено)' : ' (удалено)',
689
923
  ),
690
924
  )
691
925
  transcript.log('undo', { path: result.record.originalPath })
692
926
  } else {
693
- console.error(chalk.red(`Не удалось откатить: ${result.reason}`))
927
+ console.error(theme.error(`Не удалось откатить: ${result.reason}`))
694
928
  }
695
929
  continue
696
930
  }
@@ -698,12 +932,12 @@ async function main() {
698
932
  if (lower === '/undo-list' || lower === '/history') {
699
933
  const list = await undo.list(10)
700
934
  if (!list.length) {
701
- console.log(chalk.gray('История пуста.'))
935
+ console.log(theme.system('История пуста.'))
702
936
  } else {
703
937
  for (const r of list) {
704
938
  const stamp = new Date(r.stamp).toLocaleString()
705
939
  const flag = r.existed ? 'изменён' : 'создан'
706
- console.log(chalk.gray(`${stamp} [${flag}] ${r.originalPath}`))
940
+ console.log(theme.system(`${stamp} [${flag}] ${r.originalPath}`))
707
941
  }
708
942
  }
709
943
  continue
@@ -714,11 +948,11 @@ async function main() {
714
948
  const file = path.join(config.transcript.dir, `dom-${stamp}.html`)
715
949
  try {
716
950
  const result = await browser.dumpDom(file)
717
- console.log(chalk.green(`HTML сохранён: ${result.file}`))
718
- console.log(chalk.gray('Селекторы:'))
951
+ console.log(theme.assistant(`HTML сохранён: ${result.file}`))
952
+ console.log(theme.system('Селекторы:'))
719
953
  console.log(JSON.stringify(result.selectors, null, 2))
720
954
  } catch (e) {
721
- console.error(chalk.red('Не удалось сохранить DOM:'), e.message)
955
+ console.error(theme.error('Не удалось сохранить DOM:'), e.message)
722
956
  }
723
957
  continue
724
958
  }
@@ -731,42 +965,45 @@ async function main() {
731
965
  : sandboxRoot
732
966
  const rel = path.relative(sandboxRoot, newDir)
733
967
  if (rel.startsWith('..') || path.isAbsolute(rel)) {
734
- console.error(chalk.red('Нельзя выйти за пределы: ' + sandboxRoot))
968
+ console.error(theme.error('Нельзя выйти за пределы: ' + sandboxRoot))
735
969
  continue
736
970
  }
737
971
  const stat = await fs.stat(newDir).catch(() => null)
738
972
  if (!stat || !stat.isDirectory()) {
739
- console.error(chalk.red('Не директория: ' + newDir))
973
+ console.error(theme.error('Не директория: ' + newDir))
740
974
  continue
741
975
  }
742
976
  if (newDir === currentWorkdir) {
743
- console.log(chalk.gray('Уже здесь.'))
977
+ console.log(theme.system('Уже здесь.'))
744
978
  continue
745
979
  }
746
980
  if (reviewMode) {
747
- console.log(chalk.gray('Вышел из режима ревью (/cd).'))
981
+ console.log(theme.system('Вышел из режима ревью (/cd).'))
748
982
  reviewMode = null
749
983
  }
750
984
  currentWorkdir = newDir
751
985
  freshChatNext = true
752
986
  sendSystemPromptNext = true
753
- console.log(chalk.gray('Рабочая директория: ' + newDir))
987
+ console.log(theme.system('Рабочая директория: ' + newDir))
754
988
  } catch (e) {
755
- console.error(chalk.red('Не удалось перейти: ' + e.message))
989
+ console.error(theme.error('Не удалось перейти: ' + e.message))
756
990
  }
757
991
  continue
758
992
  }
759
993
 
760
994
  if (lower.startsWith('/')) {
761
- console.error(chalk.red(`Неизвестная команда: ${trimmed}. Набери /help.`))
995
+ console.error(theme.error(`Неизвестная команда: ${trimmed}. Набери /help.`))
762
996
  continue
763
997
  }
764
998
 
765
999
  // ---- Обычная задача (в том числе в review-режиме) ----
766
1000
 
1001
+ // Dev-режим: подхватываем свежие модули логики перед задачей.
1002
+ await autoReload()
1003
+
767
1004
  transcript.log('user_task', { task: trimmed, workdir: currentWorkdir })
768
1005
 
769
- const tools = createTools(currentWorkdir, { undo })
1006
+ const tools = mod.createTools(currentWorkdir, { undo })
770
1007
  await runTask(browser, tools, trimmed, currentWorkdir, {
771
1008
  transcript,
772
1009
  freshChat: freshChatNext,
@@ -778,15 +1015,16 @@ async function main() {
778
1015
  if (!currentChatId) {
779
1016
  currentChatId = await browser.getCurrentChatId()
780
1017
  }
1018
+ await saveLastChat(currentChatId)
781
1019
  }
782
1020
 
783
1021
  await browser.close().catch(() => {})
784
- await closeWeb().catch(() => {})
1022
+ await mod.closeWeb().catch(() => {})
785
1023
  transcript.close()
786
1024
  }
787
1025
 
788
1026
  main().catch((e) => {
789
- console.error(chalk.red('Критическая ошибка:'), e.message)
1027
+ console.error(theme.error('Критическая ошибка:'), e.message)
790
1028
  if (debug) console.error(e.stack)
791
1029
  process.exit(1)
792
1030
  })