zames_pro 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/browser.js ADDED
@@ -0,0 +1,468 @@
1
+ import { chromium } from 'playwright'
2
+ import path from 'path'
3
+ import os from 'os'
4
+ import fs from 'fs/promises'
5
+ import { execSync } from 'child_process'
6
+
7
+ const USER_DATA_DIR = path.join(os.homedir(), '.zames', 'profile')
8
+ const CHAT_URL = 'https://chat.deepseek.com/'
9
+
10
+ const INPUT_SELECTORS = [
11
+ 'textarea',
12
+ 'div[contenteditable="true"]',
13
+ '[role="textbox"]',
14
+ ]
15
+
16
+ const ANSWER_SELECTORS = [
17
+ 'div[class*="ds-markdown"]',
18
+ 'div[class*="markdown"]',
19
+ '[class*="message"]',
20
+ ]
21
+
22
+ const STOP_SELECTORS = [
23
+ 'button:has-text("Stop")',
24
+ 'button:has-text("Остановить")',
25
+ 'button[aria-label*="Stop" i]',
26
+ ]
27
+
28
+ // ---------- profile cleanup ----------
29
+
30
+ async function killStaleChrome() {
31
+ if (process.platform !== 'win32') return
32
+ try {
33
+ execSync(
34
+ `powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"Name='chrome.exe'\\" | Where-Object { $_.CommandLine -like '*\\.zames\\profile*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"`,
35
+ { stdio: 'ignore', timeout: 5000 },
36
+ )
37
+ await new Promise((r) => setTimeout(r, 800))
38
+ } catch {}
39
+ }
40
+
41
+ async function cleanSingletonFiles() {
42
+ const names = ['SingletonLock', 'SingletonCookie', 'SingletonSocket']
43
+ for (const name of names) {
44
+ const p = path.join(USER_DATA_DIR, name)
45
+ try {
46
+ await fs.unlink(p)
47
+ } catch {}
48
+ }
49
+ }
50
+
51
+ async function profileLooksLocked() {
52
+ try {
53
+ await fs.access(path.join(USER_DATA_DIR, 'SingletonLock'))
54
+ return true
55
+ } catch {
56
+ return false
57
+ }
58
+ }
59
+
60
+ async function nukeProfile() {
61
+ try {
62
+ await fs.rm(USER_DATA_DIR, { recursive: true, force: true })
63
+ } catch {}
64
+ }
65
+
66
+ // ---------- class ----------
67
+
68
+ export class DeepSeekBrowser {
69
+ constructor({
70
+ headless = false,
71
+ debug = false,
72
+ channel = 'chrome',
73
+ answerTimeoutMs = 180000,
74
+ askRetries = 3,
75
+ stabilityChecks = 3,
76
+ stabilityDelayMs = 1000,
77
+ } = {}) {
78
+ this.headless = headless
79
+ this.debug = debug
80
+ this.channel = channel
81
+ this.answerTimeoutMs = answerTimeoutMs
82
+ this.askRetries = askRetries
83
+ this.stabilityChecks = stabilityChecks
84
+ this.stabilityDelayMs = stabilityDelayMs
85
+ this.context = null
86
+ this.page = null
87
+ }
88
+
89
+ async launch() {
90
+ await killStaleChrome()
91
+ if (await profileLooksLocked()) {
92
+ if (this.debug) console.error('profile: удаляю Singleton-файлы')
93
+ await cleanSingletonFiles()
94
+ }
95
+ await this._launchOnce()
96
+ }
97
+
98
+ async _launchOnce() {
99
+ const options = {
100
+ headless: this.headless,
101
+ slowMo: 30,
102
+ args: ['--disable-blink-features=AutomationControlled'],
103
+ }
104
+ if (this.channel) options.channel = this.channel
105
+
106
+ try {
107
+ this.context = await chromium.launchPersistentContext(
108
+ USER_DATA_DIR,
109
+ options,
110
+ )
111
+ } catch (e) {
112
+ if (
113
+ /Target page, context or browser has been closed|profile.*in use/i.test(
114
+ e.message,
115
+ )
116
+ ) {
117
+ if (this.debug)
118
+ console.error('profile: занят, перезапускаю после очистки')
119
+ await killStaleChrome()
120
+ await cleanSingletonFiles()
121
+
122
+ try {
123
+ this.context = await chromium.launchPersistentContext(
124
+ USER_DATA_DIR,
125
+ options,
126
+ )
127
+ } catch (e2) {
128
+ if (this.debug) console.error('profile: сношу целиком')
129
+ await nukeProfile()
130
+ this.context = await chromium.launchPersistentContext(
131
+ USER_DATA_DIR,
132
+ options,
133
+ )
134
+ }
135
+ } else {
136
+ throw e
137
+ }
138
+ }
139
+
140
+ this.page = this.context.pages()[0] || (await this.context.newPage())
141
+ await this.page.goto(CHAT_URL, { waitUntil: 'domcontentloaded' })
142
+ return this
143
+ }
144
+
145
+ async restart() {
146
+ try {
147
+ if (this.context) await this.context.close()
148
+ } catch {}
149
+ await this.launch()
150
+ }
151
+
152
+ async waitForLogin() {
153
+ const loggedIn = await this.isLoggedIn()
154
+ if (loggedIn) return
155
+
156
+ console.log('\n🔐 Залогиньтесь в DeepSeek в открытом браузере.')
157
+ console.log(' После входа нажмите Enter в терминале...\n')
158
+
159
+ const readline = await import('readline')
160
+ const rl = readline.createInterface({
161
+ input: process.stdin,
162
+ output: process.stdout,
163
+ })
164
+ await new Promise((resolve) => {
165
+ rl.question('', () => {
166
+ rl.close()
167
+ resolve()
168
+ })
169
+ })
170
+ }
171
+
172
+ async isLoggedIn() {
173
+ for (const sel of INPUT_SELECTORS) {
174
+ try {
175
+ await this.page.locator(sel).first().waitFor({
176
+ state: 'visible',
177
+ timeout: 3000,
178
+ })
179
+ return true
180
+ } catch {}
181
+ }
182
+ return false
183
+ }
184
+
185
+ async newChat() {
186
+ const newChatBtn = this.page
187
+ .locator('button, a')
188
+ .filter({ hasText: /new chat|новый чат|новый диалог/i })
189
+ .first()
190
+ try {
191
+ await newChatBtn.click({ timeout: 2000 })
192
+ await this.page.waitForTimeout(1000)
193
+ } catch {
194
+ await this.page.goto(CHAT_URL, { waitUntil: 'domcontentloaded' })
195
+ await this.page.waitForTimeout(1000)
196
+ }
197
+ }
198
+
199
+ async _findVisible(selectors, timeout = 1000) {
200
+ for (const sel of selectors) {
201
+ const loc = this.page.locator(sel).last()
202
+ try {
203
+ await loc.waitFor({ state: 'visible', timeout })
204
+ return loc
205
+ } catch {}
206
+ }
207
+ return null
208
+ }
209
+
210
+ async _readLastAnswerText() {
211
+ return await this.page.evaluate((sels) => {
212
+ let el = null
213
+ for (const s of sels) {
214
+ const list = document.querySelectorAll(s)
215
+ if (list.length) el = list[list.length - 1]
216
+ }
217
+ if (!el) return ''
218
+
219
+ const clone = el.cloneNode(true)
220
+
221
+ Array.from(clone.querySelectorAll('pre')).forEach((pre) => {
222
+ const codeEl = pre.querySelector('code')
223
+ const source = codeEl || pre
224
+ const text = (source.textContent || '').replace(/\n$/, '')
225
+ const langMatch = (source.className || '').match(/language-([\w-]+)/)
226
+ const lang = langMatch ? langMatch[1] : ''
227
+ const replacement = document.createTextNode(
228
+ '```' + lang + '\n' + text + '\n```',
229
+ )
230
+ if (pre.parentNode) pre.parentNode.replaceChild(replacement, pre)
231
+ })
232
+
233
+ Array.from(clone.querySelectorAll('code')).forEach((c) => {
234
+ const replacement = document.createTextNode(
235
+ '`' + (c.textContent || '') + '`',
236
+ )
237
+ if (c.parentNode) c.parentNode.replaceChild(replacement, c)
238
+ })
239
+
240
+ return clone.textContent || ''
241
+ }, ANSWER_SELECTORS)
242
+ }
243
+
244
+ async _isGenerating() {
245
+ const stop = await this._findVisible(STOP_SELECTORS, 300)
246
+ return !!stop
247
+ }
248
+
249
+ async ask(prompt, { timeout = this.answerTimeoutMs } = {}) {
250
+ let lastErr = null
251
+
252
+ for (let attempt = 1; attempt <= this.askRetries; attempt++) {
253
+ try {
254
+ return await this._askOnce(prompt, { timeout })
255
+ } catch (e) {
256
+ lastErr = e
257
+ console.error(
258
+ `\n⚠ ask() попытка ${attempt}/${this.askRetries} провалилась: ${e.message}`,
259
+ )
260
+
261
+ if (/closed|crash|Target page|browser/i.test(e.message)) {
262
+ console.error('⚠ перезапускаю браузер...')
263
+ try {
264
+ await this.restart()
265
+ await this.waitForLogin()
266
+ } catch (re) {
267
+ console.error(`⚠ не удалось перезапустить: ${re.message}`)
268
+ }
269
+ }
270
+
271
+ if (attempt < this.askRetries) {
272
+ await new Promise((r) => setTimeout(r, 2000 * attempt))
273
+ }
274
+ }
275
+ }
276
+
277
+ throw new Error(
278
+ `ask() провалился после ${this.askRetries} попыток: ${lastErr?.message}`,
279
+ )
280
+ }
281
+
282
+ async _askOnce(prompt, { timeout }) {
283
+ const input = await this._findVisible(INPUT_SELECTORS, 10_000)
284
+ if (!input) {
285
+ throw new Error(
286
+ 'Не найдено поле ввода. Запустите /debug-dom и поправьте INPUT_SELECTORS.',
287
+ )
288
+ }
289
+
290
+ const beforeText = await this._readLastAnswerText().catch(() => '')
291
+
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
+ }
300
+ await this.page.waitForTimeout(200)
301
+
302
+ const sendBtn = this.page
303
+ .locator('button')
304
+ .filter({ hasText: /send|отправить/i })
305
+ .first()
306
+ try {
307
+ await sendBtn.click({ timeout: 1500 })
308
+ } catch {
309
+ await this.page.keyboard.press('Enter')
310
+ }
311
+
312
+ const startDeadline = Date.now() + 15_000
313
+ let started = false
314
+ while (Date.now() < startDeadline) {
315
+ const gen = await this._isGenerating()
316
+ const cur = await this._readLastAnswerText().catch(() => '')
317
+ if (gen || (cur && cur !== beforeText)) {
318
+ started = true
319
+ break
320
+ }
321
+ await this.page.waitForTimeout(300)
322
+ }
323
+ if (!started) {
324
+ throw new Error(
325
+ 'Ответ не начал генерироваться за 15с. Возможно, сообщение не отправилось.',
326
+ )
327
+ }
328
+
329
+ const deadline = Date.now() + timeout
330
+ let last = ''
331
+ let stable = 0
332
+ while (Date.now() < deadline) {
333
+ const gen = await this._isGenerating()
334
+ const cur = await this._readLastAnswerText().catch(() => '')
335
+ if (cur && cur === last && !gen) {
336
+ stable++
337
+ if (stable >= 2) return cur
338
+ } else {
339
+ stable = 0
340
+ }
341
+ last = cur
342
+ await this.page.waitForTimeout(800)
343
+ }
344
+
345
+ if (last) return last
346
+ throw new Error('Таймаут ожидания ответа. Попробуйте /debug-dom.')
347
+ }
348
+
349
+ async dumpDom(filePath) {
350
+ if (!this.page) throw new Error('браузер не запущен')
351
+ const html = await this.page.content()
352
+ await fs.writeFile(filePath, html, 'utf-8')
353
+
354
+ const report = await this.page.evaluate(
355
+ (sels) => {
356
+ const result = { answers: {}, stops: {}, inputs: {} }
357
+ for (const s of sels.answers) {
358
+ result.answers[s] = document.querySelectorAll(s).length
359
+ }
360
+ for (const s of sels.stops) {
361
+ result.stops[s] = document.querySelectorAll(s).length
362
+ }
363
+ for (const s of sels.inputs) {
364
+ result.inputs[s] = document.querySelectorAll(s).length
365
+ }
366
+ return result
367
+ },
368
+ {
369
+ answers: ANSWER_SELECTORS,
370
+ stops: STOP_SELECTORS,
371
+ inputs: INPUT_SELECTORS,
372
+ },
373
+ )
374
+
375
+ return { file: filePath, selectors: report }
376
+ }
377
+
378
+ // ---------- список чатов ----------
379
+
380
+ async _ensureSidebarOpen() {
381
+ const toggles = [
382
+ 'button[aria-label*="sidebar" i]',
383
+ 'button[aria-label*="история" i]',
384
+ 'button[aria-label*="history" i]',
385
+ 'button[class*="sidebar-toggle"]',
386
+ 'button[class*="sidebarToggle"]',
387
+ ]
388
+ for (const sel of toggles) {
389
+ try {
390
+ const btn = this.page.locator(sel).first()
391
+ if ((await btn.count()) === 0) continue
392
+ if (!(await btn.isVisible().catch(() => false))) continue
393
+ await btn.click({ timeout: 1500 })
394
+ await this.page.waitForTimeout(500)
395
+ return
396
+ } catch {}
397
+ }
398
+ }
399
+
400
+ async listChats(limit = 30) {
401
+ await this._ensureSidebarOpen()
402
+ return await this.page.evaluate((lim) => {
403
+ const out = []
404
+ const seen = new Set()
405
+ const anchors = document.querySelectorAll('a[href*="/chat/"]')
406
+ for (const a of anchors) {
407
+ const href = a.getAttribute('href') || ''
408
+ const m =
409
+ href.match(/\/chat\/s\/([a-zA-Z0-9_-]+)/) ||
410
+ href.match(/\/a\/chat\/s\/([a-zA-Z0-9_-]+)/)
411
+ if (!m) continue
412
+ const id = m[1]
413
+ if (seen.has(id)) continue
414
+ seen.add(id)
415
+
416
+ const titleEl = a.querySelector('[class*="title"], [class*="text"]')
417
+ let title = (titleEl ? titleEl.textContent : a.textContent) || ''
418
+ title = title.trim().replace(/\s+/g, ' ')
419
+ if (!title) title = '(без названия)'
420
+
421
+ out.push({ id, title, href })
422
+ if (out.length >= lim) break
423
+ }
424
+ return out
425
+ }, limit)
426
+ }
427
+
428
+ async openChat(id) {
429
+ const candidates = [`a[href$="/chat/s/${id}"]`, `a[href*="${id}"]`]
430
+ for (const sel of candidates) {
431
+ try {
432
+ const loc = this.page.locator(sel).first()
433
+ if ((await loc.count()) === 0) continue
434
+ await loc.click({ timeout: 3000 })
435
+ await this.page.waitForTimeout(1500)
436
+ return true
437
+ } catch {}
438
+ }
439
+
440
+ try {
441
+ await this.page.goto(`https://chat.deepseek.com/a/chat/s/${id}`, {
442
+ waitUntil: 'domcontentloaded',
443
+ timeout: 20_000,
444
+ })
445
+ await this.page.waitForTimeout(1500)
446
+ return true
447
+ } catch (e) {
448
+ throw new Error(`Не удалось открыть чат ${id}: ${e.message}`)
449
+ }
450
+ }
451
+
452
+ async getCurrentChatId() {
453
+ try {
454
+ const url = this.page.url()
455
+ const m = url.match(/\/chat\/s\/([a-zA-Z0-9_-]+)/)
456
+ return m ? m[1] : null
457
+ } catch {
458
+ return null
459
+ }
460
+ }
461
+
462
+ async close() {
463
+ try {
464
+ if (this.context) await this.context.close()
465
+ } catch {}
466
+ await killStaleChrome()
467
+ }
468
+ }
package/src/config.js ADDED
@@ -0,0 +1,80 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import os from 'os'
4
+
5
+ const ZAMES_HOME = path.join(os.homedir(), '.zames')
6
+ const HOME_CONFIG = path.join(ZAMES_HOME, 'config.json')
7
+ const PROJECT_CONFIG = path.join(process.cwd(), '.zamesrc.json')
8
+
9
+ const DEFAULTS = {
10
+ maxIterations: 40,
11
+ headless: false,
12
+ debug: false,
13
+ browserChannel: null,
14
+
15
+ confirmation: {
16
+ write: true,
17
+ edit: true,
18
+ bash: true,
19
+ alwaysConfirm: [
20
+ 'rm\\s+-rf',
21
+ 'rmdir\\s+/s',
22
+ 'del\\s+/[sqf]',
23
+ 'format\\s+[a-z]:',
24
+ 'shutdown',
25
+ 'reg\\s+delete',
26
+ 'remove-item.*-recurse',
27
+ 'git\\s+push\\s+--force',
28
+ ],
29
+ },
30
+
31
+ undo: {
32
+ enabled: true,
33
+ maxBackups: 200,
34
+ },
35
+
36
+ transcript: {
37
+ enabled: true,
38
+ dir: path.join(os.homedir(), '.zames', 'logs'),
39
+ },
40
+
41
+ browser: {
42
+ answerTimeoutMs: 180000,
43
+ askRetries: 3,
44
+ stabilityChecks: 3,
45
+ stabilityDelayMs: 1000,
46
+ },
47
+ }
48
+
49
+ export function loadConfig() {
50
+ const merged = JSON.parse(JSON.stringify(DEFAULTS))
51
+ for (const file of [HOME_CONFIG, PROJECT_CONFIG]) {
52
+ try {
53
+ const data = JSON.parse(fs.readFileSync(file, 'utf-8'))
54
+ deepMerge(merged, data)
55
+ } catch (e) {
56
+ if (e.code !== 'ENOENT') {
57
+ console.error(`config: не удалось прочитать ${file}: ${e.message}`)
58
+ }
59
+ }
60
+ }
61
+ return merged
62
+ }
63
+
64
+ function deepMerge(target, source) {
65
+ for (const key of Object.keys(source)) {
66
+ if (
67
+ source[key] &&
68
+ typeof source[key] === 'object' &&
69
+ !Array.isArray(source[key])
70
+ ) {
71
+ target[key] = target[key] || {}
72
+ deepMerge(target[key], source[key])
73
+ } else {
74
+ target[key] = source[key]
75
+ }
76
+ }
77
+ }
78
+
79
+ export const CONFIG_PATHS = { HOME_CONFIG, PROJECT_CONFIG }
80
+ export { ZAMES_HOME }
package/src/confirm.js ADDED
@@ -0,0 +1,85 @@
1
+ import * as readlinePromises from 'readline/promises'
2
+ import chalk from 'chalk'
3
+ import { unifiedDiff, colorDiff } from './diff.js'
4
+
5
+ export class ConfirmManager {
6
+ constructor({ config }) {
7
+ const c = config || {}
8
+ this.settings = {
9
+ write: c.write !== false,
10
+ edit: c.edit !== false,
11
+ bash: c.bash !== false,
12
+ }
13
+ this.alwaysConfirm = (c.alwaysConfirm || []).map((p) => new RegExp(p, 'i'))
14
+ this.allowedForSession = new Set()
15
+ }
16
+
17
+ _matchesAlwaysConfirm(command) {
18
+ return this.alwaysConfirm.some((re) => re.test(command))
19
+ }
20
+
21
+ shouldAsk(kind, target = null) {
22
+ if (kind === 'bash' && target && this._matchesAlwaysConfirm(target)) {
23
+ return true
24
+ }
25
+ if (!this.settings[kind]) return false
26
+ if (this.allowedForSession.has(kind)) return false
27
+ return true
28
+ }
29
+
30
+ async ask({ kind, target, preview = null, message }) {
31
+ if (!this.shouldAsk(kind, target)) return true
32
+
33
+ if (preview) {
34
+ console.log(preview)
35
+ }
36
+
37
+ const label = message || `Разрешить ${kind}?`
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(' = всегда для этого типа] ')
47
+
48
+ const answer = await this._prompt(question)
49
+
50
+ if (answer === 'a') {
51
+ if (kind !== 'bash' || !this._matchesAlwaysConfirm(target)) {
52
+ this.allowedForSession.add(kind)
53
+ }
54
+ return true
55
+ }
56
+ return answer === 'y'
57
+ }
58
+
59
+ async _prompt(question) {
60
+ process.stdin.resume()
61
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
62
+ process.stdin.setRawMode(false)
63
+ }
64
+
65
+ const rl = readlinePromises.createInterface({
66
+ input: process.stdin,
67
+ output: process.stdout,
68
+ })
69
+
70
+ try {
71
+ const ans = await rl.question(question)
72
+ const v = (ans || '').trim().toLowerCase()
73
+ if (v === 'a' || v === 'always' || v === 'в') return 'a'
74
+ if (v === 'y' || v === 'yes' || v === 'д' || v === 'да') return 'y'
75
+ return 'n'
76
+ } finally {
77
+ rl.close()
78
+ }
79
+ }
80
+ }
81
+
82
+ export function formatDiffPreview(oldText, newText, { label } = {}) {
83
+ const diff = unifiedDiff(oldText, newText, { label })
84
+ return colorDiff(diff, chalk)
85
+ }