zames_pro 1.0.3 → 1.1.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # zames_pro
2
2
 
3
- ![zames logo](logo.png)
3
+ ![zames logo](https://raw.githubusercontent.com/Viqto0r/zames_pro/master/logo.png)
4
4
 
5
5
  Terminal coding agent, работающий поверх chat.deepseek.com через Playwright.
6
6
  По духу похож на Claude Code / Codex CLI: запускается в текущей директории, читает и правит файлы, запускает команды, коммитит в git.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,11 +28,22 @@
28
28
  "node": ">=18"
29
29
  },
30
30
  "dependencies": {
31
- "playwright": "^1.48.0",
32
31
  "chalk": "^5.3.0",
33
- "ora": "^8.0.1"
32
+ "cli-highlight": "^2.1.11",
33
+ "markdansi": "^0.3.4",
34
+ "marked": "^15.0.12",
35
+ "ora": "^8.0.1",
36
+ "playwright": "^1.48.0"
34
37
  },
35
38
  "publishConfig": {
36
39
  "access": "public"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/Viqto0r/zames_pro.git"
44
+ },
45
+ "homepage": "https://github.com/Viqto0r/zames_pro#readme",
46
+ "bugs": {
47
+ "url": "https://github.com/Viqto0r/zames_pro/issues"
37
48
  }
38
49
  }
