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