zames_pro 1.2.1 → 2.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/src/browser.js DELETED
@@ -1,627 +0,0 @@
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
- import { theme } from './theme.js'
7
-
8
- const USER_DATA_DIR = path.join(os.homedir(), '.zames', 'profile')
9
- const CHAT_URL = 'https://chat.deepseek.com/'
10
-
11
- const INPUT_SELECTORS = [
12
- 'textarea',
13
- 'div[contenteditable="true"]',
14
- '[role="textbox"]',
15
- ]
16
-
17
- const ANSWER_SELECTORS = [
18
- 'div[class*="ds-markdown"]',
19
- 'div[class*="markdown"]',
20
- ]
21
-
22
- const STOP_SELECTORS = [
23
- 'button:has-text("Stop")',
24
- 'button:has-text("Остановить")',
25
- 'button[aria-label*="Stop" i]',
26
- ]
27
-
28
- // Служебные статусы интерфейса DeepSeek, которые НЕ являются ответом модели.
29
- // Иначе агент принимает статус (Reading...) за ответ и ломает разбор.
30
- const STATUS_RE = /^(reading|thinking|searching|analyzing|generating|stop|остановить|читаю|думаю|поиск|анализ)[\s.…]*$/i
31
-
32
-
33
- // ---------- profile cleanup ----------
34
-
35
- async function killStaleChrome() {
36
- if (process.platform !== 'win32') return
37
- try {
38
- execSync(
39
- `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 }"`,
40
- { stdio: 'ignore', timeout: 5000 },
41
- )
42
- await new Promise((r) => setTimeout(r, 800))
43
- } catch {}
44
- }
45
-
46
- async function cleanSingletonFiles() {
47
- const names = ['SingletonLock', 'SingletonCookie', 'SingletonSocket']
48
- for (const name of names) {
49
- const p = path.join(USER_DATA_DIR, name)
50
- try {
51
- await fs.unlink(p)
52
- } catch {}
53
- }
54
- }
55
-
56
- async function profileLooksLocked() {
57
- try {
58
- await fs.access(path.join(USER_DATA_DIR, 'SingletonLock'))
59
- return true
60
- } catch {
61
- return false
62
- }
63
- }
64
-
65
- async function nukeProfile() {
66
- try {
67
- await fs.rm(USER_DATA_DIR, { recursive: true, force: true })
68
- } catch {}
69
- }
70
-
71
- // ---------- class ----------
72
-
73
- export class DeepSeekBrowser {
74
- constructor({
75
- headless = false,
76
- debug = false,
77
- channel = 'chrome',
78
- answerTimeoutMs = 180000,
79
- askRetries = 3,
80
- stabilityChecks = 3,
81
- stabilityDelayMs = 1000,
82
- minSendIntervalMs = 15000,
83
- } = {}) {
84
- this.headless = headless
85
- this.debug = debug
86
- this.channel = channel
87
- this.answerTimeoutMs = answerTimeoutMs
88
- this.askRetries = askRetries
89
- this.stabilityChecks = stabilityChecks
90
- this.stabilityDelayMs = stabilityDelayMs
91
- // Минимальный интервал между отправками в чат: DeepSeek ограничивает
92
- // частоту («Messages too frequent. Try again later.»).
93
- this.minSendIntervalMs = minSendIntervalMs
94
- this._lastSentAt = 0
95
- this.context = null
96
- this.page = null
97
- }
98
-
99
- async launch() {
100
- await killStaleChrome()
101
- if (await profileLooksLocked()) {
102
- if (this.debug) console.error('profile: удаляю Singleton-файлы')
103
- await cleanSingletonFiles()
104
- }
105
- await this._launchOnce()
106
- }
107
-
108
- async _launchOnce() {
109
- const options = {
110
- headless: this.headless,
111
- slowMo: 30,
112
- args: ['--disable-blink-features=AutomationControlled'],
113
- }
114
- if (this.channel) options.channel = this.channel
115
-
116
- try {
117
- this.context = await chromium.launchPersistentContext(
118
- USER_DATA_DIR,
119
- options,
120
- )
121
- } catch (e) {
122
- if (
123
- /Target page, context or browser has been closed|profile.*in use/i.test(
124
- e.message,
125
- )
126
- ) {
127
- if (this.debug)
128
- console.error('profile: занят, перезапускаю после очистки')
129
- await killStaleChrome()
130
- await cleanSingletonFiles()
131
-
132
- try {
133
- this.context = await chromium.launchPersistentContext(
134
- USER_DATA_DIR,
135
- options,
136
- )
137
- } catch (e2) {
138
- if (this.debug) console.error('profile: сношу целиком')
139
- await nukeProfile()
140
- this.context = await chromium.launchPersistentContext(
141
- USER_DATA_DIR,
142
- options,
143
- )
144
- }
145
- } else {
146
- throw e
147
- }
148
- }
149
-
150
- this.page = this.context.pages()[0] || (await this.context.newPage())
151
- await this.page.goto(CHAT_URL, { waitUntil: 'domcontentloaded' })
152
- return this
153
- }
154
-
155
- async restart() {
156
- try {
157
- if (this.context) await this.context.close()
158
- } catch {}
159
- await this.launch()
160
- }
161
-
162
- async waitForLogin() {
163
- const loggedIn = await this.isLoggedIn()
164
- if (loggedIn) return
165
-
166
- console.log('\n🔐 Залогиньтесь в DeepSeek в открытом браузере.')
167
- console.log(' После входа нажмите Enter в терминале...\n')
168
-
169
- const readline = await import('readline')
170
- const rl = readline.createInterface({
171
- input: process.stdin,
172
- output: process.stdout,
173
- })
174
- await new Promise((resolve) => {
175
- rl.question('', () => {
176
- rl.close()
177
- resolve()
178
- })
179
- })
180
- }
181
-
182
- async isLoggedIn() {
183
- for (const sel of INPUT_SELECTORS) {
184
- try {
185
- await this.page.locator(sel).first().waitFor({
186
- state: 'visible',
187
- timeout: 3000,
188
- })
189
- return true
190
- } catch {}
191
- }
192
- return false
193
- }
194
-
195
- async newChat() {
196
- const newChatBtn = this.page
197
- .locator('button, a')
198
- .filter({ hasText: /new chat|новый чат|новый диалог/i })
199
- .first()
200
- try {
201
- await newChatBtn.click({ timeout: 2000 })
202
- await this.page.waitForTimeout(1000)
203
- } catch {
204
- await this.page.goto(CHAT_URL, { waitUntil: 'domcontentloaded' })
205
- await this.page.waitForTimeout(1000)
206
- }
207
- }
208
-
209
- async _findVisible(selectors, timeout = 1000) {
210
- for (const sel of selectors) {
211
- const loc = this.page.locator(sel).last()
212
- try {
213
- await loc.waitFor({ state: 'visible', timeout })
214
- return loc
215
- } catch {}
216
- }
217
- return null
218
- }
219
-
220
- async _readLastAnswerText() {
221
- return await this.page.evaluate((sels) => {
222
- let el = null
223
- for (const s of sels) {
224
- const list = document.querySelectorAll(s)
225
- if (list.length) el = list[list.length - 1]
226
- }
227
- if (!el) return ''
228
-
229
- const clone = el.cloneNode(true)
230
-
231
- Array.from(clone.querySelectorAll('pre')).forEach((pre) => {
232
- const codeEl = pre.querySelector('code')
233
- const source = codeEl || pre
234
- const text = (source.textContent || '').replace(/\n$/, '')
235
- const langMatch = (source.className || '').match(/language-([\w-]+)/)
236
- const lang = langMatch ? langMatch[1] : ''
237
- const replacement = document.createTextNode(
238
- '```' + lang + '\n' + text + '\n```',
239
- )
240
- if (pre.parentNode) pre.parentNode.replaceChild(replacement, pre)
241
- })
242
-
243
- Array.from(clone.querySelectorAll('code')).forEach((c) => {
244
- const replacement = document.createTextNode(
245
- '`' + (c.textContent || '') + '`',
246
- )
247
- if (c.parentNode) c.parentNode.replaceChild(replacement, c)
248
- })
249
-
250
- // textContent склеивает блоки без переводов строк, из-за чего
251
- // Markdown-рендер получает одну длинную строку. Обходим DOM сами и
252
- // расставляем переводы строк / маркеры Markdown по блочным элементам.
253
- const NL = String.fromCharCode(10)
254
- const BULLET = String.fromCharCode(45) + ' ' // '- '
255
-
256
- function domToMarkdown(node) {
257
- if (node.nodeType === 3) return node.textContent || ''
258
- if (node.nodeType !== 1) return ''
259
- const tag = node.tagName.toLowerCase()
260
-
261
- if (tag === 'br') return NL
262
-
263
- const inner = Array.from(node.childNodes)
264
- .map(domToMarkdown)
265
- .join('')
266
-
267
- const STAR = String.fromCharCode(42) // '*'
268
- if (tag === 'strong' || tag === 'b') return STAR + STAR + inner + STAR + STAR
269
- if (tag === 'em' || tag === 'i') return STAR + inner + STAR
270
- if (tag === 'del' || tag === 's') return '~~' + inner + '~~'
271
- if (tag === 'a') {
272
- const href = node.getAttribute('href') || ''
273
- return href ? '[' + inner + '](' + href + ')' : inner
274
- }
275
- if (/^h[1-6]$/.test(tag)) {
276
- const level = Number(tag[1])
277
- const hashes = '#'.repeat(level)
278
- return NL + NL + hashes + ' ' + inner.trim() + NL + NL
279
- }
280
- if (tag === 'li') {
281
- const text = inner.trim().replace(new RegExp(NL + '+', 'g'), ' ')
282
- return BULLET + text + NL
283
- }
284
- if (tag === 'ul' || tag === 'ol' || tag === 'blockquote') {
285
- return NL + inner + NL
286
- }
287
- if (
288
- tag === 'p' ||
289
- tag === 'div' ||
290
- tag === 'section' ||
291
- tag === 'article' ||
292
- tag === 'tr' ||
293
- tag === 'table'
294
- ) {
295
- const text = inner.trim()
296
- return text ? NL + NL + text : ''
297
- }
298
- return inner
299
- }
300
-
301
- const out = domToMarkdown(clone)
302
- // Схлопываем тройные+ переводы строк до двойных (разделитель блоков).
303
- return out.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim()
304
- }, ANSWER_SELECTORS)
305
- }
306
-
307
- async _readLastAnswerTextClean() {
308
- const raw = await this._readLastAnswerText().catch(() => '')
309
- const t = (raw || '').trim()
310
- if (!t) return ''
311
- // Отсекаем служебные статусы интерфейса (Reading..., Думаю...).
312
- if (STATUS_RE.test(t)) return ''
313
- return raw
314
- }
315
-
316
- async _isGenerating() {
317
- const stop = await this._findVisible(STOP_SELECTORS, 300)
318
- return !!stop
319
- }
320
-
321
- // Прервать текущую генерацию: нажать Stop в интерфейсе.
322
- // Используется при нажатии Esc пользователем.
323
- async stopGeneration() {
324
- this._abort = true
325
- const btn = await this._findVisible(STOP_SELECTORS, 500)
326
- if (btn) {
327
- try {
328
- await btn.click({ timeout: 1000 })
329
- return true
330
- } catch {}
331
- }
332
- return false
333
- }
334
-
335
- async ask(prompt, { timeout = this.answerTimeoutMs } = {}) {
336
- let lastErr = null
337
-
338
- for (let attempt = 1; attempt <= this.askRetries; attempt++) {
339
- try {
340
- return await this._askOnce(prompt, { timeout })
341
- } catch (e) {
342
- lastErr = e
343
- console.error(
344
- `\n⚠ ask() попытка ${attempt}/${this.askRetries} провалилась: ${e.message}`,
345
- )
346
-
347
- if (/closed|crash|Target page|browser/i.test(e.message)) {
348
- console.error('⚠ перезапускаю браузер...')
349
- try {
350
- await this.restart()
351
- await this.waitForLogin()
352
- } catch (re) {
353
- console.error(`⚠ не удалось перезапустить: ${re.message}`)
354
- }
355
- }
356
-
357
- if (attempt < this.askRetries) {
358
- await new Promise((r) => setTimeout(r, 2000 * attempt))
359
- }
360
- }
361
- }
362
-
363
- throw new Error(
364
- `ask() провалился после ${this.askRetries} попыток: ${lastErr?.message}`,
365
- )
366
- }
367
-
368
- // Вставка текста в поле ввода.
369
- //
370
- // fill()/insertText() ломаются на многострочном тексте в contenteditable-
371
- // редакторах (ProseMirror/Lexical/...): символ перевода строки там
372
- // трактуется как Enter, и в поле остаётся только первая строка. Поэтому
373
- // для contenteditable и [role=textbox] эмулируем paste-событие с полным
374
- // текстом — то же, что делает Shift+Insert. Для нативных textarea/input
375
- // перевод строки работает и так.
376
- async _setInputText(input, text) {
377
- const tag = await input.evaluate((el) => el.tagName.toLowerCase())
378
- const isNative = tag === 'textarea' || tag === 'input'
379
-
380
- if (isNative) {
381
- try {
382
- await input.fill(text, { timeout: 5000 })
383
- return
384
- } catch {}
385
- }
386
-
387
- await input.click()
388
- // Выделяем всё содержимое, чтобы вставка заменила его целиком.
389
- await this.page.keyboard.press('Control+A')
390
- await this.page.keyboard.press('Delete')
391
-
392
- const ok = await input.evaluate((el, value) => {
393
- el.focus()
394
- const dt = new DataTransfer()
395
- dt.setData('text/plain', value)
396
- const ev = new ClipboardEvent('paste', {
397
- clipboardData: dt,
398
- bubbles: true,
399
- cancelable: true,
400
- })
401
- el.dispatchEvent(ev)
402
- // Если обработчик не отменил вставку и поле осталось пустым —
403
- // пробуем через insertText вручную (fallback ниже).
404
- return true
405
- }, text)
406
-
407
- if (!ok) {
408
- await this.page.keyboard.insertText(text)
409
- }
410
-
411
- // Проверяем, что текст реально попал в поле. Если редактор проигнорировал
412
- // paste — падаем на insertText.
413
- const got = await input.evaluate((el) => {
414
- if (el.tagName.toLowerCase() === 'textarea' || el.tagName.toLowerCase() === 'input') {
415
- return el.value
416
- }
417
- return el.innerText || el.textContent || ''
418
- })
419
- const norm = (s) =>
420
- (s || '').replace(/\r\n/g, '\n').replace(/\u00a0/g, ' ').trim()
421
- if (!norm(got)) {
422
- await input.click()
423
- await this.page.keyboard.insertText(text)
424
- }
425
- }
426
-
427
- // Ждём, пока с прошлой отправки пройдёт minSendIntervalMs. Защита от
428
- // «Messages too frequent. Try again later.» на [chat.deepseek.com](https://chat.deepseek.com/).
429
- async _waitForSendSlot() {
430
- if (!this._lastSentAt) return // первая отправка — пауза не нужна
431
- const gap = this.minSendIntervalMs - (Date.now() - this._lastSentAt)
432
- if (gap <= 0) return
433
- console.error(
434
- theme.warn(
435
- `⏳ пауза ${Math.ceil(gap / 1000)}с перед отправкой (лимит частоты DeepSeek)...`,
436
- ),
437
- )
438
- await this.page.waitForTimeout(gap)
439
- }
440
-
441
- async _askOnce(prompt, { timeout }) {
442
- const input = await this._findVisible(INPUT_SELECTORS, 10_000)
443
- if (!input) {
444
- throw new Error(
445
- 'Не найдено поле ввода. Запустите /debug-dom и поправьте INPUT_SELECTORS.',
446
- )
447
- }
448
-
449
- const beforeText = await this._readLastAnswerTextClean().catch(() => '')
450
-
451
- await this._waitForSendSlot()
452
- await this._setInputText(input, prompt)
453
- await this.page.waitForTimeout(200)
454
-
455
- const sendBtn = this.page
456
- .locator('button')
457
- .filter({ hasText: /send|отправить/i })
458
- .first()
459
- try {
460
- await sendBtn.click({ timeout: 1500 })
461
- } catch {
462
- await this.page.keyboard.press('Enter')
463
- }
464
- this._lastSentAt = Date.now()
465
-
466
- const startDeadline = Date.now() + 15_000
467
- let started = false
468
- while (Date.now() < startDeadline) {
469
- const gen = await this._isGenerating()
470
- const cur = await this._readLastAnswerTextClean().catch(() => '')
471
- if (gen || (cur && cur !== beforeText)) {
472
- started = true
473
- break
474
- }
475
- await this.page.waitForTimeout(300)
476
- }
477
- if (!started) {
478
- throw new Error(
479
- 'Ответ не начал генерироваться за 15с. Возможно, сообщение не отправилось.',
480
- )
481
- }
482
-
483
- const deadline = Date.now() + timeout
484
- let last = ''
485
- let stable = 0
486
- this._abort = false
487
- while (Date.now() < deadline) {
488
- if (this._abort) {
489
- // Пользователь нажал Esc — вернём то, что успело сгенерироваться.
490
- return last || '(прервано пользователем)'
491
- }
492
- const gen = await this._isGenerating()
493
- const cur = await this._readLastAnswerTextClean().catch(() => '')
494
- if (cur && cur === last && !gen) {
495
- stable++
496
- if (stable >= 2) return cur
497
- } else {
498
- stable = 0
499
- }
500
- last = cur
501
- await this.page.waitForTimeout(800)
502
- }
503
-
504
- if (last) return last
505
- throw new Error('Таймаут ожидания ответа. Попробуйте /debug-dom.')
506
- }
507
-
508
- async dumpDom(filePath) {
509
- if (!this.page) throw new Error('браузер не запущен')
510
- const html = await this.page.content()
511
- await fs.writeFile(filePath, html, 'utf-8')
512
-
513
- const report = await this.page.evaluate(
514
- (sels) => {
515
- const result = { answers: {}, stops: {}, inputs: {} }
516
- for (const s of sels.answers) {
517
- result.answers[s] = document.querySelectorAll(s).length
518
- }
519
- for (const s of sels.stops) {
520
- result.stops[s] = document.querySelectorAll(s).length
521
- }
522
- for (const s of sels.inputs) {
523
- result.inputs[s] = document.querySelectorAll(s).length
524
- }
525
- return result
526
- },
527
- {
528
- answers: ANSWER_SELECTORS,
529
- stops: STOP_SELECTORS,
530
- inputs: INPUT_SELECTORS,
531
- },
532
- )
533
-
534
- return { file: filePath, selectors: report }
535
- }
536
-
537
- // ---------- список чатов ----------
538
-
539
- async _ensureSidebarOpen() {
540
- const toggles = [
541
- 'button[aria-label*="sidebar" i]',
542
- 'button[aria-label*="история" i]',
543
- 'button[aria-label*="history" i]',
544
- 'button[class*="sidebar-toggle"]',
545
- 'button[class*="sidebarToggle"]',
546
- ]
547
- for (const sel of toggles) {
548
- try {
549
- const btn = this.page.locator(sel).first()
550
- if ((await btn.count()) === 0) continue
551
- if (!(await btn.isVisible().catch(() => false))) continue
552
- await btn.click({ timeout: 1500 })
553
- await this.page.waitForTimeout(500)
554
- return
555
- } catch {}
556
- }
557
- }
558
-
559
- async listChats(limit = 30) {
560
- await this._ensureSidebarOpen()
561
- return await this.page.evaluate((lim) => {
562
- const out = []
563
- const seen = new Set()
564
- const anchors = document.querySelectorAll('a[href*="/chat/"]')
565
- for (const a of anchors) {
566
- const href = a.getAttribute('href') || ''
567
- const m =
568
- href.match(/\/chat\/s\/([a-zA-Z0-9_-]+)/) ||
569
- href.match(/\/a\/chat\/s\/([a-zA-Z0-9_-]+)/)
570
- if (!m) continue
571
- const id = m[1]
572
- if (seen.has(id)) continue
573
- seen.add(id)
574
-
575
- const titleEl = a.querySelector('[class*="title"], [class*="text"]')
576
- let title = (titleEl ? titleEl.textContent : a.textContent) || ''
577
- title = title.trim().replace(/\s+/g, ' ')
578
- if (!title) title = '(без названия)'
579
-
580
- out.push({ id, title, href })
581
- if (out.length >= lim) break
582
- }
583
- return out
584
- }, limit)
585
- }
586
-
587
- async openChat(id) {
588
- const candidates = [`a[href$="/chat/s/${id}"]`, `a[href*="${id}"]`]
589
- for (const sel of candidates) {
590
- try {
591
- const loc = this.page.locator(sel).first()
592
- if ((await loc.count()) === 0) continue
593
- await loc.click({ timeout: 3000 })
594
- await this.page.waitForTimeout(1500)
595
- return true
596
- } catch {}
597
- }
598
-
599
- try {
600
- await this.page.goto(`https://chat.deepseek.com/a/chat/s/${id}`, {
601
- waitUntil: 'domcontentloaded',
602
- timeout: 20_000,
603
- })
604
- await this.page.waitForTimeout(1500)
605
- return true
606
- } catch (e) {
607
- throw new Error(`Не удалось открыть чат ${id}: ${e.message}`)
608
- }
609
- }
610
-
611
- async getCurrentChatId() {
612
- try {
613
- const url = this.page.url()
614
- const m = url.match(/\/chat\/s\/([a-zA-Z0-9_-]+)/)
615
- return m ? m[1] : null
616
- } catch {
617
- return null
618
- }
619
- }
620
-
621
- async close() {
622
- try {
623
- if (this.context) await this.context.close()
624
- } catch {}
625
- await killStaleChrome()
626
- }
627
- }
package/src/config.js DELETED
@@ -1,81 +0,0 @@
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
- minSendIntervalMs: 15000,
47
- },
48
- }
49
-
50
- export function loadConfig() {
51
- const merged = JSON.parse(JSON.stringify(DEFAULTS))
52
- for (const file of [HOME_CONFIG, PROJECT_CONFIG]) {
53
- try {
54
- const data = JSON.parse(fs.readFileSync(file, 'utf-8'))
55
- deepMerge(merged, data)
56
- } catch (e) {
57
- if (e.code !== 'ENOENT') {
58
- console.error(`config: не удалось прочитать ${file}: ${e.message}`)
59
- }
60
- }
61
- }
62
- return merged
63
- }
64
-
65
- function deepMerge(target, source) {
66
- for (const key of Object.keys(source)) {
67
- if (
68
- source[key] &&
69
- typeof source[key] === 'object' &&
70
- !Array.isArray(source[key])
71
- ) {
72
- target[key] = target[key] || {}
73
- deepMerge(target[key], source[key])
74
- } else {
75
- target[key] = source[key]
76
- }
77
- }
78
- }
79
-
80
- export const CONFIG_PATHS = { HOME_CONFIG, PROJECT_CONFIG }
81
- export { ZAMES_HOME }