@apmantza/greedysearch-pi 1.9.0 → 1.9.1

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
@@ -2,6 +2,35 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.9.1] — 2026-05-23
6
+
7
+ ### Fixed
8
+
9
+ - **Visible Chrome launches minimized** (`bin/launch-visible.mjs`) — After Chrome's CDP endpoint becomes ready, `minimizeViaCDP` sends `Browser.setWindowBounds { windowState: "minimized" }` via the browser-level WebSocket. Chrome lands in the taskbar immediately instead of stealing focus from the user's active window. Closes [#20](https://github.com/apmantza/GreedySearch-pi/issues/20).
10
+
11
+ - **Recovery path always returns to headless** (`bin/search.mjs`) — After a visible-mode retry (triggered by Cloudflare blocking headless), the pipeline now unconditionally kills visible Chrome and relaunches headless before running Gemini synthesis. Previously the switch-back only happened when zero engines were recovered (`recovered === 0`), so a partial recovery left visible Chrome alive and caused synthesis to open the Gemini tab in the visible window.
12
+
13
+ - **ReDoS hotspots fixed** (`bin/launch.mjs`, `extractors/selectors.mjs`, `src/fetcher.mjs`, `src/search/sources.mjs`) — Four SonarCloud `javasecurity:S5852` hotspots resolved: (1) Chrome version directory regex bounded (`\d+` → `\d{1,10}` ×4 groups); (2) Perplexity citation name regex bounded (`\s+` → `\s{1,20}`, `[^.]+` → `[^.]{1,200}`); (3) seven suspicious-content regex patterns in `checkContentQuality` replaced with `String.includes` checks (faster and immune to backtracking on adversarial input); (4) trailing-slash removal regex bounded (`\/+$` → `\/{1,10}$`). Follow-up: string checks lowercased via a single `markdown.toLowerCase()` call to restore the case-insensitive matching the original regexes provided.
14
+
15
+ - **Collapsed tool rendering: consensus label fixed** (`src/tools/greedy-search-handler.ts`) — The collapsed summary was reading `synthesis.consensus` which does not exist in the schema; the field is `synthesis.agreement.level`. Collapsed view now correctly shows e.g. `→ Synthesized · 5 sources · high`.
16
+
17
+ - **`minimizeViaCDP` guard inverted in `launch.mjs`** (`bin/launch.mjs`) — The early-return guard was `if (isVisible()) return` which caused the function to exit immediately in the only case it was ever called (visible Chrome launch via `GREEDY_SEARCH_VISIBLE=1`). Changed to `if (isHeadless()) return`. Also removed the unnecessary 1s sleep (Chrome is already confirmed ready via `writePortFile()` before this is called) and applied the SonarCloud S8480 fix (`wsPath` extracted from `webSocketDebuggerUrl`, WebSocket URL reconstructed as `ws://localhost:${PORT}${wsPath}`).
18
+
19
+ - **Gemini tab no longer steals focus during synthesis** (`bin/search.mjs`) — Removed the `activateTab` call on the pre-navigated Gemini tab. `Target.activateTarget` was restoring the minimized Chrome window mid-search; CDP synthesis operates on the target ID directly and has no need for the tab to be Chrome's active tab.
20
+
21
+ ### Changed
22
+
23
+ - **Result file auto-purge** (`src/search/output.mjs`) — On each search run, files older than 7 days are deleted from the results directory. The 10 most recent files are always kept regardless of age. Runs inside `resultsDir()` so it's transparent and zero-overhead.
24
+
25
+ - **`greedy_search` tool: collapsed rendering** (`src/tools/greedy-search-handler.ts`) — Added `renderCall` and `renderResult` hooks. The call line shows the query (truncated to 60 chars) and engine. The result collapses to a one-line summary: synthesis path shows source count + consensus label; single-engine path shows source count; human-verification path shows a warning. Full output is available via expand (Ctrl+O). Also migrated peer deps from `@mariozechner/pi-coding-agent` to `@earendil-works/pi-coding-agent` and added `@earendil-works/pi-tui` for the `Text` primitive.
26
+
27
+ - **Headless stealth hardening** (`bin/launch.mjs`, `extractors/common.mjs`) — Four fingerprinting gaps closed:
28
+ - **UA version auto-detected** — `getChromeVersion` reads the versioned sub-directory inside the Chrome Application folder (e.g. `148.0.7778.168/`) to extract the real major version, then injects it into the `--user-agent` flag. Eliminates the TLS/UA mismatch that was caused by the hardcoded `Chrome/131` string (actual binary was `Chrome/148`).
29
+ - **`navigator.userAgentData`** — Spoofed to match the detected UA version and remove any `HeadlessChrome` brand entry. `getHighEntropyValues()` returns consistent architecture, platform, and full version list.
30
+ - **`window.outerWidth/Height`** — Patched from `0` (headless default) to mirror `innerWidth/Height`. A zero outer dimension is a well-known one-signal bot detector.
31
+ - **`screen.colorDepth/pixelDepth`** — Ensured to report `24` when unset.
32
+ - **GPU rendering re-enabled in headless** — Removed `--disable-gpu` and `--disable-software-rasterizer`. With `--headless=new`, Chrome uses hardware GPU acceleration (ANGLE/Direct3D on Windows), producing canvas and WebGL output identical to visible mode. Cloudflare Turnstile passes automatically on Perplexity without triggering visible-mode retry.
33
+
5
34
  ## [1.9.0] — 2026-05-22
6
35
 
7
36
  ### Added
@@ -106,6 +106,69 @@ function httpGet(url, timeoutMs = 1000) {
106
106
  });
