zames_pro 1.2.0 → 2.0.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.
Files changed (57) hide show
  1. package/dist/agent-loop.js +652 -0
  2. package/dist/agent-loop.js.map +1 -0
  3. package/dist/browser.js +573 -0
  4. package/dist/browser.js.map +1 -0
  5. package/dist/config.js +74 -0
  6. package/dist/config.js.map +1 -0
  7. package/dist/confirm.js +82 -0
  8. package/dist/confirm.js.map +1 -0
  9. package/dist/diff.js +38 -0
  10. package/dist/diff.js.map +1 -0
  11. package/dist/gitTools.js +219 -0
  12. package/dist/gitTools.js.map +1 -0
  13. package/dist/index.js +1365 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/input.js +524 -0
  16. package/dist/input.js.map +1 -0
  17. package/dist/markdown.js +48 -0
  18. package/dist/markdown.js.map +1 -0
  19. package/dist/self-review.js +281 -0
  20. package/dist/self-review.js.map +1 -0
  21. package/dist/sessions.js +123 -0
  22. package/dist/sessions.js.map +1 -0
  23. package/dist/spinner.js +138 -0
  24. package/dist/spinner.js.map +1 -0
  25. package/{src → dist}/system-prompt.js +10 -14
  26. package/dist/system-prompt.js.map +1 -0
  27. package/{src → dist}/theme.js +17 -19
  28. package/dist/theme.js.map +1 -0
  29. package/dist/tools.js +189 -0
  30. package/dist/tools.js.map +1 -0
  31. package/dist/transcript.js +51 -0
  32. package/dist/transcript.js.map +1 -0
  33. package/dist/types.js +4 -0
  34. package/dist/types.js.map +1 -0
  35. package/dist/undo.js +100 -0
  36. package/dist/undo.js.map +1 -0
  37. package/dist/web.js +220 -0
  38. package/dist/web.js.map +1 -0
  39. package/dist/xml-toolcall.js +68 -0
  40. package/dist/xml-toolcall.js.map +1 -0
  41. package/package.json +13 -5
  42. package/src/agent-loop.js +0 -575
  43. package/src/browser.js +0 -627
  44. package/src/config.js +0 -81
  45. package/src/confirm.js +0 -85
  46. package/src/diff.js +0 -32
  47. package/src/gitTools.js +0 -256
  48. package/src/index.js +0 -1461
  49. package/src/input.js +0 -447
  50. package/src/markdown.js +0 -49
  51. package/src/self-review.js +0 -319
  52. package/src/spinner.js +0 -144
  53. package/src/tools.js +0 -206
  54. package/src/transcript.js +0 -46
  55. package/src/undo.js +0 -101
  56. package/src/web.js +0 -266
  57. package/src/xml-toolcall.js +0 -69
