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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # zames_pro
2
2
 
3
+ ![zames logo](logo.png)
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
- ```bash
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
- ```bash
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 --dir <путь>
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.1",
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
- return null
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
- for (let i = openIdx + 1; i < text.length; i++) {
312
- if (text[i] !== '}') continue
313
- const argsStr = text.slice(openIdx, i + 1)
314
- const args = parseArgsPermissive(argsStr)
315
- if (args) return { tool, args }
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
- if (chatIdArg) {
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(`Открываю чат ${chatIdArg}...`))
259
- await browser.openChat(chatIdArg)
398
+ console.log(chalk.gray(`Открываю чат ${resumeId}...`))
399
+ await browser.openChat(resumeId)
260
400
  freshChat = false
261
- sendSystemPrompt = true
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
- if (chatIdArg) {
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(`Открываю чат ${chatIdArg}...`))
310
- await browser.openChat(chatIdArg)
311
- currentChatId = chatIdArg
457
+ console.log(chalk.gray(`Открываю чат ${resumeId}...`))
458
+ await browser.openChat(resumeId)
459
+ currentChatId = resumeId
312
460
  freshChatNext = false
313
- sendSystemPromptNext = true
314
- console.log(chalk.gray(`Чат открыт: ${chatIdArg}\n`))
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 = true
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
 
@@ -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
 
@@ -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.