zames_pro 2.72.8 → 2.73.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/dist/browser.js CHANGED
@@ -2,9 +2,9 @@ import { chromium, } from 'playwright';
2
2
  import { extractAnswer, extractTokenUsage, dumpNetBody, isFinishedWithoutAnswer, } from './net-capture.js';
3
3
  import { rewriteFailedAttachments } from './commands.js';
4
4
  import { parseHistoryMessages, extractChatMessages, scrapeChatList, scrapeChatMessages, } from './browser-chats.js';
5
- import { CHAT_URL, CHAT_ID_RE, chatUrl, HISTORY_MESSAGES_URL, BUTTON_CLASS_RE, INPUT_SELECTORS, SEND_SELECTORS, STOP_SELECTORS, STOP_NAME_RE, CONTINUE_NAME_RE, ANSWER_SELECTORS, THINK_CLASS_RE, MESSAGE_CONTAINER_SELECTORS, RESTORE_ANSWER_SELECTORS, ASSISTANT_CLASS_RE, ASSISTANT_SELECTOR, ANSWER_MARKDOWN_SELECTOR, MARKDOWN_SELECTOR, CHAT_SIGNAL_NODES_SELECTOR, NEW_CHAT_SELECTOR, NEW_CHAT_TEXT_RE, PASSWORD_SELECTORS, LOGIN_SELECTORS, LOGIN_SUBMIT_SELECTORS, LOGIN_BUTTON_SELECTOR, LOGIN_NAME_RE, LOGIN_FORM_SUBMIT_RE, SIDEBAR_TOGGLE_SELECTORS, TOGGLE_SELECTORS, DEEP_THINKING_RE, WEB_SEARCH_RE, ATTACH_SELECTORS, ANSWER_URL_RE, } from './deepseek-ui.js';
5
+ import { CHAT_URL, CHAT_ID_RE, chatUrl, HISTORY_MESSAGES_URL, BUTTON_CLASS_RE, INPUT_SELECTORS, SEND_SELECTORS, STOP_SELECTORS, STOP_NAME_RE, CONTINUE_NAME_RE, ANSWER_SELECTORS, THINK_CLASS_RE, MESSAGE_CONTAINER_SELECTORS, RESTORE_ANSWER_SELECTORS, ASSISTANT_CLASS_RE, ASSISTANT_SELECTOR, ANSWER_MARKDOWN_SELECTOR, MARKDOWN_SELECTOR, CHAT_SIGNAL_NODES_SELECTOR, NEW_CHAT_SELECTOR, NEW_CHAT_TEXT_RE, PASSWORD_SELECTORS, LOGIN_SELECTORS, LOGIN_SUBMIT_SELECTORS, MFA_CODE_SELECTORS, MFA_SUBMIT_SELECTORS, MFA_SEND_CODE_SELECTORS, MFA_SEND_CODE_RE, LOGIN_BUTTON_SELECTOR, LOGIN_NAME_RE, LOGIN_FORM_SUBMIT_RE, SIDEBAR_TOGGLE_SELECTORS, TOGGLE_SELECTORS, DEEP_THINKING_RE, WEB_SEARCH_RE, ATTACH_SELECTORS, ANSWER_URL_RE, } from './deepseek-ui.js';
6
6
  import { probeUiHealth, } from './ui-health.js';
7
- import { readPageToastsInDom, readLoginErrorInDom, readLastAnswerInDom, chatSignalInDom, continueVisibleCheapInDom, stopButtonVisibleInDom, clickStopInDom, continueVisibleInDom, clickContinueInDom, readUserAgentInDom, findLoginInputIndexInDom, submitLoginFormInDom, setBusyTitleInDom, pasteTextIntoElement, readElementText, elementTagNameInDom, isElementEnabledInDom, readInputTrimmedTextInDom, dumpDomCountsInDom, } from './browser-dom.js';
7
+ import { readPageToastsInDom, readLoginErrorInDom, readMfaAccountInDom, readLastAnswerInDom, chatSignalInDom, continueVisibleCheapInDom, stopButtonVisibleInDom, clickStopInDom, continueVisibleInDom, clickContinueInDom, readUserAgentInDom, findLoginInputIndexInDom, submitLoginFormInDom, setBusyTitleInDom, pasteTextIntoElement, readElementText, elementTagNameInDom, isElementEnabledInDom, readInputTrimmedTextInDom, dumpDomCountsInDom, } from './browser-dom.js';
8
8
  import path from 'path';
9
9
  import os from 'os';
10
10
  import fs from 'fs/promises';