107
107
  }
108
108
 
109
+ async function minimizeViaCDP(port) {
110
+ try {
111
+ const version = await httpGet(`http://localhost:${port}/json/version`).then(
112
+ (r) => JSON.parse(r.body),
113
+ );
114
+ const targets = await httpGet(`http://localhost:${port}/json/list`).then(
115
+ (r) => JSON.parse(r.body),
116
+ );
117
+ const targetId = targets.find((t) => t.type === "page")?.id;
118
+ if (!targetId) return;
119
+
120
+ // Validate browser WebSocket URL to prevent SSRF (SonarCloud javasecurity:S5335)
121
+ const wsUrlStr = version.webSocketDebuggerUrl;
122
+ if (typeof wsUrlStr !== "string") return;
123
+ const wsUrl = new URL(wsUrlStr);
124
+ if (wsUrl.hostname !== "localhost" && wsUrl.hostname !== "127.0.0.1")
125
+ return;
126
+ if (!/^ws:\/\/localhost:\d+/.test(`ws://${wsUrl.host}`)) return;
127
+ const wsPath = wsUrl.pathname;
128
+ const ws = new WebSocket(`ws://localhost:${port}${wsPath}`);
129
+ await new Promise((resolve) => {
130
+ ws.onopen = () =>
131
+ ws.send(
132
+ JSON.stringify({
133
+ id: 1,
134
+ method: "Browser.getWindowForTarget",
135
+ params: { targetId },
136
+ }),
137
+ );
138
+ ws.onmessage = (ev) => {
139
+ const msg = JSON.parse(ev.data);
140
+ if (msg.id === 1 && msg.result?.windowId) {
141
+ ws.send(
142
+ JSON.stringify({
143
+ id: 2,
144
+ method: "Browser.setWindowBounds",
145
+ params: {
146
+ windowId: msg.result.windowId,
147
+ bounds: { windowState: "minimized" },
148
+ },
149
+ }),
150
+ );
151
+ } else if (msg.id === 2) {
152
+ ws.close();
153
+ resolve();
154
+ }
155
+ };
156
+ ws.onerror = () => {
157
+ ws.close();
158
+ resolve();
159
+ };
160
+ setTimeout(() => {
161
+ try {
162
+ ws.close();
163
+ } catch {}
164
+ resolve();
165
+ }, 5000);
166
+ });
167
+ } catch {
168
+ // best-effort — Chrome is still usable if minimize fails
169
+ }
170
+ }
171
+
109
172
  async function waitForPort(timeoutMs = 15000) {
110
173
  const deadline = Date.now() + timeoutMs;
111
174
  while (Date.now() < deadline) {
@@ -226,6 +289,8 @@ async function main() {
226
289
  process.exit(1);
227
290
  }
228
291
 
292
+ await minimizeViaCDP(PORT);
293
+
229
294
  console.log("Visible Chrome ready on port 9222.");
230
295
  console.log("Keep this terminal open to keep Chrome alive.");
231
296
  }