zames_pro 2.96.3 → 2.96.5

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 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.5] - 2026-11-06
11
+
12
+ ### Fixed
13
+
14
+ - **web:** tolerate whitespace around = in a DuckDuckGo result attribute
15
+ - **web:** recognise x-zip and x-msdownload as binary content
16
+ - **web:** drop the page head and keep <pre> code intact when converting HTML
17
+ - **web:** share one in-flight headless context between concurrent fetches
18
+ - **scheduler:** never scan cron forever on a non-finite fromMs
19
+
20
+ ## [2.96.4] - 2026-11-06
21
+
22
+ ### Fixed
23
+
24
+ - **tui:** keep a tall suggestion popup from overflowing the layout
25
+ - **scheduler:** scan cron in UTC so a DST fall-back hour is not skipped
26
+ - **context:** parse a YAML block list of command arguments
27
+ - **provider-fallback:** probe a guest-capable provider without the guest flag
28
+ - **cli:** accept the --flag=value spelling of a value flag
29
+
10
30
  ## [2.96.3] - 2026-11-06
11
31
 
12
32
  ### Fixed
package/dist/cli-args.js CHANGED
@@ -22,6 +22,14 @@ export const SKIP_VALUE_FLAGS = [
22
22
  export function createCliArgs(argv) {
23
23
  const args = Array.isArray(argv) ? argv : [];
24
24
  const getArg = (flag, fallback = null) => {
25
+ // `--task=fix it` is the common shell spelling: the SEPARATE form below
26
+ // never matched it, so the value was dropped from the task text and the
27
+ // operator's prompt vanished. Check the `=` form first.
28
+ const eq = flag + '=';
29
+ const joined = args.find((a) => a.startsWith(eq));
30
+ if (joined !== undefined && joined.length > eq.length) {
31
+ return joined.slice(eq.length);
32
+ }
25
33
  const i = args.indexOf(flag);
26
34
  // Truthiness, not `!== undefined`: an EMPTY value (`--task ''`) must
27
35
  // fall back too - an empty task is not a task. A following FLAG is not
package/dist/context.js CHANGED
@@ -332,11 +332,15 @@ function parseFrontmatter(raw) {
332
332
  const userInvokable = typeof out['user-invokable'] === 'string'
333
333
  ? out['user-invokable'].toLowerCase() !== 'false'
334
334
  : undefined;
335
+ // A YAML BLOCK LIST (`arguments:` + ` - env`) folds into one string with
336
+ // the list markers, so splitting on whitespace produced `['-', 'env']`: every
337
+ // declared name was 'missing' and the command was refused. Strip the markers
338
+ // first; the inline form (`arguments: env mode`) still works.
335
339
  const argumentsList = typeof out['arguments'] === 'string'
336
340
  ? out['arguments']
337
341
  .split(/[,\s]+/)
338
342
  .map((s) => s.trim())
339
- .filter(Boolean)
343
+ .filter((s) => s && s !== '-')
340
344
  : undefined;
341
345
  return {
342
346
  name: out.name,
@@ -26,7 +26,10 @@ export async function pickFallbackProvider(active, candidates, probeLogin, hasMa
26
26
  continue;
27
27
  // A live GUEST session is not a fallback (it dies in a few turns), but the
28
28
  // provider is still authorizable — signing in makes it a candidate.
29
- if (c.guestAllowed && c.guestSession !== false) {
29
+ // `guestSession` is OPTIONAL: an older caller that predates the field
30
+ // leaves it undefined, and reading that as "a guest" silently refused to
31
+ // fall back to a signed-in provider with a live session.
32
+ if (c.guestAllowed && c.guestSession === true) {
30
33
  authorizable.push(c.id);
31
34
  continue;
32
35
  }
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
@@ -157,18 +160,30 @@ export function nextCronTime(expr, fromMs) {
157
160
  const c = parseCron(expr);
158
161
  if (!c)
159
162
  return null;
160
- const d = new Date(fromMs);
161
- d.setSeconds(0, 0);
162
- d.setMinutes(d.getMinutes() + 1);
163
+ // The scan steps in UTC MILLISECONDS, not local minutes: on the autumn DST
164
+ // FALL-BACK an hour repeats, and `setMinutes(getMinutes()+1)` visits every
165
+ // wall-clock minute TWICE (then jumps over the second occurrence), so a
166
+ // 02:45 job created after the first 02:45 fired the NEXT DAY. A fixed
167
+ // 60-second step visits every real minute exactly once; the field checks
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;
173
+ const step = 60_000;
174
+ let t = Math.floor(fromMs / step) * step + step;
163
175
  const limit = fromMs + 4 * 366 * 24 * 60 * 60 * 1000;
164
- while (d.getTime() <= limit) {
176
+ if (!Number.isFinite(limit))
177
+ return null;
178
+ while (t <= limit) {
179
+ const d = new Date(t);
165
180
  if (c.minute.has(d.getMinutes()) &&
166
181
  c.hour.has(d.getHours()) &&
167
182
  c.month.has(d.getMonth() + 1) &&
168
183
  dayMatches(c, d)) {
169
- return d.getTime();
184
+ return t;
170
185
  }
171
- d.setMinutes(d.getMinutes() + 1);
186
+ t += step;
172
187
  }
173
188
  return null;
174
189
  }
@@ -20,8 +20,15 @@ export function fullScreenLayout(input) {
20
20
  // width — and the pure helper stays consistent with what it renders.
21
21
  const cols = Math.max(1, Math.floor(input.cols) || 80);
22
22
  const box = inputBoxRows(input.prompt, input.buf, input.cursor, cols, input.borderCols ?? 2);
23
- const suggestRows = Math.max(0, Math.floor(input.suggestRows) || 0);
24
- const panelRows = HEADER_ROWS + STATUS_ROWS + box + suggestRows + HINT_ROWS;
23
+ const wanted = Math.max(0, Math.floor(input.suggestRows) || 0);
24
+ // The layout must FIT the terminal: with a tall popup (a project with many
25
+ // custom commands) the reserved rows exceeded `termRows`, `logHeight` was
26
+ // clamped to 1 and the panel still overflowed — the input rows were pushed
27
+ // below the screen. The popup gives way first (it is a hint list), the log
28
+ // keeps at least one row.
29
+ const fixed = HEADER_ROWS + STATUS_ROWS + box + HINT_ROWS;
30
+ const suggestRows = Math.max(0, Math.min(wanted, termRows - fixed - 1));
31
+ const panelRows = fixed + suggestRows;
25
32
  const logHeight = Math.max(1, termRows - panelRows);
26
33
  // The caret counts the SAME viewport the reservation uses.
27
34
  // The suggestion popup is drawn ABOVE the box and the box does NOT move, so
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,11 +65,18 @@ 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, '');
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, ''));
68
80
  // HTML entities — the basic ones
69
81
  s = s
70
82
  .replace(/&nbsp;/g, ' ')
@@ -406,7 +418,7 @@ export function isBinaryContentType(contentType) {
406
418
  if (/\+(json|xml)$/.test(ct))
407
419
  return false;
408
420
  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)) {
421
+ /^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
422
  return true;
411
423
  }
412
424
  return false;
@@ -503,13 +515,32 @@ async function getHeadless() {
503
515
  // tested without launching a real browser.
504
516
  export function createReusable(factory, dispose) {
505
517
  let cached = null;
518
+ // The FACTORY is async: checking `cached === null` and then awaiting it let
519
+ // two concurrent get() calls BOTH run the factory (two Chromium contexts,
520
+ // the first leaked). The in-flight promise is cached, so every caller waits
521
+ // for the SAME value.
522
+ let inflight = null;
506
523
  return {
507
524
  async get() {
508
- if (cached === null)
509
- cached = await factory();
510
- return cached;
525
+ if (cached !== null)
526
+ return cached;
527
+ if (!inflight) {
528
+ inflight = factory().then((v) => {
529
+ cached = v;
530
+ inflight = null;
531
+ return v;
532
+ }, (e) => {
533
+ inflight = null;
534
+ throw e;
535
+ });
536
+ }
537
+ return inflight;
511
538
  },
512
539
  async reset() {
540
+ if (inflight) {
541
+ // A failed factory must not leave a poisoned promise behind.
542
+ await inflight.catch(() => { });
543
+ }
513
544
  if (cached === null)
514
545
  return;
515
546
  const v = cached;
@@ -673,9 +704,11 @@ export function parseDuckDuckGoResults(body, limit) {
673
704
  // through a proxy does), so each quote style is accepted rather than only
674
705
  // `class="…"`. Rejecting them made a valid page parse to zero results and
675
706
  // WebSearch answer "the output format may have changed".
676
- const anchorRe = /<a[^>]*class=(?:"([^"]*)"|'([^']*)')[^>]*>([\s\S]*?)<\/a>/gi;
707
+ // HTML allows whitespace around the `=` of an attribute (`class = "x"`);
708
+ // requiring it to touch both sides made such a page parse to ZERO results.
709
+ const anchorRe = /<a[^>]*class\s*=\s*(?:"([^"]*)"|'([^']*)')[^>]*>([\s\S]*?)<\/a>/gi;
677
710
  const hrefOf = (tag) => {
678
- const m = tag.match(/href=(?:"([^"]*)"|'([^']*)')/i);
711
+ const m = tag.match(/href\s*=\s*(?:"([^"]*)"|'([^']*)')/i);
679
712
  return m ? (m[1] ?? m[2] ?? '') : '';
680
713
  };
681
714
  let cur = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.96.3",
3
+ "version": "2.96.5",
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": {