package/src/index.js DELETED
@@ -1,1461 +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
-
18
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
19
-
20
- // ---------- CLI parsing ----------
21
-
22
- const args = process.argv.slice(2)
23
-
24
- function getArg(flag, fallback = null) {
25
- const i = args.indexOf(flag)
26
- return i !== -1 && args[i + 1] ? args[i + 1] : fallback
27
- }
28
-
29
- function hasFlag(flag) {
30
- return args.includes(flag)
31
- }
32
-
33
- function getPositional() {
34
- const positional = []
35
- for (let i = 0; i < args.length; i++) {
36
- const a = args[i]
37
- if (['--dir', '--task', '--max-iter', '--project', '--chat'].includes(a)) {
38
- i++
39
- continue
40
- }
41
- if (a.startsWith('--')) continue
42
- positional.push(a)
43
- }
44
- return positional
45
- }
46
-
47
- const config = loadConfig()
48
-
49
- const headless = hasFlag('--headless') || config.headless
50
- const debug = hasFlag('--debug') || config.debug
51
- const calibrate = hasFlag('--calibrate')
52
- const maxIter =
53
- Number(getArg('--max-iter', String(config.maxIterations))) ||
54
- config.maxIterations
55
-
56
- const positional = getPositional()
57
- const task = getArg('--task', positional.join(' ').trim() || null)
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
- }
181
-
182
-
183
- // ---------- helpers ----------
184
-
185
- function printHelp() {
186
- console.log(`
187
- ${theme.bold('zames')} — агент поверх chat.deepseek.com через Playwright
188
-
189
- ${theme.bold('Опции CLI:')}
190
- --dir <path> рабочая директория агента
191
- --task <text> задача одной строкой
192
- --chat <id> продолжить существующий чат по id
193
- --resume-last вернуться в последний сохранённый чат
194
- --new-chat начать новый чат (по умолчанию и так новый)
195
- --resend-prompt дослать system-prompt в существующий чат
196
- --max-iter <n> лимит итераций (по умолчанию ${config.maxIterations})
197
- --headless браузер без UI
198
- --debug подробный лог
199
- --calibrate режим калибровки селекторов
200
- --dev режим разработки: авто-перечитывание модулей
201
- --version, -v показать версию
202
- --help, -h эта справка
203
-
204
- ${theme.bold('Пока агент работает:')}
205
- печать + Enter поставить сообщение в очередь (уйдёт после текущей задачи)
206
- Esc, Ctrl+C прервать текущую генерацию
207
-
208
- ${theme.bold('Обычные команды:')}
209
- /new, /clear новый чат (сброс контекста)
210
- /chats список последних чатов DeepSeek
211
- /resume <n> открыть чат №n из /chats
212
- /chat показать текущий chat id
213
- /cd <path> сменить рабочую директорию
214
- /pwd текущая директория
215
- /status состояние сессии
216
- /reload перечитать модули логики без перезапуска
217
- /undo откатить последнюю запись/правку
218
- /undo-list список того, что можно откатить
219
- /transcript путь к файлу транскрипта
220
- /config показать текущий конфиг
221
- /debug-dom сохранить HTML страницы (для отладки)
222
- /help, help справка
223
- /exit, /quit, exit выход
224
-
225
- ${theme.bold('Самообзор (отладка агента):')}
226
- /self-review [фокус] снять снапшот src/ и запустить ревью
227
- после этой команды ты остаёшься В СНАПШОТЕ
228
- и можешь писать «исправь ошибки» и т.п.
229
- /self-fix <name> [фокус] вернуться в существующий снапшот и продолжить
230
- /self-done выйти из режима ревью (вернуться в свою папку)
231
- /self-list список снапшотов
232
- /self-diff <name> различия между текущим src/ и снапшотом
233
- /self-apply <name> применить снапшот к src/ (с бэкапом)
234
-
235
- ${theme.bold('Файлы:')}
236
- Логи: ${config.transcript.dir}
237
- Undo: ~/.zames/undo
238
- Профиль: ~/.zames/profile
239
- Снапшоты: ~/.zames/snapshots
240
- Временные: <проект>/tmp (в .gitignore, чистится при запуске)
241
- Конфиг: ${CONFIG_PATHS.HOME_CONFIG}
242
- ${CONFIG_PATHS.PROJECT_CONFIG}
243
- `)
244
- }
245
-
246
- function dirLabel(p) {
247
- return path.basename(p) || p
248
- }
249
-
250
- // Временные файлы агента (одноразовые скрипты и т.п.) складываем в
251
- // <проект>/tmp — эта папка в .gitignore и очищается при каждом запуске.
252
- const TMP_DIR = path.join(__dirname, '..', 'tmp')
253
-
254
- async function cleanTmpDir() {
255
- try {
256
- await fs.rm(TMP_DIR, { recursive: true, force: true })
257
- await fs.mkdir(TMP_DIR, { recursive: true })
258
- } catch (e) {
259
- if (debug) console.error('tmp: не удалось очистить:', e.message)
260
- }
261
- }
262
-
263
- // Ввод строки в терминале с корректной обработкой вставки (Shift+Insert,
264
- // Ctrl+Shift+V, правая кнопка мыши и т.п.).
265
- //
266
- // Зачем свой ридер вместо readline:
267
- // 1) readline отправляет строку на ПЕРВОМ переводе строки. При вставке
268
- // многострочного текста это приводило к немедленной отправке и к тому,
269
- // что в чат уходила только первая строка. Здесь переводы строк внутри
270
- // вставки заменяются на пробелы, а отправка происходит только по
271
- // одиночному нажатию Enter.
272
- // 2) Включаем bracketed paste mode (\x1b[?2004h): терминал оборачивает
273
- // вставленный текст в маркеры \x1b[200~ … \x1b[201~, поэтому мы точно
274
- // знаем, что это вставка, а не набор с клавиатуры, и Enter внутри неё
275
- // не считается отправкой.
276
- async function promptOnce(question) {
277
- const stdin = process.stdin
278
- const stdout = process.stdout
279
-
280
- // Не-TTY (пайп, редирект): читаем всё до EOF одной строкой.
281
- if (!stdin.isTTY || !stdin.setRawMode) {
282
- const chunks = []
283
- return await new Promise((resolve) => {
284
- const onData = (b) => chunks.push(b)
285
- const onEnd = () => {
286
- stdin.removeListener('data', onData)
287
- stdin.removeListener('end', onEnd)
288
- resolve(Buffer.concat(chunks).toString('utf-8'))
289
- }
290
- stdin.on('data', onData)
291
- stdin.on('end', onEnd)
292
- stdin.resume()
293
- })
294
- }
295
-
296
- const wasRaw = stdin.isRaw
297
- stdin.setRawMode(true)
298
- stdin.resume()
299
-
300
- // Bracketed paste включаем/выключаем парно.
301
- stdout.write('\x1b[?2004h')
302
- stdout.write(question)
303
-
304
- let line = ''
305
- let cursor = 0
306
- let inPaste = false
307
- const PASTE_START = '\x1b[200~'
308
- const PASTE_END = '\x1b[201~'
309
-
310
- const redraw = () => {
311
- // Возвращаемся в начало строки, стираем и печатаем заново.
312
- stdout.write(String.fromCharCode(13))
313
- stdout.write('\x1b[K')
314
- stdout.write(question + line)
315
- // Ставим курсор в нужную позицию.
316
- const back = line.length - cursor
317
- if (back > 0) stdout.write('\x1b[' + back + 'D')
318
- }
319
-
320
- return await new Promise((resolve) => {
321
- const finish = (value, submit) => {
322
- stdin.removeListener('data', onData)
323
- stdout.write('\x1b[?2004l')
324
- if (stdin.setRawMode) stdin.setRawMode(wasRaw || false)
325
- if (submit) stdout.write(String.fromCharCode(10))
326
- resolve(value)
327
- }
328
-
329
- const insertText = (text) => {
330
- // Нормализуем переводы строк: они приходят от многострочной вставки,
331
- // но означают «отправить». Внутри сообщения заменяем на пробел, чтобы
332
- // вся вставка ушла ОДНИМ сообщением.
333
- const clean = text
334
- .replace(/\r\n/g, ' ')
335
- .replace(/\r/g, ' ')
336
- .replace(/\n/g, ' ')
337
- line = line.slice(0, cursor) + clean + line.slice(cursor)
338
- cursor += clean.length
339
- }
340
-
341
- const onData = (buf) => {
342
- let s = buf.toString('utf-8')
343
-
344
- // Fallback для терминалов без bracketed paste: если весь чанк — это
345
- // «голый» перевод строки (один байт), значит нажали Enter → отправляем.
346
- // Если переводы строк пришли ВМЕСТЕ с другим текстом в одном чанке —
347
- // это вставка; такие переводы строк не отправляют сообщение, а
348
- // заменяются на пробелы (см. insertText).
349
- if (!inPaste && (s === '\r' || s === '\n')) {
350
- return finish(line, true)
351
- }
352
-
353
- while (s.length) {
354
- if (inPaste) {
355
- const end = s.indexOf(PASTE_END)
356
- if (end === -1) {
357
- insertText(s)
358
- s = ''
359
- } else {
360
- insertText(s.slice(0, end))
361
- s = s.slice(end + PASTE_END.length)
362
- inPaste = false
363
- }
364
- redraw()
365
- continue
366
- }
367
-
368
- const start = s.indexOf(PASTE_START)
369
- if (start !== -1) {
370
- // Всё до маркера обрабатываем как обычный ввод.
371
- const before = s.slice(0, start)
372
- s = s.slice(start + PASTE_START.length)
373
- inPaste = true
374
- if (before) {
375
- for (const ch of before) {
376
- if (ch === '\r' || ch === '\n') { /* внутри вставки — пропускаем */ }
377
- else insertText(ch)
378
- }
379
- redraw()
380
- }
381
- continue
382
- }
383
-
384
- const ch = s[0]
385
- const code = s.charCodeAt(0)
386
- s = s.slice(1)
387
-
388
- if (ch === '\r' || ch === '\n') {
389
- // Перевод строки внутри чанка с другим текстом (вставка без
390
- // bracketed paste): не отправляем, а вставляем пробел.
391
- insertText(' ')
392
- redraw()
393
- continue
394
- }
395
- if (code === 3) {
396
- // Ctrl+C — прерываем ввод.
397
- return finish('', true)
398
- }
399
- if (code === 4) {
400
- // Ctrl+D — как отправка пустой строки.
401
- return finish(line, true)
402
- }
403
- if (code === 21) {
404
- // Ctrl+U — стереть строку.
405
- line = ''
406
- cursor = 0
407
- redraw()
408
- continue
409
- }
410
- if (code === 127 || code === 8) {
411
- // Backspace.
412
- if (cursor > 0) {
413
- line = line.slice(0, cursor - 1) + line.slice(cursor)
414
- cursor--
415
- redraw()
416
- }
417
- continue
418
- }
419
- if (ch === '\x1b') {
420
- // Escape-последовательности (стрелки, Home/End, Delete…).
421
- const rest = s
422
- if (rest.startsWith('[D')) {
423
- if (cursor > 0) cursor--
424
- s = s.slice(2)
425
- redraw()
426
- continue
427
- }
428
- if (rest.startsWith('[C')) {
429
- if (cursor < line.length) cursor++
430
- s = s.slice(2)
431
- redraw()
432
- continue
433
- }
434
- if (rest.startsWith('[H') || rest.startsWith('[1~')) {
435
- cursor = 0
436
- s = s.slice(rest.startsWith('[1~') ? 3 : 2)
437
- redraw()
438
- continue
439
- }
440
- if (rest.startsWith('[F') || rest.startsWith('[4~')) {
441
- cursor = line.length
442
- s = s.slice(rest.startsWith('[4~') ? 3 : 2)
443
- redraw()
444
- continue
445
- }
446
- if (rest.startsWith('[3~')) {
447
- // Delete.
448
- if (cursor < line.length) {
449
- line = line.slice(0, cursor) + line.slice(cursor + 1)
450
- redraw()
451
- }
452
- s = s.slice(3)
453
- continue
454
- }
455
- // Прочие ESC-последовательности пропускаем до буквы/тильды.
456
- const m = s.match(/^\[[0-9;]*[A-Za-z~]/)
457
- if (m) s = s.slice(m[0].length)
458
- continue
459
- }
460
- if (code < 32) continue // прочие управляющие символы игнорируем
461
-
462
- insertText(ch)
463
- redraw()
464
- }
465
- }
466
-
467
- stdin.on('data', onData)
468
- })
469
- }
470
-
471
- // Слежение за клавиатурой во время работы агента.
472
- //
473
- // Терминал остаётся живым, пока агент думает:
474
- // - Esc (или Ctrl+C) — прервать текущую генерацию (клик Stop в браузере);
475
- // - набор текста + Enter — поставить сообщение в очередь; оно уйдёт
476
- // агенту сразу после того, как текущая задача завершится
477
- // (как «отправить во время генерации» в веб-версии DeepSeek).
478
- //
479
- // Ввод буферизуется без построчного редактора: набранный текст отображается
480
- // в строке спиннера через onChange -> ui.setPending(). Enter отправляет буфер
481
- // в onQueue, пустой Enter игнорируется. Поддержаны Backspace, Ctrl+U, Esc-
482
- // последовательности (стрелки/Home/End/Delete игнорируются) и bracketed paste.
483
- function watchInput({ onEscape, onChange, onQueue } = {}) {
484
- const stdin = process.stdin
485
- if (!stdin.isTTY || !stdin.setRawMode) return () => {}
486
-
487
- // Управляющие символы собираем из кодов: в этом файле нельзя писать
488
- // «сырые» ESC/CR/LF в строковых литералах (см. AGENTS.md).
489
- const ESC = String.fromCharCode(27)
490
- const CSI = String.fromCharCode(91)
491
- const CR = String.fromCharCode(13)
492
- const LF = String.fromCharCode(10)
493
-
494
- const wasRaw = stdin.isRaw
495
- stdin.setRawMode(true)
496
- stdin.resume()
497
- process.stdout.write(ESC + '[?2004h')
498
-
499
- let buf = ''
500
- let inPaste = false
501
- const PASTE_START = ESC + '[200~'
502
- const PASTE_END = ESC + '[201~'
503
- const CSI_RE = new RegExp('^' + CSI + '[0-9;]*[A-Za-z~]')
504
-
505
- const emitChange = () => {
506
- if (onChange) onChange(buf)
507
- }
508
-
509
- // Вставленный текст: переводы строк внутри многострочной вставки
510
- // трактуем как пробелы — сообщение уходит одной строкой.
511
- const insert = (text) => {
512
- buf += text
513
- .split(CR + LF).join(' ')
514
- .split(CR).join(' ')
515
- .split(LF).join(' ')
516
- }
517
-
518
- function onData(data) {
519
- let s = data.toString('utf-8')
520
-
521
- // Одиночный Esc — прервать генерацию. Стрелки приходят целым чанком
522
- // и сюда не попадают.
523
- if (!inPaste && s === ESC) {
524
- if (onEscape) onEscape()
525
- return
526
- }
527
-
528
- while (s.length) {
529
- if (inPaste) {
530
- const end = s.indexOf(PASTE_END)
531
- if (end === -1) {
532
- insert(s)
533
- s = ''
534
- } else {
535
- insert(s.slice(0, end))
536
- s = s.slice(end + PASTE_END.length)
537
- inPaste = false
538
- }
539
- emitChange()
540
- continue
541
- }
542
-
543
- const start = s.indexOf(PASTE_START)
544
- if (start !== -1) {
545
- const before = s.slice(0, start)
546
- s = s.slice(start + PASTE_START.length)
547
- inPaste = true
548
- if (before) {
549
- insert(before)
550
- emitChange()
551
- }
552
- continue
553
- }
554
-
555
- const ch = s[0]
556
- const code = s.charCodeAt(0)
557
- s = s.slice(1)
558
-
559
- if (ch === CR || ch === LF) {
560
- const text = buf.trim()
561
- buf = ''
562
- emitChange()
563
- if (text && onQueue) onQueue(text)
564
- continue
565
- }
566
- if (code === 3) {
567
- // Ctrl+C — как Esc: прервать генерацию.
568
- if (onEscape) onEscape()
569
- continue
570
- }
571
- if (code === 21) {
572
- // Ctrl+U — очистить набранное.
573
- buf = ''
574
- emitChange()
575
- continue
576
- }
577
- if (code === 127 || code === 8) {
578
- // Backspace.
579
- if (buf) {
580
- buf = buf.slice(0, -1)
581
- emitChange()
582
- }
583
- continue
584
- }
585
- if (ch === ESC) {
586
- // Escape-последовательность (стрелки, Home/End, Delete) — пропускаем.
587
- const m = s.match(CSI_RE)
588
- if (m) s = s.slice(m[0].length)
589
- continue
590
- }
591
- if (code < 32) continue // прочие управляющие — игнорируем
592
-
593
- buf += ch
594
- emitChange()
595
- }
596
- }
597
-
598
- const handler = (data) => onData(data)
599
- stdin.on('data', handler)
600
- return () => {
601
- stdin.removeListener('data', handler)
602
- process.stdout.write(ESC + '[?2004l')
603
- if (stdin.setRawMode) stdin.setRawMode(wasRaw || false)
604
- }
605
- }
606
-
607
- // ---------- workdir resolution ----------
608
-
609
- // Агент работает в директории, из которой его запустили (process.cwd()).
610
- // Это корень sandbox: инструменты не могут выходить выше него.
611
- async function resolveWorkdir() {
612
- const explicitDir = getArg('--dir', null)
613
- const dir = explicitDir ? path.resolve(explicitDir) : process.cwd()
614
- const stat = await fs.stat(dir).catch(() => null)
615
- if (!stat || !stat.isDirectory()) {
616
- throw new Error('Не директория: ' + dir)
617
- }
618
- return dir
619
- }
620
-
621
- // ---------- task runner ----------
622
-
623
- async function runTask(browser, tools, taskText, workdir, opts) {
624
- const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor } = opts
625
-
626
- // В TTY-режиме UI — это LineEditor: он владеет вводом (очередь, Esc,
627
- // Ctrl+C) и рисует статус НАД постоянной строкой ввода. В не-TTY режиме
628
- // (пайпы) — обычный спиннер + watchInput.
629
- const ui = editor || mod.createSpinner()
630
- const stopWatching = editor
631
- ? () => {}
632
- : watchInput({
633
- onEscape: () => {
634
- ui.stop()
635
- console.error(theme.warn('⏹ Esc — прерываю генерацию...'))
636
- browser.stopGeneration().catch(() => {})
637
- },
638
- onChange: (text) => ui.setPending(text),
639
- onQueue: (text) => {
640
- queue.push(text)
641
- ui.setPending(null)
642
- ui.stop()
643
- console.log(
644
- theme.user('📨 В очередь (' + queue.length + '): ') + theme.assistant(text),
645
- )
646
- ui.thinking()
647
- },
648
- })
649
-
650
- try {
651
- let next = { task: taskText, freshChat, sendSystemPrompt }
652
-
653
- // Выполняем задачу, затем — всё, что пользователь успел напечатать за
654
- // время работы. Очередь может пополняться прямо во время дренажа.
655
- while (true) {
656
- ui.thinking()
657
- await mod.runAgentLoop({
658
- browser,
659
- tools,
660
- task: next.task,
661
- workdir,
662
- maxIterations: maxIter,
663
- freshChat: next.freshChat,
664
- sendSystemPrompt: next.sendSystemPrompt,
665
- transcript,
666
- onThinking: () => ui.thinking(),
667
- onToolCall: (name, toolArgs) => ui.toolCall(name, toolArgs),
668
- onToolResult: (result) => ui.toolResult(result),
669
- onAssistantMessage: (msg) => ui.assistant(msg),
670
- debugLog: debug,
671
- })
672
-
673
- if (!queue.length) break
674
-
675
- const queued = queue.shift()
676
- ui.stop()
677
- if (editor) {
678
- editor.printAbove(theme.user('▶ Из очереди: ') + theme.assistant(queued))
679
- } else {
680
- console.log(theme.user('▶ Из очереди: ') + theme.assistant(queued))
681
- }
682
- transcript?.log('queued_task', { task: queued })
683
- next = { task: queued, freshChat: false, sendSystemPrompt: false }
684
- }
685
- } catch (e) {
686
- ui.stop()
687
- console.error(theme.error(String.fromCharCode(10) + '✖ Ошибка агента:'), e.message)
688
- if (debug) console.error(e.stack)
689
- transcript?.log('agent_error', { error: e.message })
690
- } finally {
691
- stopWatching()
692
- ui.stop()
693
- }
694
- }
695
-
696
- // ---------- main ----------
697
-
698
- async function main() {
699
- if (hasFlag('--version') || hasFlag('-v')) {
700
- const pkg = JSON.parse(
701
- await fs.readFile(path.join(__dirname, '..', 'package.json'), 'utf-8'),
702
- )
703
- console.log(pkg.version)
704
- return
705
- }
706
-
707
- if (hasFlag('--help') || hasFlag('-h')) {
708
- printHelp()
709
- return
710
- }
711
-
712
- if (calibrate) {
713
- console.log(theme.warn('\n🔧 Режим калибровки селекторов\n'))
714
- }
715
-
716
- await cleanTmpDir()
717
-
718
- let currentWorkdir
719
- try {
720
- currentWorkdir = await resolveWorkdir()
721
- // Корень sandbox: агент не может выходить выше директории запуска.
722
- const sandboxRoot = currentWorkdir
723
- } catch (e) {
724
- console.error(
725
- theme.error('Не удалось определить рабочую директорию:'),
726
- e.message,
727
- )
728
- process.exit(1)
729
- }
730
-
731
- console.log(theme.system(`Рабочая директория: ${currentWorkdir}`))
732
-
733
- const transcript = new Transcript({
734
- dir: config.transcript.dir,
735
- enabled: config.transcript.enabled,
736
- sessionName: dirLabel(currentWorkdir),
737
- })
738
- if (transcript.file) {
739
- console.log(theme.system(`Транскрипт: ${transcript.file}`))
740
- }
741
-
742
- const undo = new UndoStore(config.undo)
743
-
744
- const browser = new DeepSeekBrowser({
745
- headless,
746
- debug,
747
- channel: config.browserChannel,
748
- ...config.browser,
749
- })
750
-
751
- const bootSpinner = mod.createSpinner()
752
- bootSpinner.thinking()
753
-
754
- try {
755
- await browser.launch()
756
- bootSpinner.stop()
757
- await browser.waitForLogin()
758
- } catch (e) {
759
- bootSpinner.stop()
760
- console.error(theme.error('Не удалось запустить браузер:'), e.message)
761
- if (debug) console.error(e.stack)
762
- await browser.close().catch(() => {})
763
- transcript.close()
764
- process.exit(1)
765
- }
766
-
767
- // Разовый режим
768
- if (task) {
769
- const tools = mod.createTools(currentWorkdir, { undo })
770
-
771
- let freshChat = true
772
- let sendSystemPrompt = true
773
-
774
- // По умолчанию — новый чат. Продолжить прошлый можно явно:
775
- // --chat <id> открыть конкретный чат
776
- // --resume-last вернуться в последний сохранённый чат
777
- let resumeId = chatIdArg
778
- if (!resumeId && resumeLastFlag) {
779
- resumeId = await loadLastChat()
780
- if (resumeId) console.log(theme.system(`Восстанавливаю чат ${resumeId}...`))
781
- }
782
-
783
- if (resumeId) {
784
- try {
785
- console.log(theme.system(`Открываю чат ${resumeId}...`))
786
- await browser.openChat(resumeId)
787
- freshChat = false
788
- sendSystemPrompt = resendPrompt
789
- } catch (e) {
790
- console.error(theme.error(`Не удалось открыть чат: ${e.message}`))
791
- }
792
- }
793
-
794
- await autoReload()
795
-
796
- await runTask(browser, tools, task, currentWorkdir, {
797
- transcript,
798
- freshChat,
799
- sendSystemPrompt,
800
- })
801
- await browser.close()
802
- await mod.closeWeb().catch(() => {})
803
- transcript.close()
804
- return
805
- }
806
-
807
- console.log(
808
- theme.system(
809
- 'Интерактивный режим. Введите задачу. Команды — /help. Выход — /exit.',
810
- ),
811
- )
812
- console.log(
813
- theme.system(
814
- 'Пока агент работает, можно печатать следующее сообщение — оно уйдёт в очередь (Enter — отправить, Esc — прервать).',
815
- ),
816
- )
817
-
818
- let freshChatNext = true
819
- let sendSystemPromptNext = true
820
- let lastChats = []
821
- let currentChatId = null
822
- let running = true
823
-
824
- // Сообщения, набранные пользователем, пока агент работал. runTask
825
- // забирает их по одному после завершения текущей задачи.
826
- const pendingQueue = []
827
-
828
- // ---------- review mode state ----------
829
- // null — обычный режим.
830
- // { snapDir, snapName, originalWorkdir } — мы внутри снапшота, чат уже
831
- // инициализирован review-промптом, юзер может просто писать «исправь...».
832
- let reviewMode = null
833
-
834
- process.on('SIGINT', async () => {
835
- running = false
836
- await browser.close().catch(() => {})
837
- // Закрываем ленивый headless-браузер из web.js, иначе он останется
838
- // висеть отдельным процессом после выхода агента.
839
- await mod.closeWeb().catch(() => {})
840
- transcript.close()
841
- console.log(theme.system('\nВыход.'))
842
- process.exit(0)
843
- })
844
-
845
- // По умолчанию — новый чат. Продолжить прошлый можно явно:
846
- // --chat <id> открыть конкретный чат
847
- // --resume-last вернуться в последний сохранённый чат
848
- let resumeId = chatIdArg
849
- if (!resumeId && resumeLastFlag) {
850
- resumeId = await loadLastChat()
851
- if (resumeId) console.log(theme.system(`Восстанавливаю чат ${resumeId}...`))
852
- }
853
-
854
- if (resumeId) {
855
- try {
856
- console.log(theme.system(`Открываю чат ${resumeId}...`))
857
- await browser.openChat(resumeId)
858
- currentChatId = resumeId
859
- freshChatNext = false
860
- sendSystemPromptNext = resendPrompt
861
- await saveLastChat(resumeId)
862
- console.log(theme.system(`Чат открыт: ${resumeId}\n`))
863
- } catch (e) {
864
- console.error(theme.error(`Не удалось открыть чат: ${e.message}`))
865
- }
866
- }
867
-
868
- // ---------- ввод: постоянная строка внизу + статус сверху ----------
869
- // В TTY используем LineEditor: он владеет вводом всё время, показывает
870
- // статус над строкой ввода и печатает ответы агента ВЫШЕ неё, поэтому
871
- // набранный текст никогда не затирается выводом. В не-TTY (пайп) —
872
- // старый promptOnce.
873
- let editor = null
874
- let waiter = null
875
- const takeInput = () => {
876
- if (pendingQueue.length) return Promise.resolve(pendingQueue.shift())
877
- return new Promise((resolve) => {
878
- waiter = resolve
879
- })
880
- }
881
-
882
- const buildPrompt = () => {
883
- let tail
884
- if (reviewMode) {
885
- tail = theme.warn('REVIEW') + theme.dim(':') + theme.dir(reviewMode.snapName)
886
- } else {
887
- tail = theme.dir(dirLabel(currentWorkdir))
888
- }
889
- return theme.prompt('❯ ') + tail + theme.dim(' › ')
890
- }
891
-
892
- if (process.stdin.isTTY && process.stdout.isTTY) {
893
- editor = new LineEditor({ prompt: buildPrompt() })
894
- editor.onSubmit = (text) => {
895
- pendingQueue.push(text)
896
- if (waiter) {
897
- const r = waiter
898
- waiter = null
899
- r(pendingQueue.shift())
900
- }
901
- }
902
- editor.onEscape = () => {
903
- if (editor.busy) {
904
- editor.printAbove(theme.warn('⏹ Esc — прерываю генерацию...'))
905
- browser.stopGeneration().catch(() => {})
906
- }
907
- }
908
- editor.onCtrlC = () => {
909
- if (editor.busy) {
910
- editor.printAbove(theme.warn('⏹ Ctrl+C — прерываю генерацию...'))
911
- browser.stopGeneration().catch(() => {})
912
- } else {
913
- // Не заняты — выходим. Будим takeInput(), чтобы цикл завершился.
914
- running = false
915
- if (waiter) {
916
- const r = waiter
917
- waiter = null
918
- r(null)
919
- }
920
- }
921
- }
922
- editor.start()
923
-
924
- // Весь вывод команд (console.log/error) должен идти ВЫШЕ строки ввода,
925
- // иначе он затирает набираемый текст. Пока редактор активен, заворачиваем
926
- // оба потока в editor.printAbove.
927
- const origLog = console.log.bind(console)
928
- const origErr = console.error.bind(console)
929
- const fmt = (a) =>
930
- typeof a === 'string' ? a : (() => {
931
- try {
932
- return JSON.stringify(a)
933
- } catch {
934
- return String(a)
935
- }
936
- })()
937
- console.log = (...a) => editor.printAbove(a.map(fmt).join(' '))
938
- console.error = (...a) => editor.printAbove(a.map(fmt).join(' '))
939
- // Сохраняем на случай отладки.
940
- editor._origLog = origLog
941
- editor._origErr = origErr
942
- }
943
-
944
- while (running) {
945
- let input
946
- try {
947
- if (editor) {
948
- editor.setPrompt(buildPrompt())
949
- input = await takeInput()
950
- } else {
951
- let tail
952
- if (reviewMode) {
953
- tail =
954
- theme.warn('REVIEW') + theme.dim(':') + theme.dir(reviewMode.snapName)
955
- } else {
956
- tail = theme.dir(dirLabel(currentWorkdir))
957
- }
958
- input = await promptOnce(theme.prompt('❯ ') + tail + theme.dim(' › '))
959
- }
960
- } catch {
961
- break
962
- }
963
-
964
- const trimmed = (input || '').trim()
965
- if (!trimmed) continue
966
-
967
- const lower = trimmed.toLowerCase()
968
-
969
- if (['/exit', '/quit', 'exit', 'quit'].includes(lower)) break
970
-
971
- if (lower === '/help' || lower === 'help') {
972
- printHelp()
973
- continue
974
- }
975
-
976
- if (['/new', '/clear', 'new'].includes(lower)) {
977
- console.log(theme.system('Создаю новый чат...'))
978
- try {
979
- await browser.newChat()
980
- freshChatNext = false
981
- sendSystemPromptNext = true
982
- currentChatId = await browser.getCurrentChatId()
983
- await saveLastChat(currentChatId)
984
- transcript.log('new_chat')
985
- console.log(theme.system('Новый чат.\n'))
986
- } catch (e) {
987
- console.error(theme.error('Не удалось создать новый чат:'), e.message)
988
- }
989
- continue
990
- }
991
-
992
- // ---------- Самообзор ----------
993
-
994
- if (lower === '/self-review' || lower.startsWith('/self-review ')) {
995
- const focus = trimmed.slice('/self-review'.length).trim()
996
-
997
- // Запоминаем, куда вернуться
998
- const originalWorkdir = reviewMode
999
- ? reviewMode.originalWorkdir
1000
- : currentWorkdir
1001
-
1002
- try {
1003
- const result = await mod.selfReview({
1004
- browser,
1005
- config,
1006
- focus: focus || null,
1007
- transcript,
1008
- })
1009
-
1010
- // Переходим в review-режим:
1011
- // - рабочая директория = снапшот
1012
- // - чат НЕ сбрасываем — внутри selfReview уже создан свежий чат
1013
- // и отправлен review-промпт, продолжим в нём
1014
- reviewMode = {
1015
- snapDir: result.snapDir,
1016
- snapName: path.basename(result.snapDir),
1017
- originalWorkdir,
1018
- }
1019
- currentWorkdir = result.snapDir
1020
- freshChatNext = false
1021
- sendSystemPromptNext = false
1022
- currentChatId = await browser.getCurrentChatId()
1023
- await saveLastChat(currentChatId)
1024
-
1025
- console.log(
1026
- theme.user(
1027
- '\n💡 Теперь ты в режиме ревью. Просто пиши агенту, например:\n' +
1028
- ' «исправь ошибки»\n' +
1029
- ' «доработай обработку ошибок в ask()»\n' +
1030
- ' «покажи, что не так с undo»\n' +
1031
- 'Выйти: /self-done. Применить: /self-apply ' +
1032
- reviewMode.snapName +
1033
- '\n',
1034
- ),
1035
- )
1036
- } catch (e) {
1037
- console.error(theme.error('Самообзор провалился:'), e.message)
1038
- if (debug) console.error(e.stack)
1039
- }
1040
- continue
1041
- }
1042
-
1043
- if (lower === '/self-fix' || lower.startsWith('/self-fix ')) {
1044
- const rest = trimmed.slice('/self-fix'.length).trim()
1045
- if (!rest) {
1046
- console.error(theme.error('Использование: /self-fix <name> [фокус]'))
1047
- continue
1048
- }
1049
- const sp = rest.indexOf(' ')
1050
- const name = sp === -1 ? rest : rest.slice(0, sp)
1051
- const focus = sp === -1 ? '' : rest.slice(sp + 1).trim()
1052
-
1053
- const snapRoot = path.join(ZAMES_HOME, 'snapshots', name)
1054
- const stat = await fs.stat(snapRoot).catch(() => null)
1055
- if (!stat || !stat.isDirectory()) {
1056
- console.error(theme.error(`Снапшот не найден: ${snapRoot}`))
1057
- continue
1058
- }
1059
-
1060
- const originalWorkdir = reviewMode
1061
- ? reviewMode.originalWorkdir
1062
- : currentWorkdir
1063
-
1064
- // Свежий чат + review-промпт на этот снапшот
1065
- try {
1066
- const { runAgentLoop: ral } = await import('./agent-loop.js')
1067
- const { buildSystemPrompt } = await import('./system-prompt.js')
1068
- const tools = mod.createTools(snapRoot, { undo: null })
1069
-
1070
- await browser.newChat()
1071
- const sysPrompt = mod.buildSystemPrompt({
1072
- workdir: snapRoot,
1073
- tools,
1074
- })
1075
- console.log(theme.system('Инициализирую review-чат для снапшота...'))
1076
- await browser.ask(sysPrompt, { timeout: 60_000 })
1077
-
1078
- if (focus) {
1079
- const ui = mod.createSpinner()
1080
- ui.thinking()
1081
- await ral({
1082
- browser,
1083
- tools,
1084
- task: focus,
1085
- workdir: snapRoot,
1086
- maxIterations: maxIter,
1087
- freshChat: false,
1088
- sendSystemPrompt: false,
1089
- transcript,
1090
- onThinking: () => ui.thinking(),
1091
- onToolCall: (name, toolArgs) => ui.toolCall(name, toolArgs),
1092
- onToolResult: (r) => ui.toolResult(r),
1093
- onAssistantMessage: (m) => {
1094
- ui.assistant(m)
1095
- },
1096
- })
1097
- }
1098
-
1099
- reviewMode = {
1100
- snapDir: snapRoot,
1101
- snapName: name,
1102
- originalWorkdir,
1103
- }
1104
- currentWorkdir = snapRoot
1105
- freshChatNext = false
1106
- sendSystemPromptNext = false
1107
- currentChatId = await browser.getCurrentChatId()
1108
- await saveLastChat(currentChatId)
1109
-
1110
- console.log(
1111
- theme.user(
1112
- `\n💡 Режим ревью по снапшоту ${name}. Пиши агенту задачу или /self-done.\n`,
1113
- ),
1114
- )
1115
- } catch (e) {
1116
- console.error(theme.error('Не удалось войти в снапшот:'), e.message)
1117
- }
1118
- continue
1119
- }
1120
-
1121
- if (lower === '/self-done') {
1122
- if (!reviewMode) {
1123
- console.log(theme.system('Ты и так не в режиме ревью.'))
1124
- continue
1125
- }
1126
- const back = reviewMode.originalWorkdir
1127
- reviewMode = null
1128
- currentWorkdir = back
1129
- // Раз чат занят review-контекстом, для обычной работы создадим новый
1130
- freshChatNext = true
1131
- sendSystemPromptNext = true
1132
- console.log(
1133
- theme.system(`Вернулся в ${back}. Следующая задача начнёт новый чат.\n`),
1134
- )
1135
- continue
1136
- }
1137
-
1138
- if (lower === '/self-list') {
1139
- try {
1140
- await mod.selfList({ config })
1141
- } catch (e) {
1142
- console.error(theme.error('Ошибка:'), e.message)
1143
- }
1144
- continue
1145
- }
1146
-
1147
- if (lower === '/self-diff' || lower.startsWith('/self-diff ')) {
1148
- const name = trimmed.slice('/self-diff'.length).trim()
1149
- if (!name) {
1150
- console.error(theme.error('Использование: /self-diff <name>'))
1151
- continue
1152
- }
1153
- try {
1154
- await mod.selfDiff({ config, name })
1155
- } catch (e) {
1156
- console.error(theme.error('Ошибка:'), e.message)
1157
- }
1158
- continue
1159
- }
1160
-
1161
- if (lower === '/self-apply' || lower.startsWith('/self-apply ')) {
1162
- const name = trimmed.slice('/self-apply'.length).trim()
1163
- if (!name) {
1164
- console.error(theme.error('Использование: /self-apply <name>'))
1165
- continue
1166
- }
1167
- try {
1168
- await mod.selfApply({ config, name })
1169
- } catch (e) {
1170
- console.error(theme.error('Ошибка:'), e.message)
1171
- }
1172
- continue
1173
- }
1174
-
1175
- // ---------- Обычные команды ----------
1176
-
1177
- if (lower === '/chats') {
1178
- const spin = editor || mod.createSpinner()
1179
- spin.thinking()
1180
- try {
1181
- lastChats = await browser.listChats(30)
1182
- spin.stop()
1183
- if (!lastChats.length) {
1184
- console.log(
1185
- theme.system(
1186
- 'Чатов не найдено. Возможно, сайдбар свёрнут или селекторы устарели.',
1187
- ),
1188
- )
1189
- } else {
1190
- console.log(theme.system('Последние чаты DeepSeek:'))
1191
- lastChats.forEach((c, i) => {
1192
- const n = String(i + 1).padStart(2, ' ')
1193
- console.log(
1194
- ` ${theme.user(n)}. ${c.title} ${theme.system('(' + c.id.slice(0, 8) + '…)')}`,
1195
- )
1196
- })
1197
- console.log(theme.system('\nИспользуй /resume <n> для продолжения.\n'))
1198
- }
1199
- } catch (e) {
1200
- spin.stop()
1201
- console.error(theme.error('Не удалось получить список:'), e.message)
1202
- }
1203
- continue
1204
- }
1205
-
1206
- if (lower === '/resume' || lower.startsWith('/resume ')) {
1207
- const arg = trimmed.slice(7).trim()
1208
- if (!arg) {
1209
- console.error(
1210
- theme.error('Использование: /resume <n> (или /chats для списка)'),
1211
- )
1212
- continue
1213
- }
1214
- const n = Number(arg)
1215
- if (!Number.isFinite(n) || n < 1) {
1216
- console.error(theme.error('Нужен номер из /chats.'))
1217
- continue
1218
- }
1219
- if (!lastChats.length) {
1220
- console.error(theme.error('Сначала выполни /chats.'))
1221
- continue
1222
- }
1223
- const pick = lastChats[n - 1]
1224
- if (!pick) {
1225
- console.error(theme.error(`Нет чата №${n}. Всего: ${lastChats.length}.`))
1226
- continue
1227
- }
1228
-
1229
- console.log(theme.system(`Открываю: ${pick.title}`))
1230
- try {
1231
- await browser.openChat(pick.id)
1232
- currentChatId = pick.id
1233
- freshChatNext = false
1234
- sendSystemPromptNext = resendPrompt
1235
- await saveLastChat(pick.id)
1236
- transcript.log('resume_chat', { id: pick.id, title: pick.title })
1237
- console.log(
1238
- theme.assistant(`Чат открыт.`) +
1239
- theme.system(
1240
- resendPrompt ? ' Системный промпт будет переслан на следующей задаче.\n' : ' Контекст чата сохранён. Системный промпт не пересылается (--resend-prompt чтобы дослать).\n',
1241
- ),
1242
- )
1243
- } catch (e) {
1244
- console.error(theme.error('Не удалось открыть чат:'), e.message)
1245
- }
1246
- continue
1247
- }
1248
-
1249
- if (lower === '/chat') {
1250
- if (currentChatId) {
1251
- console.log(theme.system(`Текущий chat id: ${currentChatId}`))
1252
- console.log(
1253
- theme.system(
1254
- `URL: https://chat.deepseek.com/a/chat/s/${currentChatId}`,
1255
- ),
1256
- )
1257
- } else {
1258
- const id = await browser.getCurrentChatId()
1259
- console.log(
1260
- theme.system(id ? `Текущий chat id: ${id}` : 'Чат ещё не создан.'),
1261
- )
1262
- }
1263
- continue
1264
- }
1265
-
1266
- if (lower === '/pwd') {
1267
- console.log(theme.system(currentWorkdir))
1268
- continue
1269
- }
1270
-
1271
- if (lower === '/reload') {
1272
- console.log(theme.system('Перечитываю модули логики...'))
1273
- try {
1274
- const { count, errors } = await reloadModules()
1275
- if (errors.length) {
1276
- console.error(theme.error('Часть модулей не перезагрузилась:'))
1277
- for (const e of errors) console.error(theme.error(' ' + e))
1278
- } else {
1279
- console.log(
1280
- theme.assistant(
1281
- `Перезагружено модулей: ${count}. Браузер и чат не тронуты.`,
1282
- ),
1283
- )
1284
- }
1285
- } catch (e) {
1286
- console.error(theme.error('Ошибка reload:'), e.message)
1287
- }
1288
- continue
1289
- }
1290
-
1291
- if (lower === '/status') {
1292
- console.log(theme.system(`Рабочая директория: ${currentWorkdir}`))
1293
- console.log(
1294
- theme.system(`Режим ревью: ${reviewMode ? reviewMode.snapName : 'нет'}`),
1295
- )
1296
- if (reviewMode) {
1297
- console.log(
1298
- theme.system(`Исходная директория: ${reviewMode.originalWorkdir}`),
1299
- )
1300
- }
1301
- console.log(theme.system(`Текущий чат: ${currentChatId || '(нет)'}`))
1302
- console.log(
1303
- theme.system(
1304
- `Fresh chat на след. задаче: ${freshChatNext ? 'да' : 'нет'}`,
1305
- ),
1306
- )
1307
- console.log(
1308
- theme.system(
1309
- `System prompt на след. задаче: ${sendSystemPromptNext ? 'да' : 'нет'}`,
1310
- ),
1311
- )
1312
- console.log(
1313
- theme.system(`Resend prompt (--resend-prompt): ${resendPrompt ? 'да' : 'нет'}`),
1314
- )
1315
- console.log(
1316
- theme.system(`Last chat: ${(await loadLastChat()) || 'нет'}`),
1317
- )
1318
- console.log(
1319
- theme.system(`Dev mode (auto-reload): ${devMode ? 'вкл' : 'выкл'}`),
1320
- )
1321
- console.log(theme.system(`Лимит итераций: ${maxIter}`))
1322
- console.log(theme.system(`Headless: ${headless ? 'да' : 'нет'}`))
1323
- console.log(theme.system(`Debug: ${debug ? 'да' : 'нет'}`))
1324
- console.log(theme.system(`Undo: ${config.undo.enabled ? 'вкл' : 'выкл'}`))
1325
- console.log(theme.system(`Транскрипт: ${transcript.file || 'выкл'}`))
1326
- continue
1327
- }
1328
-
1329
- if (lower === '/config') {
1330
- console.log(JSON.stringify(config, null, 2))
1331
- continue
1332
- }
1333
-
1334
- if (lower === '/transcript') {
1335
- console.log(theme.system(transcript.file || '(выключен)'))
1336
- continue
1337
- }
1338
-
1339
- if (lower === '/undo') {
1340
- const result = await undo.undoLast()
1341
- if (result.ok) {
1342
- console.log(
1343
- theme.assistant(`↶ Откатили: ${result.record.originalPath}`) +
1344
- theme.system(
1345
- result.record.existed ? ' (восстановлено)' : ' (удалено)',
1346
- ),
1347
- )
1348
- transcript.log('undo', { path: result.record.originalPath })
1349
- } else {
1350
- console.error(theme.error(`Не удалось откатить: ${result.reason}`))
1351
- }
1352
- continue
1353
- }
1354
-
1355
- if (lower === '/undo-list' || lower === '/history') {
1356
- const list = await undo.list(10)
1357
- if (!list.length) {
1358
- console.log(theme.system('История пуста.'))
1359
- } else {
1360
- for (const r of list) {
1361
- const stamp = new Date(r.stamp).toLocaleString()
1362
- const flag = r.existed ? 'изменён' : 'создан'
1363
- console.log(theme.system(`${stamp} [${flag}] ${r.originalPath}`))
1364
- }
1365
- }
1366
- continue
1367
- }
1368
-
1369
- if (lower === '/debug-dom') {
1370
- const stamp = new Date().toISOString().replace(/[:.]/g, '-')
1371
- const file = path.join(config.transcript.dir, `dom-${stamp}.html`)
1372
- try {
1373
- const result = await browser.dumpDom(file)
1374
- console.log(theme.assistant(`HTML сохранён: ${result.file}`))
1375
- console.log(theme.system('Селекторы:'))
1376
- console.log(JSON.stringify(result.selectors, null, 2))
1377
- } catch (e) {
1378
- console.error(theme.error('Не удалось сохранить DOM:'), e.message)
1379
- }
1380
- continue
1381
- }
1382
-
1383
- if (lower === '/cd' || lower.startsWith('/cd ')) {
1384
- const rawTarget = trimmed.slice(3).trim()
1385
- try {
1386
- const newDir = rawTarget
1387
- ? path.resolve(currentWorkdir, rawTarget)
1388
- : sandboxRoot
1389
- const rel = path.relative(sandboxRoot, newDir)
1390
- if (rel.startsWith('..') || path.isAbsolute(rel)) {
1391
- console.error(theme.error('Нельзя выйти за пределы: ' + sandboxRoot))
1392
- continue
1393
- }
1394
- const stat = await fs.stat(newDir).catch(() => null)
1395
- if (!stat || !stat.isDirectory()) {
1396
- console.error(theme.error('Не директория: ' + newDir))
1397
- continue
1398
- }
1399
- if (newDir === currentWorkdir) {
1400
- console.log(theme.system('Уже здесь.'))
1401
- continue
1402
- }
1403
- if (reviewMode) {
1404
- console.log(theme.system('Вышел из режима ревью (/cd).'))
1405
- reviewMode = null
1406
- }
1407
- currentWorkdir = newDir
1408
- freshChatNext = true
1409
- sendSystemPromptNext = true
1410
- console.log(theme.system('Рабочая директория: ' + newDir))
1411
- } catch (e) {
1412
- console.error(theme.error('Не удалось перейти: ' + e.message))
1413
- }
1414
- continue
1415
- }
1416
-
1417
- if (lower.startsWith('/')) {
1418
- console.error(theme.error(`Неизвестная команда: ${trimmed}. Набери /help.`))
1419
- continue
1420
- }
1421
-
1422
- // ---- Обычная задача (в том числе в review-режиме) ----
1423
-
1424
- // Dev-режим: подхватываем свежие модули логики перед задачей.
1425
- await autoReload()
1426
-
1427
- transcript.log('user_task', { task: trimmed, workdir: currentWorkdir })
1428
-
1429
- const tools = mod.createTools(currentWorkdir, { undo })
1430
- if (editor) editor.busy = true
1431
- try {
1432
- await runTask(browser, tools, trimmed, currentWorkdir, {
1433
- transcript,
1434
- freshChat: freshChatNext,
1435
- sendSystemPrompt: sendSystemPromptNext,
1436
- queue: pendingQueue,
1437
- ui: editor || null,
1438
- })
1439
- } finally {
1440
- if (editor) editor.busy = false
1441
- }
1442
-
1443
- freshChatNext = false
1444
- sendSystemPromptNext = false
1445
- if (!currentChatId) {
1446
- currentChatId = await browser.getCurrentChatId()
1447
- }
1448
- await saveLastChat(currentChatId)
1449
- }
1450
-
1451
- if (editor) editor.dispose()
1452
- await browser.close().catch(() => {})
1453
- await mod.closeWeb().catch(() => {})
1454
- transcript.close()
1455
- }
1456
-
1457
- main().catch((e) => {
1458
- console.error(theme.error('Критическая ошибка:'), e.message)
1459
- if (debug) console.error(e.stack)
1460
- process.exit(1)
1461
- })