zames_pro 1.2.1 → 2.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 DELETED
@@ -1,1515 +0,0 @@
1
- #!/usr/bin/env node
2
- import path from 'path'
3
- import fs from 'fs/promises'
4
- import { fileURLToPath } from 'url'
5
- import { theme } from './theme.js'
6
-
7
- import { DeepSeekBrowser } from './browser.js'
8
- import { createTools } from './tools.js'
9
- import { runAgentLoop } from './agent-loop.js'
10
- import { createSpinner } from './spinner.js'
11
- import { LineEditor } from './input.js'
12
- import { loadConfig, CONFIG_PATHS, ZAMES_HOME } from './config.js'
13
- import { Transcript } from './transcript.js'
14
- import { UndoStore } from './undo.js'
15
- import { selfReview, selfDiff, selfApply, selfList } from './self-review.js'
16
- import { closeWeb } from './web.js'
17
- import {
18
- saveSession,
19
- loadLastSession,
20
- listSessions,
21
- sessionsDir,
22
- } from './sessions.js'
23
-
24
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
25
-
26
- // ---------- CLI parsing ----------
27
-
28
- const args = process.argv.slice(2)
29
-
30
- function getArg(flag, fallback = null) {
31
- const i = args.indexOf(flag)
32
- return i !== -1 && args[i + 1] ? args[i + 1] : fallback
33
- }
34
-
35
- function hasFlag(flag) {
36
- return args.includes(flag)
37
- }
38
-
39
- function getPositional() {
40
- const positional = []
41
- for (let i = 0; i < args.length; i++) {
42
- const a = args[i]
43
- if (['--dir', '--task', '--max-iter', '--project', '--chat'].includes(a)) {
44
- i++
45
- continue
46
- }
47
- if (a.startsWith('--')) continue
48
- positional.push(a)
49
- }
50
- return positional
51
- }
52
-
53
- const config = loadConfig()
54
-
55
- const headless = hasFlag('--headless') || config.headless
56
- const debug = hasFlag('--debug') || config.debug
57
- const calibrate = hasFlag('--calibrate')
58
- const maxIter =
59
- Number(getArg('--max-iter', String(config.maxIterations))) ||
60
- config.maxIterations
61
-
62
- const positional = getPositional()
63
- const task = getArg('--task', positional.join(' ').trim() || null)
64
- const chatIdArg = getArg('--chat', null)
65
- // При возобновлении существующего чата system-prompt по умолчанию НЕ
66
- // переотправляется (он уже есть в начале чата). Флаг --resend-prompt
67
- // заставляет дослать его заново — например, если промпт обновился.
68
- const resendPrompt = hasFlag('--resend-prompt')
69
- // --new-chat: намеренно начать с чистого чата, игнорируя сохранённую сессию.
70
- // --resume-last: оставлен для совместимости (восстановление и так по умолчанию).
71
- // По умолчанию последняя сессия для рабочей директории восстанавливается.
72
- const newChatFlag = hasFlag('--new-chat')
73
- const resumeLastFlag = hasFlag('--resume-last')
74
-
75
- // ---------- session persistence ----------
76
- // Сессии (чаты DeepSeek) храним в ~/.zames/.sessions, чтобы они переживали
77
- // перезапуск процесса. При старте из той же рабочей директории последняя
78
- // сессия восстанавливается автоматически (если не передан --new-chat).
79
- // Раньше здесь был один файл last-chat.json, который терялся при смене
80
- // проекта и не давал списка сессий для восстановления.
81
- function saveLastChat(id, workdir = '', title = '') {
82
- if (!id) return
83
- saveSession({ id, workdir, title })
84
- }
85
-
86
- function loadLastChat(workdir = '') {
87
- const s = loadLastSession(workdir)
88
- return s ? s.id : null
89
- }
90
-
91
-
92
- // ---------- hot reload ----------
93
- // Держим ссылки на модули логики в объекте mod. Команда /reload перечитывает
94
- // их через динамический import с timestamp-query — Node кэширует ESM по URL,
95
- // поэтому такой import вернёт свежую версию модуля. Браузер, чат и текущее
96
- // состояние НЕ перезапускаются: обновляется только логика.
97
- const RELOADABLE = [
98
- './tools.js',
99
- './agent-loop.js',
100
- './system-prompt.js',
101
- './gitTools.js',
102
- './web.js',
103
- './self-review.js',
104
- './diff.js',
105
- './undo.js',
106
- './confirm.js',
107
- './transcript.js',
108
- './spinner.js',
109
- './config.js',
110
- ]
111
-
112
- const mod = {
113
- createTools,
114
- runAgentLoop,
115
- buildSystemPrompt: null, // подгрузим ниже
116
- selfReview,
117
- selfDiff,
118
- selfApply,
119
- selfList,
120
- closeWeb,
121
- createSpinner,
122
- }
123
-
124
- async function reloadModules() {
125
- const stamp = Date.now()
126
- const loaded = new Map()
127
- const errors = []
128
- for (const rel of RELOADABLE) {
129
- try {
130
- const url = new URL(rel, import.meta.url)
131
- url.searchParams.set('t', String(stamp))
132
- const m = await import(url.href)
133
- loaded.set(rel, m)
134
- } catch (e) {
135
- errors.push(`${rel}: ${e.message}`)
136
- }
137
- }
138
-
139
- const pick = (rel, name) => loaded.get(rel)?.[name]
140
-
141
- if (pick('./tools.js', 'createTools')) mod.createTools = pick('./tools.js', 'createTools')
142
- if (pick('./agent-loop.js', 'runAgentLoop')) mod.runAgentLoop = pick('./agent-loop.js', 'runAgentLoop')
143
- if (pick('./system-prompt.js', 'buildSystemPrompt')) mod.buildSystemPrompt = pick('./system-prompt.js', 'buildSystemPrompt')
144
- if (pick('./self-review.js', 'selfReview')) mod.selfReview = pick('./self-review.js', 'selfReview')
145
- if (pick('./self-review.js', 'selfDiff')) mod.selfDiff = pick('./self-review.js', 'selfDiff')
146
- if (pick('./self-review.js', 'selfApply')) mod.selfApply = pick('./self-review.js', 'selfApply')
147
- if (pick('./self-review.js', 'selfList')) mod.selfList = pick('./self-review.js', 'selfList')
148
- if (pick('./web.js', 'closeWeb')) mod.closeWeb = pick('./web.js', 'closeWeb')
149
- if (pick('./spinner.js', 'createSpinner')) mod.createSpinner = pick('./spinner.js', 'createSpinner')
150
-
151
- return { count: loaded.size, errors }
152
- }
153
-
154
- // Первичная загрузка, чтобы mod.buildSystemPrompt и остальные были заполнены.
155
- await reloadModules()
156
-
157
- // Dev-режим: авто-перечитывание модулей логики перед каждой задачей.
158
- // Включается флагом --dev (его ставит `npm run dev`) или config.hotReload === true.
159
- // В обычном режиме (npm start, глобальный zames) авто-reload выключен.
160
- const devMode = hasFlag('--dev') || config.hotReload === true
161
-
162
- // Безопасный авто-reload: при ошибке загрузки оставляем прошлые рабочие модули.
163
- async function autoReload() {
164
- if (!devMode) return
165
- const { errors } = await reloadModules()
166
- if (errors.length) {
167
- console.error(
168
- theme.warn('⚠ авто-reload: часть модулей не загрузилась, работаю на прежней версии:'),
169
- )
170
- for (const e of errors) console.error(theme.warn(' ' + e))
171
- }
172
- }
173
-
174
-
175
- // ---------- helpers ----------
176
-
177
- function printHelp() {
178
- console.log(`
179
- ${theme.bold('zames')} — агент поверх chat.deepseek.com через Playwright
180
-
181
- ${theme.bold('Опции CLI:')}
182
- --dir <path> рабочая директория агента
183
- --task <text> задача одной строкой
184
- --chat <id> продолжить существующий чат по id
185
- --resume-last вернуться в последний сохранённый чат (по умолчанию)
186
- --new-chat начать новый чат, не восстанавливать сессию
187
- --resend-prompt дослать system-prompt в существующий чат
188
- --max-iter <n> лимит итераций (по умолчанию ${config.maxIterations})
189
- --headless браузер без UI
190
- --debug подробный лог
191
- --calibrate режим калибровки селекторов
192
- --dev режим разработки: авто-перечитывание модулей
193
- --version, -v показать версию
194
- --help, -h эта справка
195
-
196
- ${theme.bold('Пока агент работает:')}
197
- печать + Enter поставить сообщение в очередь (уйдёт после текущей задачи)
198
- Esc, Ctrl+C прервать текущую генерацию
199
-
200
- ${theme.bold('Обычные команды:')}
201
- /new, /clear новый чат (сброс контекста)
202
- /sessions список сохранённых сессий (~/.zames/.sessions)
203
- /resume-id <id> восстановить сессию по полному id
204
- /chats список последних чатов DeepSeek
205
- /resume <n> открыть чат №n из /chats
206
- /chat показать текущий chat id
207
- /cd <path> сменить рабочую директорию
208
- /pwd текущая директория
209
- /status состояние сессии
210
- /reload перечитать модули логики без перезапуска
211
- /undo откатить последнюю запись/правку
212
- /undo-list список того, что можно откатить
213
- /transcript путь к файлу транскрипта
214
- /config показать текущий конфиг
215
- /debug-dom сохранить HTML страницы (для отладки)
216
- /help, help справка
217
- /exit, /quit, exit выход
218
-
219
- ${theme.bold('Самообзор (отладка агента):')}
220
- /self-review [фокус] снять снапшот src/ и запустить ревью
221
- после этой команды ты остаёшься В СНАПШОТЕ
222
- и можешь писать «исправь ошибки» и т.п.
223
- /self-fix <name> [фокус] вернуться в существующий снапшот и продолжить
224
- /self-done выйти из режима ревью (вернуться в свою папку)
225
- /self-list список снапшотов
226
- /self-diff <name> различия между текущим src/ и снапшотом
227
- /self-apply <name> применить снапшот к src/ (с бэкапом)
228
-
229
- ${theme.bold('Файлы:')}
230
- Логи: ${config.transcript.dir}
231
- Undo: ~/.zames/undo
232
- Сессии: ${sessionsDir()}
233
- Профиль: ~/.zames/profile
234
- Снапшоты: ~/.zames/snapshots
235
- Временные: <проект>/tmp (в .gitignore, чистится при запуске)
236
- Конфиг: ${CONFIG_PATHS.HOME_CONFIG}
237
- ${CONFIG_PATHS.PROJECT_CONFIG}
238
- `)
239
- }
240
-
241
- function dirLabel(p) {
242
- return path.basename(p) || p
243
- }
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
- // не считается отправкой.
271
- async function promptOnce(question) {
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')
313
- }
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)
463
- })
464
- }
465
-
466
- // Слежение за клавиатурой во время работы агента.
467
- //
468
- // Терминал остаётся живым, пока агент думает:
469
- // - Esc (или Ctrl+C) — прервать текущую генерацию (клик Stop в браузере);
470
- // - набор текста + Enter — поставить сообщение в очередь; оно уйдёт
471
- // агенту сразу после того, как текущая задача завершится
472
- // (как «отправить во время генерации» в веб-версии DeepSeek).
473
- //
474
- // Ввод буферизуется без построчного редактора: набранный текст отображается
475
- // в строке спиннера через onChange -> ui.setPending(). Enter отправляет буфер
476
- // в onQueue, пустой Enter игнорируется. Поддержаны Backspace, Ctrl+U, Esc-
477
- // последовательности (стрелки/Home/End/Delete игнорируются) и bracketed paste.
478
- function watchInput({ onEscape, onChange, onQueue } = {}) {
479
- const stdin = process.stdin
480
- if (!stdin.isTTY || !stdin.setRawMode) return () => {}
481
-
482
- // Управляющие символы собираем из кодов: в этом файле нельзя писать
483
- // «сырые» ESC/CR/LF в строковых литералах (см. AGENTS.md).
484
- const ESC = String.fromCharCode(27)
485
- const CSI = String.fromCharCode(91)
486
- const CR = String.fromCharCode(13)
487
- const LF = String.fromCharCode(10)
488
-
489
- const wasRaw = stdin.isRaw
490
- stdin.setRawMode(true)
491
- stdin.resume()
492
- process.stdout.write(ESC + '[?2004h')
493
-
494
- let buf = ''
495
- let inPaste = false
496
- const PASTE_START = ESC + '[200~'
497
- const PASTE_END = ESC + '[201~'
498
- const CSI_RE = new RegExp('^' + CSI + '[0-9;]*[A-Za-z~]')
499
-
500
- const emitChange = () => {
501
- if (onChange) onChange(buf)
502
- }
503
-
504
- // Вставленный текст: переводы строк внутри многострочной вставки
505
- // трактуем как пробелы — сообщение уходит одной строкой.
506
- const insert = (text) => {
507
- buf += text
508
- .split(CR + LF).join(' ')
509
- .split(CR).join(' ')
510
- .split(LF).join(' ')
511
- }
512
-
513
- function onData(data) {
514
- let s = data.toString('utf-8')
515
-
516
- // Одиночный Esc — прервать генерацию. Стрелки приходят целым чанком
517
- // и сюда не попадают.
518
- if (!inPaste && s === ESC) {
519
- if (onEscape) onEscape()
520
- return
521
- }
522
-
523
- while (s.length) {
524
- if (inPaste) {
525
- const end = s.indexOf(PASTE_END)
526
- if (end === -1) {
527
- insert(s)
528
- s = ''
529
- } else {
530
- insert(s.slice(0, end))
531
- s = s.slice(end + PASTE_END.length)
532
- inPaste = false
533
- }
534
- emitChange()
535
- continue
536
- }
537
-
538
- const start = s.indexOf(PASTE_START)
539
- if (start !== -1) {
540
- const before = s.slice(0, start)
541
- s = s.slice(start + PASTE_START.length)
542
- inPaste = true
543
- if (before) {
544
- insert(before)
545
- emitChange()
546
- }
547
- continue
548
- }
549
-
550
- const ch = s[0]
551
- const code = s.charCodeAt(0)
552
- s = s.slice(1)
553
-
554
- if (ch === CR || ch === LF) {
555
- const text = buf.trim()
556
- buf = ''
557
- emitChange()
558
- if (text && onQueue) onQueue(text)
559
- continue
560
- }
561
- if (code === 3) {
562
- // Ctrl+C — как Esc: прервать генерацию.
563
- if (onEscape) onEscape()
564
- continue
565
- }
566
- if (code === 21) {
567
- // Ctrl+U — очистить набранное.
568
- buf = ''
569
- emitChange()
570
- continue
571
- }
572
- if (code === 127 || code === 8) {
573
- // Backspace.
574
- if (buf) {
575
- buf = buf.slice(0, -1)
576
- emitChange()
577
- }
578
- continue
579
- }
580
- if (ch === ESC) {
581
- // Escape-последовательность (стрелки, Home/End, Delete) — пропускаем.
582
- const m = s.match(CSI_RE)
583
- if (m) s = s.slice(m[0].length)
584
- continue
585
- }
586
- if (code < 32) continue // прочие управляющие — игнорируем
587
-
588
- buf += ch
589
- emitChange()
590
- }
591
- }
592
-
593
- const handler = (data) => onData(data)
594
- stdin.on('data', handler)
595
- return () => {
596
- stdin.removeListener('data', handler)
597
- process.stdout.write(ESC + '[?2004l')
598
- if (stdin.setRawMode) stdin.setRawMode(wasRaw || false)
599
- }
600
- }
601
-
602
- // ---------- workdir resolution ----------
603
-
604
- // Агент работает в директории, из которой его запустили (process.cwd()).
605
- // Это корень sandbox: инструменты не могут выходить выше него.
606
- async function resolveWorkdir() {
607
- const explicitDir = getArg('--dir', null)
608
- const dir = explicitDir ? path.resolve(explicitDir) : process.cwd()
609
- const stat = await fs.stat(dir).catch(() => null)
610
- if (!stat || !stat.isDirectory()) {
611
- throw new Error('Не директория: ' + dir)
612
- }
613
- return dir
614
- }
615
-
616
- // ---------- task runner ----------
617
-
618
- async function runTask(browser, tools, taskText, workdir, opts) {
619
- const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor } = opts
620
-
621
- // В TTY-режиме UI — это LineEditor: он владеет вводом (очередь, Esc,
622
- // Ctrl+C) и рисует статус НАД постоянной строкой ввода. В не-TTY режиме
623
- // (пайпы) — обычный спиннер + watchInput.
624
- const ui = editor || mod.createSpinner()
625
- const stopWatching = editor
626
- ? () => {}
627
- : watchInput({
628
- onEscape: () => {
629
- ui.stop()
630
- console.error(theme.warn('⏹ Esc — прерываю генерацию...'))
631
- browser.stopGeneration().catch(() => {})
632
- },
633
- onChange: (text) => ui.setPending(text),
634
- onQueue: (text) => {
635
- queue.push(text)
636
- ui.setPending(null)
637
- ui.stop()
638
- console.log(
639
- theme.user('📨 В очередь (' + queue.length + '): ') + theme.assistant(text),
640
- )
641
- ui.thinking()
642
- },
643
- })
644
-
645
- try {
646
- let next = { task: taskText, freshChat, sendSystemPrompt }
647
-
648
- // Выполняем задачу, затем — всё, что пользователь успел напечатать за
649
- // время работы. Очередь может пополняться прямо во время дренажа.
650
- while (true) {
651
- ui.thinking()
652
- await mod.runAgentLoop({
653
- browser,
654
- tools,
655
- task: next.task,
656
- workdir,
657
- maxIterations: maxIter,
658
- freshChat: next.freshChat,
659
- sendSystemPrompt: next.sendSystemPrompt,
660
- transcript,
661
- onThinking: () => ui.thinking(),
662
- onToolCall: (name, toolArgs) => ui.toolCall(name, toolArgs),
663
- onToolResult: (result) => ui.toolResult(result),
664
- onAssistantMessage: (msg) => ui.assistant(msg),
665
- debugLog: debug,
666
- })
667
-
668
- if (!queue.length) break
669
-
670
- const queued = queue.shift()
671
- ui.stop()
672
- if (editor) {
673
- editor.printAbove(theme.user('▶ Из очереди: ') + theme.assistant(queued))
674
- } else {
675
- console.log(theme.user('▶ Из очереди: ') + theme.assistant(queued))
676
- }
677
- transcript?.log('queued_task', { task: queued })
678
- next = { task: queued, freshChat: false, sendSystemPrompt: false }
679
- }
680
- } catch (e) {
681
- ui.stop()
682
- console.error(theme.error(String.fromCharCode(10) + '✖ Ошибка агента:'), e.message)
683
- if (debug) console.error(e.stack)
684
- transcript?.log('agent_error', { error: e.message })
685
- } finally {
686
- stopWatching()
687
- ui.stop()
688
- }
689
- }
690
-
691
- // ---------- main ----------
692
-
693
- async function main() {
694
- if (hasFlag('--version') || hasFlag('-v')) {
695
- const pkg = JSON.parse(
696
- await fs.readFile(path.join(__dirname, '..', 'package.json'), 'utf-8'),
697
- )
698
- console.log(pkg.version)
699
- return
700
- }
701
-
702
- if (hasFlag('--help') || hasFlag('-h')) {
703
- printHelp()
704
- return
705
- }
706
-
707
- if (calibrate) {
708
- console.log(theme.warn('\n🔧 Режим калибровки селекторов\n'))
709
- }
710
-
711
- await cleanTmpDir()
712
-
713
- let currentWorkdir
714
- try {
715
- currentWorkdir = await resolveWorkdir()
716
- // Корень sandbox: агент не может выходить выше директории запуска.
717
- const sandboxRoot = currentWorkdir
718
- } catch (e) {
719
- console.error(
720
- theme.error('Не удалось определить рабочую директорию:'),
721
- e.message,
722
- )
723
- process.exit(1)
724
- }
725
-
726
- console.log(theme.system(`Рабочая директория: ${currentWorkdir}`))
727
-
728
- const transcript = new Transcript({
729
- dir: config.transcript.dir,
730
- enabled: config.transcript.enabled,
731
- sessionName: dirLabel(currentWorkdir),
732
- })
733
- if (transcript.file) {
734
- console.log(theme.system(`Транскрипт: ${transcript.file}`))
735
- }
736
-
737
- const undo = new UndoStore(config.undo)
738
-
739
- const browser = new DeepSeekBrowser({
740
- headless,
741
- debug,
742
- channel: config.browserChannel,
743
- ...config.browser,
744
- })
745
-
746
- const bootSpinner = mod.createSpinner()
747
- bootSpinner.thinking()
748
-
749
- try {
750
- await browser.launch()
751
- bootSpinner.stop()
752
- await browser.waitForLogin()
753
- } catch (e) {
754
- bootSpinner.stop()
755
- console.error(theme.error('Не удалось запустить браузер:'), e.message)
756
- if (debug) console.error(e.stack)
757
- await browser.close().catch(() => {})
758
- transcript.close()
759
- process.exit(1)
760
- }
761
-
762
- // Разовый режим
763
- if (task) {
764
- const tools = mod.createTools(currentWorkdir, { undo })
765
-
766
- let freshChat = true
767
- let sendSystemPrompt = true
768
-
769
- // По умолчанию восстанавливаем последнюю сессию для этой директории.
770
- // Начать с чистого листа — --new-chat.
771
- let resumeId = chatIdArg
772
- if (!resumeId && !newChatFlag) {
773
- const last = loadLastSession(currentWorkdir)
774
- if (last && last.id) {
775
- resumeId = last.id
776
- console.log(theme.system(`Восстанавливаю сессию ${resumeId}...`))
777
- }
778
- }
779
-
780
- if (resumeId) {
781
- try {
782
- console.log(theme.system(`Открываю чат ${resumeId}...`))
783
- await browser.openChat(resumeId)
784
- freshChat = false
785
- sendSystemPrompt = resendPrompt
786
- saveLastChat(resumeId, currentWorkdir)
787
- } catch (e) {
788
- console.error(theme.error(`Не удалось открыть чат: ${e.message}`))
789
- }
790
- }
791
-
792
- await autoReload()
793
-
794
- await runTask(browser, tools, task, currentWorkdir, {
795
- transcript,
796
- freshChat,
797
- sendSystemPrompt,
798
- })
799
- await browser.close()
800
- await mod.closeWeb().catch(() => {})
801
- transcript.close()
802
- return
803
- }
804
-
805
- console.log(
806
- theme.system(
807
- 'Интерактивный режим. Введите задачу. Команды — /help. Выход — /exit.',
808
- ),
809
- )
810
- console.log(
811
- theme.system(
812
- 'Пока агент работает, можно печатать следующее сообщение — оно уйдёт в очередь (Enter — отправить, Esc — прервать).',
813
- ),
814
- )
815
-
816
- let freshChatNext = true
817
- let sendSystemPromptNext = true
818
- let lastChats = []
819
- let currentChatId = null
820
- let running = true
821
-
822
- // Сообщения, набранные пользователем, пока агент работал. runTask
823
- // забирает их по одному после завершения текущей задачи.
824
- const pendingQueue = []
825
-
826
- // ---------- review mode state ----------
827
- // null — обычный режим.
828
- // { snapDir, snapName, originalWorkdir } — мы внутри снапшота, чат уже
829
- // инициализирован review-промптом, юзер может просто писать «исправь...».
830
- let reviewMode = null
831
-
832
- process.on('SIGINT', async () => {
833
- running = false
834
- await browser.close().catch(() => {})
835
- // Закрываем ленивый headless-браузер из web.js, иначе он останется
836
- // висеть отдельным процессом после выхода агента.
837
- await mod.closeWeb().catch(() => {})
838
- transcript.close()
839
- console.log(theme.system('\nВыход.'))
840
- process.exit(0)
841
- })
842
-
843
- // Восстановление сессии при старте. По умолчанию возвращаемся в последний
844
- // чат для этой рабочей директории (сессии лежат в ~/.zames/.sessions),
845
- // чтобы контекст не терялся после перезапуска. Явно начать с чистого листа
846
- // можно флагом --new-chat.
847
- // --chat <id> открыть конкретный чат
848
- // --new-chat не восстанавливать, начать новый чат
849
- let resumeId = chatIdArg
850
- if (!resumeId && !newChatFlag) {
851
- const last = loadLastSession(currentWorkdir)
852
- if (last && last.id) {
853
- resumeId = last.id
854
- console.log(
855
- theme.system(
856
- `Восстанавливаю сессию ${last.id}${last.title ? ` (${last.title})` : ''}...`,
857
- ),
858
- )
859
- }
860
- }
861
-
862
- if (resumeId) {
863
- try {
864
- console.log(theme.system(`Открываю чат ${resumeId}...`))
865
- await browser.openChat(resumeId)
866
- currentChatId = resumeId
867
- freshChatNext = false
868
- sendSystemPromptNext = resendPrompt
869
- saveLastChat(resumeId, currentWorkdir)
870
- console.log(theme.system(`Чат открыт: ${resumeId}\n`))
871
- } catch (e) {
872
- console.error(theme.error(`Не удалось открыть чат: ${e.message}`))
873
- }
874
- }
875
-
876
- // ---------- ввод: постоянная строка внизу + статус сверху ----------
877
- // В TTY используем LineEditor: он владеет вводом всё время, показывает
878
- // статус над строкой ввода и печатает ответы агента ВЫШЕ неё, поэтому
879
- // набранный текст никогда не затирается выводом. В не-TTY (пайп) —
880
- // старый promptOnce.
881
- let editor = null
882
- let waiter = null
883
- const takeInput = () => {
884
- if (pendingQueue.length) return Promise.resolve(pendingQueue.shift())
885
- return new Promise((resolve) => {
886
- waiter = resolve
887
- })
888
- }
889
-
890
- const buildPrompt = () => {
891
- let tail
892
- if (reviewMode) {
893
- tail = theme.warn('REVIEW') + theme.dim(':') + theme.dir(reviewMode.snapName)
894
- } else {
895
- tail = theme.dir(dirLabel(currentWorkdir))
896
- }
897
- return theme.prompt('❯ ') + tail + theme.dim(' › ')
898
- }
899
-
900
- if (process.stdin.isTTY && process.stdout.isTTY) {
901
- editor = new LineEditor({ prompt: buildPrompt() })
902
- editor.onSubmit = (text) => {
903
- pendingQueue.push(text)
904
- if (waiter) {
905
- const r = waiter
906
- waiter = null
907
- r(pendingQueue.shift())
908
- }
909
- }
910
- editor.onEscape = () => {
911
- if (editor.busy) {
912
- editor.printAbove(theme.warn('⏹ Esc — прерываю генерацию...'))
913
- browser.stopGeneration().catch(() => {})
914
- }
915
- }
916
- editor.onCtrlC = () => {
917
- if (editor.busy) {
918
- editor.printAbove(theme.warn('⏹ Ctrl+C — прерываю генерацию...'))
919
- browser.stopGeneration().catch(() => {})
920
- } else {
921
- // Не заняты — выходим. Будим takeInput(), чтобы цикл завершился.
922
- running = false
923
- if (waiter) {
924
- const r = waiter
925
- waiter = null
926
- r(null)
927
- }
928
- }
929
- }
930
- editor.start()
931
-
932
- // Весь вывод команд (console.log/error) должен идти ВЫШЕ строки ввода,
933
- // иначе он затирает набираемый текст. Пока редактор активен, заворачиваем
934
- // оба потока в editor.printAbove.
935
- const origLog = console.log.bind(console)
936
- const origErr = console.error.bind(console)
937
- const fmt = (a) =>
938
- typeof a === 'string' ? a : (() => {
939
- try {
940
- return JSON.stringify(a)
941
- } catch {
942
- return String(a)
943
- }
944
- })()
945
- console.log = (...a) => editor.printAbove(a.map(fmt).join(' '))
946
- console.error = (...a) => editor.printAbove(a.map(fmt).join(' '))
947
- // Сохраняем на случай отладки.
948
- editor._origLog = origLog
949
- editor._origErr = origErr
950
- }
951
-
952
- while (running) {
953
- let input
954
- try {
955
- if (editor) {
956
- editor.setPrompt(buildPrompt())
957
- input = await takeInput()
958
- } else {
959
- let tail
960
- if (reviewMode) {
961
- tail =
962
- theme.warn('REVIEW') + theme.dim(':') + theme.dir(reviewMode.snapName)
963
- } else {
964
- tail = theme.dir(dirLabel(currentWorkdir))
965
- }
966
- input = await promptOnce(theme.prompt('❯ ') + tail + theme.dim(' › '))
967
- }
968
- } catch {
969
- break
970
- }
971
-
972
- const trimmed = (input || '').trim()
973
- if (!trimmed) continue
974
-
975
- const lower = trimmed.toLowerCase()
976
-
977
- if (['/exit', '/quit', 'exit', 'quit'].includes(lower)) break
978
-
979
- if (lower === '/help' || lower === 'help') {
980
- printHelp()
981
- continue
982
- }
983
-
984
- if (['/new', '/clear', 'new'].includes(lower)) {
985
- console.log(theme.system('Создаю новый чат...'))
986
- try {
987
- await browser.newChat()
988
- freshChatNext = false
989
- sendSystemPromptNext = true
990
- currentChatId = await browser.getCurrentChatId()
991
- saveLastChat(currentChatId, currentWorkdir)
992
- transcript.log('new_chat')
993
- console.log(theme.system('Новый чат.\n'))
994
- } catch (e) {
995
- console.error(theme.error('Не удалось создать новый чат:'), e.message)
996
- }
997
- continue
998
- }
999
-
1000
- // ---------- Самообзор ----------
1001
-
1002
- if (lower === '/self-review' || lower.startsWith('/self-review ')) {
1003
- const focus = trimmed.slice('/self-review'.length).trim()
1004
-
1005
- // Запоминаем, куда вернуться
1006
- const originalWorkdir = reviewMode
1007
- ? reviewMode.originalWorkdir
1008
- : currentWorkdir
1009
-
1010
- try {
1011
- const result = await mod.selfReview({
1012
- browser,
1013
- config,
1014
- focus: focus || null,
1015
- transcript,
1016
- })
1017
-
1018
- // Переходим в review-режим:
1019
- // - рабочая директория = снапшот
1020
- // - чат НЕ сбрасываем — внутри selfReview уже создан свежий чат
1021
- // и отправлен review-промпт, продолжим в нём
1022
- reviewMode = {
1023
- snapDir: result.snapDir,
1024
- snapName: path.basename(result.snapDir),
1025
- originalWorkdir,
1026
- }
1027
- currentWorkdir = result.snapDir
1028
- freshChatNext = false
1029
- sendSystemPromptNext = false
1030
- currentChatId = await browser.getCurrentChatId()
1031
- saveLastChat(currentChatId, currentWorkdir)
1032
-
1033
- console.log(
1034
- theme.user(
1035
- '\n💡 Теперь ты в режиме ревью. Просто пиши агенту, например:\n' +
1036
- ' «исправь ошибки»\n' +
1037
- ' «доработай обработку ошибок в ask()»\n' +
1038
- ' «покажи, что не так с undo»\n' +
1039
- 'Выйти: /self-done. Применить: /self-apply ' +
1040
- reviewMode.snapName +
1041
- '\n',
1042
- ),
1043
- )
1044
- } catch (e) {
1045
- console.error(theme.error('Самообзор провалился:'), e.message)
1046
- if (debug) console.error(e.stack)
1047
- }
1048
- continue
1049
- }
1050
-
1051
- if (lower === '/self-fix' || lower.startsWith('/self-fix ')) {
1052
- const rest = trimmed.slice('/self-fix'.length).trim()
1053
- if (!rest) {
1054
- console.error(theme.error('Использование: /self-fix <name> [фокус]'))
1055
- continue
1056
- }
1057
- const sp = rest.indexOf(' ')
1058
- const name = sp === -1 ? rest : rest.slice(0, sp)
1059
- const focus = sp === -1 ? '' : rest.slice(sp + 1).trim()
1060
-
1061
- const snapRoot = path.join(ZAMES_HOME, 'snapshots', name)
1062
- const stat = await fs.stat(snapRoot).catch(() => null)
1063
- if (!stat || !stat.isDirectory()) {
1064
- console.error(theme.error(`Снапшот не найден: ${snapRoot}`))
1065
- continue
1066
- }
1067
-
1068
- const originalWorkdir = reviewMode
1069
- ? reviewMode.originalWorkdir
1070
- : currentWorkdir
1071
-
1072
- // Свежий чат + review-промпт на этот снапшот
1073
- try {
1074
- const { runAgentLoop: ral } = await import('./agent-loop.js')
1075
- const { buildSystemPrompt } = await import('./system-prompt.js')
1076
- const tools = mod.createTools(snapRoot, { undo: null })
1077
-
1078
- await browser.newChat()
1079
- const sysPrompt = mod.buildSystemPrompt({
1080
- workdir: snapRoot,
1081
- tools,
1082
- })
1083
- console.log(theme.system('Инициализирую review-чат для снапшота...'))
1084
- await browser.ask(sysPrompt, { timeout: 60_000 })
1085
-
1086
- if (focus) {
1087
- const ui = mod.createSpinner()
1088
- ui.thinking()
1089
- await ral({
1090
- browser,
1091
- tools,
1092
- task: focus,
1093
- workdir: snapRoot,
1094
- maxIterations: maxIter,
1095
- freshChat: false,
1096
- sendSystemPrompt: false,
1097
- transcript,
1098
- onThinking: () => ui.thinking(),
1099
- onToolCall: (name, toolArgs) => ui.toolCall(name, toolArgs),
1100
- onToolResult: (r) => ui.toolResult(r),
1101
- onAssistantMessage: (m) => {
1102
- ui.assistant(m)
1103
- },
1104
- })
1105
- }
1106
-
1107
- reviewMode = {
1108
- snapDir: snapRoot,
1109
- snapName: name,
1110
- originalWorkdir,
1111
- }
1112
- currentWorkdir = snapRoot
1113
- freshChatNext = false
1114
- sendSystemPromptNext = false
1115
- currentChatId = await browser.getCurrentChatId()
1116
- saveLastChat(currentChatId, currentWorkdir)
1117
-
1118
- console.log(
1119
- theme.user(
1120
- `\n💡 Режим ревью по снапшоту ${name}. Пиши агенту задачу или /self-done.\n`,
1121
- ),
1122
- )
1123
- } catch (e) {
1124
- console.error(theme.error('Не удалось войти в снапшот:'), e.message)
1125
- }
1126
- continue
1127
- }
1128
-
1129
- if (lower === '/self-done') {
1130
- if (!reviewMode) {
1131
- console.log(theme.system('Ты и так не в режиме ревью.'))
1132
- continue
1133
- }
1134
- const back = reviewMode.originalWorkdir
1135
- reviewMode = null
1136
- currentWorkdir = back
1137
- // Раз чат занят review-контекстом, для обычной работы создадим новый
1138
- freshChatNext = true
1139
- sendSystemPromptNext = true
1140
- console.log(
1141
- theme.system(`Вернулся в ${back}. Следующая задача начнёт новый чат.\n`),
1142
- )
1143
- continue
1144
- }
1145
-
1146
- if (lower === '/self-list') {
1147
- try {
1148
- await mod.selfList({ config })
1149
- } catch (e) {
1150
- console.error(theme.error('Ошибка:'), e.message)
1151
- }
1152
- continue
1153
- }
1154
-
1155
- if (lower === '/self-diff' || lower.startsWith('/self-diff ')) {
1156
- const name = trimmed.slice('/self-diff'.length).trim()
1157
- if (!name) {
1158
- console.error(theme.error('Использование: /self-diff <name>'))
1159
- continue
1160
- }
1161
- try {
1162
- await mod.selfDiff({ config, name })
1163
- } catch (e) {
1164
- console.error(theme.error('Ошибка:'), e.message)
1165
- }
1166
- continue
1167
- }
1168
-
1169
- if (lower === '/self-apply' || lower.startsWith('/self-apply ')) {
1170
- const name = trimmed.slice('/self-apply'.length).trim()
1171
- if (!name) {
1172
- console.error(theme.error('Использование: /self-apply <name>'))
1173
- continue
1174
- }
1175
- try {
1176
- await mod.selfApply({ config, name })
1177
- } catch (e) {
1178
- console.error(theme.error('Ошибка:'), e.message)
1179
- }
1180
- continue
1181
- }
1182
-
1183
- // ---------- Обычные команды ----------
1184
-
1185
- if (lower === '/chats') {
1186
- const spin = editor || mod.createSpinner()
1187
- spin.thinking()
1188
- try {
1189
- lastChats = await browser.listChats(30)
1190
- spin.stop()
1191
- if (!lastChats.length) {
1192
- console.log(
1193
- theme.system(
1194
- 'Чатов не найдено. Возможно, сайдбар свёрнут или селекторы устарели.',
1195
- ),
1196
- )
1197
- } else {
1198
- console.log(theme.system('Последние чаты DeepSeek:'))
1199
- lastChats.forEach((c, i) => {
1200
- const n = String(i + 1).padStart(2, ' ')
1201
- console.log(
1202
- ` ${theme.user(n)}. ${c.title} ${theme.system('(' + c.id.slice(0, 8) + '…)')}`,
1203
- )
1204
- })
1205
- console.log(theme.system('\nИспользуй /resume <n> для продолжения.\n'))
1206
- }
1207
- } catch (e) {
1208
- spin.stop()
1209
- console.error(theme.error('Не удалось получить список:'), e.message)
1210
- }
1211
- continue
1212
- }
1213
-
1214
- if (lower === '/resume' || lower.startsWith('/resume ')) {
1215
- const arg = trimmed.slice(7).trim()
1216
- if (!arg) {
1217
- console.error(
1218
- theme.error('Использование: /resume <n> (или /chats для списка)'),
1219
- )
1220
- continue
1221
- }
1222
- const n = Number(arg)
1223
- if (!Number.isFinite(n) || n < 1) {
1224
- console.error(theme.error('Нужен номер из /chats.'))
1225
- continue
1226
- }
1227
- if (!lastChats.length) {
1228
- console.error(theme.error('Сначала выполни /chats.'))
1229
- continue
1230
- }
1231
- const pick = lastChats[n - 1]
1232
- if (!pick) {
1233
- console.error(theme.error(`Нет чата №${n}. Всего: ${lastChats.length}.`))
1234
- continue
1235
- }
1236
-
1237
- console.log(theme.system(`Открываю: ${pick.title}`))
1238
- try {
1239
- await browser.openChat(pick.id)
1240
- currentChatId = pick.id
1241
- freshChatNext = false
1242
- sendSystemPromptNext = resendPrompt
1243
- saveLastChat(pick.id, currentWorkdir, pick.title)
1244
- transcript.log('resume_chat', { id: pick.id, title: pick.title })
1245
- console.log(
1246
- theme.assistant(`Чат открыт.`) +
1247
- theme.system(
1248
- resendPrompt ? ' Системный промпт будет переслан на следующей задаче.\n' : ' Контекст чата сохранён. Системный промпт не пересылается (--resend-prompt чтобы дослать).\n',
1249
- ),
1250
- )
1251
- } catch (e) {
1252
- console.error(theme.error('Не удалось открыть чат:'), e.message)
1253
- }
1254
- continue
1255
- }
1256
-
1257
- if (lower === '/chat') {
1258
- if (currentChatId) {
1259
- console.log(theme.system(`Текущий chat id: ${currentChatId}`))
1260
- console.log(
1261
- theme.system(
1262
- `URL: https://chat.deepseek.com/a/chat/s/${currentChatId}`,
1263
- ),
1264
- )
1265
- } else {
1266
- const id = await browser.getCurrentChatId()
1267
- console.log(
1268
- theme.system(id ? `Текущий chat id: ${id}` : 'Чат ещё не создан.'),
1269
- )
1270
- }
1271
- continue
1272
- }
1273
-
1274
- if (lower === '/sessions' || lower === '/session') {
1275
- const all = listSessions()
1276
- console.log(theme.system(`Папка сессий: ${sessionsDir()}`))
1277
- if (!all.length) {
1278
- console.log(
1279
- theme.system(
1280
- 'Сохранённых сессий нет. Они появятся после первой задачи/чата.',
1281
- ),
1282
- )
1283
- } else {
1284
- all.forEach((s, i) => {
1285
- const n = String(i + 1).padStart(2, ' ')
1286
- const mark = s.id === currentChatId ? theme.user(' *') : ''
1287
- const title = s.title ? ` ${s.title}` : ''
1288
- const wd = s.workdir ? theme.dim(` [${dirLabel(s.workdir)}]`) : ''
1289
- console.log(` ${theme.user(n)}. ${s.id.slice(0, 8)}…${title}${wd}${mark}`)
1290
- })
1291
- console.log(
1292
- theme.system('Восстановить: /resume-id <id> (полный id) или /resume <n> после /chats.'),
1293
- )
1294
- }
1295
- continue
1296
- }
1297
-
1298
- if (lower.startsWith('/resume-id ')) {
1299
- const id = trimmed.slice('/resume-id'.length).trim()
1300
- if (!id) {
1301
- console.error(theme.error('Использование: /resume-id <chat id>'))
1302
- continue
1303
- }
1304
- try {
1305
- console.log(theme.system(`Открываю чат ${id}...`))
1306
- await browser.openChat(id)
1307
- currentChatId = id
1308
- freshChatNext = false
1309
- sendSystemPromptNext = resendPrompt
1310
- saveLastChat(id, currentWorkdir)
1311
- transcript.log('resume_chat', { id })
1312
- console.log(theme.assistant('Чат открыт.') + theme.system(String.fromCharCode(10)))
1313
- } catch (e) {
1314
- console.error(theme.error('Не удалось открыть чат:'), e.message)
1315
- }
1316
- continue
1317
- }
1318
-
1319
- if (lower === '/pwd') {
1320
- console.log(theme.system(currentWorkdir))
1321
- continue
1322
- }
1323
-
1324
- if (lower === '/reload') {
1325
- console.log(theme.system('Перечитываю модули логики...'))
1326
- try {
1327
- const { count, errors } = await reloadModules()
1328
- if (errors.length) {
1329
- console.error(theme.error('Часть модулей не перезагрузилась:'))
1330
- for (const e of errors) console.error(theme.error(' ' + e))
1331
- } else {
1332
- console.log(
1333
- theme.assistant(
1334
- `Перезагружено модулей: ${count}. Браузер и чат не тронуты.`,
1335
- ),
1336
- )
1337
- }
1338
- } catch (e) {
1339
- console.error(theme.error('Ошибка reload:'), e.message)
1340
- }
1341
- continue
1342
- }
1343
-
1344
- if (lower === '/status') {
1345
- console.log(theme.system(`Рабочая директория: ${currentWorkdir}`))
1346
- console.log(
1347
- theme.system(`Режим ревью: ${reviewMode ? reviewMode.snapName : 'нет'}`),
1348
- )
1349
- if (reviewMode) {
1350
- console.log(
1351
- theme.system(`Исходная директория: ${reviewMode.originalWorkdir}`),
1352
- )
1353
- }
1354
- console.log(theme.system(`Текущий чат: ${currentChatId || '(нет)'}`))
1355
- console.log(
1356
- theme.system(
1357
- `Fresh chat на след. задаче: ${freshChatNext ? 'да' : 'нет'}`,
1358
- ),
1359
- )
1360
- console.log(
1361
- theme.system(
1362
- `System prompt на след. задаче: ${sendSystemPromptNext ? 'да' : 'нет'}`,
1363
- ),
1364
- )
1365
- console.log(
1366
- theme.system(`Resend prompt (--resend-prompt): ${resendPrompt ? 'да' : 'нет'}`),
1367
- )
1368
- console.log(
1369
- theme.system(`Last chat: ${loadLastChat(currentWorkdir) || 'нет'}`),
1370
- )
1371
- console.log(theme.system(`Сессии: ${sessionsDir()}`))
1372
- console.log(
1373
- theme.system(`Dev mode (auto-reload): ${devMode ? 'вкл' : 'выкл'}`),
1374
- )
1375
- console.log(theme.system(`Лимит итераций: ${maxIter}`))
1376
- console.log(theme.system(`Headless: ${headless ? 'да' : 'нет'}`))
1377
- console.log(theme.system(`Debug: ${debug ? 'да' : 'нет'}`))
1378
- console.log(theme.system(`Undo: ${config.undo.enabled ? 'вкл' : 'выкл'}`))
1379
- console.log(theme.system(`Транскрипт: ${transcript.file || 'выкл'}`))
1380
- continue
1381
- }
1382
-
1383
- if (lower === '/config') {
1384
- console.log(JSON.stringify(config, null, 2))
1385
- continue
1386
- }
1387
-
1388
- if (lower === '/transcript') {
1389
- console.log(theme.system(transcript.file || '(выключен)'))
1390
- continue
1391
- }
1392
-
1393
- if (lower === '/undo') {
1394
- const result = await undo.undoLast()
1395
- if (result.ok) {
1396
- console.log(
1397
- theme.assistant(`↶ Откатили: ${result.record.originalPath}`) +
1398
- theme.system(
1399
- result.record.existed ? ' (восстановлено)' : ' (удалено)',
1400
- ),
1401
- )
1402
- transcript.log('undo', { path: result.record.originalPath })
1403
- } else {
1404
- console.error(theme.error(`Не удалось откатить: ${result.reason}`))
1405
- }
1406
- continue
1407
- }
1408
-
1409
- if (lower === '/undo-list' || lower === '/history') {
1410
- const list = await undo.list(10)
1411
- if (!list.length) {
1412
- console.log(theme.system('История пуста.'))
1413
- } else {
1414
- for (const r of list) {
1415
- const stamp = new Date(r.stamp).toLocaleString()
1416
- const flag = r.existed ? 'изменён' : 'создан'
1417
- console.log(theme.system(`${stamp} [${flag}] ${r.originalPath}`))
1418
- }
1419
- }
1420
- continue
1421
- }
1422
-
1423
- if (lower === '/debug-dom') {
1424
- const stamp = new Date().toISOString().replace(/[:.]/g, '-')
1425
- const file = path.join(config.transcript.dir, `dom-${stamp}.html`)
1426
- try {
1427
- const result = await browser.dumpDom(file)
1428
- console.log(theme.assistant(`HTML сохранён: ${result.file}`))
1429
- console.log(theme.system('Селекторы:'))
1430
- console.log(JSON.stringify(result.selectors, null, 2))
1431
- } catch (e) {
1432
- console.error(theme.error('Не удалось сохранить DOM:'), e.message)
1433
- }
1434
- continue
1435
- }
1436
-
1437
- if (lower === '/cd' || lower.startsWith('/cd ')) {
1438
- const rawTarget = trimmed.slice(3).trim()
1439
- try {
1440
- const newDir = rawTarget
1441
- ? path.resolve(currentWorkdir, rawTarget)
1442
- : sandboxRoot
1443
- const rel = path.relative(sandboxRoot, newDir)
1444
- if (rel.startsWith('..') || path.isAbsolute(rel)) {
1445
- console.error(theme.error('Нельзя выйти за пределы: ' + sandboxRoot))
1446
- continue
1447
- }
1448
- const stat = await fs.stat(newDir).catch(() => null)
1449
- if (!stat || !stat.isDirectory()) {
1450
- console.error(theme.error('Не директория: ' + newDir))
1451
- continue
1452
- }
1453
- if (newDir === currentWorkdir) {
1454
- console.log(theme.system('Уже здесь.'))
1455
- continue
1456
- }
1457
- if (reviewMode) {
1458
- console.log(theme.system('Вышел из режима ревью (/cd).'))
1459
- reviewMode = null
1460
- }
1461
- currentWorkdir = newDir
1462
- freshChatNext = true
1463
- sendSystemPromptNext = true
1464
- console.log(theme.system('Рабочая директория: ' + newDir))
1465
- } catch (e) {
1466
- console.error(theme.error('Не удалось перейти: ' + e.message))
1467
- }
1468
- continue
1469
- }
1470
-
1471
- if (lower.startsWith('/')) {
1472
- console.error(theme.error(`Неизвестная команда: ${trimmed}. Набери /help.`))
1473
- continue
1474
- }
1475
-
1476
- // ---- Обычная задача (в том числе в review-режиме) ----
1477
-
1478
- // Dev-режим: подхватываем свежие модули логики перед задачей.
1479
- await autoReload()
1480
-
1481
- transcript.log('user_task', { task: trimmed, workdir: currentWorkdir })
1482
-
1483
- const tools = mod.createTools(currentWorkdir, { undo })
1484
- if (editor) editor.busy = true
1485
- try {
1486
- await runTask(browser, tools, trimmed, currentWorkdir, {
1487
- transcript,
1488
- freshChat: freshChatNext,
1489
- sendSystemPrompt: sendSystemPromptNext,
1490
- queue: pendingQueue,
1491
- ui: editor || null,
1492
- })
1493
- } finally {
1494
- if (editor) editor.busy = false
1495
- }
1496
-
1497
- freshChatNext = false
1498
- sendSystemPromptNext = false
1499
- if (!currentChatId) {
1500
- currentChatId = await browser.getCurrentChatId()
1501
- }
1502
- saveLastChat(currentChatId, currentWorkdir)
1503
- }
1504
-
1505
- if (editor) editor.dispose()
1506
- await browser.close().catch(() => {})
1507
- await mod.closeWeb().catch(() => {})
1508
- transcript.close()
1509
- }
1510
-
1511
- main().catch((e) => {
1512
- console.error(theme.error('Критическая ошибка:'), e.message)
1513
- if (debug) console.error(e.stack)
1514
- process.exit(1)
1515
- })