zames_pro 1.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.
- package/LICENSE +21 -0
- package/README.md +48 -0
- package/package.json +36 -0
- package/src/agent-loop.js +419 -0
- package/src/browser.js +468 -0
- package/src/config.js +80 -0
- package/src/confirm.js +85 -0
- package/src/diff.js +124 -0
- package/src/gitTools.js +256 -0
- package/src/index.js +792 -0
- package/src/self-review.js +304 -0
- package/src/spinner.js +55 -0
- package/src/system-prompt.js +84 -0
- package/src/tools.js +206 -0
- package/src/transcript.js +46 -0
- package/src/undo.js +101 -0
- package/src/web.js +266 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import fs from 'fs/promises'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
import * as readlinePromises from 'readline/promises'
|
|
7
|
+
|
|
8
|
+
import { DeepSeekBrowser } from './browser.js'
|
|
9
|
+
import { createTools } from './tools.js'
|
|
10
|
+
import { runAgentLoop } from './agent-loop.js'
|
|
11
|
+
import { createSpinner } from './spinner.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
|
+
|
|
60
|
+
|
|
61
|
+
// ---------- helpers ----------
|
|
62
|
+
|
|
63
|
+
function printHelp() {
|
|
64
|
+
console.log(`
|
|
65
|
+
${chalk.bold('zames')} — агент поверх chat.deepseek.com через Playwright
|
|
66
|
+
|
|
67
|
+
${chalk.bold('Опции CLI:')}
|
|
68
|
+
--dir <path> рабочая директория агента
|
|
69
|
+
--task <text> задача одной строкой
|
|
70
|
+
--chat <id> продолжить существующий чат по id
|
|
71
|
+
--max-iter <n> лимит итераций (по умолчанию ${config.maxIterations})
|
|
72
|
+
--headless браузер без UI
|
|
73
|
+
--debug подробный лог
|
|
74
|
+
--calibrate режим калибровки селекторов
|
|
75
|
+
--help, -h эта справка
|
|
76
|
+
|
|
77
|
+
${chalk.bold('Обычные команды:')}
|
|
78
|
+
/new, /clear новый чат (сброс контекста)
|
|
79
|
+
/chats список последних чатов DeepSeek
|
|
80
|
+
/resume <n> открыть чат №n из /chats
|
|
81
|
+
/chat показать текущий chat id
|
|
82
|
+
/cd <path> сменить рабочую директорию
|
|
83
|
+
/pwd текущая директория
|
|
84
|
+
/status состояние сессии
|
|
85
|
+
/undo откатить последнюю запись/правку
|
|
86
|
+
/undo-list список того, что можно откатить
|
|
87
|
+
/transcript путь к файлу транскрипта
|
|
88
|
+
/config показать текущий конфиг
|
|
89
|
+
/debug-dom сохранить HTML страницы (для отладки)
|
|
90
|
+
/help, help справка
|
|
91
|
+
/exit, /quit, exit выход
|
|
92
|
+
|
|
93
|
+
${chalk.bold('Самообзор (отладка агента):')}
|
|
94
|
+
/self-review [фокус] снять снапшот src/ и запустить ревью
|
|
95
|
+
после этой команды ты остаёшься В СНАПШОТЕ
|
|
96
|
+
и можешь писать «исправь ошибки» и т.п.
|
|
97
|
+
/self-fix <name> [фокус] вернуться в существующий снапшот и продолжить
|
|
98
|
+
/self-done выйти из режима ревью (вернуться в свою папку)
|
|
99
|
+
/self-list список снапшотов
|
|
100
|
+
/self-diff <name> различия между текущим src/ и снапшотом
|
|
101
|
+
/self-apply <name> применить снапшот к src/ (с бэкапом)
|
|
102
|
+
|
|
103
|
+
${chalk.bold('Файлы:')}
|
|
104
|
+
Логи: ${config.transcript.dir}
|
|
105
|
+
Undo: ~/.zames/undo
|
|
106
|
+
Профиль: ~/.zames/profile
|
|
107
|
+
Снапшоты: ~/.zames/snapshots
|
|
108
|
+
Конфиг: ${CONFIG_PATHS.HOME_CONFIG}
|
|
109
|
+
${CONFIG_PATHS.PROJECT_CONFIG}
|
|
110
|
+
`)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function dirLabel(p) {
|
|
114
|
+
return path.basename(p) || p
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function promptOnce(question) {
|
|
118
|
+
process.stdin.resume()
|
|
119
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
120
|
+
process.stdin.setRawMode(false)
|
|
121
|
+
}
|
|
122
|
+
const rl = readlinePromises.createInterface({
|
|
123
|
+
input: process.stdin,
|
|
124
|
+
output: process.stdout,
|
|
125
|
+
})
|
|
126
|
+
try {
|
|
127
|
+
return await rl.question(question)
|
|
128
|
+
} finally {
|
|
129
|
+
rl.close()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------- workdir resolution ----------
|
|
134
|
+
|
|
135
|
+
// ---------- workdir resolution ----------
|
|
136
|
+
|
|
137
|
+
// Агент работает в директории, из которой его запустили (process.cwd()).
|
|
138
|
+
// Это корень sandbox: инструменты не могут выходить выше него.
|
|
139
|
+
async function resolveWorkdir() {
|
|
140
|
+
const explicitDir = getArg('--dir', null)
|
|
141
|
+
const dir = explicitDir ? path.resolve(explicitDir) : process.cwd()
|
|
142
|
+
const stat = await fs.stat(dir).catch(() => null)
|
|
143
|
+
if (!stat || !stat.isDirectory()) {
|
|
144
|
+
throw new Error('Не директория: ' + dir)
|
|
145
|
+
}
|
|
146
|
+
return dir
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------- task runner ----------
|
|
150
|
+
|
|
151
|
+
async function runTask(browser, tools, taskText, workdir, opts) {
|
|
152
|
+
const { transcript, freshChat, sendSystemPrompt } = opts
|
|
153
|
+
|
|
154
|
+
const ui = createSpinner()
|
|
155
|
+
ui.taskHeader(taskText)
|
|
156
|
+
ui.thinking()
|
|
157
|
+
|
|
158
|
+
let finished = false
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
await runAgentLoop({
|
|
162
|
+
browser,
|
|
163
|
+
tools,
|
|
164
|
+
task: taskText,
|
|
165
|
+
workdir,
|
|
166
|
+
maxIterations: maxIter,
|
|
167
|
+
freshChat,
|
|
168
|
+
sendSystemPrompt,
|
|
169
|
+
transcript,
|
|
170
|
+
onThinking: () => ui.thinking(),
|
|
171
|
+
onToolCall: (name, toolArgs) => ui.toolCall(name, toolArgs),
|
|
172
|
+
onToolResult: (result) => ui.toolResult(result),
|
|
173
|
+
onAssistantMessage: (msg) => {
|
|
174
|
+
ui.assistant(msg)
|
|
175
|
+
finished = true
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
} catch (e) {
|
|
179
|
+
ui.stop()
|
|
180
|
+
console.error(chalk.red('\n✖ Ошибка агента:'), e.message)
|
|
181
|
+
if (debug) console.error(e.stack)
|
|
182
|
+
transcript?.log('agent_error', { error: e.message })
|
|
183
|
+
} finally {
|
|
184
|
+
if (!finished) ui.stop()
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------- main ----------
|
|
189
|
+
|
|
190
|
+
async function main() {
|
|
191
|
+
if (hasFlag('--help') || hasFlag('-h')) {
|
|
192
|
+
printHelp()
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (calibrate) {
|
|
197
|
+
console.log(chalk.yellow('\n🔧 Режим калибровки селекторов\n'))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let currentWorkdir
|
|
201
|
+
try {
|
|
202
|
+
currentWorkdir = await resolveWorkdir()
|
|
203
|
+
// Корень sandbox: агент не может выходить выше директории запуска.
|
|
204
|
+
const sandboxRoot = currentWorkdir
|
|
205
|
+
} catch (e) {
|
|
206
|
+
console.error(
|
|
207
|
+
chalk.red('Не удалось определить рабочую директорию:'),
|
|
208
|
+
e.message,
|
|
209
|
+
)
|
|
210
|
+
process.exit(1)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log(chalk.gray(`Рабочая директория: ${currentWorkdir}`))
|
|
214
|
+
|
|
215
|
+
const transcript = new Transcript({
|
|
216
|
+
dir: config.transcript.dir,
|
|
217
|
+
enabled: config.transcript.enabled,
|
|
218
|
+
sessionName: dirLabel(currentWorkdir),
|
|
219
|
+
})
|
|
220
|
+
if (transcript.file) {
|
|
221
|
+
console.log(chalk.gray(`Транскрипт: ${transcript.file}`))
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const undo = new UndoStore(config.undo)
|
|
225
|
+
|
|
226
|
+
const browser = new DeepSeekBrowser({
|
|
227
|
+
headless,
|
|
228
|
+
debug,
|
|
229
|
+
channel: config.browserChannel,
|
|
230
|
+
...config.browser,
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
const bootSpinner = createSpinner()
|
|
234
|
+
bootSpinner.thinking()
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
await browser.launch()
|
|
238
|
+
bootSpinner.stop()
|
|
239
|
+
await browser.waitForLogin()
|
|
240
|
+
} catch (e) {
|
|
241
|
+
bootSpinner.stop()
|
|
242
|
+
console.error(chalk.red('Не удалось запустить браузер:'), e.message)
|
|
243
|
+
if (debug) console.error(e.stack)
|
|
244
|
+
await browser.close().catch(() => {})
|
|
245
|
+
transcript.close()
|
|
246
|
+
process.exit(1)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Разовый режим
|
|
250
|
+
if (task) {
|
|
251
|
+
const tools = createTools(currentWorkdir, { undo })
|
|
252
|
+
|
|
253
|
+
let freshChat = true
|
|
254
|
+
let sendSystemPrompt = true
|
|
255
|
+
|
|
256
|
+
if (chatIdArg) {
|
|
257
|
+
try {
|
|
258
|
+
console.log(chalk.gray(`Открываю чат ${chatIdArg}...`))
|
|
259
|
+
await browser.openChat(chatIdArg)
|
|
260
|
+
freshChat = false
|
|
261
|
+
sendSystemPrompt = true
|
|
262
|
+
} catch (e) {
|
|
263
|
+
console.error(chalk.red(`Не удалось открыть чат: ${e.message}`))
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await runTask(browser, tools, task, currentWorkdir, {
|
|
268
|
+
transcript,
|
|
269
|
+
freshChat,
|
|
270
|
+
sendSystemPrompt,
|
|
271
|
+
})
|
|
272
|
+
await browser.close()
|
|
273
|
+
await closeWeb().catch(() => {})
|
|
274
|
+
transcript.close()
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.log(
|
|
279
|
+
chalk.gray(
|
|
280
|
+
'Интерактивный режим. Введите задачу. Команды — /help. Выход — /exit.\n',
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
let freshChatNext = true
|
|
285
|
+
let sendSystemPromptNext = true
|
|
286
|
+
let lastChats = []
|
|
287
|
+
let currentChatId = null
|
|
288
|
+
let running = true
|
|
289
|
+
|
|
290
|
+
// ---------- review mode state ----------
|
|
291
|
+
// null — обычный режим.
|
|
292
|
+
// { snapDir, snapName, originalWorkdir } — мы внутри снапшота, чат уже
|
|
293
|
+
// инициализирован review-промптом, юзер может просто писать «исправь...».
|
|
294
|
+
let reviewMode = null
|
|
295
|
+
|
|
296
|
+
process.on('SIGINT', async () => {
|
|
297
|
+
running = false
|
|
298
|
+
await browser.close().catch(() => {})
|
|
299
|
+
// Закрываем ленивый headless-браузер из web.js, иначе он останется
|
|
300
|
+
// висеть отдельным процессом после выхода агента.
|
|
301
|
+
await closeWeb().catch(() => {})
|
|
302
|
+
transcript.close()
|
|
303
|
+
console.log(chalk.gray('\nВыход.'))
|
|
304
|
+
process.exit(0)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
if (chatIdArg) {
|
|
308
|
+
try {
|
|
309
|
+
console.log(chalk.gray(`Открываю чат ${chatIdArg}...`))
|
|
310
|
+
await browser.openChat(chatIdArg)
|
|
311
|
+
currentChatId = chatIdArg
|
|
312
|
+
freshChatNext = false
|
|
313
|
+
sendSystemPromptNext = true
|
|
314
|
+
console.log(chalk.gray(`Чат открыт: ${chatIdArg}\n`))
|
|
315
|
+
} catch (e) {
|
|
316
|
+
console.error(chalk.red(`Не удалось открыть чат: ${e.message}`))
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
while (running) {
|
|
321
|
+
let input
|
|
322
|
+
try {
|
|
323
|
+
let label
|
|
324
|
+
if (reviewMode) {
|
|
325
|
+
label = `zames[REVIEW:${reviewMode.snapName}]> `
|
|
326
|
+
} else if (currentChatId) {
|
|
327
|
+
label = `zames[${dirLabel(currentWorkdir)}|${currentChatId.slice(0, 6)}]> `
|
|
328
|
+
} else {
|
|
329
|
+
label = `zames[${dirLabel(currentWorkdir)}]> `
|
|
330
|
+
}
|
|
331
|
+
input = await promptOnce(chalk.cyan(label))
|
|
332
|
+
} catch {
|
|
333
|
+
break
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const trimmed = (input || '').trim()
|
|
337
|
+
if (!trimmed) continue
|
|
338
|
+
|
|
339
|
+
const lower = trimmed.toLowerCase()
|
|
340
|
+
|
|
341
|
+
if (['/exit', '/quit', 'exit', 'quit'].includes(lower)) break
|
|
342
|
+
|
|
343
|
+
if (lower === '/help' || lower === 'help') {
|
|
344
|
+
printHelp()
|
|
345
|
+
continue
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (['/new', '/clear', 'new'].includes(lower)) {
|
|
349
|
+
console.log(chalk.gray('Создаю новый чат...'))
|
|
350
|
+
try {
|
|
351
|
+
await browser.newChat()
|
|
352
|
+
freshChatNext = false
|
|
353
|
+
sendSystemPromptNext = true
|
|
354
|
+
currentChatId = await browser.getCurrentChatId()
|
|
355
|
+
transcript.log('new_chat')
|
|
356
|
+
console.log(chalk.gray('Новый чат.\n'))
|
|
357
|
+
} catch (e) {
|
|
358
|
+
console.error(chalk.red('Не удалось создать новый чат:'), e.message)
|
|
359
|
+
}
|
|
360
|
+
continue
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ---------- Самообзор ----------
|
|
364
|
+
|
|
365
|
+
if (lower === '/self-review' || lower.startsWith('/self-review ')) {
|
|
366
|
+
const focus = trimmed.slice('/self-review'.length).trim()
|
|
367
|
+
|
|
368
|
+
// Запоминаем, куда вернуться
|
|
369
|
+
const originalWorkdir = reviewMode
|
|
370
|
+
? reviewMode.originalWorkdir
|
|
371
|
+
: currentWorkdir
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const result = await selfReview({
|
|
375
|
+
browser,
|
|
376
|
+
config,
|
|
377
|
+
focus: focus || null,
|
|
378
|
+
transcript,
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
// Переходим в review-режим:
|
|
382
|
+
// - рабочая директория = снапшот
|
|
383
|
+
// - чат НЕ сбрасываем — внутри selfReview уже создан свежий чат
|
|
384
|
+
// и отправлен review-промпт, продолжим в нём
|
|
385
|
+
reviewMode = {
|
|
386
|
+
snapDir: result.snapDir,
|
|
387
|
+
snapName: path.basename(result.snapDir),
|
|
388
|
+
originalWorkdir,
|
|
389
|
+
}
|
|
390
|
+
currentWorkdir = result.snapDir
|
|
391
|
+
freshChatNext = false
|
|
392
|
+
sendSystemPromptNext = false
|
|
393
|
+
currentChatId = await browser.getCurrentChatId()
|
|
394
|
+
|
|
395
|
+
console.log(
|
|
396
|
+
chalk.cyan(
|
|
397
|
+
'\n💡 Теперь ты в режиме ревью. Просто пиши агенту, например:\n' +
|
|
398
|
+
' «исправь ошибки»\n' +
|
|
399
|
+
' «доработай обработку ошибок в ask()»\n' +
|
|
400
|
+
' «покажи, что не так с undo»\n' +
|
|
401
|
+
'Выйти: /self-done. Применить: /self-apply ' +
|
|
402
|
+
reviewMode.snapName +
|
|
403
|
+
'\n',
|
|
404
|
+
),
|
|
405
|
+
)
|
|
406
|
+
} catch (e) {
|
|
407
|
+
console.error(chalk.red('Самообзор провалился:'), e.message)
|
|
408
|
+
if (debug) console.error(e.stack)
|
|
409
|
+
}
|
|
410
|
+
continue
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (lower === '/self-fix' || lower.startsWith('/self-fix ')) {
|
|
414
|
+
const rest = trimmed.slice('/self-fix'.length).trim()
|
|
415
|
+
if (!rest) {
|
|
416
|
+
console.error(chalk.red('Использование: /self-fix <name> [фокус]'))
|
|
417
|
+
continue
|
|
418
|
+
}
|
|
419
|
+
const sp = rest.indexOf(' ')
|
|
420
|
+
const name = sp === -1 ? rest : rest.slice(0, sp)
|
|
421
|
+
const focus = sp === -1 ? '' : rest.slice(sp + 1).trim()
|
|
422
|
+
|
|
423
|
+
const snapRoot = path.join(ZAMES_HOME, 'snapshots', name)
|
|
424
|
+
const stat = await fs.stat(snapRoot).catch(() => null)
|
|
425
|
+
if (!stat || !stat.isDirectory()) {
|
|
426
|
+
console.error(chalk.red(`Снапшот не найден: ${snapRoot}`))
|
|
427
|
+
continue
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const originalWorkdir = reviewMode
|
|
431
|
+
? reviewMode.originalWorkdir
|
|
432
|
+
: currentWorkdir
|
|
433
|
+
|
|
434
|
+
// Свежий чат + review-промпт на этот снапшот
|
|
435
|
+
try {
|
|
436
|
+
const { runAgentLoop: ral } = await import('./agent-loop.js')
|
|
437
|
+
const { buildSystemPrompt } = await import('./system-prompt.js')
|
|
438
|
+
const tools = createTools(snapRoot, { undo: null })
|
|
439
|
+
|
|
440
|
+
await browser.newChat()
|
|
441
|
+
const sysPrompt = buildSystemPrompt({
|
|
442
|
+
workdir: snapRoot,
|
|
443
|
+
tools,
|
|
444
|
+
})
|
|
445
|
+
console.log(chalk.gray('Инициализирую review-чат для снапшота...'))
|
|
446
|
+
await browser.ask(sysPrompt, { timeout: 60_000 })
|
|
447
|
+
|
|
448
|
+
if (focus) {
|
|
449
|
+
const ui = createSpinner()
|
|
450
|
+
ui.thinking()
|
|
451
|
+
await ral({
|
|
452
|
+
browser,
|
|
453
|
+
tools,
|
|
454
|
+
task: focus,
|
|
455
|
+
workdir: snapRoot,
|
|
456
|
+
maxIterations: maxIter,
|
|
457
|
+
freshChat: false,
|
|
458
|
+
sendSystemPrompt: false,
|
|
459
|
+
transcript,
|
|
460
|
+
onThinking: () => ui.thinking(),
|
|
461
|
+
onAssistantThought: (text) => {
|
|
462
|
+
ui.stop()
|
|
463
|
+
console.log(chalk.gray('\n💭 ' + text.slice(0, 1200) + '\n'))
|
|
464
|
+
},
|
|
465
|
+
onToolCall: (name, toolArgs) => ui.toolCall(name, toolArgs),
|
|
466
|
+
onToolResult: (r) => ui.toolResult(r),
|
|
467
|
+
onAssistantMessage: (m) => {
|
|
468
|
+
ui.assistant(m)
|
|
469
|
+
},
|
|
470
|
+
})
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
reviewMode = {
|
|
474
|
+
snapDir: snapRoot,
|
|
475
|
+
snapName: name,
|
|
476
|
+
originalWorkdir,
|
|
477
|
+
}
|
|
478
|
+
currentWorkdir = snapRoot
|
|
479
|
+
freshChatNext = false
|
|
480
|
+
sendSystemPromptNext = false
|
|
481
|
+
currentChatId = await browser.getCurrentChatId()
|
|
482
|
+
|
|
483
|
+
console.log(
|
|
484
|
+
chalk.cyan(
|
|
485
|
+
`\n💡 Режим ревью по снапшоту ${name}. Пиши агенту задачу или /self-done.\n`,
|
|
486
|
+
),
|
|
487
|
+
)
|
|
488
|
+
} catch (e) {
|
|
489
|
+
console.error(chalk.red('Не удалось войти в снапшот:'), e.message)
|
|
490
|
+
}
|
|
491
|
+
continue
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (lower === '/self-done') {
|
|
495
|
+
if (!reviewMode) {
|
|
496
|
+
console.log(chalk.gray('Ты и так не в режиме ревью.'))
|
|
497
|
+
continue
|
|
498
|
+
}
|
|
499
|
+
const back = reviewMode.originalWorkdir
|
|
500
|
+
reviewMode = null
|
|
501
|
+
currentWorkdir = back
|
|
502
|
+
// Раз чат занят review-контекстом, для обычной работы создадим новый
|
|
503
|
+
freshChatNext = true
|
|
504
|
+
sendSystemPromptNext = true
|
|
505
|
+
console.log(
|
|
506
|
+
chalk.gray(`Вернулся в ${back}. Следующая задача начнёт новый чат.\n`),
|
|
507
|
+
)
|
|
508
|
+
continue
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (lower === '/self-list') {
|
|
512
|
+
try {
|
|
513
|
+
await selfList({ config })
|
|
514
|
+
} catch (e) {
|
|
515
|
+
console.error(chalk.red('Ошибка:'), e.message)
|
|
516
|
+
}
|
|
517
|
+
continue
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (lower === '/self-diff' || lower.startsWith('/self-diff ')) {
|
|
521
|
+
const name = trimmed.slice('/self-diff'.length).trim()
|
|
522
|
+
if (!name) {
|
|
523
|
+
console.error(chalk.red('Использование: /self-diff <name>'))
|
|
524
|
+
continue
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
await selfDiff({ config, name })
|
|
528
|
+
} catch (e) {
|
|
529
|
+
console.error(chalk.red('Ошибка:'), e.message)
|
|
530
|
+
}
|
|
531
|
+
continue
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (lower === '/self-apply' || lower.startsWith('/self-apply ')) {
|
|
535
|
+
const name = trimmed.slice('/self-apply'.length).trim()
|
|
536
|
+
if (!name) {
|
|
537
|
+
console.error(chalk.red('Использование: /self-apply <name>'))
|
|
538
|
+
continue
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
await selfApply({ config, name })
|
|
542
|
+
} catch (e) {
|
|
543
|
+
console.error(chalk.red('Ошибка:'), e.message)
|
|
544
|
+
}
|
|
545
|
+
continue
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ---------- Обычные команды ----------
|
|
549
|
+
|
|
550
|
+
if (lower === '/chats') {
|
|
551
|
+
const spin = createSpinner()
|
|
552
|
+
spin.thinking()
|
|
553
|
+
try {
|
|
554
|
+
lastChats = await browser.listChats(30)
|
|
555
|
+
spin.stop()
|
|
556
|
+
if (!lastChats.length) {
|
|
557
|
+
console.log(
|
|
558
|
+
chalk.gray(
|
|
559
|
+
'Чатов не найдено. Возможно, сайдбар свёрнут или селекторы устарели.',
|
|
560
|
+
),
|
|
561
|
+
)
|
|
562
|
+
} else {
|
|
563
|
+
console.log(chalk.gray('Последние чаты DeepSeek:'))
|
|
564
|
+
lastChats.forEach((c, i) => {
|
|
565
|
+
const n = String(i + 1).padStart(2, ' ')
|
|
566
|
+
console.log(
|
|
567
|
+
` ${chalk.cyan(n)}. ${c.title} ${chalk.gray('(' + c.id.slice(0, 8) + '…)')}`,
|
|
568
|
+
)
|
|
569
|
+
})
|
|
570
|
+
console.log(chalk.gray('\nИспользуй /resume <n> для продолжения.\n'))
|
|
571
|
+
}
|
|
572
|
+
} catch (e) {
|
|
573
|
+
spin.stop()
|
|
574
|
+
console.error(chalk.red('Не удалось получить список:'), e.message)
|
|
575
|
+
}
|
|
576
|
+
continue
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (lower === '/resume' || lower.startsWith('/resume ')) {
|
|
580
|
+
const arg = trimmed.slice(7).trim()
|
|
581
|
+
if (!arg) {
|
|
582
|
+
console.error(
|
|
583
|
+
chalk.red('Использование: /resume <n> (или /chats для списка)'),
|
|
584
|
+
)
|
|
585
|
+
continue
|
|
586
|
+
}
|
|
587
|
+
const n = Number(arg)
|
|
588
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
589
|
+
console.error(chalk.red('Нужен номер из /chats.'))
|
|
590
|
+
continue
|
|
591
|
+
}
|
|
592
|
+
if (!lastChats.length) {
|
|
593
|
+
console.error(chalk.red('Сначала выполни /chats.'))
|
|
594
|
+
continue
|
|
595
|
+
}
|
|
596
|
+
const pick = lastChats[n - 1]
|
|
597
|
+
if (!pick) {
|
|
598
|
+
console.error(chalk.red(`Нет чата №${n}. Всего: ${lastChats.length}.`))
|
|
599
|
+
continue
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
console.log(chalk.gray(`Открываю: ${pick.title}`))
|
|
603
|
+
try {
|
|
604
|
+
await browser.openChat(pick.id)
|
|
605
|
+
currentChatId = pick.id
|
|
606
|
+
freshChatNext = false
|
|
607
|
+
sendSystemPromptNext = true
|
|
608
|
+
transcript.log('resume_chat', { id: pick.id, title: pick.title })
|
|
609
|
+
console.log(
|
|
610
|
+
chalk.green(`Чат открыт.`) +
|
|
611
|
+
chalk.gray(
|
|
612
|
+
' Системный промпт будет переслан на следующей задаче.\n',
|
|
613
|
+
),
|
|
614
|
+
)
|
|
615
|
+
} catch (e) {
|
|
616
|
+
console.error(chalk.red('Не удалось открыть чат:'), e.message)
|
|
617
|
+
}
|
|
618
|
+
continue
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (lower === '/chat') {
|
|
622
|
+
if (currentChatId) {
|
|
623
|
+
console.log(chalk.gray(`Текущий chat id: ${currentChatId}`))
|
|
624
|
+
console.log(
|
|
625
|
+
chalk.gray(
|
|
626
|
+
`URL: https://chat.deepseek.com/a/chat/s/${currentChatId}`,
|
|
627
|
+
),
|
|
628
|
+
)
|
|
629
|
+
} else {
|
|
630
|
+
const id = await browser.getCurrentChatId()
|
|
631
|
+
console.log(
|
|
632
|
+
chalk.gray(id ? `Текущий chat id: ${id}` : 'Чат ещё не создан.'),
|
|
633
|
+
)
|
|
634
|
+
}
|
|
635
|
+
continue
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (lower === '/pwd') {
|
|
639
|
+
console.log(chalk.gray(currentWorkdir))
|
|
640
|
+
continue
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (lower === '/status') {
|
|
644
|
+
console.log(chalk.gray(`Рабочая директория: ${currentWorkdir}`))
|
|
645
|
+
console.log(
|
|
646
|
+
chalk.gray(`Режим ревью: ${reviewMode ? reviewMode.snapName : 'нет'}`),
|
|
647
|
+
)
|
|
648
|
+
if (reviewMode) {
|
|
649
|
+
console.log(
|
|
650
|
+
chalk.gray(`Исходная директория: ${reviewMode.originalWorkdir}`),
|
|
651
|
+
)
|
|
652
|
+
}
|
|
653
|
+
console.log(chalk.gray(`Текущий чат: ${currentChatId || '(нет)'}`))
|
|
654
|
+
console.log(
|
|
655
|
+
chalk.gray(
|
|
656
|
+
`Fresh chat на след. задаче: ${freshChatNext ? 'да' : 'нет'}`,
|
|
657
|
+
),
|
|
658
|
+
)
|
|
659
|
+
console.log(
|
|
660
|
+
chalk.gray(
|
|
661
|
+
`System prompt на след. задаче: ${sendSystemPromptNext ? 'да' : 'нет'}`,
|
|
662
|
+
),
|
|
663
|
+
)
|
|
664
|
+
console.log(chalk.gray(`Лимит итераций: ${maxIter}`))
|
|
665
|
+
console.log(chalk.gray(`Headless: ${headless ? 'да' : 'нет'}`))
|
|
666
|
+
console.log(chalk.gray(`Debug: ${debug ? 'да' : 'нет'}`))
|
|
667
|
+
console.log(chalk.gray(`Undo: ${config.undo.enabled ? 'вкл' : 'выкл'}`))
|
|
668
|
+
console.log(chalk.gray(`Транскрипт: ${transcript.file || 'выкл'}`))
|
|
669
|
+
continue
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (lower === '/config') {
|
|
673
|
+
console.log(JSON.stringify(config, null, 2))
|
|
674
|
+
continue
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (lower === '/transcript') {
|
|
678
|
+
console.log(chalk.gray(transcript.file || '(выключен)'))
|
|
679
|
+
continue
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (lower === '/undo') {
|
|
683
|
+
const result = await undo.undoLast()
|
|
684
|
+
if (result.ok) {
|
|
685
|
+
console.log(
|
|
686
|
+
chalk.green(`↶ Откатили: ${result.record.originalPath}`) +
|
|
687
|
+
chalk.gray(
|
|
688
|
+
result.record.existed ? ' (восстановлено)' : ' (удалено)',
|
|
689
|
+
),
|
|
690
|
+
)
|
|
691
|
+
transcript.log('undo', { path: result.record.originalPath })
|
|
692
|
+
} else {
|
|
693
|
+
console.error(chalk.red(`Не удалось откатить: ${result.reason}`))
|
|
694
|
+
}
|
|
695
|
+
continue
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (lower === '/undo-list' || lower === '/history') {
|
|
699
|
+
const list = await undo.list(10)
|
|
700
|
+
if (!list.length) {
|
|
701
|
+
console.log(chalk.gray('История пуста.'))
|
|
702
|
+
} else {
|
|
703
|
+
for (const r of list) {
|
|
704
|
+
const stamp = new Date(r.stamp).toLocaleString()
|
|
705
|
+
const flag = r.existed ? 'изменён' : 'создан'
|
|
706
|
+
console.log(chalk.gray(`${stamp} [${flag}] ${r.originalPath}`))
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
continue
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (lower === '/debug-dom') {
|
|
713
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
714
|
+
const file = path.join(config.transcript.dir, `dom-${stamp}.html`)
|
|
715
|
+
try {
|
|
716
|
+
const result = await browser.dumpDom(file)
|
|
717
|
+
console.log(chalk.green(`HTML сохранён: ${result.file}`))
|
|
718
|
+
console.log(chalk.gray('Селекторы:'))
|
|
719
|
+
console.log(JSON.stringify(result.selectors, null, 2))
|
|
720
|
+
} catch (e) {
|
|
721
|
+
console.error(chalk.red('Не удалось сохранить DOM:'), e.message)
|
|
722
|
+
}
|
|
723
|
+
continue
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (lower === '/cd' || lower.startsWith('/cd ')) {
|
|
727
|
+
const rawTarget = trimmed.slice(3).trim()
|
|
728
|
+
try {
|
|
729
|
+
const newDir = rawTarget
|
|
730
|
+
? path.resolve(currentWorkdir, rawTarget)
|
|
731
|
+
: sandboxRoot
|
|
732
|
+
const rel = path.relative(sandboxRoot, newDir)
|
|
733
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
734
|
+
console.error(chalk.red('Нельзя выйти за пределы: ' + sandboxRoot))
|
|
735
|
+
continue
|
|
736
|
+
}
|
|
737
|
+
const stat = await fs.stat(newDir).catch(() => null)
|
|
738
|
+
if (!stat || !stat.isDirectory()) {
|
|
739
|
+
console.error(chalk.red('Не директория: ' + newDir))
|
|
740
|
+
continue
|
|
741
|
+
}
|
|
742
|
+
if (newDir === currentWorkdir) {
|
|
743
|
+
console.log(chalk.gray('Уже здесь.'))
|
|
744
|
+
continue
|
|
745
|
+
}
|
|
746
|
+
if (reviewMode) {
|
|
747
|
+
console.log(chalk.gray('Вышел из режима ревью (/cd).'))
|
|
748
|
+
reviewMode = null
|
|
749
|
+
}
|
|
750
|
+
currentWorkdir = newDir
|
|
751
|
+
freshChatNext = true
|
|
752
|
+
sendSystemPromptNext = true
|
|
753
|
+
console.log(chalk.gray('Рабочая директория: ' + newDir))
|
|
754
|
+
} catch (e) {
|
|
755
|
+
console.error(chalk.red('Не удалось перейти: ' + e.message))
|
|
756
|
+
}
|
|
757
|
+
continue
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (lower.startsWith('/')) {
|
|
761
|
+
console.error(chalk.red(`Неизвестная команда: ${trimmed}. Набери /help.`))
|
|
762
|
+
continue
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ---- Обычная задача (в том числе в review-режиме) ----
|
|
766
|
+
|
|
767
|
+
transcript.log('user_task', { task: trimmed, workdir: currentWorkdir })
|
|
768
|
+
|
|
769
|
+
const tools = createTools(currentWorkdir, { undo })
|
|
770
|
+
await runTask(browser, tools, trimmed, currentWorkdir, {
|
|
771
|
+
transcript,
|
|
772
|
+
freshChat: freshChatNext,
|
|
773
|
+
sendSystemPrompt: sendSystemPromptNext,
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
freshChatNext = false
|
|
777
|
+
sendSystemPromptNext = false
|
|
778
|
+
if (!currentChatId) {
|
|
779
|
+
currentChatId = await browser.getCurrentChatId()
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
await browser.close().catch(() => {})
|
|
784
|
+
await closeWeb().catch(() => {})
|
|
785
|
+
transcript.close()
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
main().catch((e) => {
|
|
789
|
+
console.error(chalk.red('Критическая ошибка:'), e.message)
|
|
790
|
+
if (debug) console.error(e.stack)
|
|
791
|
+
process.exit(1)
|
|
792
|
+
})
|