zames_pro 1.0.1 → 1.0.3
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/README.md +28 -6
- package/logo.png +0 -0
- package/package.json +5 -3
- package/src/agent-loop.js +130 -6
- package/src/index.js +214 -28
- package/src/self-review.js +17 -2
- package/src/system-prompt.js +10 -0
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# zames_pro
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
Terminal coding agent, работающий поверх chat.deepseek.com через Playwright.
|
|
4
6
|
По духу похож на Claude Code / Codex CLI: запускается в текущей директории, читает и правит файлы, запускает команды, коммитит в git.
|
|
5
7
|
|
|
@@ -11,18 +13,18 @@ Terminal coding agent, работающий поверх chat.deepseek.com че
|
|
|
11
13
|
|
|
12
14
|
## Установка
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
`bash
|
|
15
17
|
npm install -g zames_pro
|
|
16
18
|
npx playwright install chromium
|
|
17
|
-
|
|
19
|
+
`
|
|
18
20
|
|
|
19
21
|
## Запуск
|
|
20
22
|
|
|
21
23
|
Перейди в папку проекта и запусти:
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
`bash
|
|
24
26
|
zames
|
|
25
|
-
|
|
27
|
+
`
|
|
26
28
|
|
|
27
29
|
Агент работает в директории запуска и не может выходить за её пределы (sandbox).
|
|
28
30
|
|
|
@@ -30,9 +32,14 @@ zames
|
|
|
30
32
|
|
|
31
33
|
```
|
|
32
34
|
zames --task <текст задачи>
|
|
35
|
+
zames --chat <id>
|
|
36
|
+
zames --new-chat
|
|
37
|
+
zames --resend-prompt
|
|
38
|
+
zames --dir <путь>
|
|
33
39
|
zames --headless
|
|
34
40
|
zames --debug
|
|
35
|
-
zames --
|
|
41
|
+
zames --dev
|
|
42
|
+
zames --version
|
|
36
43
|
zames --help
|
|
37
44
|
```
|
|
38
45
|
|
|
@@ -43,6 +50,21 @@ zames --help
|
|
|
43
50
|
|
|
44
51
|
Данные агента хранятся в `~/.zames`: профиль браузера, логи, история undo, снапшоты self-review.
|
|
45
52
|
|
|
46
|
-
##
|
|
53
|
+
## Разработка
|
|
54
|
+
|
|
55
|
+
Для работы над самим агентом используй dev-режим:
|
|
56
|
+
|
|
57
|
+
`bash
|
|
58
|
+
npm run dev
|
|
59
|
+
`
|
|
60
|
+
|
|
61
|
+
В этом режиме агент перед каждой задачей автоматически перечитывает модули логики (tools, agent-loop, system-prompt и др.) с диска — правки подхватываются без перезапуска браузера и без потери контекста чата. В обычном режиме (zames, npm start) авто-перечитывание выключено.
|
|
62
|
+
|
|
63
|
+
Служебные команды:
|
|
64
|
+
- /reload — принудительно перечитать модули логики
|
|
65
|
+
- /self-review — снять снапшот src/ и запустить самообзор
|
|
66
|
+
- /status — состояние сессии
|
|
67
|
+
|
|
68
|
+
## Лицензия и авторство
|
|
47
69
|
|
|
48
70
|
MIT
|
package/logo.png
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Terminal coding agent over chat.deepseek.com via Playwright",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"src",
|
|
11
11
|
"README.md",
|
|
12
|
-
"LICENSE"
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"logo.png"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
15
|
-
"start": "node ./src/index.js"
|
|
16
|
+
"start": "node ./src/index.js",
|
|
17
|
+
"dev": "node ./src/index.js --dev"
|
|
16
18
|
},
|
|
17
19
|
"keywords": [
|
|
18
20
|
"agent",
|
package/src/agent-loop.js
CHANGED
|
@@ -175,6 +175,68 @@ function tryParseArray(str) {
|
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
// Жадный разбор args для грязного JSON (незаэкранированные кавычки в строке).
|
|
179
|
+
function parseArgsGreedy(str) {
|
|
180
|
+
const result = {}
|
|
181
|
+
let i = str.indexOf('{') + 1
|
|
182
|
+
if (i === 0) return null
|
|
183
|
+
const skipWs = () => {
|
|
184
|
+
while (i < str.length && /[\s,]/.test(str[i])) i++
|
|
185
|
+
}
|
|
186
|
+
while (i < str.length) {
|
|
187
|
+
skipWs()
|
|
188
|
+
if (i >= str.length || str[i] === '}') break
|
|
189
|
+
if (str[i] !== '"') return null
|
|
190
|
+
const keyEnd = str.indexOf('"', i + 1)
|
|
191
|
+
if (keyEnd === -1) return null
|
|
192
|
+
const key = str.slice(i + 1, keyEnd)
|
|
193
|
+
i = keyEnd + 1
|
|
194
|
+
while (i < str.length && /\s/.test(str[i])) i++
|
|
195
|
+
if (str[i] !== ':') return null
|
|
196
|
+
i++
|
|
197
|
+
while (i < str.length && /\s/.test(str[i])) i++
|
|
198
|
+
if (str[i] === '"') {
|
|
199
|
+
let lastEnd = -1
|
|
200
|
+
let k = i + 1
|
|
201
|
+
while (k < str.length) {
|
|
202
|
+
if (str.charCodeAt(k) === 92) { k += 2; continue }
|
|
203
|
+
if (str[k] === '"') {
|
|
204
|
+
let t = k + 1
|
|
205
|
+
while (t < str.length && /\s/.test(str[t])) t++
|
|
206
|
+
if (t >= str.length || str[t] === ',' || str[t] === '}') lastEnd = k
|
|
207
|
+
}
|
|
208
|
+
k++
|
|
209
|
+
}
|
|
210
|
+
if (lastEnd === -1) return null
|
|
211
|
+
result[key] = unescapeValue(str.slice(i + 1, lastEnd))
|
|
212
|
+
i = lastEnd + 1
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
if (str[i] === '{' || str[i] === '[') {
|
|
216
|
+
const open = str[i]
|
|
217
|
+
const close = open === '{' ? '}' : ']'
|
|
218
|
+
const end = findMatching(str, i, open, close)
|
|
219
|
+
if (end === -1) return null
|
|
220
|
+
let parsed = null
|
|
221
|
+
try { parsed = JSON.parse(str.slice(i, end + 1)) }
|
|
222
|
+
catch { if (open === '{') parsed = parseArgsPermissive(str.slice(i, end + 1)) }
|
|
223
|
+
if (parsed === null) return null
|
|
224
|
+
result[key] = parsed
|
|
225
|
+
i = end + 1
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
let j = i
|
|
229
|
+
while (j < str.length && !/[,}]/.test(str[j])) j++
|
|
230
|
+
const raw = str.slice(i, j).trim()
|
|
231
|
+
if (raw === 'true') result[key] = true
|
|
232
|
+
else if (raw === 'false') result[key] = false
|
|
233
|
+
else if (raw === 'null') result[key] = null
|
|
234
|
+
else { const n = Number(raw); result[key] = Number.isNaN(n) ? raw : n }
|
|
235
|
+
i = j
|
|
236
|
+
}
|
|
237
|
+
return result
|
|
238
|
+
}
|
|
239
|
+
|
|
178
240
|
function parseArgsPermissive(str) {
|
|
179
241
|
const result = {}
|
|
180
242
|
let i = 1
|
|
@@ -225,7 +287,22 @@ function parseArgsPermissive(str) {
|
|
|
225
287
|
result[key] = unescapeValue(value)
|
|
226
288
|
i = j + 1
|
|
227
289
|
} else if (str[i] === '{' || str[i] === '[') {
|
|
228
|
-
|
|
290
|
+
// Вложенный объект/массив: находим сбалансированный фрагмент и
|
|
291
|
+
// пытаемся распарсить его как JSON (или как вложенный permissive).
|
|
292
|
+
const open = str[i]
|
|
293
|
+
const close = open === '{' ? '}' : ']'
|
|
294
|
+
const end = findMatching(str, i, open, close)
|
|
295
|
+
if (end === -1) return null
|
|
296
|
+
const frag = str.slice(i, end + 1)
|
|
297
|
+
let parsedFrag = null
|
|
298
|
+
try {
|
|
299
|
+
parsedFrag = JSON.parse(frag)
|
|
300
|
+
} catch {
|
|
301
|
+
if (open === '{') parsedFrag = parseArgsPermissive(frag)
|
|
302
|
+
}
|
|
303
|
+
if (parsedFrag === null) return null
|
|
304
|
+
result[key] = parsedFrag
|
|
305
|
+
i = end + 1
|
|
229
306
|
} else {
|
|
230
307
|
let j = i
|
|
231
308
|
while (j < str.length && !/[,}]/.test(str[j])) j++
|
|
@@ -308,15 +385,51 @@ function parseToolCallPermissive(text) {
|
|
|
308
385
|
const openIdx = text.indexOf('{', argsIdx)
|
|
309
386
|
if (openIdx === -1) return null
|
|
310
387
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
388
|
+
// Берём сбалансированный по скобкам фрагмент args (с учётом строк),
|
|
389
|
+
// а не первый попавшийся '}'. Иначе вложенные объекты/массивы или
|
|
390
|
+
// фигурные скобки внутри строк ломают разбор.
|
|
391
|
+
const endIdx = findMatching(text, openIdx, '{', '}')
|
|
392
|
+
if (endIdx === -1) return null
|
|
393
|
+
|
|
394
|
+
const argsStr = text.slice(openIdx, endIdx + 1)
|
|
395
|
+
if (tool === 'Edit') {
|
|
396
|
+
const e = parseEditArgs(argsStr)
|
|
397
|
+
if (e) return { tool, args: e }
|
|
316
398
|
}
|
|
399
|
+
const args = parseArgsPermissive(argsStr)
|
|
400
|
+
if (args) return { tool, args }
|
|
401
|
+
const greedy = parseArgsGreedy(argsStr)
|
|
402
|
+
if (greedy) return { tool, args: greedy }
|
|
317
403
|
return null
|
|
318
404
|
}
|
|
319
405
|
|
|
406
|
+
// Специализированный разбор args для Edit: ровно три поля path/old_string/
|
|
407
|
+
// new_string, значения могут содержать сырые кавычки и переводы строк.
|
|
408
|
+
function parseEditArgs(str) {
|
|
409
|
+
const keyRe = (name) => new RegExp('"' + name + '"\\s*:\\s*"')
|
|
410
|
+
const readValue = (name, nextNames) => {
|
|
411
|
+
const m = keyRe(name).exec(str)
|
|
412
|
+
if (!m) return null
|
|
413
|
+
const start = m.index + m[0].length
|
|
414
|
+
let end = str.length
|
|
415
|
+
for (const n of nextNames) {
|
|
416
|
+
const mm = keyRe(n).exec(str.slice(start))
|
|
417
|
+
if (mm) {
|
|
418
|
+
const absIdx = start + mm.index
|
|
419
|
+
if (absIdx < end) end = absIdx
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
let val = str.slice(start, end)
|
|
423
|
+
val = val.replace(/"\s*[,}]?\s*$/, '')
|
|
424
|
+
return val
|
|
425
|
+
}
|
|
426
|
+
const path = readValue('path', ['old_string', 'new_string'])
|
|
427
|
+
const oldStr = readValue('old_string', ['new_string'])
|
|
428
|
+
const newStr = readValue('new_string', [])
|
|
429
|
+
if (path === null || oldStr === null || newStr === null) return null
|
|
430
|
+
return { path: path, old_string: oldStr, new_string: newStr }
|
|
431
|
+
}
|
|
432
|
+
|
|
320
433
|
function extractJsonObjects(text) {
|
|
321
434
|
const objects = []
|
|
322
435
|
let i = 0
|
|
@@ -403,6 +516,17 @@ function parseToolCall(text) {
|
|
|
403
516
|
const permissive = parseToolCallPermissive(cleaned)
|
|
404
517
|
if (permissive) return { ...permissive, _permissive: true }
|
|
405
518
|
|
|
519
|
+
// Fallback: модель могла обернуть JSON в прозу и/или добавить мусорные
|
|
520
|
+
// теги после него. Отрезаем всё до первого '"tool"' и всё, что похоже
|
|
521
|
+
// на XML/DSML-хвост, затем пробуем permissive-разбор ещё раз.
|
|
522
|
+
const toolIdx = cleaned.search(/["']?tool["']?\s:/)
|
|
523
|
+
if (toolIdx > 0) {
|
|
524
|
+
let tail = cleaned.slice(toolIdx)
|
|
525
|
+
tail = tail.replace(/<[^>]>.*$/s, '').trim()
|
|
526
|
+
const tailPermissive = parseToolCallPermissive('{"' + tail)
|
|
527
|
+
if (tailPermissive) return { ...tailPermissive, _permissive: true }
|
|
528
|
+
}
|
|
529
|
+
|
|
406
530
|
return null
|
|
407
531
|
}
|
|
408
532
|
|
package/src/index.js
CHANGED
|
@@ -56,6 +56,126 @@ const maxIter =
|
|
|
56
56
|
const positional = getPositional()
|
|
57
57
|
const task = getArg('--task', positional.join(' ').trim() || null)
|
|
58
58
|
const chatIdArg = getArg('--chat', null)
|
|
59
|
+
// При возобновлении существующего чата system-prompt по умолчанию НЕ
|
|
60
|
+
// переотправляется (он уже есть в начале чата). Флаг --resend-prompt
|
|
61
|
+
// заставляет дослать его заново — например, если промпт обновился.
|
|
62
|
+
const resendPrompt = hasFlag('--resend-prompt')
|
|
63
|
+
// --new-chat: намеренно начать с чистого чата, игнорируя сохранённый
|
|
64
|
+
// last-chat (полезно в watch-режиме, где по умолчанию чат восстанавливается).
|
|
65
|
+
const newChatFlag = hasFlag('--new-chat')
|
|
66
|
+
|
|
67
|
+
// ---------- last chat persistence ----------
|
|
68
|
+
// Запоминаем последний открытый chat id, чтобы после перезапуска процесса
|
|
69
|
+
// (в т.ч. автоперезапуска через `node --watch`) автоматически вернуться
|
|
70
|
+
// в ту же сессию, а не создавать новый чат.
|
|
71
|
+
const LAST_CHAT_FILE = path.join(ZAMES_HOME, 'last-chat.json')
|
|
72
|
+
|
|
73
|
+
async function saveLastChat(id) {
|
|
74
|
+
if (!id) return
|
|
75
|
+
try {
|
|
76
|
+
await fs.mkdir(ZAMES_HOME, { recursive: true })
|
|
77
|
+
await fs.writeFile(
|
|
78
|
+
LAST_CHAT_FILE,
|
|
79
|
+
JSON.stringify({ id, updatedAt: new Date().toISOString() }, null, 2),
|
|
80
|
+
'utf-8',
|
|
81
|
+
)
|
|
82
|
+
} catch (e) {
|
|
83
|
+
if (debug) console.error('last-chat: не удалось сохранить:', e.message)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function loadLastChat() {
|
|
88
|
+
try {
|
|
89
|
+
const raw = await fs.readFile(LAST_CHAT_FILE, 'utf-8')
|
|
90
|
+
const data = JSON.parse(raw)
|
|
91
|
+
return data && typeof data.id === 'string' ? data.id : null
|
|
92
|
+
} catch {
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
// ---------- hot reload ----------
|
|
99
|
+
// Держим ссылки на модули логики в объекте mod. Команда /reload перечитывает
|
|
100
|
+
// их через динамический import с timestamp-query — Node кэширует ESM по URL,
|
|
101
|
+
// поэтому такой import вернёт свежую версию модуля. Браузер, чат и текущее
|
|
102
|
+
// состояние НЕ перезапускаются: обновляется только логика.
|
|
103
|
+
const RELOADABLE = [
|
|
104
|
+
'./tools.js',
|
|
105
|
+
'./agent-loop.js',
|
|
106
|
+
'./system-prompt.js',
|
|
107
|
+
'./gitTools.js',
|
|
108
|
+
'./web.js',
|
|
109
|
+
'./self-review.js',
|
|
110
|
+
'./diff.js',
|
|
111
|
+
'./undo.js',
|
|
112
|
+
'./confirm.js',
|
|
113
|
+
'./transcript.js',
|
|
114
|
+
'./spinner.js',
|
|
115
|
+
'./config.js',
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
const mod = {
|
|
119
|
+
createTools,
|
|
120
|
+
runAgentLoop,
|
|
121
|
+
buildSystemPrompt: null, // подгрузим ниже
|
|
122
|
+
selfReview,
|
|
123
|
+
selfDiff,
|
|
124
|
+
selfApply,
|
|
125
|
+
selfList,
|
|
126
|
+
closeWeb,
|
|
127
|
+
createSpinner,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function reloadModules() {
|
|
131
|
+
const stamp = Date.now()
|
|
132
|
+
const loaded = new Map()
|
|
133
|
+
const errors = []
|
|
134
|
+
for (const rel of RELOADABLE) {
|
|
135
|
+
try {
|
|
136
|
+
const url = new URL(rel, import.meta.url)
|
|
137
|
+
url.searchParams.set('t', String(stamp))
|
|
138
|
+
const m = await import(url.href)
|
|
139
|
+
loaded.set(rel, m)
|
|
140
|
+
} catch (e) {
|
|
141
|
+
errors.push(`${rel}: ${e.message}`)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const pick = (rel, name) => loaded.get(rel)?.[name]
|
|
146
|
+
|
|
147
|
+
if (pick('./tools.js', 'createTools')) mod.createTools = pick('./tools.js', 'createTools')
|
|
148
|
+
if (pick('./agent-loop.js', 'runAgentLoop')) mod.runAgentLoop = pick('./agent-loop.js', 'runAgentLoop')
|
|
149
|
+
if (pick('./system-prompt.js', 'buildSystemPrompt')) mod.buildSystemPrompt = pick('./system-prompt.js', 'buildSystemPrompt')
|
|
150
|
+
if (pick('./self-review.js', 'selfReview')) mod.selfReview = pick('./self-review.js', 'selfReview')
|
|
151
|
+
if (pick('./self-review.js', 'selfDiff')) mod.selfDiff = pick('./self-review.js', 'selfDiff')
|
|
152
|
+
if (pick('./self-review.js', 'selfApply')) mod.selfApply = pick('./self-review.js', 'selfApply')
|
|
153
|
+
if (pick('./self-review.js', 'selfList')) mod.selfList = pick('./self-review.js', 'selfList')
|
|
154
|
+
if (pick('./web.js', 'closeWeb')) mod.closeWeb = pick('./web.js', 'closeWeb')
|
|
155
|
+
if (pick('./spinner.js', 'createSpinner')) mod.createSpinner = pick('./spinner.js', 'createSpinner')
|
|
156
|
+
|
|
157
|
+
return { count: loaded.size, errors }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Первичная загрузка, чтобы mod.buildSystemPrompt и остальные были заполнены.
|
|
161
|
+
await reloadModules()
|
|
162
|
+
|
|
163
|
+
// Dev-режим: авто-перечитывание модулей логики перед каждой задачей.
|
|
164
|
+
// Включается флагом --dev (его ставит `npm run dev`) или config.hotReload === true.
|
|
165
|
+
// В обычном режиме (npm start, глобальный zames) авто-reload выключен.
|
|
166
|
+
const devMode = hasFlag('--dev') || config.hotReload === true
|
|
167
|
+
|
|
168
|
+
// Безопасный авто-reload: при ошибке загрузки оставляем прошлые рабочие модули.
|
|
169
|
+
async function autoReload() {
|
|
170
|
+
if (!devMode) return
|
|
171
|
+
const { errors } = await reloadModules()
|
|
172
|
+
if (errors.length) {
|
|
173
|
+
console.error(
|
|
174
|
+
chalk.yellow('⚠ авто-reload: часть модулей не загрузилась, работаю на прежней версии:'),
|
|
175
|
+
)
|
|
176
|
+
for (const e of errors) console.error(chalk.yellow(' ' + e))
|
|
177
|
+
}
|
|
178
|
+
}
|
|
59
179
|
|
|
60
180
|
|
|
61
181
|
// ---------- helpers ----------
|
|
@@ -68,10 +188,14 @@ ${chalk.bold('Опции CLI:')}
|
|
|
68
188
|
--dir <path> рабочая директория агента
|
|
69
189
|
--task <text> задача одной строкой
|
|
70
190
|
--chat <id> продолжить существующий чат по id
|
|
191
|
+
--new-chat начать новый чат (игнорировать сохранённый)
|
|
192
|
+
--resend-prompt дослать system-prompt в существующий чат
|
|
71
193
|
--max-iter <n> лимит итераций (по умолчанию ${config.maxIterations})
|
|
72
194
|
--headless браузер без UI
|
|
73
195
|
--debug подробный лог
|
|
74
196
|
--calibrate режим калибровки селекторов
|
|
197
|
+
--dev режим разработки: авто-перечитывание модулей
|
|
198
|
+
--version, -v показать версию
|
|
75
199
|
--help, -h эта справка
|
|
76
200
|
|
|
77
201
|
${chalk.bold('Обычные команды:')}
|
|
@@ -82,6 +206,7 @@ ${chalk.bold('Обычные команды:')}
|
|
|
82
206
|
/cd <path> сменить рабочую директорию
|
|
83
207
|
/pwd текущая директория
|
|
84
208
|
/status состояние сессии
|
|
209
|
+
/reload перечитать модули логики без перезапуска
|
|
85
210
|
/undo откатить последнюю запись/правку
|
|
86
211
|
/undo-list список того, что можно откатить
|
|
87
212
|
/transcript путь к файлу транскрипта
|
|
@@ -151,14 +276,14 @@ async function resolveWorkdir() {
|
|
|
151
276
|
async function runTask(browser, tools, taskText, workdir, opts) {
|
|
152
277
|
const { transcript, freshChat, sendSystemPrompt } = opts
|
|
153
278
|
|
|
154
|
-
const ui = createSpinner()
|
|
279
|
+
const ui = mod.createSpinner()
|
|
155
280
|
ui.taskHeader(taskText)
|
|
156
281
|
ui.thinking()
|
|
157
282
|
|
|
158
283
|
let finished = false
|
|
159
284
|
|
|
160
285
|
try {
|
|
161
|
-
await runAgentLoop({
|
|
286
|
+
await mod.runAgentLoop({
|
|
162
287
|
browser,
|
|
163
288
|
tools,
|
|
164
289
|
task: taskText,
|
|
@@ -188,6 +313,14 @@ async function runTask(browser, tools, taskText, workdir, opts) {
|
|
|
188
313
|
// ---------- main ----------
|
|
189
314
|
|
|
190
315
|
async function main() {
|
|
316
|
+
if (hasFlag('--version') || hasFlag('-v')) {
|
|
317
|
+
const pkg = JSON.parse(
|
|
318
|
+
await fs.readFile(path.join(__dirname, '..', 'package.json'), 'utf-8'),
|
|
319
|
+
)
|
|
320
|
+
console.log(pkg.version)
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
|
|
191
324
|
if (hasFlag('--help') || hasFlag('-h')) {
|
|
192
325
|
printHelp()
|
|
193
326
|
return
|
|
@@ -230,7 +363,7 @@ async function main() {
|
|
|
230
363
|
...config.browser,
|
|
231
364
|
})
|
|
232
365
|
|
|
233
|
-
const bootSpinner = createSpinner()
|
|
366
|
+
const bootSpinner = mod.createSpinner()
|
|
234
367
|
bootSpinner.thinking()
|
|
235
368
|
|
|
236
369
|
try {
|
|
@@ -248,29 +381,38 @@ async function main() {
|
|
|
248
381
|
|
|
249
382
|
// Разовый режим
|
|
250
383
|
if (task) {
|
|
251
|
-
const tools = createTools(currentWorkdir, { undo })
|
|
384
|
+
const tools = mod.createTools(currentWorkdir, { undo })
|
|
252
385
|
|
|
253
386
|
let freshChat = true
|
|
254
387
|
let sendSystemPrompt = true
|
|
255
388
|
|
|
256
|
-
|
|
389
|
+
// Приоритет: явный --chat, иначе сохранённый last-chat (если не --new-chat).
|
|
390
|
+
let resumeId = chatIdArg
|
|
391
|
+
if (!resumeId && !newChatFlag) {
|
|
392
|
+
resumeId = await loadLastChat()
|
|
393
|
+
if (resumeId) console.log(chalk.gray(`Восстанавливаю чат ${resumeId}...`))
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (resumeId) {
|
|
257
397
|
try {
|
|
258
|
-
console.log(chalk.gray(`Открываю чат ${
|
|
259
|
-
await browser.openChat(
|
|
398
|
+
console.log(chalk.gray(`Открываю чат ${resumeId}...`))
|
|
399
|
+
await browser.openChat(resumeId)
|
|
260
400
|
freshChat = false
|
|
261
|
-
sendSystemPrompt =
|
|
401
|
+
sendSystemPrompt = resendPrompt
|
|
262
402
|
} catch (e) {
|
|
263
403
|
console.error(chalk.red(`Не удалось открыть чат: ${e.message}`))
|
|
264
404
|
}
|
|
265
405
|
}
|
|
266
406
|
|
|
407
|
+
await autoReload()
|
|
408
|
+
|
|
267
409
|
await runTask(browser, tools, task, currentWorkdir, {
|
|
268
410
|
transcript,
|
|
269
411
|
freshChat,
|
|
270
412
|
sendSystemPrompt,
|
|
271
413
|
})
|
|
272
414
|
await browser.close()
|
|
273
|
-
await closeWeb().catch(() => {})
|
|
415
|
+
await mod.closeWeb().catch(() => {})
|
|
274
416
|
transcript.close()
|
|
275
417
|
return
|
|
276
418
|
}
|
|
@@ -298,20 +440,27 @@ async function main() {
|
|
|
298
440
|
await browser.close().catch(() => {})
|
|
299
441
|
// Закрываем ленивый headless-браузер из web.js, иначе он останется
|
|
300
442
|
// висеть отдельным процессом после выхода агента.
|
|
301
|
-
await closeWeb().catch(() => {})
|
|
443
|
+
await mod.closeWeb().catch(() => {})
|
|
302
444
|
transcript.close()
|
|
303
445
|
console.log(chalk.gray('\nВыход.'))
|
|
304
446
|
process.exit(0)
|
|
305
447
|
})
|
|
306
448
|
|
|
307
|
-
|
|
449
|
+
// Приоритет: явный --chat, иначе сохранённый last-chat (если не --new-chat).
|
|
450
|
+
let resumeId = chatIdArg
|
|
451
|
+
if (!resumeId && !newChatFlag) {
|
|
452
|
+
resumeId = await loadLastChat()
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (resumeId) {
|
|
308
456
|
try {
|
|
309
|
-
console.log(chalk.gray(`Открываю чат ${
|
|
310
|
-
await browser.openChat(
|
|
311
|
-
currentChatId =
|
|
457
|
+
console.log(chalk.gray(`Открываю чат ${resumeId}...`))
|
|
458
|
+
await browser.openChat(resumeId)
|
|
459
|
+
currentChatId = resumeId
|
|
312
460
|
freshChatNext = false
|
|
313
|
-
sendSystemPromptNext =
|
|
314
|
-
|
|
461
|
+
sendSystemPromptNext = resendPrompt
|
|
462
|
+
await saveLastChat(resumeId)
|
|
463
|
+
console.log(chalk.gray(`Чат открыт: ${resumeId}\n`))
|
|
315
464
|
} catch (e) {
|
|
316
465
|
console.error(chalk.red(`Не удалось открыть чат: ${e.message}`))
|
|
317
466
|
}
|
|
@@ -352,6 +501,7 @@ async function main() {
|
|
|
352
501
|
freshChatNext = false
|
|
353
502
|
sendSystemPromptNext = true
|
|
354
503
|
currentChatId = await browser.getCurrentChatId()
|
|
504
|
+
await saveLastChat(currentChatId)
|
|
355
505
|
transcript.log('new_chat')
|
|
356
506
|
console.log(chalk.gray('Новый чат.\n'))
|
|
357
507
|
} catch (e) {
|
|
@@ -371,7 +521,7 @@ async function main() {
|
|
|
371
521
|
: currentWorkdir
|
|
372
522
|
|
|
373
523
|
try {
|
|
374
|
-
const result = await selfReview({
|
|
524
|
+
const result = await mod.selfReview({
|
|
375
525
|
browser,
|
|
376
526
|
config,
|
|
377
527
|
focus: focus || null,
|
|
@@ -391,6 +541,7 @@ async function main() {
|
|
|
391
541
|
freshChatNext = false
|
|
392
542
|
sendSystemPromptNext = false
|
|
393
543
|
currentChatId = await browser.getCurrentChatId()
|
|
544
|
+
await saveLastChat(currentChatId)
|
|
394
545
|
|
|
395
546
|
console.log(
|
|
396
547
|
chalk.cyan(
|
|
@@ -435,10 +586,10 @@ async function main() {
|
|
|
435
586
|
try {
|
|
436
587
|
const { runAgentLoop: ral } = await import('./agent-loop.js')
|
|
437
588
|
const { buildSystemPrompt } = await import('./system-prompt.js')
|
|
438
|
-
const tools = createTools(snapRoot, { undo: null })
|
|
589
|
+
const tools = mod.createTools(snapRoot, { undo: null })
|
|
439
590
|
|
|
440
591
|
await browser.newChat()
|
|
441
|
-
const sysPrompt = buildSystemPrompt({
|
|
592
|
+
const sysPrompt = mod.buildSystemPrompt({
|
|
442
593
|
workdir: snapRoot,
|
|
443
594
|
tools,
|
|
444
595
|
})
|
|
@@ -446,7 +597,7 @@ async function main() {
|
|
|
446
597
|
await browser.ask(sysPrompt, { timeout: 60_000 })
|
|
447
598
|
|
|
448
599
|
if (focus) {
|
|
449
|
-
const ui = createSpinner()
|
|
600
|
+
const ui = mod.createSpinner()
|
|
450
601
|
ui.thinking()
|
|
451
602
|
await ral({
|
|
452
603
|
browser,
|
|
@@ -479,6 +630,7 @@ async function main() {
|
|
|
479
630
|
freshChatNext = false
|
|
480
631
|
sendSystemPromptNext = false
|
|
481
632
|
currentChatId = await browser.getCurrentChatId()
|
|
633
|
+
await saveLastChat(currentChatId)
|
|
482
634
|
|
|
483
635
|
console.log(
|
|
484
636
|
chalk.cyan(
|
|
@@ -510,7 +662,7 @@ async function main() {
|
|
|
510
662
|
|
|
511
663
|
if (lower === '/self-list') {
|
|
512
664
|
try {
|
|
513
|
-
await selfList({ config })
|
|
665
|
+
await mod.selfList({ config })
|
|
514
666
|
} catch (e) {
|
|
515
667
|
console.error(chalk.red('Ошибка:'), e.message)
|
|
516
668
|
}
|
|
@@ -524,7 +676,7 @@ async function main() {
|
|
|
524
676
|
continue
|
|
525
677
|
}
|
|
526
678
|
try {
|
|
527
|
-
await selfDiff({ config, name })
|
|
679
|
+
await mod.selfDiff({ config, name })
|
|
528
680
|
} catch (e) {
|
|
529
681
|
console.error(chalk.red('Ошибка:'), e.message)
|
|
530
682
|
}
|
|
@@ -538,7 +690,7 @@ async function main() {
|
|
|
538
690
|
continue
|
|
539
691
|
}
|
|
540
692
|
try {
|
|
541
|
-
await selfApply({ config, name })
|
|
693
|
+
await mod.selfApply({ config, name })
|
|
542
694
|
} catch (e) {
|
|
543
695
|
console.error(chalk.red('Ошибка:'), e.message)
|
|
544
696
|
}
|
|
@@ -548,7 +700,7 @@ async function main() {
|
|
|
548
700
|
// ---------- Обычные команды ----------
|
|
549
701
|
|
|
550
702
|
if (lower === '/chats') {
|
|
551
|
-
const spin = createSpinner()
|
|
703
|
+
const spin = mod.createSpinner()
|
|
552
704
|
spin.thinking()
|
|
553
705
|
try {
|
|
554
706
|
lastChats = await browser.listChats(30)
|
|
@@ -604,12 +756,13 @@ async function main() {
|
|
|
604
756
|
await browser.openChat(pick.id)
|
|
605
757
|
currentChatId = pick.id
|
|
606
758
|
freshChatNext = false
|
|
607
|
-
sendSystemPromptNext =
|
|
759
|
+
sendSystemPromptNext = resendPrompt
|
|
760
|
+
await saveLastChat(pick.id)
|
|
608
761
|
transcript.log('resume_chat', { id: pick.id, title: pick.title })
|
|
609
762
|
console.log(
|
|
610
763
|
chalk.green(`Чат открыт.`) +
|
|
611
764
|
chalk.gray(
|
|
612
|
-
' Системный промпт будет переслан на следующей задаче.\n',
|
|
765
|
+
resendPrompt ? ' Системный промпт будет переслан на следующей задаче.\n' : ' Контекст чата сохранён. Системный промпт не пересылается (--resend-prompt чтобы дослать).\n',
|
|
613
766
|
),
|
|
614
767
|
)
|
|
615
768
|
} catch (e) {
|
|
@@ -640,6 +793,26 @@ async function main() {
|
|
|
640
793
|
continue
|
|
641
794
|
}
|
|
642
795
|
|
|
796
|
+
if (lower === '/reload') {
|
|
797
|
+
console.log(chalk.gray('Перечитываю модули логики...'))
|
|
798
|
+
try {
|
|
799
|
+
const { count, errors } = await reloadModules()
|
|
800
|
+
if (errors.length) {
|
|
801
|
+
console.error(chalk.red('Часть модулей не перезагрузилась:'))
|
|
802
|
+
for (const e of errors) console.error(chalk.red(' ' + e))
|
|
803
|
+
} else {
|
|
804
|
+
console.log(
|
|
805
|
+
chalk.green(
|
|
806
|
+
`Перезагружено модулей: ${count}. Браузер и чат не тронуты.`,
|
|
807
|
+
),
|
|
808
|
+
)
|
|
809
|
+
}
|
|
810
|
+
} catch (e) {
|
|
811
|
+
console.error(chalk.red('Ошибка reload:'), e.message)
|
|
812
|
+
}
|
|
813
|
+
continue
|
|
814
|
+
}
|
|
815
|
+
|
|
643
816
|
if (lower === '/status') {
|
|
644
817
|
console.log(chalk.gray(`Рабочая директория: ${currentWorkdir}`))
|
|
645
818
|
console.log(
|
|
@@ -661,6 +834,15 @@ async function main() {
|
|
|
661
834
|
`System prompt на след. задаче: ${sendSystemPromptNext ? 'да' : 'нет'}`,
|
|
662
835
|
),
|
|
663
836
|
)
|
|
837
|
+
console.log(
|
|
838
|
+
chalk.gray(`Resend prompt (--resend-prompt): ${resendPrompt ? 'да' : 'нет'}`),
|
|
839
|
+
)
|
|
840
|
+
console.log(
|
|
841
|
+
chalk.gray(`Last chat: ${(await loadLastChat()) || 'нет'}`),
|
|
842
|
+
)
|
|
843
|
+
console.log(
|
|
844
|
+
chalk.gray(`Dev mode (auto-reload): ${devMode ? 'вкл' : 'выкл'}`),
|
|
845
|
+
)
|
|
664
846
|
console.log(chalk.gray(`Лимит итераций: ${maxIter}`))
|
|
665
847
|
console.log(chalk.gray(`Headless: ${headless ? 'да' : 'нет'}`))
|
|
666
848
|
console.log(chalk.gray(`Debug: ${debug ? 'да' : 'нет'}`))
|
|
@@ -764,9 +946,12 @@ async function main() {
|
|
|
764
946
|
|
|
765
947
|
// ---- Обычная задача (в том числе в review-режиме) ----
|
|
766
948
|
|
|
949
|
+
// Dev-режим: подхватываем свежие модули логики перед задачей.
|
|
950
|
+
await autoReload()
|
|
951
|
+
|
|
767
952
|
transcript.log('user_task', { task: trimmed, workdir: currentWorkdir })
|
|
768
953
|
|
|
769
|
-
const tools = createTools(currentWorkdir, { undo })
|
|
954
|
+
const tools = mod.createTools(currentWorkdir, { undo })
|
|
770
955
|
await runTask(browser, tools, trimmed, currentWorkdir, {
|
|
771
956
|
transcript,
|
|
772
957
|
freshChat: freshChatNext,
|
|
@@ -778,10 +963,11 @@ async function main() {
|
|
|
778
963
|
if (!currentChatId) {
|
|
779
964
|
currentChatId = await browser.getCurrentChatId()
|
|
780
965
|
}
|
|
966
|
+
await saveLastChat(currentChatId)
|
|
781
967
|
}
|
|
782
968
|
|
|
783
969
|
await browser.close().catch(() => {})
|
|
784
|
-
await closeWeb().catch(() => {})
|
|
970
|
+
await mod.closeWeb().catch(() => {})
|
|
785
971
|
transcript.close()
|
|
786
972
|
}
|
|
787
973
|
|
package/src/self-review.js
CHANGED
|
@@ -3,14 +3,28 @@ import fs from 'fs/promises'
|
|
|
3
3
|
import chalk from 'chalk'
|
|
4
4
|
import { fileURLToPath } from 'url'
|
|
5
5
|
|
|
6
|
-
import { createTools } from './tools.js'
|
|
7
|
-
import { runAgentLoop } from './agent-loop.js'
|
|
8
6
|
import { ZAMES_HOME } from './config.js'
|
|
9
7
|
|
|
10
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
11
9
|
const SRC_DIR = __dirname
|
|
12
10
|
const SNAP_ROOT = path.join(ZAMES_HOME, 'snapshots')
|
|
13
11
|
|
|
12
|
+
// Динамический импорт логики с timestamp — чтобы при /reload (или авто-reload)
|
|
13
|
+
// self-review использовал СВЕЖИЕ tools/agent-loop, а не закэшированные при
|
|
14
|
+
// первой загрузке. Иначе reload не доходил бы до зависимостей self-review.
|
|
15
|
+
async function loadFresh() {
|
|
16
|
+
const stamp = Date.now()
|
|
17
|
+
const toolsUrl = new URL('./tools.js', import.meta.url)
|
|
18
|
+
toolsUrl.searchParams.set('t', String(stamp))
|
|
19
|
+
const loopUrl = new URL('./agent-loop.js', import.meta.url)
|
|
20
|
+
loopUrl.searchParams.set('t', String(stamp))
|
|
21
|
+
const [{ createTools }, { runAgentLoop }] = await Promise.all([
|
|
22
|
+
import(toolsUrl.href),
|
|
23
|
+
import(loopUrl.href),
|
|
24
|
+
])
|
|
25
|
+
return { createTools, runAgentLoop }
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
async function ensureDir(p) {
|
|
15
29
|
await fs.mkdir(p, { recursive: true })
|
|
16
30
|
}
|
|
@@ -65,6 +79,7 @@ export async function selfReview({ browser, config, focus, transcript }) {
|
|
|
65
79
|
|
|
66
80
|
const taskPrompt = buildReviewPrompt({ focus, snapDir })
|
|
67
81
|
|
|
82
|
+
const { createTools, runAgentLoop } = await loadFresh()
|
|
68
83
|
const tools = createTools(snapDir, { undo: null })
|
|
69
84
|
let finalMessage = ''
|
|
70
85
|
|
package/src/system-prompt.js
CHANGED
|
@@ -62,6 +62,16 @@ Inside that string:
|
|
|
62
62
|
- OR avoid quotes entirely. For Windows paths use forward slashes.
|
|
63
63
|
- Pipes, redirects and chained commands work: "cd dir && dir /b"
|
|
64
64
|
|
|
65
|
+
## CRITICAL: output format
|
|
66
|
+
|
|
67
|
+
- Your ENTIRE response MUST be exactly ONE JSON object or ONE JSON array.
|
|
68
|
+
- Do NOT write any text before or after the JSON. No explanations, no greetings, no plans.
|
|
69
|
+
- Do NOT wrap JSON in markdown fences.
|
|
70
|
+
- Do NOT use XML-like tags such as tool_calls, invoke, parameter, or their DSML variants.
|
|
71
|
+
- If you want to tell the user something, call the respond tool with a message.
|
|
72
|
+
- If JSON parsing fails, the tool call will NOT run and the user will see raw text. Keep JSON valid: escape every double quote inside strings, escape backslashes, use
|
|
73
|
+
for newlines.
|
|
74
|
+
|
|
65
75
|
## Rules
|
|
66
76
|
|
|
67
77
|
- Always read a file before editing it.
|