@@ -22,6 +22,7 @@ export { STOP_NAME_RE, CONTINUE_NAME_RE };
22
22
  import { askPassword, USER_DATA_DIR, parseHeadlessUACache, headlessEngineKey, readCachedHeadlessUA, writeCachedHeadlessUA, headlessUACacheReady, sanitizeHeadlessUA, AUTH_MARKER_FILE, isGenerationIncompleteText, isRateLimitText, isServerBusyText, normText, cleanAnswer, RateLimitError, ServerBusyError, GenerationIncompleteError, cleanSingletonFiles, profileLooksLocked, nukeProfile, } from './browser-pure.js';
23
23
  export { askPassword, parseHeadlessUACache, headlessUACacheReady, sanitizeHeadlessUA, isGenerationIncompleteText, isRateLimitText, isServerBusyText, normText, RateLimitError, ServerBusyError, GenerationIncompleteError, };
24
24
  export class DeepSeekBrowser {
25
+ providerName = 'deepseek';
25
26
  headless;
26
27
  debug;
27
28
  channel;
@@ -214,7 +215,10 @@ export class DeepSeekBrowser {
214
215
  }
215
216
  /** Translation bound to this browser's locale. */
216
217
  _t(key, params) {
217
- return translate(this.locale)(key, params);
218
+ // Default the {provider} placeholder to DeepSeek for the shared `ds.*`
219
+ // messages (QwenBrowser overrides it with 'Qwen'), so a provider-agnostic
220
+ // string never lies about which backend is talking.
221
+ return translate(this.locale)(key, { provider: 'DeepSeek', ...params });
218
222
  }
219
223
  /**
220
224
  * Switch the language of the messages the browser itself prints (login
@@ -554,10 +558,17 @@ export class DeepSeekBrowser {
554
558
  const filled = await this._fillLoginForm(this.auth.username, this.auth.password);
555
559
  if (!filled)
556
560
  return false;
557
- const ok = await this._waitLoggedIn(20_000);
558
- if (ok)
561
+ const ok = await this._waitLoggedInOrMfa(20_000);
562
+ if (ok) {
559
563
  await this._persistSessionIfNeeded();
560
- return ok;
564
+ return true;
565
+ }
566
+ // Password accepted but DeepSeek now wants an email one-time code: ask the
567
+ // operator for it instead of failing the whole auto-login.
568
+ if (process.stdin.isTTY && (await this._mfaVisible())) {
569
+ return await this._handleMfaIfPresent();
570
+ }
571
+ return false;
561
572
  }
562
573
  /**
563
574
  * Ask the operator for login/password in the terminal (only in a TTY) and
@@ -602,7 +613,7 @@ export class DeepSeekBrowser {
602
613
  console.log(theme.warn(this._t('auth.form_not_found')));
603
614
  return false;
604
615
  }
605
- const ok = await this._waitLoggedIn(20_000);
616
+ const ok = await this._waitLoggedInOrMfa(20_000);
606
617
  if (ok) {
607
618
  // Remember the credentials so the next launch can re-login silently.
608
619
  this.auth.username = user;
@@ -610,10 +621,23 @@ export class DeepSeekBrowser {
610
621
  await this._persistSessionIfNeeded();
611
622
  this.onAuthSave?.(user, password);
612
623
  console.log(theme.assistant(this._t('auth.auto_login_ok')));
624
+ return true;
613
625
  }
614
- else {
615
- // Detect a credentials error shown by DeepSeek, otherwise report a
616
- // generic failure with the page hint so the operator can react.
626
+ // Password accepted, but DeepSeek wants an email one-time code. Ask the
627
+ // operator for it and submit the dialog.
628
+ if (await this._mfaVisible()) {
629
+ // The credentials are valid — remember them even before MFA completes,
630
+ // so the next launch can skip straight to the code dialog.
631
+ this.auth.username = user;
632
+ this.auth.password = password;
633
+ this.onAuthSave?.(user, password);
634
+ const mfaOk = await this._handleMfaIfPresent();
635
+ if (mfaOk)
636
+ return true;
637
+ }
638
+ // Detect a credentials error shown by DeepSeek, otherwise report a
639
+ // generic failure with the page hint so the operator can react.
640
+ {
617
641
  const reason = await this._readLoginError();
618
642
  if (reason) {
619
643
  console.log(theme.error(this._t('auth.login_rejected', { v: reason })));
@@ -622,7 +646,7 @@ export class DeepSeekBrowser {
622
646
  console.log(theme.warn(this._t('auth.auto_login_failed', { v: this._t('auth.no_reason') })));
623
647
  }
624
648
  }
625
- return ok;
649
+ return false;
626
650
  }
627
651
  /**
628
652
  * Read a visible login/credentials error message from the page, if any.
@@ -677,7 +701,11 @@ export class DeepSeekBrowser {
677
701
  // After each attempt we give the login a short window and stop as soon as
678
702
  // it succeeds, so a stray click does not fire on an already-logged-in page.
679
703
  const tryLogin = async () => {
680
- if (await this._waitLoggedIn(3000))
704
+ if (await this._waitLoggedInOrMfa(3000))
705
+ return true;
706
+ // An MFA dialog also means the password step was ACCEPTED — stop
707
+ // submitting, the caller will ask the operator for the one-time code.
708
+ if (await this._mfaVisible())
681
709
  return true;
682
710
  return false;
683
711
  };
@@ -734,6 +762,121 @@ export class DeepSeekBrowser {
734
762
  }
735
763
  return false;
736
764
  }
765
+ /**
766
+ * Like `_waitLoggedIn`, but the input field ALSO appears right after the
767
+ * password step even when DeepSeek demands an MFA one-time code — the code
768
+ * dialog only BLOCKS the first message. A plain `_waitLoggedIn` would then
769
+ * report success, swallow the real login failure (the next send would hit the
770
+ * dialog and hang), and never ask the operator for the code. Detect the MFA
771
+ * input first and treat it as "not logged in yet".
772
+ */
773
+ async _waitLoggedInOrMfa(timeoutMs) {
774
+ const deadline = Date.now() + timeoutMs;
775
+ while (Date.now() < deadline) {
776
+ if (await this._mfaVisible())
777
+ return false;
778
+ if (await this.isLoggedIn().catch(() => false))
779
+ return true;
780
+ await this.page.waitForTimeout(500);
781
+ }
782
+ return false;
783
+ }
784
+ /** Is the email one-time-code (MFA) dialog currently shown? */
785
+ async _mfaVisible() {
786
+ return await this._findVisible(MFA_CODE_SELECTORS, 300)
787
+ .then((l) => !!l)
788
+ .catch(() => false);
789
+ }
790
+ /**
791
+ * Handle DeepSeek's email MFA dialog ("Verify your account"). It appears
792
+ * after a correct password when the account has two-step verification on. The
793
+ * code is sent by email; we ask the operator for it in the terminal and type
794
+ * it into the dialog. Returns true when the session became active, false when
795
+ * there is no dialog or the operator could not complete it.
796
+ */
797
+ async _handleMfaIfPresent() {
798
+ const codeInput = await this._findVisible(MFA_CODE_SELECTORS, 1000);
799
+ if (!codeInput)
800
+ return false;
801
+ const account = await this.page
802
+ .evaluate(readMfaAccountInDom)
803
+ .catch(() => '');
804
+ // The dialog opens with an EMPTY field: the email is only sent after
805
+ // "Send code" is clicked. Press it first (only when it is not already
806
+ // counting down), then ask the operator for the code that just arrived.
807
+ const clicked = await this._clickMfaSendCode();
808
+ console.log('\n' +
809
+ theme.warn(this._t(clicked ? 'auth.mfa_code_sent' : 'auth.mfa_required', {
810
+ v: account || '—',
811
+ })));
812
+ const readline = await import('readline');
813
+ const rl = readline.createInterface({
814
+ input: process.stdin,
815
+ output: process.stdout,
816
+ terminal: true,
817
+ });
818
+ const code = (await new Promise((resolve) => rl.question(this._t('auth.mfa_prompt'), (a) => resolve(a))))
819
+ .trim()
820
+ .replace(/\s+/g, '');
821
+ rl.close();
822
+ if (!code)
823
+ return false;
824
+ await this._setInputText(codeInput, code);
825
+ await this.page.waitForTimeout(150);
826
+ // Submit the code. Prefer the dialog's own submit button; fall back to
827
+ // Enter in the code field (the form submits on Enter).
828
+ let submitted = false;
829
+ for (const sel of MFA_SUBMIT_SELECTORS) {
830
+ const btn = this.page.locator(sel).last();
831
+ try {
832
+ if ((await btn.count()) === 0)
833
+ continue;
834
+ if (!(await btn.isVisible().catch(() => false)))
835
+ continue;
836
+ await btn.click({ timeout: 2000 });
837
+ submitted = true;
838
+ break;
839
+ }
840
+ catch { }
841
+ }
842
+ if (!submitted)
843
+ await codeInput.press('Enter').catch(() => { });
844
+ const ok = await this._waitLoggedIn(20_000);
845
+ if (ok) {
846
+ await this._persistSessionIfNeeded();
847
+ console.log(theme.assistant(this._t('auth.auto_login_ok')));
848
+ }
849
+ else {
850
+ const reason = await this._readLoginError();
851
+ console.log(theme.warn(this._t('auth.mfa_failed', {
852
+ v: reason || this._t('auth.no_reason'),
853
+ })));
854
+ }
855
+ return ok;
856
+ }
857
+ /**
858
+ * Click the dialog's "Send code" button. Returns false when it is absent or
859
+ * already counting down (a resend cooldown), so we never hammer the endpoint.
860
+ */
861
+ async _clickMfaSendCode() {
862
+ for (const sel of MFA_SEND_CODE_SELECTORS) {
863
+ const btn = this.page.locator(sel).last();
864
+ try {
865
+ if ((await btn.count()) === 0)
866
+ continue;
867
+ if (!(await btn.isVisible().catch(() => false)))
868
+ continue;
869
+ const label = ((await btn.innerText().catch(() => '')) || '').trim();
870
+ // Only click a REAL request button, never the countdown that replaces it.
871
+ if (label && !MFA_SEND_CODE_RE.test(label))
872
+ continue;
873
+ await btn.click({ timeout: 2000 });
874
+ return true;
875
+ }
876
+ catch { }
877
+ }
878
+ return false;
879
+ }
737
880
  /**
738
881
  * Persist the authenticated session. The persistent profile already keeps
739
882
  * cookies on disk; `saveSession` (config) is kept as an explicit toggle for
package/dist/commands.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { providerLabel } from './providers.js';
1
2
  import path from 'path';
2
3
  import { isImageName } from './attachments.js';
3
4
  import { translate } from './i18n.js';
@@ -276,6 +277,10 @@ export function renderDoctor(d, t = translate('en')) {
276
277
  row(true, 'node', d.nodeVersion);
277
278
  row(true, 'platform', d.platform);
278
279
  row(true, 'workdir', d.workdir);
280
+ if (d.provider) {
281
+ const label = providerLabel(d.provider).toLowerCase();
282
+ row(true, 'provider', label + (d.providerModel ? ' (' + d.providerModel + ')' : ''));
283
+ }
279
284
  row(d.gitOk, 'git', d.gitOk
280
285
  ? t('doctor.repo', { v: d.gitBranch || t('doctor.detached') })
281
286
  : t('doctor.not_repo'));
@@ -1013,6 +1018,8 @@ export const SLASH_COMMANDS = [
1013
1018
  { name: '/queue', key: 'help.cmd.queue' },
1014
1019
  { name: '/tasks', key: 'help.cmd.tasks' },
1015
1020
  { name: '/config', key: 'help.cmd.config' },
1021
+ { name: '/provider', key: 'help.cmd.provider' },
1022
+ { name: '/model', key: 'help.cmd.model' },
1016
1023
  { name: '/skills', key: 'help.cmd.skills' },
1017
1024
  { name: '/memory', key: 'help.cmd.memory' },
1018
1025
  { name: '/remember', key: 'help.cmd.remember' },
@@ -0,0 +1,118 @@
1
+ // The /config subcommand DISPATCH (BACKLOG C3). The PURE text rendering lives
2
+ // in config-commands.ts; this module owns the parsing/routing of the text
3
+ // subcommands (list/get/set/reset/lang/path) and prints through injected
4
+ // callbacks. Everything that touches live state (the runtime config object,
5
+ // the browser toggles, the LineEditor, the interactive menu) is injected, so
6
+ // the dispatch itself is unit-tested without a live agent.
7
+ //
8
+ // The interactive menu (`/config` bare or `menu`) is delegated to openMenu()
9
+ // because it pauses the editor and reads keys itself.
10
+ import { theme } from './theme.js';
11
+ import { configGetValue, configValueHint } from './config-commands.js';
12
+ /**
13
+ * Handle a `/config <sub>` text command. Never throws for a bad subcommand or
14
+ * argument: it prints a message and returns, so the caller can `await` it
15
+ * blindly from the main loop and from the live mid-run interception.
16
+ */
17
+ export async function handleConfigCommand(input, deps) {
18
+ const { t, print, printErr, getValue, setValue, resetField, showList, openMenu, currentLocale, normalizeLocale, localeDisplayName, homeConfigPath, projectConfigPath, } = deps;
19
+ const schema = deps.schema;
20
+ const parts = input.trim().split(/\s+/);
21
+ const sub = (parts[1] || '').toLowerCase();
22
+ // With no subcommand - the menu (or a text list in non-TTY).
23
+ if (!sub || sub === 'menu' || sub === 'ui') {
24
+ await openMenu();
25
+ return;
26
+ }
27
+ if (sub === 'list' || sub === 'show' || sub === 'ls') {
28
+ showList();
29
+ return;
30
+ }
31
+ if (sub === 'path' || sub === 'paths') {
32
+ print(theme.system(t('cfg.paths', { global: homeConfigPath, project: projectConfigPath })));
33
+ return;
34
+ }
35
+ // /config lang <ru|en> - quick access to ui.locale
36
+ if (sub === 'lang' || sub === 'language' || sub === 'язык') {
37
+ const val = parts[2];
38
+ if (!val) {
39
+ print(theme.system(t('status.locale', { v: localeDisplayName(currentLocale()) })));
40
+ print(theme.dim(t('cfg.lang_usage')));
41
+ return;
42
+ }
43
+ const field = schema.find((f) => f.path === 'ui.locale');
44
+ if (!field)
45
+ return;
46
+ const loc = normalizeLocale(val);
47
+ try {
48
+ setValue(field, loc);
49
+ }
50
+ catch (e) {
51
+ printErr(theme.error(t('cfg.write_error', { v: e.message })));
52
+ return;
53
+ }
54
+ print(theme.assistant(t('cfg.lang_set', { v: localeDisplayName(loc) })));
55
+ return;
56
+ }
57
+ if (sub === 'get') {
58
+ const key = parts[2];
59
+ if (!key) {
60
+ print(theme.dim(t('cfg.usage')));
61
+ return;
62
+ }
63
+ const field = schema.find((f) => f.path === key);
64
+ if (!field) {
65
+ printErr(theme.error(t('cfg.unknown_key', { v: key })));
66
+ return;
67
+ }
68
+ const cur = getValue(key);
69
+ print(theme.system(t('cfg.value', { v: key, value: configGetValue(key, cur, t) })));
70
+ return;
71
+ }
72
+ if (sub === 'set') {
73
+ const key = parts[2];
74
+ const raw = parts.slice(3).join(' ');
75
+ const field = schema.find((f) => f.path === key);
76
+ if (!field) {
77
+ printErr(theme.error(t('cfg.unknown_key', { v: key || '' })));
78
+ return;
79
+ }
80
+ if (!raw) {
81
+ print(theme.dim(t('cfg.usage')));
82
+ return;
83
+ }
84
+ try {
85
+ setValue(field, raw);
86
+ }
87
+ catch {
88
+ printErr(theme.error(t('cfg.bad_value', { v: key, type: configValueHint(field, t) })));
89
+ return;
90
+ }
91
+ print(theme.assistant(t('cfg.saved', {
92
+ v: key,
93
+ value: JSON.stringify(getValue(key)),
94
+ file: homeConfigPath,
95
+ })));
96
+ return;
97
+ }
98
+ if (sub === 'reset' || sub === 'unset') {
99
+ const key = parts[2];
100
+ const field = schema.find((f) => f.path === key);
101
+ if (!field) {
102
+ printErr(theme.error(t('cfg.unknown_key', { v: key || '' })));
103
+ return;
104
+ }
105
+ try {
106
+ resetField(field);
107
+ }
108
+ catch (e) {
109
+ printErr(theme.error(t('cfg.write_error', { v: e.message })));
110
+ return;
111
+ }
112
+ print(theme.assistant(t('cfg.reset', { v: key })));
113
+ return;
114
+ }
115
+ // Unknown subcommand - show the list.
116
+ printErr(theme.error(t('cfg.unknown_key', { v: sub })));
117
+ showList();
118
+ }
package/dist/config.js CHANGED
@@ -4,6 +4,7 @@ import os from 'os';
4
4
  import { DEFAULT_DIAGNOSTICS } from './diagnostics.js';
5
5
  import { DEFAULT_LOCALE } from './i18n.js';
6
6
  import { writeJsonAtomic } from './fsutil.js';
7
+ import { PROVIDER_IDS } from './providers.js';
7
8
  const ZAMES_HOME = path.join(os.homedir(), '.zames');
8
9
  const HOME_CONFIG = path.join(ZAMES_HOME, 'config.json');
9
10
  // The PROJECT config path is resolved LAZILY: `/cd` changes process.cwd() at
@@ -14,8 +15,23 @@ export function projectConfigPath() {
14
15
  }
15
16
  export const DEFAULTS = {
16
17
  maxIterations: 0,
18
+ // Default backend stays DeepSeek so existing users see no change; the
19
+ // operator switches with /provider qwen (persisted to the home config).
20
+ provider: 'deepseek',
21
+ qwen: {
22
+ model: '',
23
+ enabled: false,
24
+ // Qwen tolerates back-to-back sends (verified live: 4 sends at 0 ms gap,
25
+ // no rate limit), so the DeepSeek 15 s throttle is pure latency here.
26
+ minSendIntervalMs: 1000,
27
+ // The first send after a cold load can take minutes ("Model loading…" +
28
+ // thinking); a GUEST turn measured up to ~9 min live on a cold profile, so
29
+ // give it generous room. A signed-in session answers in seconds; this only
30
+ // bounds the worst case.
31
+ answerTimeoutMs: 900000,
32
+ },
17
33
  // Headless by default: the browser runs without a visible window. Set
18
- // headless: false (or pass --headed) to watch/debug the DeepSeek page.
34
+ // headless: false (or pass --headed) to watch/debug the page.
19
35
  headless: true,
20
36
  debug: false,
21
37
  browserChannel: null,
@@ -91,6 +107,16 @@ export function loadConfig() {
91
107
  }
92
108
  return merged;
93
109
  }
110
+ // The agent-loop watchdog deadline per provider. The loop default (240s) is
111
+ // tuned for DeepSeek; a Qwen GUEST turn regularly takes 7-8 minutes (cold
112
+ // "Model loading…" + a long thinking phase) and would trip the watchdog on
113
+ // every send. Derive the deadline from the provider's own answer timeout so
114
+ // there is headroom. DeepSeek keeps browser.askDeadlineMs (the loop default).
115
+ export function providerAskDeadlineMs(cfg) {
116
+ if (cfg.provider === 'qwen')
117
+ return cfg.qwen.answerTimeoutMs + 120_000;
118
+ return cfg.browser.askDeadlineMs;
119
+ }
94
120
  function deepMerge(target, source) {
95
121
  for (const key of Object.keys(source)) {
96
122
  if (source[key] &&
@@ -152,6 +178,35 @@ export const CONFIG_SCHEMA = [
152
178
  labelKey: 'cfg.f.browser_authSaveSession',
153
179
  groupKey: 'cfg.group.browser',
154
180
  },
181
+ {
182
+ path: 'provider',
183
+ type: 'enum',
184
+ values: [...PROVIDER_IDS],
185
+ labelKey: 'cfg.f.provider',
186
+ groupKey: 'cfg.group.browser',
187
+ },
188
+ {
189
+ path: 'qwen.model',
190
+ type: 'string',
191
+ labelKey: 'cfg.f.qwen_model',
192
+ groupKey: 'cfg.group.browser',
193
+ },
194
+ {
195
+ path: 'qwen.minSendIntervalMs',
196
+ type: 'number',
197
+ min: 0,
198
+ max: 600000,
199
+ labelKey: 'cfg.f.qwen_minSendIntervalMs',
200
+ groupKey: 'cfg.group.browser',
201
+ },
202
+ {
203
+ path: 'qwen.answerTimeoutMs',
204
+ type: 'number',
205
+ min: 1000,
206
+ max: 3600000,
207
+ labelKey: 'cfg.f.qwen_answerTimeoutMs',
208
+ groupKey: 'cfg.group.browser',
209
+ },
155
210
  {
156
211
  path: 'debug',
157
212
  type: 'boolean',
package/dist/context.js CHANGED
@@ -58,25 +58,45 @@ const BUILTIN_SKILLS = [
58
58
  '4. Do NOT change code. Answer in the operator language.',
59
59
  },
60
60
  ];
61
- const MAX_FILE_BASE = 60000;
62
- const MAX_TOTAL = 240000;
61
+ // Per-file budgets. AGENTS.md/MEMORY.md are hand-maintained and grow over
62
+ // time, so the caps are generous: an AGENTS.md of a real project (this repo's
63
+ // own file is ~69k) must NOT be silently cut. The old 60k cap already
64
+ // truncated it. Set higher and split per kind so a huge AGENTS.md can no
65
+ // longer starve MEMORY.md of the shared pool (the old single pool filled on
66
+ // agents first, so memory was dropped WHOLE when agents were large).
67
+ export const MAX_FILE_BASE = 120000;
68
+ /** MEMORY.md is append-only, so it gets its own (larger) budget. */
69
+ export const MAX_MEMORY_FILE = 200000;
70
+ /** Total spent on the AGENTS.md chain (project instructions + scoped). */
71
+ const MAX_AGENTS_TOTAL = 240000;
72
+ /** Total spent on the MEMORY.md chain, independent of the agents pool. */
73
+ const MAX_MEMORY_TOTAL = 200000;
63
74
  const CRLF_RE = /\r\n/g;
64
75
  const NL = String.fromCharCode(10);
65
- function clipIf(name, content, budget) {
76
+ function clipIf(name, content, budget, fromTail = false) {
66
77
  const cleaned = content.replace(CRLF_RE, NL).trim();
67
78
  if (cleaned.length <= budget)
68
79
  return cleaned;
69
80
  const keep = Math.max(0, budget - 120);
70
- return (cleaned.slice(0, keep) +
71
- NL +
72
- NL +
73
- '[... ' +
81
+ const marker = '[... ' +
74
82
  name +
75
83
  ' truncated (' +
76
84
  (cleaned.length - keep) +
77
- ' chars omitted) ...]');
85
+ ' chars omitted) ...]';
86
+ // MEMORY.md is APPENDED to over time, so its newest (most relevant) notes
87
+ // live at the TAIL — a head clip would drop exactly those. Keep the tail
88
+ // for memory files, the head for AGENTS.md (whose top is the summary).
89
+ if (!fromTail)
90
+ return cleaned.slice(0, keep) + NL + NL + marker;
91
+ let slice = cleaned.slice(cleaned.length - keep);
92
+ // Cut at a NOTE boundary, not mid-note: start the kept tail at the next line
93
+ // that begins a bullet or a heading, so a half-sentence note is not left.
94
+ const m = slice.search(/\n(?=[-*] |#{1,6} )/);
95
+ if (m > 0)
96
+ slice = slice.slice(m + 1);
97
+ return marker + NL + NL + slice;
78
98
  }
79
- async function readIfFile(p, budget) {
99
+ async function readIfFile(p, budget, fromTail = false) {
80
100
  try {
81
101
  const st = await fs.stat(p);
82
102
  if (!st.isFile())
@@ -85,7 +105,7 @@ async function readIfFile(p, budget) {
85
105
  // An EXISTING but empty file is still present: keep it so the prompt
86
106
  // shows an (empty) AGENTS.md/MEMORY.md section instead of silently
87
107
  // dropping the file. Only a read failure returns null.
88
- const content = clipIf(path.basename(p), raw, budget);
108
+ const content = clipIf(path.basename(p), raw, budget, fromTail);
89
109
  return { path: p, content };
90
110
  }
91
111
  catch {
@@ -367,7 +387,8 @@ async function loadMemoryChain(workdir) {
367
387
  path.join(os.homedir(), '.zames', 'MEMORY.md'),
368
388
  path.join(os.homedir(), '.claude', 'MEMORY.md'),
369
389
  ]) {
370
- const f = await readIfFile(p, MAX_FILE_BASE);
390
+ // Memory is append-only, so clip from the TAIL to keep the newest notes.
391
+ const f = await readIfFile(p, MAX_MEMORY_FILE, true);
371
392
  if (f && !seen.has(f.path)) {
372
393
  seen.add(f.path);
373
394
  out.push(f);
@@ -378,7 +399,7 @@ async function loadMemoryChain(workdir) {
378
399
  const p = await findNamedFile(dir, MEMORY_NAMES);
379
400
  if (!p || seen.has(p))
380
401
  continue;
381
- const f = await readIfFile(p, MAX_FILE_BASE);
402
+ const f = await readIfFile(p, MAX_MEMORY_FILE, true);
382
403
  if (f) {
383
404
  seen.add(p);
384
405
  out.push(f);
@@ -394,24 +415,31 @@ export async function loadProjectContext(workdir, touchPaths = []) {
394
415
  loadCommands(workdir).catch(() => []),
395
416
  ]);
396
417
  const scopedAgents = await loadScopedAgents(workdir, touchPaths).catch(() => []);
397
- let used = 0;
398
- const withinBudget = (files) => {
418
+ // SEPARATE pools for agents and memory: the old single pool filled on the
419
+ // agents chain first, so a large AGENTS.md dropped MEMORY.md WHOLE. Each
420
+ // category now has its own budget, and memory can never be starved by
421
+ // project instructions (or vice versa).
422
+ const takeWithin = (files, total) => {
423
+ let used = 0;
399
424
  const kept = [];
400
425
  for (const f of files) {
401
- if (used + f.content.length > MAX_TOTAL)
426
+ if (used + f.content.length > total)
402
427
  break;
403
428
  used += f.content.length;
404
429
  kept.push(f);
405
430
  }
406
431
  return kept;
407
432
  };
433
+ const keptAgents = takeWithin(agents, MAX_AGENTS_TOTAL);
434
+ const agentsUsed = keptAgents.reduce((n, f) => n + f.content.length, 0);
435
+ const keptScoped = takeWithin(scopedAgents, MAX_AGENTS_TOTAL - agentsUsed);
408
436
  return {
409
- agents: withinBudget(agents),
410
- // Scoped files are dropped silently when the total budget is already spent
437
+ agents: keptAgents,
438
+ // Scoped files are dropped silently when the AGENTS pool is already spent
411
439
  // on the main chain: a nested AGENTS.md is an optimization, not a hard
412
440
  // requirement, and must never push out a top-level instruction.
413
- scopedAgents: withinBudget(scopedAgents),
414
- memory: withinBudget(memory),
441
+ scopedAgents: keptScoped,
442
+ memory: takeWithin(memory, MAX_MEMORY_TOTAL),
415
443
  skills,
416
444
  commands,
417
445
  };
@@ -164,6 +164,39 @@ export const LOGIN_SUBMIT_SELECTORS = [
164
164
  'div[role="button"]:has-text("Sign in")',
165
165
  'div[role="button"]:has-text("Войти")',
166
166
  ];
167
+ // ---------- MFA / email verification ----------
168
+ //
169
+ // After a correct password DeepSeek may demand a one-time code sent to the
170
+ // account email (the `.ds-mfa-verification-modal` dialog). The code input is
171
+ // the reliable anchor: `autocomplete=one-time-code` is a semantic attribute
172
+ // that survives class renames, so detection does not depend on DeepSeek's CSS.
173
+ // The submit button is matched both by its own class and by its label, since
174
+ // the modal is a newer component whose classes we cannot pin down.
175
+ export const MFA_CODE_SELECTORS = [
176
+ 'input[autocomplete="one-time-code"]',
177
+ '.ds-mfa-verification-modal input[type="tel"]',
178
+ 'input[inputmode="numeric"][maxlength="6"]',
179
+ ];
180
+ export const MFA_MODAL_SELECTOR = '.ds-mfa-verification-modal, [role="dialog"]';
181
+ export const MFA_SUBMIT_SELECTORS = [
182
+ 'div[role="button"].ds-mfa-verification-submit',
183
+ '.ds-mfa-verification-modal div[role="button"].ds-button--primary',
184
+ 'div[role="button"]:has-text("Verify and log in")',
185
+ 'div[role="button"]:has-text("Verify")',
186
+ 'div[role="button"]:has-text("Подтвердить")',
187
+ 'div[role="button"]:has-text("Войти")',
188
+ ];
189
+ // The email is NOT sent until the operator clicks "Send code" — the dialog only
190
+ // shows an empty field and a request button. The button lives inside the code
191
+ // field suffix and turns into a countdown ("Resend in 60s") once clicked, so we
192
+ // match it by its TEXT and skip it when it is already counting down.
193
+ export const MFA_SEND_CODE_RE = /^(send code|resend|отправить\s*код|отправить\s*повторно|выслать\s*код)\s*$/i;
194
+ export const MFA_SEND_CODE_SELECTORS = [
195
+ '.ds-verify-code-input-countdown',
196
+ '.ds-mfa-verification-modal div[role="button"]',
197
+ 'div[role="button"]:has-text("Send code")',
198
+ 'div[role="button"]:has-text("Отправить код")',
199
+ ];
167
200
  // ---------- sidebar / chats ----------
168
201
  export const SIDEBAR_TOGGLE_SELECTORS = [
169
202
  'button[aria-label*="sidebar" i]',
@@ -29,10 +29,6 @@ export const commonMessages = {
29
29
  ru: 'System prompt на след. задаче: {v}',
30
30
  en: 'System prompt next task: {v}',
31
31
  },
32
- 'status.resend': {
33
- ru: 'Resend prompt (--resend-prompt): {v}',
34
- en: 'Resend prompt (--resend-prompt): {v}',
35
- },
36
32
  'status.last_chat': { ru: 'Last chat: {v}', en: 'Last chat: {v}' },
37
33
  'status.sessions': { ru: 'Сессии: {v}', en: 'Sessions: {v}' },
38
34
  'status.dev': {
@@ -145,14 +141,6 @@ export const commonMessages = {
145
141
  ru: 'Транскрипт: {transcript} · чат: {chat}',
146
142
  en: 'Transcript: {transcript} · chat: {chat}',
147
143
  },
148
- 'msg.abort_gen': {
149
- ru: '⏹ Esc — прерываю генерацию...',
150
- en: '⏹ Esc — aborting generation...',
151
- },
152
- 'msg.abort_ctrlc': {
153
- ru: '⏹ Ctrl+C — прерываю генерацию...',
154
- en: '⏹ Ctrl+C — aborting generation...',
155
- },
156
144
  'msg.queued': { ru: '📨 В очередь ({n}): ', en: '📨 Queued ({n}): ' },
157
145
  // TTY variant: the editor already echoed the message text, so only the
158
146
  // count is shown (no repeated text).
@@ -301,10 +289,6 @@ export const commonMessages = {
301
289
  en: 'undo is disabled in the config',
302
290
  },
303
291
  'rewind.empty': { ru: 'Чекпойнтов нет.', en: 'No checkpoints.' },
304
- 'rewind.created': {
305
- ru: '📸 Чекпойнт: {v}',
306
- en: '📸 Checkpoint: {v}',
307
- },
308
292
  'rewind.list_title': { ru: 'Чекпойнты:', en: 'Checkpoints:' },
309
293
  'rewind.reverted': {
310
294
  ru: '↶ Откатили рабочее дерево к чекпойнту {v}',