package/src/agent-loop.js CHANGED
@@ -15,6 +15,7 @@ export async function runAgentLoop({
15
15
  onToolCall = () => {},
16
16
  onToolResult = () => {},
17
17
  onAssistantMessage = () => {},
18
+ debugLog = false,
18
19
  }) {
19
20
  if (freshChat) {
20
21
  await browser.newChat()
@@ -60,8 +61,11 @@ export async function runAgentLoop({
60
61
 
61
62
  const parsedCalls = Array.isArray(parsed) ? parsed : parsed ? [parsed] : []
62
63
  if (parsedCalls.some((p) => p && p._permissive)) {
64
+ // Пишем в транскрипт (для отладки), но не сыпем в консоль.
63
65
  transcript?.log('permissive_parse', { response: rawResponse })
64
- console.error('внимание: tool-call распознан нестрогим парсером')
66
+ if (debugLog) {
67
+ console.error('внимание: tool-call распознан нестрогим парсером')
68
+ }
65
69
  }
66
70
 
67
71
  if (!parsed) {
package/src/browser.js CHANGED
@@ -16,7 +16,6 @@ const INPUT_SELECTORS = [
16
16
  const ANSWER_SELECTORS = [
17
17
  'div[class*="ds-markdown"]',
18
18
  'div[class*="markdown"]',
19
- '[class*="message"]',
20
19
  ]
21
20
 
22
21
  const STOP_SELECTORS = [
@@ -25,6 +24,11 @@ const STOP_SELECTORS = [
25
24
  'button[aria-label*="Stop" i]',
26
25
  ]
27
26
 
27
+ // Служебные статусы интерфейса DeepSeek, которые НЕ являются ответом модели.
28
+ // Иначе агент принимает статус (Reading...) за ответ и ломает разбор.
29
+ const STATUS_RE = /^(reading|thinking|searching|analyzing|generating|stop|остановить|читаю|думаю|поиск|анализ)[\s.…]*$/i
30
+
31
+
28
32
  // ---------- profile cleanup ----------
29
33
 
30
34
  async function killStaleChrome() {
@@ -237,15 +241,91 @@ export class DeepSeekBrowser {
237
241
  if (c.parentNode) c.parentNode.replaceChild(replacement, c)
238
242
  })
239
243
 
240
- return clone.textContent || ''
244
+ // textContent склеивает блоки без переводов строк, из-за чего
245
+ // Markdown-рендер получает одну длинную строку. Обходим DOM сами и
246
+ // расставляем переводы строк / маркеры Markdown по блочным элементам.
247
+ const NL = String.fromCharCode(10)
248
+ const BULLET = String.fromCharCode(45) + ' ' // '- '
249
+
250
+ function domToMarkdown(node) {
251
+ if (node.nodeType === 3) return node.textContent || ''
252
+ if (node.nodeType !== 1) return ''
253
+ const tag = node.tagName.toLowerCase()
254
+
255
+ if (tag === 'br') return NL
256
+
257
+ const inner = Array.from(node.childNodes)
258
+ .map(domToMarkdown)
259
+ .join('')
260
+
261
+ const STAR = String.fromCharCode(42) // '*'
262
+ if (tag === 'strong' || tag === 'b') return STAR + STAR + inner + STAR + STAR
263
+ if (tag === 'em' || tag === 'i') return STAR + inner + STAR
264
+ if (tag === 'del' || tag === 's') return '~~' + inner + '~~'
265
+ if (tag === 'a') {
266
+ const href = node.getAttribute('href') || ''
267
+ return href ? '[' + inner + '](' + href + ')' : inner
268
+ }
269
+ if (/^h[1-6]$/.test(tag)) {
270
+ const level = Number(tag[1])
271
+ const hashes = '#'.repeat(level)
272
+ return NL + NL + hashes + ' ' + inner.trim() + NL + NL
273
+ }
274
+ if (tag === 'li') {
275
+ const text = inner.trim().replace(new RegExp(NL + '+', 'g'), ' ')
276
+ return BULLET + text + NL
277
+ }
278
+ if (tag === 'ul' || tag === 'ol' || tag === 'blockquote') {
279
+ return NL + inner + NL
280
+ }
281
+ if (
282
+ tag === 'p' ||
283
+ tag === 'div' ||
284
+ tag === 'section' ||
285
+ tag === 'article' ||
286
+ tag === 'tr' ||
287
+ tag === 'table'
288
+ ) {
289
+ const text = inner.trim()
290
+ return text ? NL + NL + text : ''
291
+ }
292
+ return inner
293
+ }
294
+
295
+ const out = domToMarkdown(clone)
296
+ // Схлопываем тройные+ переводы строк до двойных (разделитель блоков).
297
+ return out.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim()
241
298
  }, ANSWER_SELECTORS)
242
299
  }
243
300
 
301
+ async _readLastAnswerTextClean() {
302
+ const raw = await this._readLastAnswerText().catch(() => '')
303
+ const t = (raw || '').trim()
304
+ if (!t) return ''
305
+ // Отсекаем служебные статусы интерфейса (Reading..., Думаю...).
306
+ if (STATUS_RE.test(t)) return ''
307
+ return raw
308
+ }
309
+
244
310
  async _isGenerating() {
245
311
  const stop = await this._findVisible(STOP_SELECTORS, 300)
246
312
  return !!stop
247
313
  }
248
314
 
315
+ // Прервать текущую генерацию: нажать Stop в интерфейсе.
316
+ // Используется при нажатии Esc пользователем.
317
+ async stopGeneration() {
318
+ this._abort = true
319
+ const btn = await this._findVisible(STOP_SELECTORS, 500)
320
+ if (btn) {
321
+ try {
322
+ await btn.click({ timeout: 1000 })
323
+ return true
324
+ } catch {}
325
+ }
326
+ return false
327
+ }
328
+
249
329
  async ask(prompt, { timeout = this.answerTimeoutMs } = {}) {
250
330
  let lastErr = null
251
331
 
@@ -279,6 +359,65 @@ export class DeepSeekBrowser {
279
359
  )
280
360
  }
281
361
 
362
+ // Вставка текста в поле ввода.
363
+ //
364
+ // fill()/insertText() ломаются на многострочном тексте в contenteditable-
365
+ // редакторах (ProseMirror/Lexical/...): символ перевода строки там
366
+ // трактуется как Enter, и в поле остаётся только первая строка. Поэтому
367
+ // для contenteditable и [role=textbox] эмулируем paste-событие с полным
368
+ // текстом — то же, что делает Shift+Insert. Для нативных textarea/input
369
+ // перевод строки работает и так.
370
+ async _setInputText(input, text) {
371
+ const tag = await input.evaluate((el) => el.tagName.toLowerCase())
372
+ const isNative = tag === 'textarea' || tag === 'input'
373
+
374
+ if (isNative) {
375
+ try {
376
+ await input.fill(text, { timeout: 5000 })
377
+ return
378
+ } catch {}
379
+ }
380
+
381
+ await input.click()
382
+ // Выделяем всё содержимое, чтобы вставка заменила его целиком.
383
+ await this.page.keyboard.press('Control+A')
384
+ await this.page.keyboard.press('Delete')
385
+
386
+ const ok = await input.evaluate((el, value) => {
387
+ el.focus()
388
+ const dt = new DataTransfer()
389
+ dt.setData('text/plain', value)
390
+ const ev = new ClipboardEvent('paste', {
391
+ clipboardData: dt,
392
+ bubbles: true,
393
+ cancelable: true,
394
+ })
395
+ el.dispatchEvent(ev)
396
+ // Если обработчик не отменил вставку и поле осталось пустым —
397
+ // пробуем через insertText вручную (fallback ниже).
398
+ return true
399
+ }, text)
400
+
401
+ if (!ok) {
402
+ await this.page.keyboard.insertText(text)
403
+ }
404
+
405
+ // Проверяем, что текст реально попал в поле. Если редактор проигнорировал
406
+ // paste — падаем на insertText.
407
+ const got = await input.evaluate((el) => {
408
+ if (el.tagName.toLowerCase() === 'textarea' || el.tagName.toLowerCase() === 'input') {
409
+ return el.value
410
+ }
411
+ return el.innerText || el.textContent || ''
412
+ })
413
+ const norm = (s) =>
414
+ (s || '').replace(/\r\n/g, '\n').replace(/\u00a0/g, ' ').trim()
415
+ if (!norm(got)) {
416
+ await input.click()
417
+ await this.page.keyboard.insertText(text)
418
+ }
419
+ }
420
+
282
421
  async _askOnce(prompt, { timeout }) {
283
422
  const input = await this._findVisible(INPUT_SELECTORS, 10_000)
284
423
  if (!input) {
@@ -287,16 +426,9 @@ export class DeepSeekBrowser {
287
426
  )
288
427
  }
289
428
 
290
- const beforeText = await this._readLastAnswerText().catch(() => '')
429
+ const beforeText = await this._readLastAnswerTextClean().catch(() => '')
291
430
 
292
- await input.click()
293
- try {
294
- await input.fill(prompt, { timeout: 5000 })
295
- } catch {
296
- // contenteditable / [role=textbox] не поддерживает fill() — печатаем
297
- // текст в уже сфокусированное поле, не задевая раскладку.
298
- await this.page.keyboard.insertText(prompt)
299
- }
431
+ await this._setInputText(input, prompt)
300
432
  await this.page.waitForTimeout(200)
301
433
 
302
434
  const sendBtn = this.page
@@ -313,7 +445,7 @@ export class DeepSeekBrowser {
313
445
  let started = false
314
446
  while (Date.now() < startDeadline) {
315
447
  const gen = await this._isGenerating()
316
- const cur = await this._readLastAnswerText().catch(() => '')
448
+ const cur = await this._readLastAnswerTextClean().catch(() => '')
317
449
  if (gen || (cur && cur !== beforeText)) {
318
450
  started = true
319
451
  break
@@ -329,9 +461,14 @@ export class DeepSeekBrowser {
329
461
  const deadline = Date.now() + timeout
330
462
  let last = ''
331
463
  let stable = 0
464
+ this._abort = false
332
465
  while (Date.now() < deadline) {
466
+ if (this._abort) {
467
+ // Пользователь нажал Esc — вернём то, что успело сгенерироваться.
468
+ return last || '(прервано пользователем)'
469
+ }
333
470
  const gen = await this._isGenerating()
334
- const cur = await this._readLastAnswerText().catch(() => '')
471
+ const cur = await this._readLastAnswerTextClean().catch(() => '')
335
472
  if (cur && cur === last && !gen) {
336
473
  stable++
337
474
  if (stable >= 2) return cur
package/src/confirm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as readlinePromises from 'readline/promises'
2
- import chalk from 'chalk'
2
+ import { theme } from './theme.js'
3
3
  import { unifiedDiff, colorDiff } from './diff.js'
4
4
 
5
5
  export class ConfirmManager {
@@ -36,14 +36,14 @@ export class ConfirmManager {
36
36
 
37
37
  const label = message || `Разрешить ${kind}?`
38
38
  const question =
39
- chalk.yellow(`${label} `) +
40
- chalk.gray('[') +
41
- chalk.green('y') +
42
- chalk.gray(' = да, ') +
43
- chalk.red('n') +
44
- chalk.gray(' = нет, ') +
45
- chalk.cyan('a') +
46
- chalk.gray(' = всегда для этого типа] ')
39
+ theme.warn(`${label} `) +
40
+ theme.system('[') +
41
+ theme.assistant('y') +
42
+ theme.system(' = да, ') +
43
+ theme.error('n') +
44
+ theme.system(' = нет, ') +
45
+ theme.user('a') +
46
+ theme.system(' = всегда для этого типа] ')
47
47
 
48
48
  const answer = await this._prompt(question)
49
49
 
@@ -81,5 +81,5 @@ export class ConfirmManager {
81
81
 
82
82
  export function formatDiffPreview(oldText, newText, { label } = {}) {
83
83
  const diff = unifiedDiff(oldText, newText, { label })
84
- return colorDiff(diff, chalk)
84
+ return colorDiff(diff)
85
85
  }
package/src/diff.js CHANGED
@@ -1,124 +1,32 @@
1
- export function unifiedDiff(
2
- oldText,
3
- newText,
4
- { context = 3, label = '' } = {},
5
- ) {
6
- const a = (oldText ?? '').split('\n')
7
- const b = (newText ?? '').split('\n')
1
+ import { theme } from './theme.js'
8
2
 
9
- if (oldText === newText) return '(без изменений)'
10
-
11
- const ops = diffOps(a, b)
3
+ export function unifiedDiff(a, b, { label = '' } = {}) {
4
+ const la = a.split(String.fromCharCode(10))
5
+ const lb = b.split(String.fromCharCode(10))
6
+ const n = Math.max(la.length, lb.length)
12
7
  const out = []
13
- if (label) out.push(`--- ${label}`)
14
-
15
- let i = 0
16
- while (i < ops.length) {
17
- const op = ops[i]
18
-
19
- if (op.type === 'same') {
20
- let j = i
21
- while (j < ops.length && ops[j].type === 'same') j++
22
- const blockLen = j - i
23
-
24
- if (i === 0 && j === ops.length) {
25
- i = j
26
- continue
27
- }
28
-
29
- if (i === 0) {
30
- const end = Math.min(j, i + context)
31
- for (let k = i; k < end; k++) out.push(' ' + ops[k].line)
32
- if (end < j) out.push('...')
33
- } else if (j === ops.length) {
34
- const start = Math.max(i, j - context)
35
- if (start > i) out.push('...')
36
- for (let k = start; k < j; k++) out.push(' ' + ops[k].line)
37
- } else {
38
- const start = Math.max(i, j - context)
39
- const end = Math.min(j, i + context)
40
- if (start > i) out.push('...')
41
- for (let k = start; k < end; k++) out.push(' ' + ops[k].line)
42
- if (end < j) out.push('...')
43
- }
44
-
45
- i = j
46
- continue
47
- }
48
-
49
- const blockStart = i
50
- let j = i
51
- while (j < ops.length && ops[j].type !== 'same') j++
52
-
53
- const preStart = Math.max(0, blockStart - context)
54
- if (preStart < blockStart) {
55
- if (preStart > 0) out.push('...')
56
- for (let k = preStart; k < blockStart; k++) {
57
- out.push(' ' + ops[k].line)
58
- }
59
- }
60
-
61
- for (let k = blockStart; k < j; k++) {
62
- const o = ops[k]
63
- out.push((o.type === 'del' ? '-' : '+') + o.line)
64
- }
65
-
66
- const postEnd = Math.min(ops.length, j + context)
67
- for (let k = j; k < postEnd; k++) {
68
- out.push(' ' + ops[k].line)
69
- }
70
- if (postEnd < ops.length) out.push('...')
71
-
72
- i = j
8
+ if (label) out.push('... ' + label)
9
+ for (let i = 0; i < n; i++) {
10
+ const x = la[i]
11
+ const y = lb[i]
12
+ if (x === y) continue
13
+ if (x !== undefined) out.push('-' + x)
14
+ if (y !== undefined) out.push('+' + y)
73
15
  }
74
-
75
- return out.join('\n')
16
+ return out.join(String.fromCharCode(10))
76
17
  }
77
18
 
78
- export function colorDiff(diffText, chalk) {
19
+ export function colorDiff(diffText) {
79
20
  return diffText
80
- .split('\n')
21
+ .split(String.fromCharCode(10))
81
22
  .map((line) => {
82
- if (line.startsWith('+++') || line.startsWith('---'))
83
- return chalk.bold(line)
84
- if (line.startsWith('+')) return chalk.green(line)
85
- if (line.startsWith('-')) return chalk.red(line)
86
- if (line.startsWith('...')) return chalk.gray(line)
23
+ if (line.startsWith('+++') || line.startsWith('---')) return theme.bold(line)
24
+ if (line.startsWith('+')) return theme.assistant(line)
25
+ if (line.startsWith('-')) return theme.error(line)
26
+ if (line.startsWith('...')) return theme.system(line)
87
27
  return line
88
28
  })
89
- .join('\n')
29
+ .join(String.fromCharCode(10))
90
30
  }
91
31
 
92
- function diffOps(a, b) {
93
- const n = a.length
94
- const m = b.length
95
- const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1))
96
-
97
- for (let i = n - 1; i >= 0; i--) {
98
- for (let j = m - 1; j >= 0; j--) {
99
- if (a[i] === b[j]) dp[i][j] = dp[i + 1][j + 1] + 1
100
- else dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1])
101
- }
102
- }
103
-
104
- const ops = []
105
- let i = 0
106
- let j = 0
107
- while (i < n && j < m) {
108
- if (a[i] === b[j]) {
109
- ops.push({ type: 'same', line: a[i] })
110
- i++
111
- j++
112
- } else if (dp[i + 1][j] >= dp[i][j + 1]) {
113
- ops.push({ type: 'del', line: a[i] })
114
- i++
115
- } else {
116
- ops.push({ type: 'add', line: b[j] })
117
- j++
118
- }
119
- }
120
- while (i < n) ops.push({ type: 'del', line: a[i++] })
121
- while (j < m) ops.push({ type: 'add', line: b[j++] })
122
-
123
- return ops
124
- }
32
+ export default { unifiedDiff, colorDiff }