zames_pro 2.81.13 → 2.81.15

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,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.81.15] - 2026-10-21
11
+
12
+ ### Fixed
13
+
14
+ - **web:** a search result without a snippet no longer borrows the next one
15
+
16
+ ## [2.81.14] - 2026-10-21
17
+
18
+ ### Fixed
19
+
20
+ - **glm:** an answer that mentions the captcha is not the captcha
21
+
10
22
  ## [2.81.13] - 2026-10-21
11
23
 
12
24
  ### Fixed
@@ -320,7 +320,16 @@ export function isCaptchaText(body) {
320
320
  const b = String(body || '');
321
321
  if (!b)
322
322
  return false;
323
- return /security verification|drag the slider|CertifyId/i.test(b);
323
+ const hit = /security verification|drag the slider|CertifyId/i.test(b);
324
+ if (!hit)
325
+ return false;
326
+ // The SAME body may be the answer SSE stream (this classifier runs on every
327
+ // response). An answer that merely MENTIONS the captcha must not be taken
328
+ // for the challenge page: an error payload or a body WITHOUT answer text
329
+ // only (same rule as the rate-limit / guest classifiers).
330
+ if (!ERROR_PAYLOAD_RE.test(b) && hasAnswerContent(b))
331
+ return false;
332
+ return true;
324
333
  }
325
334
  /**
326
335
  * Read the chat id off the RAW body (the chat list / history JSON carries it as
package/dist/web.js CHANGED
@@ -645,33 +645,47 @@ export function createWebTools() {
645
645
  // wraps links in a /l/?uddg=... redirect which must be unwrapped.
646
646
  export function parseDuckDuckGoResults(body, limit) {
647
647
  const results = [];
648
- const re = /<a[^>]*class="[^"]*result__a[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?(?:<a[^>]*class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>)?/gi;
648
+ // Parse the page ROW BY ROW: each `result__a` title opens a result, and a
649
+ // `result__snippet` belongs to the result opened most recently. The old
650
+ // single regex made the snippet group OPTIONAL with an unbounded gap, so a
651
+ // result without a snippet picked up the FOLLOWING result's snippet (wrong
652
+ // text under a URL). A row scan ties every snippet to its own result.
653
+ const anchorRe = /<a[^>]*class="[^"]*result__(a|snippet)[^"]*"[^>]*>([\s\S]*?)<\/a>/gi;
654
+ const hrefOf = (tag) => {
655
+ const m = tag.match(/href="([^"]*)"/i);
656
+ return m ? m[1] : '';
657
+ };
658
+ let cur = null;
649
659
  let m;
650
- while ((m = re.exec(body)) && results.length < limit) {
651
- let href = m[1];
652
- const uddgMatch = href.match(/[?&]uddg=([^&]+)/);
653
- if (uddgMatch) {
654
- // decodeURIComponent THROWS on a malformed percent escape (`%ZZ`, a lone
655
- // `%`). One broken href in the page must not abort the whole search: keep
656
- // the raw href, which the http(s) check below then rejects.
657
- try {
658
- href = decodeURIComponent(uddgMatch[1]);
660
+ while ((m = anchorRe.exec(body))) {
661
+ const kind = m[1].toLowerCase();
662
+ if (kind === 'a') {
663
+ if (cur && cur.title && /^https?:\/\//i.test(cur.url)) {
664
+ results.push(cur);
665
+ if (results.length >= limit)
666
+ return results;
659
667
  }
660
- catch {
661
- href = uddgMatch[1];
668
+ cur = { title: htmlToText(m[2] || '').trim(), url: '', snippet: '' };
669
+ // The href lives in the OPENING tag, which m[0] starts with.
670
+ let href = hrefOf(m[0].slice(0, m[0].indexOf('>') + 1));
671
+ const uddg = href.match(/[?&]uddg=([^&]+)/);
672
+ if (uddg) {
673
+ try {
674
+ href = decodeURIComponent(uddg[1]);
675
+ }
676
+ catch {
677
+ href = uddg[1];
678
+ }
662
679
  }
663
- }
664
- const title = htmlToText(m[2] || '').trim();
665
- const snippet = htmlToText(m[3] || '').trim();
666
- // Only an absolute http(s) link is a real result. The page can carry a
667
- // `javascript:`/`data:` href (malicious or broken markup) or a
668
- // site-relative one; handing either to the model as a result URL is wrong
669
- // (a `javascript:` URL looks fetchable, a relative one is not).
670
- if (!title || !/^https?:\/\//i.test(href))
680
+ cur.url = href;
671
681
  continue;
672
- results.push({ title, url: href, snippet });
682
+ }
683
+ if (cur && !cur.snippet)
684
+ cur.snippet = htmlToText(m[2] || '').trim();
673
685
  }
674
- return results;
686
+ if (cur && cur.title && /^https?:\/\//i.test(cur.url))
687
+ results.push(cur);
688
+ return results.slice(0, limit);
675
689
  }
676
690
  function formatResult(status, url, text, limit) {
677
691
  let out = text;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.81.13",
3
+ "version": "2.81.15",
4
4
  "description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen and other web LLMs — 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": {