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/CHANGELOG.md +84 -0
- package/README.md +111 -19
- package/dist/agent-loop.js +15 -5
- package/dist/browser-dom.js +21 -0
- package/dist/browser-pure.js +6 -6
- package/dist/browser.js +155 -12
- package/dist/commands.js +7 -0
- package/dist/config-command.js +118 -0
- package/dist/config.js +56 -1
- package/dist/context.js +47 -19
- package/dist/deepseek-ui.js +33 -0
- package/dist/i18n/common.js +0 -16
- package/dist/i18n/config.js +14 -18
- package/dist/i18n/deepseek.js +39 -39
- package/dist/i18n/doctor.js +0 -4
- package/dist/i18n/help.js +8 -8
- package/dist/i18n/prompt.js +0 -4
- package/dist/i18n/qwen.js +85 -0
- package/dist/i18n/self-review.js +0 -5
- package/dist/i18n.js +2 -0
- package/dist/index.js +273 -137
- package/dist/input.js +23 -0
- package/dist/provider-handoff.js +73 -0
- package/dist/providers.js +50 -0
- package/dist/qwen-browser.js +1668 -0
- package/dist/qwen-chats.js +183 -0
- package/dist/qwen-net-capture.js +221 -0
- package/dist/qwen-ui.js +160 -0
- package/dist/run-task.js +2 -1
- package/dist/sessions.js +56 -7
- package/dist/subagent.js +28 -3
- package/dist/system-prompt.js +15 -4
- package/dist/tools.js +19 -6
- package/dist/ui-health.js +27 -21
- package/package.json +7 -3
- package/scripts/coverage-gate.mts +8 -1
- package/scripts/release.mts +9 -7
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
|
-
|
|
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.
|
|
558
|
-
if (ok)
|
|
561
|
+
const ok = await this._waitLoggedInOrMfa(20_000);
|
|
562
|
+
if (ok) {
|
|
559
563
|
await this._persistSessionIfNeeded();
|
|
560
|
-
|
|
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.
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
398
|
-
|
|
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 >
|
|
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:
|
|
410
|
-
// Scoped files are dropped silently when the
|
|
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:
|
|
414
|
-
memory:
|
|
441
|
+
scopedAgents: keptScoped,
|
|
442
|
+
memory: takeWithin(memory, MAX_MEMORY_TOTAL),
|
|
415
443
|
skills,
|
|
416
444
|
commands,
|
|
417
445
|
};
|
package/dist/deepseek-ui.js
CHANGED
|
@@ -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]',
|
package/dist/i18n/common.js
CHANGED
|
@@ -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}',
|