zames_pro 2.75.6 → 2.77.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.
@@ -48,7 +48,16 @@ export function parseWheel(chunk) {
48
48
  let m;
49
49
  while ((m = re.exec(chunk)) !== null) {
50
50
  const cb = Number(m[1]);
51
+ // SGR reports a RELEASE as the same button number with a lowercase `m`.
52
+ // Only press events are wheels: a terminal that sends 1000/1002/1003
53
+ // release reports (xterm does for button events) would otherwise count
54
+ // every notch TWICE — once for the press and once for its release.
55
+ if (m[0].endsWith('m'))
56
+ continue;
51
57
  // Button code 64 = wheel up (scroll back), 65 = wheel down (scroll fwd).
58
+ // Modifier bits (4=shift, 8=alt, 16=ctrl) must be masked OFF first: the
59
+ // mask below used to keep bit 0, so Ctrl/Alt-wheel (Cb 80/72/... — odd or
60
+ // even) fell through and that notch was lost.
52
61
  const button = cb & 0b11000011;
53
62
  if (button === 64)
54
63
  delta += 1;
package/dist/web.js CHANGED
@@ -5,7 +5,37 @@ const DEFAULT_TIMEOUT = 20_000;
5
5
  const MAX_TEXT = 12_000;
6
6
  // Removes scripts, styles, and turns HTML into readable text. Exported for
7
7
  // tests — the entity/whitespace/collapse branches are pure and worth pinning.
8
+ // Decode ONE numeric HTML entity to its character. A hostile/broken page can
9
+ // carry `�` (beyond Unicode), a surrogate (`�`) or a huge
10
+ // hex value; String.fromCodePoint THROWS RangeError on those, and the throw
11
+ // escaped htmlToText into the WebFetch handler — the whole page came back as
12
+ // "Error fetching <url>: Invalid code point 999999999". An undecodable
13
+ // reference is kept VERBATIM instead.
14
+ //
15
+ // A surrogate PAIR written as two references (`&#55357;&#56832;`) is ONE
16
+ // astral character (an emoji): decoding each half alone left two lone
17
+ // surrogates in the text. A lone high surrogate is remembered and JOINED with
18
+ // the low surrogate that immediately follows it (the replacer runs in order).
19
+ let pendingHighSurrogate = null;
20
+ function decodeEntityCode(cp) {
21
+ if (!Number.isInteger(cp) || cp < 0 || cp > 0x10ffff)
22
+ return '';
23
+ if (cp >= 0xd800 && cp <= 0xdbff) {
24
+ pendingHighSurrogate = String.fromCharCode(cp);
25
+ return '';
26
+ }
27
+ if (cp >= 0xdc00 && cp <= 0xdfff) {
28
+ const high = pendingHighSurrogate;
29
+ pendingHighSurrogate = null;
30
+ return high ? high + String.fromCharCode(cp) : String.fromCharCode(cp);
31
+ }
32
+ pendingHighSurrogate = null;
33
+ return String.fromCodePoint(cp);
34
+ }
8
35
  export function htmlToText(html) {
36
+ // Reset the cross-reference surrogate state: it must never leak from one
37
+ // document to the next.
38
+ pendingHighSurrogate = null;
9
39
  // Remove blocks we don't need
10
40
  let s = html
11
41
  .replace(/<script[\s\S]*?<\/script>/gi, '')
@@ -35,8 +65,8 @@ export function htmlToText(html) {
35
65
  .replace(/&mdash;/g, '—')
36
66
  .replace(/&ndash;/g, '–')
37
67
  .replace(/&hellip;/g, '…')
38
- .replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n)))
39
- .replace(/&#x([0-9a-f]+);/gi, (_, n) => String.fromCodePoint(parseInt(n, 16)));
68
+ .replace(/&#(\d+);/g, (_, n) => decodeEntityCode(Number(n)))
69
+ .replace(/&#x([0-9a-f]+);/gi, (_, n) => decodeEntityCode(parseInt(n, 16)));
40
70
  // Collapse blank lines
41
71
  s = s
42
72
  .split('\n')
@@ -52,6 +82,49 @@ export function htmlToText(html) {
52
82
  // or localhost services. We resolve the hostname and refuse private / loopback
53
83
  // / link-local addresses. Best-effort: a DNS rebind between the check and the
54
84
  // fetch is out of scope for a CLI tool, but the common cases are covered.
85
+ // Parse an inet_aton-style IPv4 literal (the form WHATWG URL and the OS also
86
+ // accept) into a uint32, or null when it is not one. Rules: 1-4 parts; each
87
+ // part may be hex (0x…), octal (leading 0) or decimal; the LAST part fills all
88
+ // remaining bytes, so `127.1` == 127.0.0.1 and `2130706433` == 127.0.0.1.
89
+ export function parseLegacyIpv4(host) {
90
+ const h = String(host || '').trim();
91
+ if (!h)
92
+ return null;
93
+ const parts = h.split('.');
94
+ if (parts.length < 1 || parts.length > 4)
95
+ return null;
96
+ const nums = [];
97
+ for (const p of parts) {
98
+ if (p === '')
99
+ return null;
100
+ let v;
101
+ if (/^0[xX][0-9a-fA-F]+$/.test(p))
102
+ v = parseInt(p.slice(2), 16);
103
+ else if (/^0[0-7]+$/.test(p))
104
+ v = parseInt(p.slice(1), 8);
105
+ else if (/^\d+$/.test(p))
106
+ v = parseInt(p, 10);
107
+ else
108
+ return null;
109
+ if (!Number.isFinite(v) || v < 0)
110
+ return null;
111
+ nums.push(v);
112
+ }
113
+ // Every part except the last must fit one byte; the last fills the rest.
114
+ for (let i = 0; i < nums.length - 1; i++) {
115
+ if (nums[i] > 0xff)
116
+ return null;
117
+ }
118
+ const lastMax = Math.pow(256, 5 - nums.length) - 1;
119
+ if (nums[nums.length - 1] > lastMax)
120
+ return null;
121
+ let n = 0;
122
+ for (let i = 0; i < nums.length - 1; i++) {
123
+ n += nums[i] * Math.pow(256, 3 - i);
124
+ }
125
+ n += nums[nums.length - 1];
126
+ return n >>> 0;
127
+ }
55
128
  export function isPrivateIp(ip) {
56
129
  const v4 = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
57
130
  if (v4) {
@@ -68,6 +141,22 @@ export function isPrivateIp(ip) {
68
141
  return true; // CGNAT
69
142
  return false;
70
143
  }
144
+ // A NON-canonical IPv4 literal is still a number: WHATWG URL and inet_aton
145
+ // accept 1-4 parts with decimal/octal/hex digits (`2130706433`,
146
+ // `0x7f.1`, `0177.0.0.1`, `127.1` all mean 127.0.0.1). Recognising only the
147
+ // dotted-quad form let those through the SSRF guard (a classic bypass).
148
+ if (/^[0-9a-fx.]+$/i.test(ip) && !ip.includes(':')) {
149
+ const n = parseLegacyIpv4(ip);
150
+ if (n !== null) {
151
+ return isPrivateIp(((n >>> 24) & 0xff) +
152
+ '.' +
153
+ ((n >>> 16) & 0xff) +
154
+ '.' +
155
+ ((n >>> 8) & 0xff) +
156
+ '.' +
157
+ (n & 0xff));
158
+ }
159
+ }
71
160
  const low = ip.toLowerCase();
72
161
  if (low === '::1' || low === '::')
73
162
  return true;
@@ -123,7 +212,10 @@ export async function assertPublicUrl(rawUrl) {
123
212
  if (u.protocol !== 'http:' && u.protocol !== 'https:') {
124
213
  throw new Error(`unsupported protocol: ${u.protocol}`);
125
214
  }
126
- const host = u.hostname;
215
+ // The URL API keeps the BRACKETS of an IPv6 literal (`[::ffff:7f00:1]`),
216
+ // and net.isIP() rejects a bracketed host — so the address fell through to
217
+ // the DNS lookup branch and the literal was never checked as an address.
218
+ const host = u.hostname.replace(/^\[(.*)\]$/, '$1');
127
219
  if (isPrivateHostname(host)) {
128
220
  throw new Error(`blocked private host: ${host}`);
129
221
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.75.6",
3
+ "version": "2.77.0",
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": {