zames_pro 2.72.7 → 2.72.9
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 +21 -0
- package/dist/browser-dom.js +21 -0
- package/dist/browser-pure.js +4 -2
- package/dist/browser.js +150 -11
- package/dist/config-command.js +118 -0
- package/dist/context.js +47 -19
- package/dist/deepseek-ui.js +39 -0
- package/dist/i18n/common.js +0 -16
- package/dist/i18n/config.js +0 -18
- package/dist/i18n/deepseek.js +13 -13
- package/dist/i18n/doctor.js +0 -4
- package/dist/i18n/help.js +0 -8
- package/dist/i18n/prompt.js +0 -4
- package/dist/i18n/self-review.js +0 -5
- package/dist/index.js +47 -109
- package/dist/sessions.js +41 -3
- package/dist/subagent.js +28 -3
- package/dist/system-prompt.js +15 -4
- package/dist/tools.js +19 -6
- package/package.json +1 -1
- package/scripts/coverage-gate.mts +3 -0
- package/scripts/release.mts +9 -7
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.72.9] - 2026-10-10
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **release:** do not duplicate the Unreleased body into the version section
|
|
15
|
+
- **context:** raise the context budget and split agents/memory pools
|
|
16
|
+
- **browser:** handle the DeepSeek email MFA dialog on login
|
|
17
|
+
- **subagent:** fail safe when the parent chat id is unknown
|
|
18
|
+
- **tools:** honor a skill's allowed-tools frontmatter
|
|
19
|
+
- **context:** keep newest MEMORY.md notes and write facts to the project memory
|
|
20
|
+
|
|
21
|
+
## [2.72.8] - 2026-10-09
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **browser:** also treat generation_timeout as a truncated turn
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **browser:** also treat generation_timeout as a truncated turn
|
|
30
|
+
|
|
10
31
|
## [2.72.7] - 2026-10-09
|
|
11
32
|
|
|
12
33
|
### Fixed
|
package/dist/browser-dom.js
CHANGED
|
@@ -57,6 +57,27 @@ export function readLoginErrorInDom() {
|
|
|
57
57
|
}
|
|
58
58
|
return out.replace(/\s+/g, ' ').trim();
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Read the account email shown in the MFA verification dialog, so the operator
|
|
62
|
+
* knows WHERE the one-time code was sent (the code arrives by email; without
|
|
63
|
+
* this the operator cannot tell which inbox to check). Best-effort: '' when the
|
|
64
|
+
* dialog or the email line is absent.
|
|
65
|
+
*/
|
|
66
|
+
export function readMfaAccountInDom() {
|
|
67
|
+
const sels = [
|
|
68
|
+
'.ds-mfa-verification-account',
|
|
69
|
+
'.ds-mfa-verification-modal [title*="@"]',
|
|
70
|
+
];
|
|
71
|
+
for (const s of sels) {
|
|
72
|
+
const el = document.querySelector(s);
|
|
73
|
+
if (!el)
|
|
74
|
+
continue;
|
|
75
|
+
const t = (el.getAttribute('title') || el.innerText || '').trim();
|
|
76
|
+
if (t)
|
|
77
|
+
return t;
|
|
78
|
+
}
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
60
81
|
/**
|
|
61
82
|
* The answer as RENDERED on the page, ALWAYS from the DOM (never the network
|
|
62
83
|
* capture). Reasoning blocks (think class) are skipped and must never be
|
package/dist/browser-pure.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from 'path';
|
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import fs from 'fs/promises';
|
|
4
4
|
import { chromium } from 'playwright';
|
|
5
|
-
import { GENERATION_ERR_RE, INCOMPLETE_STATUS_RE, RATE_LIMIT_RE, SERVER_BUSY_RE, STATUS_RE, } from './deepseek-ui.js';
|
|
5
|
+
import { GENERATION_ERR_RE, GENERATION_TIMEOUT_RE, INCOMPLETE_STATUS_RE, RATE_LIMIT_RE, SERVER_BUSY_RE, STATUS_RE, } from './deepseek-ui.js';
|
|
6
6
|
// Drop service placeholders ("Reading…", "Thinking…") so they are not mistaken
|
|
7
7
|
// for an answer; keep everything else as-is (including the whitespace the
|
|
8
8
|
// tool-call relies on). Pure: no `this`, no page.
|
|
@@ -172,7 +172,9 @@ export function sanitizeHeadlessUA(ua) {
|
|
|
172
172
|
export const AUTH_MARKER_FILE = path.join(os.homedir(), '.zames', 'auth.json');
|
|
173
173
|
export function isGenerationIncompleteText(text) {
|
|
174
174
|
const t = String(text || '');
|
|
175
|
-
return GENERATION_ERR_RE.test(t) ||
|
|
175
|
+
return (GENERATION_ERR_RE.test(t) ||
|
|
176
|
+
GENERATION_TIMEOUT_RE.test(t) ||
|
|
177
|
+
INCOMPLETE_STATUS_RE.test(t));
|
|
176
178
|
}
|
|
177
179
|
export function isRateLimitText(text) {
|
|
178
180
|
return RATE_LIMIT_RE.test(String(text || ''));
|
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';
|
|
@@ -554,10 +554,17 @@ export class DeepSeekBrowser {
|
|
|
554
554
|
const filled = await this._fillLoginForm(this.auth.username, this.auth.password);
|
|
555
555
|
if (!filled)
|
|
556
556
|
return false;
|
|
557
|
-
const ok = await this.
|
|
558
|
-
if (ok)
|
|
557
|
+
const ok = await this._waitLoggedInOrMfa(20_000);
|
|
558
|
+
if (ok) {
|
|
559
559
|
await this._persistSessionIfNeeded();
|
|
560
|
-
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
// Password accepted but DeepSeek now wants an email one-time code: ask the
|
|
563
|
+
// operator for it instead of failing the whole auto-login.
|
|
564
|
+
if (process.stdin.isTTY && (await this._mfaVisible())) {
|
|
565
|
+
return await this._handleMfaIfPresent();
|
|
566
|
+
}
|
|
567
|
+
return false;
|
|
561
568
|
}
|
|
562
569
|
/**
|
|
563
570
|
* Ask the operator for login/password in the terminal (only in a TTY) and
|
|
@@ -602,7 +609,7 @@ export class DeepSeekBrowser {
|
|
|
602
609
|
console.log(theme.warn(this._t('auth.form_not_found')));
|
|
603
610
|
return false;
|
|
604
611
|
}
|
|
605
|
-
const ok = await this.
|
|
612
|
+
const ok = await this._waitLoggedInOrMfa(20_000);
|
|
606
613
|
if (ok) {
|
|
607
614
|
// Remember the credentials so the next launch can re-login silently.
|
|
608
615
|
this.auth.username = user;
|
|
@@ -610,10 +617,23 @@ export class DeepSeekBrowser {
|
|
|
610
617
|
await this._persistSessionIfNeeded();
|
|
611
618
|
this.onAuthSave?.(user, password);
|
|
612
619
|
console.log(theme.assistant(this._t('auth.auto_login_ok')));
|
|
620
|
+
return true;
|
|
613
621
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
622
|
+
// Password accepted, but DeepSeek wants an email one-time code. Ask the
|
|
623
|
+
// operator for it and submit the dialog.
|
|
624
|
+
if (await this._mfaVisible()) {
|
|
625
|
+
// The credentials are valid — remember them even before MFA completes,
|
|
626
|
+
// so the next launch can skip straight to the code dialog.
|
|
627
|
+
this.auth.username = user;
|
|
628
|
+
this.auth.password = password;
|
|
629
|
+
this.onAuthSave?.(user, password);
|
|
630
|
+
const mfaOk = await this._handleMfaIfPresent();
|
|
631
|
+
if (mfaOk)
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
634
|
+
// Detect a credentials error shown by DeepSeek, otherwise report a
|
|
635
|
+
// generic failure with the page hint so the operator can react.
|
|
636
|
+
{
|
|
617
637
|
const reason = await this._readLoginError();
|
|
618
638
|
if (reason) {
|
|
619
639
|
console.log(theme.error(this._t('auth.login_rejected', { v: reason })));
|
|
@@ -622,7 +642,7 @@ export class DeepSeekBrowser {
|
|
|
622
642
|
console.log(theme.warn(this._t('auth.auto_login_failed', { v: this._t('auth.no_reason') })));
|
|
623
643
|
}
|
|
624
644
|
}
|
|
625
|
-
return
|
|
645
|
+
return false;
|
|
626
646
|
}
|
|
627
647
|
/**
|
|
628
648
|
* Read a visible login/credentials error message from the page, if any.
|
|
@@ -677,7 +697,11 @@ export class DeepSeekBrowser {
|
|
|
677
697
|
// After each attempt we give the login a short window and stop as soon as
|
|
678
698
|
// it succeeds, so a stray click does not fire on an already-logged-in page.
|
|
679
699
|
const tryLogin = async () => {
|
|
680
|
-
if (await this.
|
|
700
|
+
if (await this._waitLoggedInOrMfa(3000))
|
|
701
|
+
return true;
|
|
702
|
+
// An MFA dialog also means the password step was ACCEPTED — stop
|
|
703
|
+
// submitting, the caller will ask the operator for the one-time code.
|
|
704
|
+
if (await this._mfaVisible())
|
|
681
705
|
return true;
|
|
682
706
|
return false;
|
|
683
707
|
};
|
|
@@ -734,6 +758,121 @@ export class DeepSeekBrowser {
|
|
|
734
758
|
}
|
|
735
759
|
return false;
|
|
736
760
|
}
|
|
761
|
+
/**
|
|
762
|
+
* Like `_waitLoggedIn`, but the input field ALSO appears right after the
|
|
763
|
+
* password step even when DeepSeek demands an MFA one-time code — the code
|
|
764
|
+
* dialog only BLOCKS the first message. A plain `_waitLoggedIn` would then
|
|
765
|
+
* report success, swallow the real login failure (the next send would hit the
|
|
766
|
+
* dialog and hang), and never ask the operator for the code. Detect the MFA
|
|
767
|
+
* input first and treat it as "not logged in yet".
|
|
768
|
+
*/
|
|
769
|
+
async _waitLoggedInOrMfa(timeoutMs) {
|
|
770
|
+
const deadline = Date.now() + timeoutMs;
|
|
771
|
+
while (Date.now() < deadline) {
|
|
772
|
+
if (await this._mfaVisible())
|
|
773
|
+
return false;
|
|
774
|
+
if (await this.isLoggedIn().catch(() => false))
|
|
775
|
+
return true;
|
|
776
|
+
await this.page.waitForTimeout(500);
|
|
777
|
+
}
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
/** Is the email one-time-code (MFA) dialog currently shown? */
|
|
781
|
+
async _mfaVisible() {
|
|
782
|
+
return await this._findVisible(MFA_CODE_SELECTORS, 300)
|
|
783
|
+
.then((l) => !!l)
|
|
784
|
+
.catch(() => false);
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Handle DeepSeek's email MFA dialog ("Verify your account"). It appears
|
|
788
|
+
* after a correct password when the account has two-step verification on. The
|
|
789
|
+
* code is sent by email; we ask the operator for it in the terminal and type
|
|
790
|
+
* it into the dialog. Returns true when the session became active, false when
|
|
791
|
+
* there is no dialog or the operator could not complete it.
|
|
792
|
+
*/
|
|
793
|
+
async _handleMfaIfPresent() {
|
|
794
|
+
const codeInput = await this._findVisible(MFA_CODE_SELECTORS, 1000);
|
|
795
|
+
if (!codeInput)
|
|
796
|
+
return false;
|
|
797
|
+
const account = await this.page
|
|
798
|
+
.evaluate(readMfaAccountInDom)
|
|
799
|
+
.catch(() => '');
|
|
800
|
+
// The dialog opens with an EMPTY field: the email is only sent after
|
|
801
|
+
// "Send code" is clicked. Press it first (only when it is not already
|
|
802
|
+
// counting down), then ask the operator for the code that just arrived.
|
|
803
|
+
const clicked = await this._clickMfaSendCode();
|
|
804
|
+
console.log('\n' +
|
|
805
|
+
theme.warn(this._t(clicked ? 'auth.mfa_code_sent' : 'auth.mfa_required', {
|
|
806
|
+
v: account || '—',
|
|
807
|
+
})));
|
|
808
|
+
const readline = await import('readline');
|
|
809
|
+
const rl = readline.createInterface({
|
|
810
|
+
input: process.stdin,
|
|
811
|
+
output: process.stdout,
|
|
812
|
+
terminal: true,
|
|
813
|
+
});
|
|
814
|
+
const code = (await new Promise((resolve) => rl.question(this._t('auth.mfa_prompt'), (a) => resolve(a))))
|
|
815
|
+
.trim()
|
|
816
|
+
.replace(/\s+/g, '');
|
|
817
|
+
rl.close();
|
|
818
|
+
if (!code)
|
|
819
|
+
return false;
|
|
820
|
+
await this._setInputText(codeInput, code);
|
|
821
|
+
await this.page.waitForTimeout(150);
|
|
822
|
+
// Submit the code. Prefer the dialog's own submit button; fall back to
|
|
823
|
+
// Enter in the code field (the form submits on Enter).
|
|
824
|
+
let submitted = false;
|
|
825
|
+
for (const sel of MFA_SUBMIT_SELECTORS) {
|
|
826
|
+
const btn = this.page.locator(sel).last();
|
|
827
|
+
try {
|
|
828
|
+
if ((await btn.count()) === 0)
|
|
829
|
+
continue;
|
|
830
|
+
if (!(await btn.isVisible().catch(() => false)))
|
|
831
|
+
continue;
|
|
832
|
+
await btn.click({ timeout: 2000 });
|
|
833
|
+
submitted = true;
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
catch { }
|
|
837
|
+
}
|
|
838
|
+
if (!submitted)
|
|
839
|
+
await codeInput.press('Enter').catch(() => { });
|
|
840
|
+
const ok = await this._waitLoggedIn(20_000);
|
|
841
|
+
if (ok) {
|
|
842
|
+
await this._persistSessionIfNeeded();
|
|
843
|
+
console.log(theme.assistant(this._t('auth.auto_login_ok')));
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
const reason = await this._readLoginError();
|
|
847
|
+
console.log(theme.warn(this._t('auth.mfa_failed', {
|
|
848
|
+
v: reason || this._t('auth.no_reason'),
|
|
849
|
+
})));
|
|
850
|
+
}
|
|
851
|
+
return ok;
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Click the dialog's "Send code" button. Returns false when it is absent or
|
|
855
|
+
* already counting down (a resend cooldown), so we never hammer the endpoint.
|
|
856
|
+
*/
|
|
857
|
+
async _clickMfaSendCode() {
|
|
858
|
+
for (const sel of MFA_SEND_CODE_SELECTORS) {
|
|
859
|
+
const btn = this.page.locator(sel).last();
|
|
860
|
+
try {
|
|
861
|
+
if ((await btn.count()) === 0)
|
|
862
|
+
continue;
|
|
863
|
+
if (!(await btn.isVisible().catch(() => false)))
|
|
864
|
+
continue;
|
|
865
|
+
const label = ((await btn.innerText().catch(() => '')) || '').trim();
|
|
866
|
+
// Only click a REAL request button, never the countdown that replaces it.
|
|
867
|
+
if (label && !MFA_SEND_CODE_RE.test(label))
|
|
868
|
+
continue;
|
|
869
|
+
await btn.click({ timeout: 2000 });
|
|
870
|
+
return true;
|
|
871
|
+
}
|
|
872
|
+
catch { }
|
|
873
|
+
}
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
737
876
|
/**
|
|
738
877
|
* Persist the authenticated session. The persistent profile already keeps
|
|
739
878
|
* cookies on disk; `saveSession` (config) is kept as an explicit toggle for
|
|
@@ -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/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]',
|
|
@@ -213,6 +246,12 @@ export const RATE_LIMIT_RE = /(messages? too frequent|too many requests|rate lim
|
|
|
213
246
|
export const SERVER_BUSY_RE = /(server (is )?busy|server error|service (is )?unavailable|temporarily unavailable|internal server error|502|503|504|server overloaded|сервер занят|сервер перегружен|сервис недоступен|внутренняя ошибка|попробуйте позже)/i;
|
|
214
247
|
// ---------- network protocol (SSE) ----------
|
|
215
248
|
export const GENERATION_ERR_RE = /"finish_reason":\s*"generation_err"/i;
|
|
249
|
+
// The server gave up on the turn ("Server busy, please try again later."):
|
|
250
|
+
// finish_reason is `generation_timeout` and there is NO RESPONSE fragment, so
|
|
251
|
+
// extractAnswer() returns '' and the finish loop would wait out the whole
|
|
252
|
+
// timeout. Same failure family as generation_err — the turn did not complete,
|
|
253
|
+
// so ask() must resend instead of hanging.
|
|
254
|
+
export const GENERATION_TIMEOUT_RE = /"finish_reason":\s*"generation_timeout"/i;
|
|
216
255
|
// DeepSeek emits the generation status in TWO shapes: the initial message as a
|
|
217
256
|
// JSON property (`"quasi_status":"FINISHED"` inside v.response) and later
|
|
218
257
|
// BATCH updates (`{"p":"quasi_status","v":"FINISHED"}`). The old regex only
|
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}',
|
package/dist/i18n/config.js
CHANGED
|
@@ -148,7 +148,6 @@ export const configMessages = {
|
|
|
148
148
|
en: 'Persist the login session in the profile',
|
|
149
149
|
},
|
|
150
150
|
'cfg.title': { ru: 'Настройки zames', en: 'zames settings' },
|
|
151
|
-
'cfg.current': { ru: 'Текущий конфиг:', en: 'Current config:' },
|
|
152
151
|
'cfg.usage': {
|
|
153
152
|
ru: 'Использование: /config [menu | list | get <путь> | set <путь> <значение> | reset <путь> | path | lang <ru|en>]',
|
|
154
153
|
en: 'Usage: /config [menu | list | get <path> | set <path> <value> | reset <path> | path | lang <ru|en>]',
|
|
@@ -179,7 +178,6 @@ export const configMessages = {
|
|
|
179
178
|
ru: 'Конфиг (глобальный): {global}\nКонфиг (проект): {project}',
|
|
180
179
|
en: 'Config (global): {global}\nConfig (project): {project}',
|
|
181
180
|
},
|
|
182
|
-
'cfg.keys': { ru: 'Доступные параметры:', en: 'Available settings:' },
|
|
183
181
|
'cfg.lang_set': {
|
|
184
182
|
ru: 'Язык переключён на {v}.',
|
|
185
183
|
en: 'Language switched to {v}.',
|
|
@@ -188,20 +186,4 @@ export const configMessages = {
|
|
|
188
186
|
ru: 'Использование: /config lang <ru|en>',
|
|
189
187
|
en: 'Usage: /config lang <ru|en>',
|
|
190
188
|
},
|
|
191
|
-
'cfg.scope_current': {
|
|
192
|
-
ru: 'Запись конфига: {v} ({file})',
|
|
193
|
-
en: 'Config write scope: {v} ({file})',
|
|
194
|
-
},
|
|
195
|
-
'cfg.scope_usage': {
|
|
196
|
-
ru: 'Использование: /config scope <project|home>. project = .zamesrc.json (в git), home = ~/.zames/. Для личных настроек выбери home, чтобы не коммитить их.',
|
|
197
|
-
en: 'Usage: /config scope <project|home>. project = .zamesrc.json (committed), home = ~/.zames/. Use home for personal settings so they are not committed.',
|
|
198
|
-
},
|
|
199
|
-
'cfg.scope_set': {
|
|
200
|
-
ru: 'Запись конфига: {v} ({file})',
|
|
201
|
-
en: 'Config write scope: {v} ({file})',
|
|
202
|
-
},
|
|
203
|
-
'cfg.scope_bad': {
|
|
204
|
-
ru: 'Неизвестный scope: {v}. Используй project или home.',
|
|
205
|
-
en: 'Unknown scope: {v}. Use project or home.',
|
|
206
|
-
},
|
|
207
189
|
};
|
package/dist/i18n/deepseek.js
CHANGED
|
@@ -88,10 +88,6 @@ export const deepseekMessages = {
|
|
|
88
88
|
ru: '⏳ Агент не распознал ответ модели — прошу продолжить ({attempt}/{max})...',
|
|
89
89
|
en: '⏳ The agent did not recognize the model answer — asking it to continue ({attempt}/{max})...',
|
|
90
90
|
},
|
|
91
|
-
'ds.stalled': {
|
|
92
|
-
ru: '⚠ Агент остановился, не завершив задачу (модель перестала вызывать инструменты). Проверь чат DeepSeek — задача может быть не выполнена.',
|
|
93
|
-
en: '⚠ The agent stopped before finishing (the model stopped calling tools). Check the DeepSeek chat — the task may be incomplete.',
|
|
94
|
-
},
|
|
95
91
|
'ds.not_launched': {
|
|
96
92
|
ru: '✖ Браузер не запущен.',
|
|
97
93
|
en: '✖ The browser is not launched.',
|
|
@@ -145,17 +141,21 @@ export const deepseekMessages = {
|
|
|
145
141
|
ru: 'Войдите в DeepSeek: откройте https://chat.deepseek.com/ в своём браузере. Если включён headless — задайте логин/пароль через /config или запустите с --headed.',
|
|
146
142
|
en: 'Sign in to DeepSeek: open https://chat.deepseek.com/ in your browser. If headless is on — set the login/password via /config or run with --headed.',
|
|
147
143
|
},
|
|
148
|
-
'auth.
|
|
149
|
-
ru: '
|
|
150
|
-
en: 'DeepSeek
|
|
144
|
+
'auth.mfa_required': {
|
|
145
|
+
ru: '🔐 DeepSeek требует код подтверждения на почту {v}.',
|
|
146
|
+
en: '🔐 DeepSeek requires a verification code sent to {v}.',
|
|
147
|
+
},
|
|
148
|
+
'auth.mfa_code_sent': {
|
|
149
|
+
ru: '🔐 DeepSeek требует код подтверждения. Запросил код на почту {v} — проверь почту.',
|
|
150
|
+
en: '🔐 DeepSeek requires a verification code. Requested a code to {v} — check your inbox.',
|
|
151
151
|
},
|
|
152
|
-
'auth.
|
|
153
|
-
ru: '
|
|
154
|
-
en: '
|
|
152
|
+
'auth.mfa_prompt': {
|
|
153
|
+
ru: 'Код из письма: ',
|
|
154
|
+
en: 'Code from the email: ',
|
|
155
155
|
},
|
|
156
|
-
'auth.
|
|
157
|
-
ru: '
|
|
158
|
-
en: '
|
|
156
|
+
'auth.mfa_failed': {
|
|
157
|
+
ru: 'Код подтверждения не принят: {v}',
|
|
158
|
+
en: 'The verification code was not accepted: {v}',
|
|
159
159
|
},
|
|
160
160
|
'cfg.menu.title': {
|
|
161
161
|
ru: '⚙ Настройки — выбери параметр',
|
package/dist/i18n/doctor.js
CHANGED
|
@@ -127,10 +127,6 @@ export const doctorMessages = {
|
|
|
127
127
|
ru: 'Часть модулей не перезагрузилась:',
|
|
128
128
|
en: 'Some modules failed to reload:',
|
|
129
129
|
},
|
|
130
|
-
'msg.reload_done': {
|
|
131
|
-
ru: 'Перезагружено модулей: {n}. Браузер и чат не тронуты.',
|
|
132
|
-
en: 'Reloaded modules: {n}. Browser and chat untouched.',
|
|
133
|
-
},
|
|
134
130
|
'msg.reload_error': { ru: 'Ошибка reload:', en: 'Reload error:' },
|
|
135
131
|
'msg.abort_gen_short': {
|
|
136
132
|
ru: '⏹ Esc — прерываю генерацию...',
|
package/dist/i18n/help.js
CHANGED
|
@@ -377,10 +377,6 @@ export const helpMessages = {
|
|
|
377
377
|
ru: '🗜️ Сжимаю историю чата (DeepSeek)...',
|
|
378
378
|
en: '🗜️ Compacting the chat history (DeepSeek)...',
|
|
379
379
|
},
|
|
380
|
-
'compact.empty': {
|
|
381
|
-
ru: 'Нечего сжимать: в чате ещё нет ответов.',
|
|
382
|
-
en: 'Nothing to compact: the chat has no answers yet.',
|
|
383
|
-
},
|
|
384
380
|
'compact.summary_failed': {
|
|
385
381
|
ru: 'Не удалось получить резюме от модели: {v}',
|
|
386
382
|
en: 'Could not get the summary from the model: {v}',
|
|
@@ -461,10 +457,6 @@ export const helpMessages = {
|
|
|
461
457
|
ru: '🧠 Заметка добавлена в {v}',
|
|
462
458
|
en: '🧠 Note appended to {v}',
|
|
463
459
|
},
|
|
464
|
-
'init.exists': {
|
|
465
|
-
ru: 'AGENTS.md уже существует: {v}',
|
|
466
|
-
en: 'AGENTS.md already exists: {v}',
|
|
467
|
-
},
|
|
468
460
|
'init.created': { ru: 'Создан {v}', en: 'Created {v}' },
|
|
469
461
|
'init.analyzing': {
|
|
470
462
|
ru: 'Изучаю проект и готовлю AGENTS.md...',
|
package/dist/i18n/prompt.js
CHANGED
|
@@ -4,10 +4,6 @@ export const promptMessages = {
|
|
|
4
4
|
ru: 'ВАЖНО: отвечай оператору на русском языке. Весь текст в поле message инструмента respond, а также любые пояснения — на русском.',
|
|
5
5
|
en: 'IMPORTANT: reply to the operator in English. All text in the respond tool message field, and any explanations, must be in English.',
|
|
6
6
|
},
|
|
7
|
-
'prompt.tools_header': {
|
|
8
|
-
ru: 'You have access to the following tools:',
|
|
9
|
-
en: 'You have access to the following tools:',
|
|
10
|
-
},
|
|
11
7
|
// The hard "ONLY TOOL CALLS" block. All prose around a tool call is a
|
|
12
8
|
// protocol violation: the operator never sees it (only tool calls and the
|
|
13
9
|
// final respond reach the terminal), so it is pure pollution. We cannot
|
package/dist/i18n/self-review.js
CHANGED
|
@@ -112,7 +112,6 @@ export const selfReviewMessages = {
|
|
|
112
112
|
ru: '\nИспользуй /resume <n> для продолжения.\n',
|
|
113
113
|
en: '\nUse /resume <n> to continue.\n',
|
|
114
114
|
},
|
|
115
|
-
'chats.none': { ru: 'Чатов не найдено.', en: 'No chats found.' },
|
|
116
115
|
'chats.none_hint': {
|
|
117
116
|
ru: 'Чатов не найдено. Возможно, сайдбар свёрнут или селекторы устарели.',
|
|
118
117
|
en: 'No chats found. The sidebar may be collapsed or the selectors are outdated.',
|
|
@@ -142,10 +141,6 @@ export const selfReviewMessages = {
|
|
|
142
141
|
en: 'No chat #{n}. Total: {total}.',
|
|
143
142
|
},
|
|
144
143
|
'chats.opening': { ru: 'Открываю: {v}', en: 'Opening: {v}' },
|
|
145
|
-
'chats.context_kept': {
|
|
146
|
-
ru: ' Контекст чата сохранён. Системный промпт будет переслан на следующей задаче.\n',
|
|
147
|
-
en: ' Chat context kept. The system prompt will be resent on the next task.\n',
|
|
148
|
-
},
|
|
149
144
|
'chats.prompt_will_resend': {
|
|
150
145
|
ru: ' Системный промпт будет переслан на следующей задаче.\n',
|
|
151
146
|
en: ' System prompt will be resent on the next task.\n',
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { existsSync, statSync } from 'fs';
|
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { theme } from './theme.js';
|
|
8
8
|
import { DeepSeekBrowser, headlessUACacheReady } from './browser.js';
|
|
9
|
-
import { createTools, mergeTools, filterToolsForReadOnly } from './tools.js';
|
|
9
|
+
import { createTools, mergeTools, filterToolsForReadOnly, filterToolsByAllowList, } from './tools.js';
|
|
10
10
|
import { createTodoStore, getTodos, setTodos, resetTodos, renderTodos, } from './extraTools.js';
|
|
11
11
|
import { runAgentLoop } from './agent-loop.js';
|
|
12
12
|
import { createSubagentRunner } from './subagent.js';
|
|
@@ -16,7 +16,8 @@ import { promptOnce } from './prompt.js';
|
|
|
16
16
|
import { parseImagePaste, extForMime, saveToTemp, guessMime, formatSize, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, writeClipboardText, contentHash, } from './attachments.js';
|
|
17
17
|
import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
|
|
18
18
|
import { runConfigMenu } from './config-menu.js';
|
|
19
|
-
import {
|
|
19
|
+
import { formatConfigList } from './config-commands.js';
|
|
20
|
+
import { handleConfigCommand as runConfigCommand } from './config-command.js';
|
|
20
21
|
import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n.js';
|
|
21
22
|
import { Transcript } from './transcript.js';
|
|
22
23
|
import { UndoStore } from './undo.js';
|
|
@@ -31,7 +32,7 @@ import { parseBacklogNote } from './backlog.js';
|
|
|
31
32
|
import { askOperatorConfirm, } from './run-task.js';
|
|
32
33
|
import { closeWeb } from './web.js';
|
|
33
34
|
import { assertCommandInsideRoot, runShell, killAllBackground, } from './shell.js';
|
|
34
|
-
import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, loadHistory, saveHistory, } from './sessions.js';
|
|
35
|
+
import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, loadHistory, saveHistory, isInternalChat, } from './sessions.js';
|
|
35
36
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
36
37
|
const NL = String.fromCharCode(10);
|
|
37
38
|
// The last final assistant message, for the /copy command. Module-level
|
|
@@ -536,7 +537,15 @@ async function expandSlashTarget(workdir, name, rest) {
|
|
|
536
537
|
const extra = rest
|
|
537
538
|
? '\n\nAdditional instructions from the operator: ' + rest
|
|
538
539
|
: '';
|
|
539
|
-
|
|
540
|
+
// A skill may declare allowed-tools (Claude Code frontmatter): the
|
|
541
|
+
// operator expects the skill to run with ONLY those tools. Carry the
|
|
542
|
+
// list to the task so the main loop can narrow the tool set.
|
|
543
|
+
return {
|
|
544
|
+
text: header + '\n\n' + body + extra,
|
|
545
|
+
allowedTools: skill.allowedTools && skill.allowedTools.length
|
|
546
|
+
? skill.allowedTools
|
|
547
|
+
: undefined,
|
|
548
|
+
};
|
|
540
549
|
}
|
|
541
550
|
}
|
|
542
551
|
catch {
|
|
@@ -1574,108 +1583,22 @@ async function main() {
|
|
|
1574
1583
|
}
|
|
1575
1584
|
}
|
|
1576
1585
|
async function handleConfigCommand(input) {
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
// /config lang <ru|en> — quick access to ui.locale
|
|
1596
|
-
if (sub === 'lang' || sub === 'language' || sub === 'язык') {
|
|
1597
|
-
const val = parts[2];
|
|
1598
|
-
if (!val) {
|
|
1599
|
-
console.log(theme.system(t('status.locale', { v: localeDisplayName(currentLocale) })));
|
|
1600
|
-
console.log(theme.dim(t('cfg.lang_usage')));
|
|
1601
|
-
return;
|
|
1602
|
-
}
|
|
1603
|
-
const field = CONFIG_SCHEMA.find((f) => f.path === 'ui.locale');
|
|
1604
|
-
const loc = normalizeLocale(val);
|
|
1605
|
-
try {
|
|
1606
|
-
configSetRaw(field, loc);
|
|
1607
|
-
}
|
|
1608
|
-
catch (e) {
|
|
1609
|
-
console.error(theme.error(t('cfg.write_error', { v: e.message })));
|
|
1610
|
-
return;
|
|
1611
|
-
}
|
|
1612
|
-
console.log(theme.assistant(t('cfg.lang_set', { v: localeDisplayName(loc) })));
|
|
1613
|
-
return;
|
|
1614
|
-
}
|
|
1615
|
-
if (sub === 'get') {
|
|
1616
|
-
const key = parts[2];
|
|
1617
|
-
if (!key) {
|
|
1618
|
-
console.log(theme.dim(t('cfg.usage')));
|
|
1619
|
-
return;
|
|
1620
|
-
}
|
|
1621
|
-
const field = CONFIG_SCHEMA.find((f) => f.path === key);
|
|
1622
|
-
if (!field) {
|
|
1623
|
-
console.error(theme.error(t('cfg.unknown_key', { v: key })));
|
|
1624
|
-
return;
|
|
1625
|
-
}
|
|
1626
|
-
const cur = getByPath(config, key);
|
|
1627
|
-
console.log(theme.system(t('cfg.value', { v: key, value: configGetValue(key, cur, t) })));
|
|
1628
|
-
return;
|
|
1629
|
-
}
|
|
1630
|
-
if (sub === 'set') {
|
|
1631
|
-
const key = parts[2];
|
|
1632
|
-
const raw = parts.slice(3).join(' ');
|
|
1633
|
-
const field = CONFIG_SCHEMA.find((f) => f.path === key);
|
|
1634
|
-
if (!field) {
|
|
1635
|
-
console.error(theme.error(t('cfg.unknown_key', { v: key || '' })));
|
|
1636
|
-
return;
|
|
1637
|
-
}
|
|
1638
|
-
if (!raw) {
|
|
1639
|
-
console.log(theme.dim(t('cfg.usage')));
|
|
1640
|
-
return;
|
|
1641
|
-
}
|
|
1642
|
-
try {
|
|
1643
|
-
configSetRaw(field, raw);
|
|
1644
|
-
}
|
|
1645
|
-
catch {
|
|
1646
|
-
console.error(theme.error(t('cfg.bad_value', {
|
|
1647
|
-
v: key,
|
|
1648
|
-
type: configValueHint(field, t),
|
|
1649
|
-
})));
|
|
1650
|
-
return;
|
|
1651
|
-
}
|
|
1652
|
-
console.log(theme.assistant(t('cfg.saved', {
|
|
1653
|
-
v: key,
|
|
1654
|
-
value: JSON.stringify(getByPath(config, key)),
|
|
1655
|
-
file: CONFIG_PATHS.HOME_CONFIG,
|
|
1656
|
-
})));
|
|
1657
|
-
return;
|
|
1658
|
-
}
|
|
1659
|
-
if (sub === 'reset' || sub === 'unset') {
|
|
1660
|
-
const key = parts[2];
|
|
1661
|
-
const field = CONFIG_SCHEMA.find((f) => f.path === key);
|
|
1662
|
-
if (!field) {
|
|
1663
|
-
console.error(theme.error(t('cfg.unknown_key', { v: key || '' })));
|
|
1664
|
-
return;
|
|
1665
|
-
}
|
|
1666
|
-
try {
|
|
1667
|
-
configResetField(field);
|
|
1668
|
-
}
|
|
1669
|
-
catch (e) {
|
|
1670
|
-
console.error(theme.error(t('cfg.write_error', { v: e.message })));
|
|
1671
|
-
return;
|
|
1672
|
-
}
|
|
1673
|
-
console.log(theme.assistant(t('cfg.reset', { v: key })));
|
|
1674
|
-
return;
|
|
1675
|
-
}
|
|
1676
|
-
// Unknown subcommand — show the list.
|
|
1677
|
-
console.error(theme.error(t('cfg.unknown_key', { v: sub })));
|
|
1678
|
-
configShowList();
|
|
1586
|
+
await runConfigCommand(input, {
|
|
1587
|
+
t,
|
|
1588
|
+
print: (line) => console.log(line),
|
|
1589
|
+
printErr: (line) => console.error(line),
|
|
1590
|
+
getValue: (p) => getByPath(config, p),
|
|
1591
|
+
setValue: (field, raw) => configSetRaw(field, raw),
|
|
1592
|
+
resetField: (field) => configResetField(field),
|
|
1593
|
+
showList: () => configShowList(),
|
|
1594
|
+
openMenu: () => configOpenMenu(),
|
|
1595
|
+
currentLocale: () => currentLocale,
|
|
1596
|
+
normalizeLocale,
|
|
1597
|
+
localeDisplayName,
|
|
1598
|
+
schema: CONFIG_SCHEMA,
|
|
1599
|
+
homeConfigPath: CONFIG_PATHS.HOME_CONFIG,
|
|
1600
|
+
projectConfigPath: CONFIG_PATHS.PROJECT_CONFIG,
|
|
1601
|
+
});
|
|
1679
1602
|
}
|
|
1680
1603
|
while (running) {
|
|
1681
1604
|
let input;
|
|
@@ -1975,7 +1898,11 @@ async function main() {
|
|
|
1975
1898
|
editor.lock(t('msg.input_locked'));
|
|
1976
1899
|
spin.thinking();
|
|
1977
1900
|
try {
|
|
1978
|
-
|
|
1901
|
+
const all = await browser.listChats(30);
|
|
1902
|
+
// Hide subagent chats: they are real DeepSeek chats, but they are
|
|
1903
|
+
// internal context-isolation artifacts whose report already lives in
|
|
1904
|
+
// the parent chat, so resuming one is never what the operator wants.
|
|
1905
|
+
lastChats = all.filter((c) => !isInternalChat(c.id));
|
|
1979
1906
|
spin.stop();
|
|
1980
1907
|
// An optional query filters by title (substring, case-insensitive),
|
|
1981
1908
|
// so a long chat list does not have to be scanned by eye.
|
|
@@ -3145,14 +3072,19 @@ async function main() {
|
|
|
3145
3072
|
// prompt templates with $ARGUMENTS / {{args}} placeholders.
|
|
3146
3073
|
let expandedTask = null;
|
|
3147
3074
|
let missingArgs = null;
|
|
3075
|
+
let skillAllowedTools = null;
|
|
3148
3076
|
if (lower.startsWith('/')) {
|
|
3149
3077
|
const name = trimmed.slice(1).split(/\s+/)[0];
|
|
3150
3078
|
const rest = trimmed.slice(1 + name.length).trim();
|
|
3151
3079
|
const res = await expandSlashTarget(currentWorkdir, name, rest);
|
|
3152
3080
|
if (res && 'missing' in res)
|
|
3153
3081
|
missingArgs = res.missing;
|
|
3154
|
-
else if (res)
|
|
3082
|
+
else if (res) {
|
|
3155
3083
|
expandedTask = res.text;
|
|
3084
|
+
if ('allowedTools' in res && res.allowedTools) {
|
|
3085
|
+
skillAllowedTools = res.allowedTools;
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3156
3088
|
}
|
|
3157
3089
|
// B7: a custom command with mandatory `arguments:` refuses to run until the
|
|
3158
3090
|
// operator supplies them (by position).
|
|
@@ -3209,7 +3141,13 @@ async function main() {
|
|
|
3209
3141
|
// plan (read-only) mode — a `general` subagent could write otherwise.
|
|
3210
3142
|
subagents: config.browser.subagents && !planMode,
|
|
3211
3143
|
});
|
|
3212
|
-
|
|
3144
|
+
let tools = mergeTools(baseTools, mcpPool?.tools, planMode);
|
|
3145
|
+
// A skill invoked via /skill with `allowed-tools:` narrows the tool set to
|
|
3146
|
+
// that allow-list (Claude Code contract). respond is always kept so the run
|
|
3147
|
+
// can still finish.
|
|
3148
|
+
if (skillAllowedTools) {
|
|
3149
|
+
tools = filterToolsByAllowList(tools, skillAllowedTools);
|
|
3150
|
+
}
|
|
3213
3151
|
if (editor)
|
|
3214
3152
|
editor.busy = true;
|
|
3215
3153
|
try {
|
package/dist/sessions.js
CHANGED
|
@@ -9,6 +9,40 @@ import { writeJsonAtomic } from './fsutil.js';
|
|
|
9
9
|
const SESSIONS_DIR = path.join(os.homedir(), '.zames', '.sessions');
|
|
10
10
|
const INDEX_FILE = path.join(SESSIONS_DIR, 'last.json');
|
|
11
11
|
export const SESSIONS_FORMAT_VERSION = 1;
|
|
12
|
+
// Subagent chats are real DeepSeek chats, but they are INTERNAL: they exist
|
|
13
|
+
// only to isolate a sub-task's context and their report is already folded back
|
|
14
|
+
// into the parent chat. They must never be resumable. DeepSeek's own sidebar
|
|
15
|
+
// (`/chats`, which scrapes the DOM) still shows them under auto-generated
|
|
16
|
+
// titles, so we cannot hide them there; but we CAN hide them from `/sessions`
|
|
17
|
+
// (our own store) by keeping an id denylist here and filtering every read path.
|
|
18
|
+
const INTERNAL_FILE = path.join(SESSIONS_DIR, 'internal.json');
|
|
19
|
+
function readInternal() {
|
|
20
|
+
try {
|
|
21
|
+
const raw = JSON.parse(fs.readFileSync(INTERNAL_FILE, 'utf-8'));
|
|
22
|
+
return new Set(Array.isArray(raw) ? raw.filter((x) => typeof x === 'string') : []);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return new Set();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Remember a chat id as internal (a subagent chat) so it is never resumed. */
|
|
29
|
+
export function markInternalChat(id) {
|
|
30
|
+
if (!id)
|
|
31
|
+
return;
|
|
32
|
+
try {
|
|
33
|
+
ensureDir();
|
|
34
|
+
const set = readInternal();
|
|
35
|
+
set.add(String(id));
|
|
36
|
+
writeJsonAtomic(INTERNAL_FILE, Array.from(set));
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
}
|
|
40
|
+
/** True when `id` is a known internal (subagent) chat. */
|
|
41
|
+
export function isInternalChat(id) {
|
|
42
|
+
if (!id)
|
|
43
|
+
return false;
|
|
44
|
+
return readInternal().has(String(id));
|
|
45
|
+
}
|
|
12
46
|
function ensureDir() {
|
|
13
47
|
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
14
48
|
}
|
|
@@ -78,9 +112,12 @@ export function loadLastSession(workdir = '') {
|
|
|
78
112
|
const index = JSON.parse(fs.readFileSync(INDEX_FILE, 'utf-8'));
|
|
79
113
|
const byWorkdir = index && index.byWorkdir ? index.byWorkdir : {};
|
|
80
114
|
const candidates = [byWorkdir[workdir], index.last].filter((x) => Boolean(x));
|
|
115
|
+
const internal = readInternal();
|
|
81
116
|
for (const id of candidates) {
|
|
82
117
|
// The session file may have been deleted — the index is then stale and
|
|
83
118
|
// the session cannot be restored. We try the next candidate.
|
|
119
|
+
if (internal.has(id))
|
|
120
|
+
continue;
|
|
84
121
|
const s = readSession(id);
|
|
85
122
|
if (s)
|
|
86
123
|
return s;
|
|
@@ -102,18 +139,19 @@ export function readSession(id) {
|
|
|
102
139
|
return null;
|
|
103
140
|
}
|
|
104
141
|
}
|
|
105
|
-
// List all sessions, newest first.
|
|
142
|
+
// List all sessions, newest first. Internal (subagent) chats are excluded.
|
|
106
143
|
export function listSessions() {
|
|
107
144
|
try {
|
|
108
145
|
ensureDir();
|
|
146
|
+
const internal = readInternal();
|
|
109
147
|
const files = fs
|
|
110
148
|
.readdirSync(SESSIONS_DIR)
|
|
111
|
-
.filter((f) => f.endsWith('.json') && f !== 'last.json');
|
|
149
|
+
.filter((f) => f.endsWith('.json') && f !== 'last.json' && f !== 'internal.json');
|
|
112
150
|
const out = [];
|
|
113
151
|
for (const f of files) {
|
|
114
152
|
try {
|
|
115
153
|
const data = JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, f), 'utf-8'));
|
|
116
|
-
if (data && data.id)
|
|
154
|
+
if (data && data.id && !internal.has(String(data.id)))
|
|
117
155
|
out.push(data);
|
|
118
156
|
}
|
|
119
157
|
catch { }
|
package/dist/subagent.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadHooks, runLifecycleHooks } from './hooks.js';
|
|
2
|
+
import { markInternalChat } from './sessions.js';
|
|
2
3
|
// Subagent runner (BACKLOG N36). A `Task` tool call from the model is handled
|
|
3
4
|
// by the agent loop as a SEAM (like onAutoCompact): it calls back into the
|
|
4
5
|
// caller, which owns the whole lifecycle. This module builds that callback.
|
|
@@ -69,7 +70,27 @@ export function createSubagentRunner(opts) {
|
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
72
|
used++;
|
|
72
|
-
|
|
73
|
+
// Resolve the parent chat id BEFORE anything else. The runner restores the
|
|
74
|
+
// parent chat in its finally block, and that restore is what keeps the
|
|
75
|
+
// subagent's isolated context from swallowing the parent's turn. If the id
|
|
76
|
+
// cannot be resolved we must NOT run the nested loop: without it the finally
|
|
77
|
+
// would skip openChat and the parent would continue INSIDE the subagent
|
|
78
|
+
// chat. A couple of quick retries cover the "id not known yet" window on a
|
|
79
|
+
// freshly opened chat; after that we fail safe and tell the model.
|
|
80
|
+
let parentChatId = null;
|
|
81
|
+
for (let attempt = 0; attempt < 3 && !parentChatId; attempt++) {
|
|
82
|
+
parentChatId = await browser.getCurrentChatId().catch(() => null);
|
|
83
|
+
if (!parentChatId)
|
|
84
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
85
|
+
}
|
|
86
|
+
if (!parentChatId) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
text: 'Cannot start a subagent: the current chat id is unknown, so the ' +
|
|
90
|
+
'parent chat could not be restored afterwards (context isolation ' +
|
|
91
|
+
'would be lost). Do the work yourself with the other tools.',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
73
94
|
const subTools = buildTools(req.type === 'explore');
|
|
74
95
|
const taskText = subagentPersona(req.type) + '\n\n--- SUB-TASK ---\n' + req.prompt;
|
|
75
96
|
// Save the parent's send hooks: the nested loop OVERWRITES them on
|
|
@@ -104,8 +125,12 @@ export function createSubagentRunner(opts) {
|
|
|
104
125
|
selfImprovement: false,
|
|
105
126
|
// No recursion: the subagent cannot spawn its own subagents.
|
|
106
127
|
onSubagent: null,
|
|
107
|
-
// Do not persist the subagent chat as a session
|
|
108
|
-
|
|
128
|
+
// Do not persist the subagent chat as a session, but REMEMBER its id
|
|
129
|
+
// as internal so it is hidden from /sessions and never resumed.
|
|
130
|
+
onChatReady: (chatId) => {
|
|
131
|
+
if (chatId)
|
|
132
|
+
markInternalChat(chatId);
|
|
133
|
+
},
|
|
109
134
|
// Keep the PARENT's UI alive during the nested run. The nested loop
|
|
110
135
|
// OVERWRITES browser.onSendStart/… with its own callbacks; without
|
|
111
136
|
// forwarding the parent's hooks the spinner and the throttle countdown
|
package/dist/system-prompt.js
CHANGED
|
@@ -34,14 +34,18 @@ its OWN separate chat with an ISOLATED context and returns ONLY a final
|
|
|
34
34
|
report. The subagent does NOT see this conversation, so the prompt you pass
|
|
35
35
|
must be fully self-contained (goal, exact questions, file paths, context).
|
|
36
36
|
|
|
37
|
-
Use it to PROTECT this chat's context
|
|
37
|
+
Use it to PROTECT this chat's context — that is its ONLY purpose. zames
|
|
38
|
+
runs subagents SEQUENTIALLY (one browser, one shared send slot), so a Task
|
|
39
|
+
call does NOT speed anything up; it only keeps this chat's context small.
|
|
38
40
|
|
|
39
41
|
- Broad exploration whose raw hits would flood this chat ("find every use of
|
|
40
42
|
X", "map how module Y works end to end"): delegate it with
|
|
41
43
|
subagent_type "explore" (read-only) and keep only the report.
|
|
42
44
|
- A self-contained research or verification sub-task you can specify in a
|
|
43
|
-
paragraph
|
|
44
|
-
-
|
|
45
|
+
paragraph.
|
|
46
|
+
- A long investigation, or several unrelated ones: call Task again for each.
|
|
47
|
+
EVERY call opens a FRESH isolated chat (a new subagent) — there is no
|
|
48
|
+
"reuse" of a previous subagent, so pass everything it needs in the prompt.
|
|
45
49
|
|
|
46
50
|
Do NOT use it for:
|
|
47
51
|
- work that depends on this conversation's history (the subagent cannot see
|
|
@@ -90,10 +94,17 @@ export function renderContextSection(context) {
|
|
|
90
94
|
'\n';
|
|
91
95
|
}
|
|
92
96
|
if (context.memory.length) {
|
|
97
|
+
// Point at the DEEPEST memory file (last in the chain: the loader pushes
|
|
98
|
+
// globals first, then the dir chain root->workdir). Before, this used
|
|
99
|
+
// memory[0] — the GLOBAL ~/.zames/MEMORY.md whenever it existed — so
|
|
100
|
+
// project-specific facts leaked into the global file and then loaded for
|
|
101
|
+
// every unrelated project. Fall back to the global file only when no
|
|
102
|
+
// project-level MEMORY.md exists.
|
|
103
|
+
const memoryTarget = context.memory[context.memory.length - 1].path;
|
|
93
104
|
out +=
|
|
94
105
|
'\nWhen you learn a durable fact about this project or the operator preferences (build quirks, ' +
|
|
95
106
|
'conventions, gotchas), append a short bullet to ' +
|
|
96
|
-
|
|
107
|
+
memoryTarget +
|
|
97
108
|
' via Edit/Write so it survives ' +
|
|
98
109
|
'into future sessions. Keep entries concise.\n';
|
|
99
110
|
}
|
package/dist/tools.js
CHANGED
|
@@ -279,12 +279,14 @@ export function createTools(workdir, { undo, todos, readOnly = false, subagents
|
|
|
279
279
|
name: 'Task',
|
|
280
280
|
description: 'Delegate a self-contained sub-task to a SUBAGENT that works in its own ' +
|
|
281
281
|
'separate chat with an isolated context, then returns ONLY a final ' +
|
|
282
|
-
'report. Use it to keep heavy research out of the main context
|
|
283
|
-
'
|
|
284
|
-
'
|
|
285
|
-
'
|
|
286
|
-
'
|
|
287
|
-
'
|
|
282
|
+
'report. Use it to keep heavy research out of the main context — it ' +
|
|
283
|
+
'does NOT run in parallel (one browser, one send slot), so it saves ' +
|
|
284
|
+
'context, not time. subagent_type: "explore" (read-only: search/read, ' +
|
|
285
|
+
'cannot modify) or "general" (full tools). Each call opens a FRESH ' +
|
|
286
|
+
'subagent chat; nothing is reused between calls. The subagent does NOT ' +
|
|
287
|
+
'see this conversation, so `prompt` must be fully self-contained: state ' +
|
|
288
|
+
'the goal, the exact questions, and any file paths/context it needs. ' +
|
|
289
|
+
'Returns the subagent report as text.',
|
|
288
290
|
parameters: {
|
|
289
291
|
description: 'string',
|
|
290
292
|
prompt: 'string',
|
|
@@ -324,6 +326,17 @@ export const MUTATING_TOOLS = new Set([
|
|
|
324
326
|
export function filterToolsForReadOnly(tools) {
|
|
325
327
|
return tools.filter((t) => !MUTATING_TOOLS.has(t.name));
|
|
326
328
|
}
|
|
329
|
+
// A skill may declare `allowed-tools:` in its SKILL.md frontmatter (the Claude
|
|
330
|
+
// Code contract): the skill then runs with ONLY those tools. We keep the
|
|
331
|
+
// `respond` seam so the run can still finish, and match names case-insensitively
|
|
332
|
+
// (frontmatter is human-written). PURE; unit-tested.
|
|
333
|
+
export function filterToolsByAllowList(tools, allowed) {
|
|
334
|
+
if (!allowed || !allowed.length)
|
|
335
|
+
return tools;
|
|
336
|
+
const allow = new Set(allowed.map((n) => n.toLowerCase()));
|
|
337
|
+
allow.add('respond');
|
|
338
|
+
return tools.filter((t) => allow.has(t.name.toLowerCase()));
|
|
339
|
+
}
|
|
327
340
|
// MCP tool names are opaque (server + '__' + tool), so the exact-name
|
|
328
341
|
// MUTATING_TOOLS set cannot classify them. Instead we match the common
|
|
329
342
|
// mutating VERBS in the tool name. It is a heuristic deny-list, not a proof:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.72.
|
|
3
|
+
"version": "2.72.9",
|
|
4
4
|
"description": "Terminal coding agent that drives chat.deepseek.com through Playwright — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,6 +32,9 @@ const FLOORS = {
|
|
|
32
32
|
'permissions.ts': 85,
|
|
33
33
|
'changelog.ts': 85,
|
|
34
34
|
'attach-refs.ts': 85,
|
|
35
|
+
// C3: the pure /config subcommand dispatcher — silent routing regressions
|
|
36
|
+
// (a wrong subcommand branch) are invisible without tests.
|
|
37
|
+
'config-command.ts': 85,
|
|
35
38
|
// N12: web.ts holds the SSRF guard and the DuckDuckGo/HTML parsers — a
|
|
36
39
|
// regression there is silent. Floors sit below the current values.
|
|
37
40
|
'web.ts': 40,
|
package/scripts/release.mts
CHANGED
|
@@ -160,19 +160,21 @@ if (dryRun) {
|
|
|
160
160
|
process.exit(0)
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
// 5. Insert the section
|
|
163
|
+
// 5. Insert the version section in place of the Unreleased BODY. The
|
|
164
|
+
// Unreleased body is auto-synced from commits (see changelog:sync), so it is
|
|
165
|
+
// the same content as the new section — leaving it in place duplicated every
|
|
166
|
+
// entry (the real "### Fixed" block twice). Replace the whole body up to the
|
|
167
|
+
// next version heading with the new section.
|
|
164
168
|
const changelogPath = path.join(root, 'CHANGELOG.md')
|
|
165
169
|
const changelog = fs.readFileSync(changelogPath, 'utf-8')
|
|
166
170
|
const anchor = '## [Unreleased]'
|
|
167
171
|
const idx = changelog.indexOf(anchor)
|
|
168
172
|
if (idx < 0) fail('CHANGELOG.md has no "' + anchor + '" heading')
|
|
169
173
|
const afterAnchor = idx + anchor.length
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
section +
|
|
175
|
-
changelog.slice(afterAnchor).replace(/^\n+/, NL)
|
|
174
|
+
const rest = changelog.slice(afterAnchor)
|
|
175
|
+
const nextHeading = rest.search(/\n## \[/)
|
|
176
|
+
const tail = nextHeading < 0 ? NL : rest.slice(nextHeading)
|
|
177
|
+
const updated = changelog.slice(0, afterAnchor) + NL + NL + section + tail
|
|
176
178
|
fs.writeFileSync(changelogPath, updated)
|
|
177
179
|
|
|
178
180
|
// 6. Bump package.json, preserving its exact formatting (single-line replace
|