zames_pro 2.71.0 → 2.72.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 +26 -0
- package/dist/agent-loop.js +255 -924
- package/dist/answer-parse.js +765 -0
- package/dist/browser-chats.js +161 -0
- package/dist/browser-pure.js +238 -0
- package/dist/browser.js +45 -390
- package/dist/changelog.js +29 -0
- package/dist/cli-args.js +48 -0
- package/dist/compact.js +9 -0
- package/dist/config.js +6 -0
- package/dist/diagnostics.js +71 -0
- package/dist/hooks.js +30 -2
- package/dist/i18n/common.js +4 -0
- package/dist/index.js +49 -24
- package/dist/input.js +25 -1
- package/dist/run-task.js +31 -0
- package/dist/shell.js +102 -1
- package/dist/statusline.js +52 -0
- package/dist/subagent.js +40 -3
- package/dist/tools.js +81 -8
- package/package.json +10 -8
- package/scripts/changelog-draft.mts +38 -1
- package/scripts/{eslint-ts6.mjs → eslint-ts6.mts} +11 -4
- package/scripts/release.mts +36 -7
- package/scripts/{render-demo.mjs → render-demo.mts} +12 -11
- package/scripts/{self-smoke.mjs → self-smoke.mts} +41 -4
- /package/scripts/{clean-dist.mjs → clean-dist.mts} +0 -0
- /package/scripts/{coverage-gate.mjs → coverage-gate.mts} +0 -0
package/dist/browser.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { chromium, } from 'playwright';
|
|
2
2
|
import { extractAnswer, extractTokenUsage, dumpNetBody, isFinishedWithoutAnswer, } from './net-capture.js';
|
|
3
3
|
import { rewriteFailedAttachments } from './commands.js';
|
|
4
|
-
import {
|
|
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, STATUS_RE, ANSWER_URL_RE, } from './deepseek-ui.js';
|
|
5
6
|
import { probeUiHealth, } from './ui-health.js';
|
|
6
7
|
import path from 'path';
|
|
7
8
|
import os from 'os';
|
|
@@ -13,234 +14,12 @@ import { translate, DEFAULT_LOCALE } from './i18n.js';
|
|
|
13
14
|
// here preserves the existing public API (test/*.test.ts import them from
|
|
14
15
|
// browser.ts).
|
|
15
16
|
export { STOP_NAME_RE, CONTINUE_NAME_RE };
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export function askPassword(question) {
|
|
23
|
-
return new Promise((resolve) => {
|
|
24
|
-
process.stdout.write(question);
|
|
25
|
-
// Non-interactive: read a single line from stdin as-is.
|
|
26
|
-
if (!process.stdin.isTTY) {
|
|
27
|
-
void import('readline').then(({ createInterface }) => {
|
|
28
|
-
const rl = createInterface({
|
|
29
|
-
input: process.stdin,
|
|
30
|
-
output: process.stdout,
|
|
31
|
-
});
|
|
32
|
-
rl.question('', (answer) => {
|
|
33
|
-
rl.close();
|
|
34
|
-
resolve(answer);
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
const stdin = process.stdin;
|
|
40
|
-
const wasRaw = stdin.isRaw;
|
|
41
|
-
stdin.setRawMode(true);
|
|
42
|
-
stdin.resume();
|
|
43
|
-
let value = '';
|
|
44
|
-
const onData = (buf) => {
|
|
45
|
-
const s = buf.toString('utf8');
|
|
46
|
-
for (const ch of s) {
|
|
47
|
-
const code = ch.charCodeAt(0);
|
|
48
|
-
// Enter / Ctrl+J -> done.
|
|
49
|
-
if (ch === '\r' || ch === '\n') {
|
|
50
|
-
done();
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
// Ctrl+C -> abort the whole process (same as elsewhere in the CLI).
|
|
54
|
-
if (code === 3) {
|
|
55
|
-
cleanup();
|
|
56
|
-
process.stdout.write('\n');
|
|
57
|
-
process.exit(130);
|
|
58
|
-
}
|
|
59
|
-
// Ctrl+D -> finish with whatever we have.
|
|
60
|
-
if (code === 4) {
|
|
61
|
-
done();
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
// Backspace (0x7f or 0x08): remove the last char and one star.
|
|
65
|
-
if (code === 127 || code === 8) {
|
|
66
|
-
if (value.length > 0) {
|
|
67
|
-
value = value.slice(0, -1);
|
|
68
|
-
process.stdout.write('\b \b');
|
|
69
|
-
}
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
// Ignore other control characters (arrows, etc.).
|
|
73
|
-
if (code < 32)
|
|
74
|
-
continue;
|
|
75
|
-
value += ch;
|
|
76
|
-
process.stdout.write('*');
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
const cleanup = () => {
|
|
80
|
-
stdin.removeListener('data', onData);
|
|
81
|
-
try {
|
|
82
|
-
stdin.setRawMode(wasRaw || false);
|
|
83
|
-
}
|
|
84
|
-
catch { }
|
|
85
|
-
stdin.pause();
|
|
86
|
-
};
|
|
87
|
-
const done = () => {
|
|
88
|
-
cleanup();
|
|
89
|
-
process.stdout.write('\n');
|
|
90
|
-
resolve(value);
|
|
91
|
-
};
|
|
92
|
-
stdin.on('data', onData);
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
const USER_DATA_DIR = path.join(os.homedir(), '.zames', 'profile');
|
|
96
|
-
// Cache of the sanitized (non-Headless) User-Agent, keyed by the browser
|
|
97
|
-
// engine path. On the FIRST headless run we detect the real UA, strip the
|
|
98
|
-
// "Headless" marker and store it here; later runs pass it straight to
|
|
99
|
-
// _launchOnce, so the browser starts with a good UA and the "launch → read UA →
|
|
100
|
-
// relaunch" dance happens only once (it otherwise doubled every headless
|
|
101
|
-
// start). Keying by the executable path means a Playwright update (new engine
|
|
102
|
-
// path) invalidates the cache and we re-derive the UA once for the new build.
|
|
103
|
-
const HEADLESS_UA_CACHE = path.join(os.homedir(), '.zames', 'headless-ua.json');
|
|
104
|
-
// Pure: pick a reusable UA from the cache file's text for the given engine, or
|
|
105
|
-
// null. Exported so the policy is unit-tested without touching ~/.zames.
|
|
106
|
-
export function parseHeadlessUACache(raw, engine) {
|
|
107
|
-
try {
|
|
108
|
-
const j = JSON.parse(raw);
|
|
109
|
-
if (j &&
|
|
110
|
-
j.engine === engine &&
|
|
111
|
-
typeof j.ua === 'string' &&
|
|
112
|
-
j.ua &&
|
|
113
|
-
!/Headless/i.test(j.ua)) {
|
|
114
|
-
return j.ua;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
catch { }
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
function headlessEngineKey() {
|
|
121
|
-
try {
|
|
122
|
-
return chromium.executablePath();
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
return '';
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
async function readCachedHeadlessUA(engine) {
|
|
129
|
-
if (!engine)
|
|
130
|
-
return null;
|
|
131
|
-
try {
|
|
132
|
-
const raw = await fs.readFile(HEADLESS_UA_CACHE, 'utf-8');
|
|
133
|
-
return parseHeadlessUACache(raw, engine);
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
async function writeCachedHeadlessUA(engine, ua) {
|
|
140
|
-
if (!engine || !ua)
|
|
141
|
-
return;
|
|
142
|
-
try {
|
|
143
|
-
await fs.mkdir(path.dirname(HEADLESS_UA_CACHE), { recursive: true });
|
|
144
|
-
await fs.writeFile(HEADLESS_UA_CACHE, JSON.stringify({ engine, ua }, null, 2), 'utf-8');
|
|
145
|
-
}
|
|
146
|
-
catch { }
|
|
147
|
-
}
|
|
148
|
-
// For /doctor: is a sanitized UA cached for the CURRENT engine build? A missing
|
|
149
|
-
// cache only means the next headless start relaunches once (not an error), so
|
|
150
|
-
// this is informational. Best-effort.
|
|
151
|
-
export async function headlessUACacheReady() {
|
|
152
|
-
const ua = await readCachedHeadlessUA(headlessEngineKey());
|
|
153
|
-
return !!ua;
|
|
154
|
-
}
|
|
155
|
-
// A headless Chrome advertises "HeadlessChrome/..." in its User-Agent, and
|
|
156
|
-
// DeepSeek's CDN (CloudFront/WAF) rejects that UA with a plain "403 ERROR"
|
|
157
|
-
// page before the app is even served — so a headless login looked broken while
|
|
158
|
-
// a headed one worked (headed Chrome sends "Chrome/..."). We keep the real
|
|
159
|
-
// engine version and only drop the "Headless" marker. When the operator
|
|
160
|
-
// explicitly set a UA in the config, that one wins (see _launchOnce).
|
|
161
|
-
export function sanitizeHeadlessUA(ua) {
|
|
162
|
-
return String(ua || '').replace(/HeadlessChrome/g, 'Chrome');
|
|
163
|
-
}
|
|
164
|
-
// Written after a successful sign-in so the next launch knows a session was
|
|
165
|
-
// stored in the persistent profile (used by /doctor and diagnostics).
|
|
166
|
-
const AUTH_MARKER_FILE = path.join(os.homedir(), '.zames', 'auth.json');
|
|
167
|
-
export function isGenerationIncompleteText(text) {
|
|
168
|
-
const t = String(text || '');
|
|
169
|
-
return GENERATION_ERR_RE.test(t) || INCOMPLETE_STATUS_RE.test(t);
|
|
170
|
-
}
|
|
171
|
-
export function isRateLimitText(text) {
|
|
172
|
-
return RATE_LIMIT_RE.test(String(text || ''));
|
|
173
|
-
}
|
|
174
|
-
export function isServerBusyText(text) {
|
|
175
|
-
const t = String(text || '');
|
|
176
|
-
// Rate limit takes priority (it needs the long wait).
|
|
177
|
-
if (RATE_LIMIT_RE.test(t))
|
|
178
|
-
return false;
|
|
179
|
-
return SERVER_BUSY_RE.test(t);
|
|
180
|
-
}
|
|
181
|
-
// Normalize text for comparison: collapse whitespace so that tiny DOM
|
|
182
|
-
// differences (nbsp, trailing spaces) don't count as "a new answer".
|
|
183
|
-
export function normText(s) {
|
|
184
|
-
return String(s || '')
|
|
185
|
-
.replace(/\u00a0/g, ' ')
|
|
186
|
-
.replace(/\s+/g, ' ')
|
|
187
|
-
.trim();
|
|
188
|
-
}
|
|
189
|
-
// The "too frequent" error: distinct from others so ask() waits a long time
|
|
190
|
-
// (DeepSeek limits reset over minutes) and retries the send itself.
|
|
191
|
-
export class RateLimitError extends Error {
|
|
192
|
-
constructor(detail) {
|
|
193
|
-
super('Messages too frequent. Try again later. ' + detail);
|
|
194
|
-
this.name = 'RateLimitError';
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
// DeepSeek transient server error (Server busy).
|
|
198
|
-
export class ServerBusyError extends Error {
|
|
199
|
-
constructor(detail) {
|
|
200
|
-
super('Server busy. Try again later. ' + detail);
|
|
201
|
-
this.name = 'ServerBusyError';
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
// The generation was truncated by the server (`generation_err` / INCOMPLETE).
|
|
205
|
-
// Distinct from ServerBusyError: the DeepSeek backend answered, but the turn
|
|
206
|
-
// did not finish, and the UI offers a "Continue" button. The agent retries the
|
|
207
|
-
// same send (a new message into the chat) instead of waiting for an answer
|
|
208
|
-
// that will never arrive. This is what the operator hit with the reasoning
|
|
209
|
-
// (deep thinking) mode on: the long THINK phase makes the truncation far more
|
|
210
|
-
// likely, so with thinking off everything worked and with it on the turn kept
|
|
211
|
-
// stopping and showing "Continue".
|
|
212
|
-
export class GenerationIncompleteError extends Error {
|
|
213
|
-
constructor(detail) {
|
|
214
|
-
super('Generation incomplete (server truncated the answer). ' + detail);
|
|
215
|
-
this.name = 'GenerationIncompleteError';
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
// ---------- profile cleanup ----------
|
|
219
|
-
async function cleanSingletonFiles() {
|
|
220
|
-
const names = ['SingletonLock', 'SingletonCookie', 'SingletonSocket'];
|
|
221
|
-
for (const name of names) {
|
|
222
|
-
const p = path.join(USER_DATA_DIR, name);
|
|
223
|
-
try {
|
|
224
|
-
await fs.unlink(p);
|
|
225
|
-
}
|
|
226
|
-
catch { }
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
async function profileLooksLocked() {
|
|
230
|
-
try {
|
|
231
|
-
await fs.access(path.join(USER_DATA_DIR, 'SingletonLock'));
|
|
232
|
-
return true;
|
|
233
|
-
}
|
|
234
|
-
catch {
|
|
235
|
-
return false;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
async function nukeProfile() {
|
|
239
|
-
try {
|
|
240
|
-
await fs.rm(USER_DATA_DIR, { recursive: true, force: true });
|
|
241
|
-
}
|
|
242
|
-
catch { }
|
|
243
|
-
}
|
|
17
|
+
// The pure, state-free helpers live in browser-pure.ts (BACKLOG C3): they are
|
|
18
|
+
// imported for the class below and RE-EXPORTED here, so the public API of this
|
|
19
|
+
// module is unchanged (tests and other modules import these names from
|
|
20
|
+
// './browser.js').
|
|
21
|
+
import { askPassword, USER_DATA_DIR, parseHeadlessUACache, headlessEngineKey, readCachedHeadlessUA, writeCachedHeadlessUA, headlessUACacheReady, sanitizeHeadlessUA, AUTH_MARKER_FILE, isGenerationIncompleteText, isRateLimitText, isServerBusyText, normText, RateLimitError, ServerBusyError, GenerationIncompleteError, cleanSingletonFiles, profileLooksLocked, nukeProfile, } from './browser-pure.js';
|
|
22
|
+
export { askPassword, parseHeadlessUACache, headlessUACacheReady, sanitizeHeadlessUA, isGenerationIncompleteText, isRateLimitText, isServerBusyText, normText, RateLimitError, ServerBusyError, GenerationIncompleteError, };
|
|
244
23
|
export class DeepSeekBrowser {
|
|
245
24
|
headless;
|
|
246
25
|
debug;
|
|
@@ -2605,31 +2384,9 @@ export class DeepSeekBrowser {
|
|
|
2605
2384
|
}
|
|
2606
2385
|
async listChats(limit = 30) {
|
|
2607
2386
|
await this._ensureSidebarOpen();
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
const anchors = document.querySelectorAll('a[href*="/chat/"]');
|
|
2612
|
-
for (const a of anchors) {
|
|
2613
|
-
const href = a.getAttribute('href') || '';
|
|
2614
|
-
const m = href.match(/\/chat\/s\/([a-zA-Z0-9_-]+)/) ||
|
|
2615
|
-
href.match(/\/a\/chat\/s\/([a-zA-Z0-9_-]+)/);
|
|
2616
|
-
if (!m)
|
|
2617
|
-
continue;
|
|
2618
|
-
const id = m[1];
|
|
2619
|
-
if (seen.has(id))
|
|
2620
|
-
continue;
|
|
2621
|
-
seen.add(id);
|
|
2622
|
-
const titleEl = a.querySelector('[class*="title"], [class*="text"]');
|
|
2623
|
-
let title = (titleEl ? titleEl.textContent : a.textContent) || '';
|
|
2624
|
-
title = title.trim().replace(/\s+/g, ' ');
|
|
2625
|
-
if (!title)
|
|
2626
|
-
title = '(без названия)';
|
|
2627
|
-
out.push({ id, title, href });
|
|
2628
|
-
if (out.length >= lim)
|
|
2629
|
-
break;
|
|
2630
|
-
}
|
|
2631
|
-
return out;
|
|
2632
|
-
}, limit);
|
|
2387
|
+
// The scraper is a named export (browser-chats.ts) so the class stays thin
|
|
2388
|
+
// and the scraping policy has one home; it runs inside the page.
|
|
2389
|
+
return await this.page.evaluate(scrapeChatList, limit);
|
|
2633
2390
|
}
|
|
2634
2391
|
async openChat(id) {
|
|
2635
2392
|
// Click the chat link and VERIFY the URL actually points at the
|
|
@@ -2696,6 +2453,11 @@ export class DeepSeekBrowser {
|
|
|
2696
2453
|
}
|
|
2697
2454
|
const auth = this._apiAuth;
|
|
2698
2455
|
const pow = this._apiPow;
|
|
2456
|
+
// The in-page fetch only RETURNS the raw chat_messages; the parsing happens
|
|
2457
|
+
// OUTSIDE, in the pure parseHistoryMessages() (browser-chats.ts), so the
|
|
2458
|
+
// in-page and the Playwright-request fallback paths share ONE parser and
|
|
2459
|
+
// cannot drift. The in-page callback cannot call our helpers (it runs in
|
|
2460
|
+
// the page context), so it must stay a plain data fetch.
|
|
2699
2461
|
const res = await this.page
|
|
2700
2462
|
.evaluate(async (opts) => {
|
|
2701
2463
|
try {
|
|
@@ -2714,53 +2476,35 @@ export class DeepSeekBrowser {
|
|
|
2714
2476
|
headers,
|
|
2715
2477
|
});
|
|
2716
2478
|
if (!resp.ok)
|
|
2717
|
-
return { error: 'HTTP ' + resp.status,
|
|
2479
|
+
return { error: 'HTTP ' + resp.status, messages: null };
|
|
2718
2480
|
const json = await resp.json();
|
|
2719
2481
|
const messages = json && json.data && json.data.biz_data
|
|
2720
2482
|
? json.data.biz_data.chat_messages
|
|
2721
2483
|
: null;
|
|
2722
2484
|
if (!Array.isArray(messages)) {
|
|
2723
|
-
return { error: 'no chat_messages in response',
|
|
2485
|
+
return { error: 'no chat_messages in response', messages: null };
|
|
2724
2486
|
}
|
|
2725
|
-
|
|
2726
|
-
const out = [];
|
|
2727
|
-
// The context size: the LATEST accumulated_token_usage in the chat
|
|
2728
|
-
// (each message carries the running counter).
|
|
2729
|
-
let usage = null;
|
|
2730
|
-
for (const m of messages) {
|
|
2731
|
-
if (m && typeof m.accumulated_token_usage === 'number') {
|
|
2732
|
-
usage = m.accumulated_token_usage;
|
|
2733
|
-
}
|
|
2734
|
-
const role = m && m.role === 'ASSISTANT' ? 'assistant' : 'user';
|
|
2735
|
-
const want = role === 'assistant' ? 'RESPONSE' : 'REQUEST';
|
|
2736
|
-
let text = '';
|
|
2737
|
-
for (const fr of (m && m.fragments) || []) {
|
|
2738
|
-
if (!fr || fr.type !== want)
|
|
2739
|
-
continue;
|
|
2740
|
-
if (typeof fr.content === 'string')
|
|
2741
|
-
text += fr.content;
|
|
2742
|
-
}
|
|
2743
|
-
text = text.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim();
|
|
2744
|
-
if (text)
|
|
2745
|
-
out.push({ role, text });
|
|
2746
|
-
}
|
|
2747
|
-
return { error: '', list: out, usage };
|
|
2487
|
+
return { error: '', messages };
|
|
2748
2488
|
}
|
|
2749
2489
|
catch (e) {
|
|
2750
|
-
return {
|
|
2490
|
+
return {
|
|
2491
|
+
error: 'fetch failed: ' + e.message,
|
|
2492
|
+
messages: null,
|
|
2493
|
+
};
|
|
2751
2494
|
}
|
|
2752
2495
|
}, { chatId: id, auth, pow, historyUrl: HISTORY_MESSAGES_URL })
|
|
2753
2496
|
.catch((e) => ({
|
|
2754
2497
|
error: 'evaluate failed: ' + e.message,
|
|
2755
|
-
|
|
2498
|
+
messages: null,
|
|
2756
2499
|
}));
|
|
2757
2500
|
this._lastHistoryError = res?.error || '';
|
|
2501
|
+
// Parse the raw messages with the SAME pure parser the fallback uses.
|
|
2502
|
+
const parsed = parseHistoryMessages(res?.messages);
|
|
2758
2503
|
// Pick up the context size the history carries, so /resume (and /cost
|
|
2759
2504
|
// right after it) shows a real number even before the first answer.
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
const list = (res?.list || []);
|
|
2505
|
+
if (typeof parsed.usage === 'number')
|
|
2506
|
+
this._lastTokenUsage = parsed.usage;
|
|
2507
|
+
const list = parsed.list;
|
|
2764
2508
|
if (list.length || !res?.error)
|
|
2765
2509
|
return list;
|
|
2766
2510
|
// Fallback: Playwright's own request context (shares the browser cookies)
|
|
@@ -2770,31 +2514,17 @@ export class DeepSeekBrowser {
|
|
|
2770
2514
|
const resp = await this.page.request.get(HISTORY_MESSAGES_URL + '?chat_session_id=' + encodeURIComponent(id), { headers: { accept: 'application/json' } });
|
|
2771
2515
|
if (!resp.ok())
|
|
2772
2516
|
return list;
|
|
2773
|
-
const json =
|
|
2774
|
-
const messages = json
|
|
2775
|
-
if (!
|
|
2517
|
+
const json = await resp.json();
|
|
2518
|
+
const messages = extractChatMessages(json);
|
|
2519
|
+
if (!messages)
|
|
2776
2520
|
return list;
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
}
|
|
2783
|
-
const role = m && m.role === 'ASSISTANT' ? 'assistant' : 'user';
|
|
2784
|
-
const want = role === 'assistant' ? 'RESPONSE' : 'REQUEST';
|
|
2785
|
-
let text = '';
|
|
2786
|
-
for (const fr of (m && m.fragments) || []) {
|
|
2787
|
-
if (!fr || fr.type !== want)
|
|
2788
|
-
continue;
|
|
2789
|
-
if (typeof fr.content === 'string')
|
|
2790
|
-
text += fr.content;
|
|
2791
|
-
}
|
|
2792
|
-
text = text.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim();
|
|
2793
|
-
if (text)
|
|
2794
|
-
out.push({ role, text });
|
|
2795
|
-
}
|
|
2521
|
+
// Same pure parser as the in-page path (browser-chats.ts), so the two
|
|
2522
|
+
// cannot drift.
|
|
2523
|
+
const parsed = parseHistoryMessages(messages);
|
|
2524
|
+
if (typeof parsed.usage === 'number')
|
|
2525
|
+
this._lastTokenUsage = parsed.usage;
|
|
2796
2526
|
this._lastHistoryError = '';
|
|
2797
|
-
return
|
|
2527
|
+
return parsed.list;
|
|
2798
2528
|
}
|
|
2799
2529
|
catch (e) {
|
|
2800
2530
|
this._lastHistoryError = 'request failed: ' + e.message;
|
|
@@ -2810,92 +2540,17 @@ export class DeepSeekBrowser {
|
|
|
2810
2540
|
// ds-assistant-message-* class; anything that is not an assistant block is
|
|
2811
2541
|
// treated as a user message. The model's reasoning (.ds-think-content) is
|
|
2812
2542
|
// skipped, exactly like in _readLastAnswerText.
|
|
2543
|
+
// Read the WHOLE visible dialogue of the currently open chat, top to
|
|
2544
|
+
// bottom. Used by /resume and /resume-id so the operator sees the restored
|
|
2545
|
+
// context in the terminal instead of just "Chat opened.".
|
|
2546
|
+
//
|
|
2547
|
+
// The in-page scraper (scrapeChatMessages, browser-chats.ts) is a named
|
|
2548
|
+
// export so the class body stays thin; it runs inside the page.
|
|
2813
2549
|
async readChatMessages() {
|
|
2814
2550
|
if (!this.page)
|
|
2815
2551
|
return [];
|
|
2816
2552
|
const raw = await this.page
|
|
2817
|
-
.evaluate(
|
|
2818
|
-
const think = new RegExp(opts.thinkRe, 'i');
|
|
2819
|
-
const assistantRe = new RegExp(opts.assistantRe, 'i');
|
|
2820
|
-
const inThink = (e) => {
|
|
2821
|
-
let n = e;
|
|
2822
|
-
while (n) {
|
|
2823
|
-
const cls = (n.className || '').toString();
|
|
2824
|
-
if (think.test(cls))
|
|
2825
|
-
return true;
|
|
2826
|
-
n = n.parentElement;
|
|
2827
|
-
}
|
|
2828
|
-
return false;
|
|
2829
|
-
};
|
|
2830
|
-
const textOf = (e) => {
|
|
2831
|
-
const h = e;
|
|
2832
|
-
const t = h.innerText || h.textContent || '';
|
|
2833
|
-
const NL = String.fromCharCode(10);
|
|
2834
|
-
return t.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim();
|
|
2835
|
-
};
|
|
2836
|
-
// A message block is "assistant" when it contains an answer markdown
|
|
2837
|
-
// wrapper (ds-markdown / ds-assistant-message), otherwise it is a
|
|
2838
|
-
// user bubble. DeepSeek's class names drift between builds, so the
|
|
2839
|
-
// detection is content-based, not class-prefix-based.
|
|
2840
|
-
const looksAssistant = (e) => {
|
|
2841
|
-
const cls = (e.className || '').toString();
|
|
2842
|
-
if (assistantRe.test(cls))
|
|
2843
|
-
return true;
|
|
2844
|
-
if (e.querySelector(opts.assistantSel))
|
|
2845
|
-
return true;
|
|
2846
|
-
// A user bubble has no rendered markdown; an answer does.
|
|
2847
|
-
if (e.querySelector(opts.markdownSel))
|
|
2848
|
-
return true;
|
|
2849
|
-
return false;
|
|
2850
|
-
};
|
|
2851
|
-
// Message-level containers first (broad), then the answer wrappers.
|
|
2852
|
-
// See MESSAGE_CONTAINER_SELECTORS in deepseek-ui.ts.
|
|
2853
|
-
let blocks = [];
|
|
2854
|
-
for (const s of opts.containerSels) {
|
|
2855
|
-
const found = Array.from(document.querySelectorAll(s)).filter((e) => !inThink(e) && textOf(e).length > 0);
|
|
2856
|
-
if (found.length) {
|
|
2857
|
-
blocks = found;
|
|
2858
|
-
break;
|
|
2859
|
-
}
|
|
2860
|
-
}
|
|
2861
|
-
// De-duplicate by node identity and drop a block that is NESTED
|
|
2862
|
-
// inside another block. Some builds match both an outer message
|
|
2863
|
-
// container and an inner one; without this the restored dialogue
|
|
2864
|
-
// printed each turn twice. We keep only the OUTERMOST blocks.
|
|
2865
|
-
if (blocks.length > 1) {
|
|
2866
|
-
const unique = Array.from(new Set(blocks));
|
|
2867
|
-
blocks = unique.filter((b) => !unique.some((other) => other !== b && other.contains(b)));
|
|
2868
|
-
}
|
|
2869
|
-
const out = [];
|
|
2870
|
-
if (blocks.length) {
|
|
2871
|
-
for (const b of blocks) {
|
|
2872
|
-
const t = textOf(b);
|
|
2873
|
-
if (!t)
|
|
2874
|
-
continue;
|
|
2875
|
-
out.push({
|
|
2876
|
-
role: looksAssistant(b) ? 'assistant' : 'user',
|
|
2877
|
-
text: t,
|
|
2878
|
-
});
|
|
2879
|
-
}
|
|
2880
|
-
if (out.length)
|
|
2881
|
-
return out;
|
|
2882
|
-
}
|
|
2883
|
-
// Last resort: assistant answers only (no user turns) — better than
|
|
2884
|
-
// nothing when no message container matched.
|
|
2885
|
-
// See RESTORE_ANSWER_SELECTORS in deepseek-ui.ts.
|
|
2886
|
-
for (const s of opts.answerSels) {
|
|
2887
|
-
const list = Array.from(document.querySelectorAll(s)).filter((e) => !inThink(e));
|
|
2888
|
-
if (!list.length)
|
|
2889
|
-
continue;
|
|
2890
|
-
for (const el of list) {
|
|
2891
|
-
const t = textOf(el);
|
|
2892
|
-
if (t)
|
|
2893
|
-
out.push({ role: 'assistant', text: t });
|
|
2894
|
-
}
|
|
2895
|
-
break;
|
|
2896
|
-
}
|
|
2897
|
-
return out;
|
|
2898
|
-
}, {
|
|
2553
|
+
.evaluate(scrapeChatMessages, {
|
|
2899
2554
|
containerSels: MESSAGE_CONTAINER_SELECTORS,
|
|
2900
2555
|
answerSels: RESTORE_ANSWER_SELECTORS,
|
|
2901
2556
|
thinkRe: THINK_CLASS_RE.source,
|
package/dist/changelog.js
CHANGED
|
@@ -78,3 +78,32 @@ export function renderChangelogDraft(groups) {
|
|
|
78
78
|
}
|
|
79
79
|
return out.join('\n').trimEnd();
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Replace the body between `## [Unreleased]` and the next `## [` heading with
|
|
83
|
+
* a freshly drafted body (D2). PURE so a test can pin the exact behavior.
|
|
84
|
+
*
|
|
85
|
+
* When `body` is empty the existing Unreleased section is CLEARED to just the
|
|
86
|
+
* heading (no stray blank content). A changelog without an Unreleased heading
|
|
87
|
+
* is returned UNCHANGED (we never guess where to insert one).
|
|
88
|
+
*/
|
|
89
|
+
export function syncUnreleased(changelog, body) {
|
|
90
|
+
const nl = '\n';
|
|
91
|
+
const anchor = '## [Unreleased]';
|
|
92
|
+
const idx = changelog.indexOf(anchor);
|
|
93
|
+
if (idx < 0)
|
|
94
|
+
return changelog;
|
|
95
|
+
const afterAnchor = idx + anchor.length;
|
|
96
|
+
// The section ends at the next top-level `## [` (the next version).
|
|
97
|
+
const rest = changelog.slice(afterAnchor);
|
|
98
|
+
const next = rest.search(/\n## \[/);
|
|
99
|
+
const tail = next < 0 ? '' : rest.slice(next);
|
|
100
|
+
const cleanBody = String(body || '')
|
|
101
|
+
.replace(/^## \[Unreleased\]\s*/m, '')
|
|
102
|
+
.trim()
|
|
103
|
+
// The draft renders groups as `## Added` (top-level), but INSIDE the file
|
|
104
|
+
// a version's groups are `### Added` (a subsection of the version heading).
|
|
105
|
+
// `## [` (the next version) is left alone.
|
|
106
|
+
.replace(/^## (?!\[)/gm, '### ');
|
|
107
|
+
const section = cleanBody ? nl + nl + cleanBody + nl : nl;
|
|
108
|
+
return changelog.slice(0, afterAnchor) + section + tail;
|
|
109
|
+
}
|
package/dist/cli-args.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// PURE CLI argument parsing (BACKLOG C3). index.ts used to hold `args` as a
|
|
2
|
+
// module-level constant plus three helpers closing over it, which made the
|
|
3
|
+
// parsing impossible to unit-test in isolation (index.ts is the entry point and
|
|
4
|
+
// is not covered). `createCliArgs(argv)` binds the argv ONCE and returns the
|
|
5
|
+
// same three helpers, so every call site in index.ts keeps its shape
|
|
6
|
+
// (`getArg('--x')` / `hasFlag('--x')`) while the LOGIC is now pure and tested.
|
|
7
|
+
//
|
|
8
|
+
// Behavior is intentionally identical to the old inline helpers, including the
|
|
9
|
+
// quirk that a value-flag appearing WITHOUT a value returns the fallback (never
|
|
10
|
+
// the flag itself or the next flag).
|
|
11
|
+
// Flags that consume the NEXT argv entry as their value, so the positional
|
|
12
|
+
// parser must skip it. Kept in sync with the getArg call sites by
|
|
13
|
+
// test/cli-flags.test.ts.
|
|
14
|
+
export const SKIP_VALUE_FLAGS = [
|
|
15
|
+
'--dir',
|
|
16
|
+
'--task',
|
|
17
|
+
'--max-iter',
|
|
18
|
+
'--chat',
|
|
19
|
+
'--output-format',
|
|
20
|
+
];
|
|
21
|
+
export function createCliArgs(argv) {
|
|
22
|
+
const args = Array.isArray(argv) ? argv : [];
|
|
23
|
+
const getArg = (flag, fallback = null) => {
|
|
24
|
+
const i = args.indexOf(flag);
|
|
25
|
+
// `args[i + 1]` truthiness means an empty string or a missing value falls
|
|
26
|
+
// back — the historical behavior (an empty `--task ''` is not a task).
|
|
27
|
+
// NOTE: this does NOT special-case a following FLAG, so `--task --debug`
|
|
28
|
+
// returns `--debug` (the original inline helper did the same); we preserve
|
|
29
|
+
// it rather than "fix" a long-standing quirk in a refactor.
|
|
30
|
+
return i !== -1 && args[i + 1] ? args[i + 1] : fallback;
|
|
31
|
+
};
|
|
32
|
+
const hasFlag = (flag) => args.includes(flag);
|
|
33
|
+
const getPositional = () => {
|
|
34
|
+
const positional = [];
|
|
35
|
+
for (let i = 0; i < args.length; i++) {
|
|
36
|
+
const a = args[i];
|
|
37
|
+
if (SKIP_VALUE_FLAGS.includes(a)) {
|
|
38
|
+
i++;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (a.startsWith('--'))
|
|
42
|
+
continue;
|
|
43
|
+
positional.push(a);
|
|
44
|
+
}
|
|
45
|
+
return positional;
|
|
46
|
+
};
|
|
47
|
+
return { getArg, hasFlag, getPositional };
|
|
48
|
+
}
|
package/dist/compact.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { translate } from './i18n.js';
|
|
2
2
|
import { buildCompactPrompt, buildCompactCarryover, isUsableCompactSummary, trimRestoredMessages, formatRestoredHistory, } from './commands.js';
|
|
3
|
+
import { loadHooks, runLifecycleHooks } from './hooks.js';
|
|
3
4
|
const NL = String.fromCharCode(10);
|
|
4
5
|
/** How many times the summary call is retried before the local fallback. */
|
|
5
6
|
export const COMPACT_SUMMARY_ATTEMPTS = 3;
|
|
@@ -42,6 +43,14 @@ export async function performCompact(opts) {
|
|
|
42
43
|
try {
|
|
43
44
|
if (!quiet)
|
|
44
45
|
log(t('compact.start'));
|
|
46
|
+
// PreCompact hook (N32): runs BEFORE the chat is summarized/rewritten, so
|
|
47
|
+
// a hook can persist state or notify. Best-effort.
|
|
48
|
+
try {
|
|
49
|
+
const out = await runLifecycleHooks(loadHooks(workdir), 'PreCompact', workdir, { chatId: currentChatId });
|
|
50
|
+
if (out)
|
|
51
|
+
transcript?.log('hook_pre_compact', { output: out });
|
|
52
|
+
}
|
|
53
|
+
catch { }
|
|
45
54
|
// 1) Ask the OLD chat to summarize itself.
|
|
46
55
|
let summary = '';
|
|
47
56
|
let summaryErr = '';
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
+
import { DEFAULT_DIAGNOSTICS } from './diagnostics.js';
|
|
4
5
|
import { DEFAULT_LOCALE } from './i18n.js';
|
|
5
6
|
import { writeJsonAtomic } from './fsutil.js';
|
|
6
7
|
const ZAMES_HOME = path.join(os.homedir(), '.zames');
|
|
@@ -67,7 +68,12 @@ export const DEFAULTS = {
|
|
|
67
68
|
contextLimit: 1_000_000,
|
|
68
69
|
// 0 = auto: terminal width, capped at 100 columns (the historical width).
|
|
69
70
|
answerWidth: 0,
|
|
71
|
+
// N37 statusLine hook. Off by default; edit config.json to set a command.
|
|
72
|
+
statusLineCommand: '',
|
|
73
|
+
statusLineIntervalMs: 10_000,
|
|
70
74
|
},
|
|
75
|
+
// Off by default: a full tsc/eslint run after every edit is slow; opt in.
|
|
76
|
+
diagnostics: { ...DEFAULT_DIAGNOSTICS },
|
|
71
77
|
};
|
|
72
78
|
export function loadConfig() {
|
|
73
79
|
const merged = JSON.parse(JSON.stringify(DEFAULTS));
|