zames_pro 1.0.2 → 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
@@ -13,18 +13,18 @@ Terminal coding agent, работающий поверх chat.deepseek.com че
13
13
 
14
14
  ## Установка
15
15
 
16
- ```bash
16
+ `bash
17
17
  npm install -g zames_pro
18
18
  npx playwright install chromium
19
- ```
19
+ `
20
20
 
21
21
  ## Запуск
22
22
 
23
23
  Перейди в папку проекта и запусти:
24
24
 
25
- ```bash
25
+ `bash
26
26
  zames
27
- ```
27
+ `
28
28
 
29
29
  Агент работает в директории запуска и не может выходить за её пределы (sandbox).
30
30
 
@@ -32,10 +32,14 @@ zames
32
32
 
33
33
  ```
34
34
  zames --task <текст задачи>
35
+ zames --chat <id>
36
+ zames --new-chat
37
+ zames --resend-prompt
38
+ zames --dir <путь>
35
39
  zames --headless
36
40
  zames --debug
37
- zames --dir <путь>
38
- zames --version показать версию
41
+ zames --dev
42
+ zames --version
39
43
  zames --help
40
44
  ```
41
45
 
@@ -46,6 +50,21 @@ zames --help
46
50
 
47
51
  Данные агента хранятся в `~/.zames`: профиль браузера, логи, история undo, снапшоты self-review.
48
52
 
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
+
49
68
  ## Лицензия и авторство
50
69
 
51
70
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,8 @@
13
13
  "logo.png"
14
14
  ],
15
15
  "scripts": {
16
- "start": "node ./src/index.js"
16
+ "start": "node ./src/index.js",
17
+ "dev": "node ./src/index.js --dev"
17
18
  },
18
19
  "keywords": [
19
20
  "agent",
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