zames_pro 2.96.4 → 2.96.6
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 +20 -0
- package/dist/input/layout.js +14 -0
- package/dist/markdown.js +25 -5
- package/dist/scheduler.js +9 -0
- package/dist/tui/background.js +7 -1
- package/dist/web.js +47 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.96.6] - 2026-11-06
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **input:** drop a dangling ZWJ when a truncation cuts an emoji sequence
|
|
15
|
+
- **input:** count a ZWJ emoji sequence as one glyph in the layout
|
|
16
|
+
- **tui:** ZAMES_NO_BG=0 no longer disables the themed background
|
|
17
|
+
- **markdown:** an unknown code-fence language falls back to plain text
|
|
18
|
+
- **web:** decode an escaped HTML entity only once
|
|
19
|
+
|
|
20
|
+
## [2.96.5] - 2026-11-06
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **web:** tolerate whitespace around = in a DuckDuckGo result attribute
|
|
25
|
+
- **web:** recognise x-zip and x-msdownload as binary content
|
|
26
|
+
- **web:** drop the page head and keep <pre> code intact when converting HTML
|
|
27
|
+
- **web:** share one in-flight headless context between concurrent fetches
|
|
28
|
+
- **scheduler:** never scan cron forever on a non-finite fromMs
|
|
29
|
+
|
|
10
30
|
## [2.96.4] - 2026-11-06
|
|
11
31
|
|
|
12
32
|
### Fixed
|
package/dist/input/layout.js
CHANGED
|
@@ -167,6 +167,15 @@ export function visRows(s, cols) {
|
|
|
167
167
|
// zero-width joiner/space/variation selectors count as 0.
|
|
168
168
|
export function visLen(s) {
|
|
169
169
|
s = String(s);
|
|
170
|
+
// An emoji SEQUENCE renders as ONE glyph but occupies several code
|
|
171
|
+
// points: ZWJ-joined, a skin-tone modifier, a keycap. Counting each
|
|
172
|
+
// point separately measured a family as 6 columns instead of 2, so the
|
|
173
|
+
// layout reserved extra rows and the cursor column drifted. The sequence
|
|
174
|
+
// is replaced by ONE placeholder cell before measuring.
|
|
175
|
+
s = s.replace(/(?:\p{Extended_Pictographic}|\p{Emoji_Modifier}|\u200d|\ufe0f)+/gu,
|
|
176
|
+
// The placeholder is a WIDE glyph (2 columns), the width an emoji renders
|
|
177
|
+
// at: a single `X` would under-count it.
|
|
178
|
+
'😀');
|
|
170
179
|
let n = 0;
|
|
171
180
|
let i = 0;
|
|
172
181
|
while (i < s.length) {
|
|
@@ -399,6 +408,11 @@ export function truncateToWidth(s, maxCols) {
|
|
|
399
408
|
used += w;
|
|
400
409
|
i += ch.length;
|
|
401
410
|
}
|
|
411
|
+
// A cut inside a ZWJ emoji sequence leaves a DANGLING joiner: the terminal
|
|
412
|
+
// glues the following `…` to the emoji, so the marker becomes invisible and
|
|
413
|
+
// the visible line is wider than requested. Drop the dangling tail (the
|
|
414
|
+
// whole incomplete grapheme) before closing the style.
|
|
415
|
+
out = out.replace(/\u200d+$/, '');
|
|
402
416
|
// The visible text was cut BEFORE any trailing RESET, so the ellipsis and
|
|
403
417
|
// everything printed after it on the row would inherit the last colour.
|
|
404
418
|
// Close any style the kept prefix opened (the text may still be plain, in
|
package/dist/markdown.js
CHANGED
|
@@ -98,11 +98,31 @@ export function renderMarkdown(text) {
|
|
|
98
98
|
// opt-out is NO_COLOR, handled above.
|
|
99
99
|
color: true,
|
|
100
100
|
theme: mdTheme(p),
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
// An UNKNOWN language tag throws inside cli-highlight, and the throw
|
|
102
|
+
// escaped the whole render: ONE ```` ```kotlin-ish ```` fence turned the
|
|
103
|
+
// entire answer into raw markdown (no wrap, no theme, no code box). Fall
|
|
104
|
+
// back to plaintext for this block only.
|
|
105
|
+
highlighter: (code, lang) => {
|
|
106
|
+
try {
|
|
107
|
+
return highlight(code, {
|
|
108
|
+
language: lang || 'plaintext',
|
|
109
|
+
ignoreIllegals: true,
|
|
110
|
+
theme: hlTheme(p),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
try {
|
|
115
|
+
return highlight(code, {
|
|
116
|
+
language: 'plaintext',
|
|
117
|
+
ignoreIllegals: true,
|
|
118
|
+
theme: hlTheme(p),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return String(code);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
106
126
|
codeBox: true,
|
|
107
127
|
tableBorder: 'unicode',
|
|
108
128
|
listIndent: 2,
|
package/dist/scheduler.js
CHANGED
|
@@ -10,6 +10,9 @@ export function parseInterval(text) {
|
|
|
10
10
|
const n = Number(m[1]);
|
|
11
11
|
if (!Number.isFinite(n) || n <= 0)
|
|
12
12
|
return null;
|
|
13
|
+
// `n * mult` can overflow to Infinity for a huge digit string; the range
|
|
14
|
+
// check below then compares Infinity > MAX_TIMER_MS and rejects it, but only
|
|
15
|
+
// AFTER the multiplication — keep the guard explicit for both.
|
|
13
16
|
const unit = (m[2] || 's').toLowerCase();
|
|
14
17
|
const mult = unit.startsWith('h')
|
|
15
18
|
? 3_600_000
|
|
@@ -163,9 +166,15 @@ export function nextCronTime(expr, fromMs) {
|
|
|
163
166
|
// 02:45 job created after the first 02:45 fired the NEXT DAY. A fixed
|
|
164
167
|
// 60-second step visits every real minute exactly once; the field checks
|
|
165
168
|
// still use the LOCAL getters, so the wall-clock match is unchanged.
|
|
169
|
+
// A non-finite/huge `fromMs` (a clock past 8.64e15, a hand-edited job) made
|
|
170
|
+
// `limit` Infinity and the scan walked one minute at a time FOREVER.
|
|
171
|
+
if (!Number.isFinite(fromMs) || Math.abs(fromMs) > 8.64e15)
|
|
172
|
+
return null;
|
|
166
173
|
const step = 60_000;
|
|
167
174
|
let t = Math.floor(fromMs / step) * step + step;
|
|
168
175
|
const limit = fromMs + 4 * 366 * 24 * 60 * 60 * 1000;
|
|
176
|
+
if (!Number.isFinite(limit))
|
|
177
|
+
return null;
|
|
169
178
|
while (t <= limit) {
|
|
170
179
|
const d = new Date(t);
|
|
171
180
|
if (c.minute.has(d.getMinutes()) &&
|
package/dist/tui/background.js
CHANGED
|
@@ -23,7 +23,13 @@ function hexToOsc(hex) {
|
|
|
23
23
|
* testable without a terminal.
|
|
24
24
|
*/
|
|
25
25
|
export function backgroundOsc(name) {
|
|
26
|
-
|
|
26
|
+
// NO_COLOR is "present means off" (no-color.org). ZAMES_NO_BG is our OWN
|
|
27
|
+
// switch: a shell profile writing `ZAMES_NO_BG=0` clearly means KEEP it, so
|
|
28
|
+
// only a truthy value opts out.
|
|
29
|
+
const noBg = String(process.env.ZAMES_NO_BG ?? '').toLowerCase();
|
|
30
|
+
if (process.env.NO_COLOR)
|
|
31
|
+
return '';
|
|
32
|
+
if (noBg && noBg !== '0' && noBg !== 'false' && noBg !== 'no')
|
|
27
33
|
return '';
|
|
28
34
|
const id = isThemeName(name) ? name : DEFAULT_THEME;
|
|
29
35
|
return '\x1b]11;rgb:' + hexToOsc(ALL_PALETTES[id].bg) + '\x07';
|
package/dist/web.js
CHANGED
|
@@ -37,6 +37,7 @@ export function htmlToText(html) {
|
|
|
37
37
|
// document to the next.
|
|
38
38
|
pendingHighSurrogate = null;
|
|
39
39
|
// Remove blocks we don't need
|
|
40
|
+
const parkedPre = [];
|
|
40
41
|
let s = html
|
|
41
42
|
// A tag that never CLOSES (a truncated body, or HTML the regex cannot
|
|
42
43
|
// pair) used to leak its raw JS/CSS into the text: also drop everything
|
|
@@ -47,6 +48,10 @@ export function htmlToText(html) {
|
|
|
47
48
|
.replace(/<style\b[\s\S]*$/i, '')
|
|
48
49
|
.replace(/<noscript[\s\S]*?<\/noscript>/gi, '')
|
|
49
50
|
.replace(/<noscript\b[\s\S]*$/i, '')
|
|
51
|
+
// The <head> (title, meta) is page CHROME, not content: left in place the
|
|
52
|
+
// title glued itself to the first paragraph (`My pageBody`).
|
|
53
|
+
.replace(/<head[\s\S]*?<\/head>/gi, '')
|
|
54
|
+
.replace(/<title[\s\S]*?<\/title>/gi, '')
|
|
50
55
|
.replace(/<svg[\s\S]*?<\/svg>/gi, '')
|
|
51
56
|
.replace(/<svg\b[\s\S]*$/i, '')
|
|
52
57
|
.replace(/<!--[\s\S]*?-->/g, '');
|
|
@@ -60,15 +65,25 @@ export function htmlToText(html) {
|
|
|
60
65
|
// `<td>42</td>` became `Name42` — two independent values fused into one, and
|
|
61
66
|
// the model reported a number that never appeared on the page.
|
|
62
67
|
s = s
|
|
68
|
+
// A `<pre>` block is CODE: a bare `<` inside it (`if (a < b)`) is text, not
|
|
69
|
+
// the start of a tag, so the generic strip below would eat the rest of the
|
|
70
|
+
// line. Park the block, then restore it after the tag removal.
|
|
71
|
+
.replace(/<pre\b[^>]*>([\s\S]*?)<\/pre>/gi, (_m, code) => '\u0000PRE' + (parkedPre.push(code) - 1) + 'PRE\u0000')
|
|
63
72
|
.replace(/<\/(h[1-6]|p|div|li|tr|section|article)>/gi, '\n')
|
|
64
73
|
.replace(/<\/(td|th)>/gi, ' ')
|
|
65
74
|
.replace(/<br\s*\/?>/gi, '\n')
|
|
66
75
|
.replace(/<li[^>]*>/gi, '- ')
|
|
67
76
|
.replace(/<[^>]+>/g, '');
|
|
68
|
-
//
|
|
77
|
+
// A parked `<pre>` block comes back AFTER the tag strip, with only its own
|
|
78
|
+
// tags removed and its text kept verbatim.
|
|
79
|
+
s = s.replace(/\u0000PRE(\d+)PRE\u0000/g, (_m, i) => (parkedPre[Number(i)] ?? '').replace(/<[^>]+>/g, ''));
|
|
80
|
+
// HTML entities — the basic ones. `&` is decoded LAST, otherwise an
|
|
81
|
+
// escaped entity (`&#38;` — the literal text `&`) is decoded TWICE
|
|
82
|
+
// and the page's text is changed.
|
|
83
|
+
const AMP = String.fromCharCode(0) + 'amp' + String.fromCharCode(0);
|
|
84
|
+
s = s.replace(/&/g, AMP);
|
|
69
85
|
s = s
|
|
70
86
|
.replace(/ /g, ' ')
|
|
71
|
-
.replace(/&/g, '&')
|
|
72
87
|
.replace(/</g, '<')
|
|
73
88
|
.replace(/>/g, '>')
|
|
74
89
|
.replace(/"/g, '"')
|
|
@@ -77,7 +92,9 @@ export function htmlToText(html) {
|
|
|
77
92
|
.replace(/–/g, '–')
|
|
78
93
|
.replace(/…/g, '…')
|
|
79
94
|
.replace(/&#(\d+);/g, (_, n) => decodeEntityCode(Number(n)))
|
|
80
|
-
.replace(/&#x([0-9a-f]+);/gi, (_, n) => decodeEntityCode(parseInt(n, 16)))
|
|
95
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, n) => decodeEntityCode(parseInt(n, 16)))
|
|
96
|
+
.split(AMP)
|
|
97
|
+
.join('&');
|
|
81
98
|
// Collapse blank lines
|
|
82
99
|
s = s
|
|
83
100
|
.split('\n')
|
|
@@ -406,7 +423,7 @@ export function isBinaryContentType(contentType) {
|
|
|
406
423
|
if (/\+(json|xml)$/.test(ct))
|
|
407
424
|
return false;
|
|
408
425
|
if (/^(image|audio|video|font|model)\//.test(ct) ||
|
|
409
|
-
/^application\/(pdf|zip|gzip|x-gzip|x-tar|octet-stream|msword|vnd\.|x-7z|x-rar|x-bzip|x-protobuf|wasm)/.test(ct)) {
|
|
426
|
+
/^application\/(pdf|zip|gzip|x-gzip|x-tar|octet-stream|msword|vnd\.|x-7z|x-rar|x-bzip|x-zip|x-msdownload|x-protobuf|wasm|epub\+zip)/.test(ct)) {
|
|
410
427
|
return true;
|
|
411
428
|
}
|
|
412
429
|
return false;
|
|
@@ -503,13 +520,32 @@ async function getHeadless() {
|
|
|
503
520
|
// tested without launching a real browser.
|
|
504
521
|
export function createReusable(factory, dispose) {
|
|
505
522
|
let cached = null;
|
|
523
|
+
// The FACTORY is async: checking `cached === null` and then awaiting it let
|
|
524
|
+
// two concurrent get() calls BOTH run the factory (two Chromium contexts,
|
|
525
|
+
// the first leaked). The in-flight promise is cached, so every caller waits
|
|
526
|
+
// for the SAME value.
|
|
527
|
+
let inflight = null;
|
|
506
528
|
return {
|
|
507
529
|
async get() {
|
|
508
|
-
if (cached
|
|
509
|
-
cached
|
|
510
|
-
|
|
530
|
+
if (cached !== null)
|
|
531
|
+
return cached;
|
|
532
|
+
if (!inflight) {
|
|
533
|
+
inflight = factory().then((v) => {
|
|
534
|
+
cached = v;
|
|
535
|
+
inflight = null;
|
|
536
|
+
return v;
|
|
537
|
+
}, (e) => {
|
|
538
|
+
inflight = null;
|
|
539
|
+
throw e;
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
return inflight;
|
|
511
543
|
},
|
|
512
544
|
async reset() {
|
|
545
|
+
if (inflight) {
|
|
546
|
+
// A failed factory must not leave a poisoned promise behind.
|
|
547
|
+
await inflight.catch(() => { });
|
|
548
|
+
}
|
|
513
549
|
if (cached === null)
|
|
514
550
|
return;
|
|
515
551
|
const v = cached;
|
|
@@ -673,9 +709,11 @@ export function parseDuckDuckGoResults(body, limit) {
|
|
|
673
709
|
// through a proxy does), so each quote style is accepted rather than only
|
|
674
710
|
// `class="…"`. Rejecting them made a valid page parse to zero results and
|
|
675
711
|
// WebSearch answer "the output format may have changed".
|
|
676
|
-
|
|
712
|
+
// HTML allows whitespace around the `=` of an attribute (`class = "x"`);
|
|
713
|
+
// requiring it to touch both sides made such a page parse to ZERO results.
|
|
714
|
+
const anchorRe = /<a[^>]*class\s*=\s*(?:"([^"]*)"|'([^']*)')[^>]*>([\s\S]*?)<\/a>/gi;
|
|
677
715
|
const hrefOf = (tag) => {
|
|
678
|
-
const m = tag.match(/href
|
|
716
|
+
const m = tag.match(/href\s*=\s*(?:"([^"]*)"|'([^']*)')/i);
|
|
679
717
|
return m ? (m[1] ?? m[2] ?? '') : '';
|
|
680
718
|
};
|
|
681
719
|
let cur = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.96.
|
|
3
|
+
"version": "2.96.6",
|
|
4
4
|
"description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen or GLM — 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": {
|