@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/bin/search.mjs CHANGED
@@ -25,7 +25,6 @@ import { existsSync, readFileSync } from "node:fs";
25
25
  import { homedir } from "node:os";
26
26
  import { join } from "node:path";
27
27
  import {
28
- activateTab,
29
28
  cdp,
30
29
  closeTab,
31
30
  closeTabs,
@@ -388,11 +387,13 @@ async function main() {
388
387
  // in the shared profile and future headless runs will reuse them.
389
388
  }
390
389
  } finally {
391
- // Keep visible Chrome alive if engines were recovered (cookies now cached)
392
- // or if the user needs to solve verification manually.
393
- // Killing Chrome with taskkill /F would lose the cookie database writes.
394
- if (!keepVisibleForHuman && recovered === 0) {
395
- // Kill visible Chrome, relaunch headless for remaining pipeline
390
+ if (keepVisibleForHuman) {
391
+ // User must interact keep visible Chrome open but out of the way
392
+ minimizeChrome().catch(() => {});
393
+ } else {
394
+ // Switch back to headless for synthesis + source fetch.
395
+ // killHeadlessChrome() sends Browser.close first so Chrome flushes
396
+ // its cookie database before the force-kill — cookies are preserved.
396
397
  await closeTabs(retryTabs);
397
398
  process.stderr.write(
398
399
  "[greedysearch] Switching back to headless Chrome...\n",
@@ -405,11 +406,6 @@ async function main() {
405
406
  }
406
407
  }
407
408
 
408
- // Minimize visible Chrome if it was kept alive (recovery succeeded or needs-human)
409
- if (keepVisibleForHuman || recovered > 0) {
410
- minimizeChrome().catch(() => {});
411
- }
412
-
413
409
  // Clear engineTabs — finally{} closeTabs handles empty arrays gracefully
414
410
  engineTabs.length = 0;
415
411
  }
@@ -422,7 +418,6 @@ async function main() {
422
418
  let geminiTabPromise = null;
423
419
  if (depth !== "fast") {
424
420
  geminiTabPromise = openNewTab("https://gemini.google.com/app")
425
- .then((tab) => { activateTab(tab).catch(() => {}); return tab; })
426
421
  .catch(() => null);
427
422
  }
428
423
 
@@ -195,6 +195,55 @@ export async function injectHeadlessStealth(tab) {
195
195
  };
196
196
  } catch(_) {}
197
197
 
198
+ // ── window outer dimensions ──────────────────────────
199
+ // outerWidth/Height = 0 in headless — a well-known bot signal.
200
+ // Mirror innerWidth/Height (set by --window-size flag) so the ratio is sane.
201
+ try {
202
+ if (!window.outerWidth) Object.defineProperty(window, 'outerWidth', { get: () => window.innerWidth || 1920, configurable: true });
203
+ if (!window.outerHeight) Object.defineProperty(window, 'outerHeight', { get: () => window.innerHeight || 1080, configurable: true });
204
+ } catch(_) {}
205
+
206
+ // ── screen properties ─────────────────────────────────
207
+ try {
208
+ if (!screen.colorDepth) Object.defineProperty(screen, 'colorDepth', { get: () => 24, configurable: true });
209
+ if (!screen.pixelDepth) Object.defineProperty(screen, 'pixelDepth', { get: () => 24, configurable: true });
210
+ } catch(_) {}
211
+
212
+ // ── navigator.userAgentData (UA Client Hints) ─────────
213
+ // Derive version from the UA string already set by --user-agent flag so the
214
+ // two APIs are always consistent. Removes any "HeadlessChrome" brand entry.
215
+ try {
216
+ var _uaMajor = (navigator.userAgent.match(/Chrome\/(\d+)/) || [])[1] || '136';
217
+ var _uaFull = (navigator.userAgent.match(/Chrome\/([\d.]+)/) || [])[1] || (_uaMajor + '.0.0.0');
218
+ var _brands = [
219
+ { brand: 'Not)A;Brand', version: '99' },
220
+ { brand: 'Google Chrome', version: _uaMajor },
221
+ { brand: 'Chromium', version: _uaMajor },
222
+ ];
223
+ Object.defineProperty(navigator, 'userAgentData', {
224
+ get: function() {
225
+ return {
226
+ brands: _brands, mobile: false, platform: 'Windows',
227
+ getHighEntropyValues: function() {
228
+ return Promise.resolve({
229
+ architecture: 'x86', bitness: '64',
230
+ brands: _brands,
231
+ fullVersionList: [
232
+ { brand: 'Not)A;Brand', version: '99.0.0.0' },
233
+ { brand: 'Google Chrome', version: _uaFull },
234
+ { brand: 'Chromium', version: _uaFull },
235
+ ],
236
+ mobile: false, model: '', platform: 'Windows',
237
+ platformVersion: '15.0.0', uaFullVersion: _uaFull, wow64: false,
238
+ });
239
+ },
240
+ toJSON: function() { return { brands: _brands, mobile: false, platform: 'Windows' }; },
241
+ };
242
+ },
243
+ configurable: true,
244
+ });
245
+ } catch(_) {}
246
+
198
247
  // ── CDP Runtime serialization guard ──────────────────
199
248
  // Sites detect CDP by putting a getter on Error.prototype.stack
200
249
  // and checking if console.log triggers it (only happens when
@@ -1,54 +1,55 @@
1
- // extractors/selectors.mjs
2
- // Centralized CSS selectors for all engines.
3
- // Update selectors here when a site changes its UI.
4
-
5
- export const SELECTORS = {
6
- // ──────────────────────────────────────────────
7
- // Perplexity (perplexity.ai)
8
- // ──────────────────────────────────────────────
9
- perplexity: {
10
- input: "#ask-input",
11
- // Note: copy button found via JS in extractor (language-agnostic)
12
- copyButton: null,
13
- sourceItem: "[data-pplx-citation-url]",
14
- sourceLink: "a",
15
- consent: "#onetrust-accept-btn-handler",
16
- },
17
-
18
- // ──────────────────────────────────────────────
19
- // Bing Copilot (copilot.microsoft.com)
20
- // ──────────────────────────────────────────────
21
- bing: {
22
- input: "#userInput",
23
- copyButton: 'button[data-testid="copy-ai-message-button"]',
24
- sourceLink: 'a[href^="http"][target="_blank"]',
25
- sourceExclude: "copilot.microsoft.com",
26
- consent: "#onetrust-accept-btn-handler",
27
- },
28
-
29
- // ──────────────────────────────────────────────
30
- // Google AI Mode (google.com/search?udm=50)
31
- // ──────────────────────────────────────────────
32
- google: {
33
- answerContainer: ".pWvJNd",
34
- sourceLink: 'a[href^="http"]',
35
- sourceExclude: ["google.", "gstatic", "googleapis"],
36
- sourceHeadingParent: "[data-snhf]",
37
- consent: '#L2AGLb, button[jsname="b3VHJd"], .tHlp8d',
38
- },
39
-
40
- // ──────────────────────────────────────────────
41
- // Gemini (gemini.google.com/app)
42
- // ──────────────────────────────────────────────
43
- gemini: {
44
- input: "rich-textarea .ql-editor",
45
- // Language-agnostic: use Material icon data attributes (work across locales)
46
- copyButton: 'button:has(mat-icon[data-mat-icon-name="content_copy"])',
47
- sendButton: 'button:has(mat-icon[data-mat-icon-name="send"]), .send-button',
48
- sourcesSidebarButton: "button.legacy-sources-sidebar-button",
49
- sourcesExclude: ["gemini.google", "gstatic", "google.com/search"],
50
- citationButtonPattern: 'button[aria-label*="citation from"]',
51
- // For parsing citation aria-labels: "View source details for citation from {name}. Opens side panel."
52
- citationNameRegex: /from\s+([^.]+)\.\s/,
53
- },
54
- };
1
+ // extractors/selectors.mjs
2
+ // Centralized CSS selectors for all engines.
3
+ // Update selectors here when a site changes its UI.
4
+
5
+ export const SELECTORS = {
6
+ // ──────────────────────────────────────────────
7
+ // Perplexity (perplexity.ai)
8
+ // ──────────────────────────────────────────────
9
+ perplexity: {
10
+ input: "#ask-input",
11
+ // Note: copy button found via JS in extractor (language-agnostic)
12
+ copyButton: null,
13
+ sourceItem: "[data-pplx-citation-url]",
14
+ sourceLink: "a",
15
+ consent: "#onetrust-accept-btn-handler",
16
+ },
17
+
18
+ // ──────────────────────────────────────────────
19
+ // Bing Copilot (copilot.microsoft.com)
20
+ // ──────────────────────────────────────────────
21
+ bing: {
22
+ input: "#userInput",
23
+ copyButton: 'button[data-testid="copy-ai-message-button"]',
24
+ sourceLink: 'a[href^="http"][target="_blank"]',
25
+ sourceExclude: "copilot.microsoft.com",
26
+ consent: "#onetrust-accept-btn-handler",
27
+ },
28
+
29
+ // ──────────────────────────────────────────────
30
+ // Google AI Mode (google.com/search?udm=50)
31
+ // ──────────────────────────────────────────────
32
+ google: {
33
+ answerContainer: ".pWvJNd",
34
+ sourceLink: 'a[href^="http"]',
35
+ sourceExclude: ["google.", "gstatic", "googleapis"],
36
+ sourceHeadingParent: "[data-snhf]",
37
+ consent: '#L2AGLb, button[jsname="b3VHJd"], .tHlp8d',
38
+ },
39
+
40
+ // ──────────────────────────────────────────────
41
+ // Gemini (gemini.google.com/app)
42
+ // ──────────────────────────────────────────────
43
+ gemini: {
44
+ input: "rich-textarea .ql-editor",
45
+ // Language-agnostic: use Material icon data attributes (work across locales)
46
+ copyButton: 'button:has(mat-icon[data-mat-icon-name="content_copy"])',
47
+ sendButton: 'button:has(mat-icon[data-mat-icon-name="send"]), .send-button',
48
+ sourcesSidebarButton: "button.legacy-sources-sidebar-button",
49
+ sourcesExclude: ["gemini.google", "gstatic", "google.com/search"],
50
+ citationButtonPattern: 'button[aria-label*="citation from"]',
51
+ // For parsing citation aria-labels: "View source details for citation from {name}. Opens side panel."
52
+ // Bounded + non-overlapping character classes to prevent ReDoS
53
+ citationNameRegex: /from\s{1,20}([^.]{1,200})\.\s/,
54
+ },
55
+ };
package/index.ts CHANGED
@@ -1,177 +1,175 @@
1
- /**
2
- * GreedySearch Pi Extension
3
- *
4
- * Adds `greedy_search` tool to Pi.
5
- * Use depth: "deep" for deep research (source fetching + synthesis + confidence).
6
- *
7
- * Reports streaming progress as each engine completes.
8
- * Requires Chrome to be running (or it auto-launches a dedicated instance).
9
- */
10
-
11
- import { spawn } from "node:child_process";
12
- import { dirname, join } from "node:path";
13
- import { fileURLToPath } from "node:url";
14
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
15
-
16
- import { registerGreedySearchTool } from "./src/tools/greedy-search-handler.js";
17
- import { cdpAvailable } from "./src/tools/shared.js";
18
-
19
- const __dir = dirname(fileURLToPath(import.meta.url));
20
-
21
- export default function greedySearchExtension(pi: ExtensionAPI) {
22
- pi.on("session_start", async (_event, ctx) => {
23
- if (!cdpAvailable(__dir)) {
24
- ctx.ui.notify(
25
- "GreedySearch: cdp.mjs missing from package directory — try reinstalling: pi install git:github.com/apmantza/GreedySearch-pi",
26
- "warning",
27
- );
28
- }
29
- });
30
-
31
- // ─── greedy_search ────────────────────────────────────────────────────────
32
- registerGreedySearchTool(pi, __dir);
33
-
34
- // ─── GreedySearch Chrome commands ─────────────────────────────────────────
35
- pi.registerCommand("greedy-visible", {
36
- description:
37
- "Launch GreedySearch Chrome in visible mode for captcha/login/cookie setup.",
38
- handler: async (_args, ctx) => {
39
- await runChromeCommand([], ctx, "Visible GreedySearch Chrome launched.");
40
- },
41
- });
42
-
43
- pi.registerCommand("greedy-status", {
44
- description: "Show GreedySearch Chrome status.",
45
- handler: async (_args, ctx) => {
46
- await runChromeCommand(["--status"], ctx);
47
- },
48
- });
49
-
50
- pi.registerCommand("greedy-kill", {
51
- description: "Stop GreedySearch Chrome.",
52
- handler: async (_args, ctx) => {
53
- await runChromeCommand(["--kill"], ctx, "GreedySearch Chrome stopped.");
54
- },
55
- });
56
-
57
- // ─── /set-greedy-locale command ───────────────────────────────────────────
58
- pi.registerCommand("set-greedy-locale", {
59
- description:
60
- "Set default locale for GreedySearch results (e.g., /set-greedy-locale de, /set-greedy-locale --clear, /set-greedy-locale --show)",
61
- handler: async (args, ctx) => {
62
- const arg = args.trim() || "--show";
63
-
64
- if (arg === "--show") {
65
- const config = loadUserConfig();
66
- if (config.locale) {
67
- ctx.ui.notify(`Default locale: ${config.locale}`, "info");
68
- } else {
69
- ctx.ui.notify("No default locale (uses: en)", "info");
70
- }
71
- return;
72
- }
73
-
74
- if (arg === "--clear") {
75
- const config = loadUserConfig();
76
- delete config.locale;
77
- saveUserConfig(config);
78
- ctx.ui.notify("Default locale cleared (now uses: en).", "info");
79
- return;
80
- }
81
-
82
- // Set locale
83
- const locale = arg.toLowerCase();
84
- const VALID_LOCALES = [
85
- "en",
86
- "de",
87
- "fr",
88
- "es",
89
- "it",
90
- "pt",
91
- "nl",
92
- "pl",
93
- "ru",
94
- "ja",
95
- "ko",
96
- "zh",
97
- "ar",
98
- "hi",
99
- "tr",
100
- "sv",
101
- "da",
102
- "no",
103
- "fi",
104
- "cs",
105
- "hu",
106
- "ro",
107
- "el",
108
- ];
109
-
110
- if (!VALID_LOCALES.includes(locale)) {
111
- ctx.ui.notify(
112
- `Invalid locale "${locale}". Valid: ${VALID_LOCALES.join(", ")}`,
113
- "error",
114
- );
115
- return;
116
- }
117
-
118
- const config = loadUserConfig();
119
- config.locale = locale;
120
- saveUserConfig(config);
121
- ctx.ui.notify(`Default locale set to: ${locale}`, "info");
122
- },
123
- });
124
- }
125
-
126
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
127
- // Config helpers for /set-greedy-locale command
128
- import { homedir } from "node:os";
129
-
130
- const USER_CONFIG_DIR = join(homedir(), ".config", "greedysearch");
131
- const USER_CONFIG_FILE = join(USER_CONFIG_DIR, "config.json");
132
-
133
- async function runChromeCommand(
134
- args: string[],
135
- ctx: any,
136
- successMessage?: string,
137
- ): Promise<void> {
138
- const visibleBin = join(__dir, "bin", "visible.mjs");
139
- const { code, output } = await new Promise<{
140
- code: number | null;
141
- output: string;
142
- }>((resolve) => {
143
- const proc = spawn(process.execPath, [visibleBin, ...args], {
144
- stdio: ["ignore", "pipe", "pipe"],
145
- env: { ...process.env, GREEDY_SEARCH_VISIBLE: "1" },
146
- });
147
- let output = "";
148
- proc.stdout.on("data", (d: Buffer) => (output += d.toString()));
149
- proc.stderr.on("data", (d: Buffer) => (output += d.toString()));
150
- proc.on("close", (code: number | null) => resolve({ code, output }));
151
- });
152
-
153
- if (code === 0) {
154
- ctx.ui.notify((successMessage || output.trim() || "Done.").trim(), "info");
155
- } else {
156
- ctx.ui.notify(
157
- output.trim() || `GreedySearch Chrome command failed (${code})`,
158
- "error",
159
- );
160
- }
161
- }
162
-
163
- function loadUserConfig(): Record<string, string> {
164
- try {
165
- if (existsSync(USER_CONFIG_FILE)) {
166
- return JSON.parse(readFileSync(USER_CONFIG_FILE, "utf8"));
167
- }
168
- } catch {
169
- // Ignore parse errors
170
- }
171
- return {};
172
- }
173
-
174
- function saveUserConfig(config: Record<string, string>): void {
175
- mkdirSync(USER_CONFIG_DIR, { recursive: true });
176
- writeFileSync(USER_CONFIG_FILE, JSON.stringify(config, null, 2), "utf8");
177
- }
1
+ /**
2
+ * GreedySearch Pi Extension
3
+ *
4
+ * Adds `greedy_search` tool to Pi.
5
+ * Use depth: "deep" for deep research (source fetching + synthesis + confidence).
6
+ *
7
+ * Reports streaming progress as each engine completes.
8
+ * Requires Chrome to be running (or it auto-launches a dedicated instance).
9
+ */
10
+
11
+ import { spawn } from "node:child_process";
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { homedir } from "node:os";
14
+ import { dirname, join } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
+
18
+ import { registerGreedySearchTool } from "./src/tools/greedy-search-handler.js";
19
+ import { cdpAvailable } from "./src/tools/shared.js";
20
+
21
+ const __dir = dirname(fileURLToPath(import.meta.url));
22
+
23
+ export default function greedySearchExtension(pi: ExtensionAPI) {
24
+ pi.on("session_start", async (_event, ctx) => {
25
+ if (!cdpAvailable(__dir)) {
26
+ ctx.ui.notify(
27
+ "GreedySearch: cdp.mjs missing from package directory — try reinstalling: pi install git:github.com/apmantza/GreedySearch-pi",
28
+ "warning",
29
+ );
30
+ }
31
+ });
32
+
33
+ // ─── greedy_search ────────────────────────────────────────────────────────
34
+ registerGreedySearchTool(pi, __dir);
35
+
36
+ // ─── GreedySearch Chrome commands ─────────────────────────────────────────
37
+ pi.registerCommand("greedy-visible", {
38
+ description:
39
+ "Launch GreedySearch Chrome in visible mode for captcha/login/cookie setup.",
40
+ handler: async (_args, ctx) => {
41
+ await runChromeCommand([], ctx, "Visible GreedySearch Chrome launched.");
42
+ },
43
+ });
44
+
45
+ pi.registerCommand("greedy-status", {
46
+ description: "Show GreedySearch Chrome status.",
47
+ handler: async (_args, ctx) => {
48
+ await runChromeCommand(["--status"], ctx);
49
+ },
50
+ });
51
+
52
+ pi.registerCommand("greedy-kill", {
53
+ description: "Stop GreedySearch Chrome.",
54
+ handler: async (_args, ctx) => {
55
+ await runChromeCommand(["--kill"], ctx, "GreedySearch Chrome stopped.");
56
+ },
57
+ });
58
+
59
+ // ─── /set-greedy-locale command ───────────────────────────────────────────
60
+ pi.registerCommand("set-greedy-locale", {
61
+ description:
62
+ "Set default locale for GreedySearch results (e.g., /set-greedy-locale de, /set-greedy-locale --clear, /set-greedy-locale --show)",
63
+ handler: async (args, ctx) => {
64
+ const arg = args.trim() || "--show";
65
+
66
+ if (arg === "--show") {
67
+ const config = loadUserConfig();
68
+ if (config.locale) {
69
+ ctx.ui.notify(`Default locale: ${config.locale}`, "info");
70
+ } else {
71
+ ctx.ui.notify("No default locale (uses: en)", "info");
72
+ }
73
+ return;
74
+ }
75
+
76
+ if (arg === "--clear") {
77
+ const config = loadUserConfig();
78
+ delete config.locale;
79
+ saveUserConfig(config);
80
+ ctx.ui.notify("Default locale cleared (now uses: en).", "info");
81
+ return;
82
+ }
83
+
84
+ // Set locale
85
+ const locale = arg.toLowerCase();
86
+ const VALID_LOCALES = [
87
+ "en",
88
+ "de",
89
+ "fr",
90
+ "es",
91
+ "it",
92
+ "pt",
93
+ "nl",
94
+ "pl",
95
+ "ru",
96
+ "ja",
97
+ "ko",
98
+ "zh",
99
+ "ar",
100
+ "hi",
101
+ "tr",
102
+ "sv",
103
+ "da",
104
+ "no",
105
+ "fi",
106
+ "cs",
107
+ "hu",
108
+ "ro",
109
+ "el",
110
+ ];
111
+
112
+ if (!VALID_LOCALES.includes(locale)) {
113
+ ctx.ui.notify(
114
+ `Invalid locale "${locale}". Valid: ${VALID_LOCALES.join(", ")}`,
115
+ "error",
116
+ );
117
+ return;
118
+ }
119
+
120
+ const config = loadUserConfig();
121
+ config.locale = locale;
122
+ saveUserConfig(config);
123
+ ctx.ui.notify(`Default locale set to: ${locale}`, "info");
124
+ },
125
+ });
126
+ }
127
+
128
+ const USER_CONFIG_DIR = join(homedir(), ".config", "greedysearch");
129
+ const USER_CONFIG_FILE = join(USER_CONFIG_DIR, "config.json");
130
+
131
+ async function runChromeCommand(
132
+ args: string[],
133
+ ctx: any,
134
+ successMessage?: string,
135
+ ): Promise<void> {
136
+ const visibleBin = join(__dir, "bin", "visible.mjs");
137
+ const { code, output } = await new Promise<{
138
+ code: number | null;
139
+ output: string;
140
+ }>((resolve) => {
141
+ const proc = spawn(process.execPath, [visibleBin, ...args], {
142
+ stdio: ["ignore", "pipe", "pipe"],
143
+ env: { ...process.env, GREEDY_SEARCH_VISIBLE: "1" },
144
+ });
145
+ let output = "";
146
+ proc.stdout.on("data", (d: Buffer) => (output += d.toString()));
147
+ proc.stderr.on("data", (d: Buffer) => (output += d.toString()));
148
+ proc.on("close", (code: number | null) => resolve({ code, output }));
149
+ });
150
+
151
+ if (code === 0) {
152
+ ctx.ui.notify((successMessage || output.trim() || "Done.").trim(), "info");
153
+ } else {
154
+ ctx.ui.notify(
155
+ output.trim() || `GreedySearch Chrome command failed (${code})`,
156
+ "error",
157
+ );
158
+ }
159
+ }
160
+
161
+ function loadUserConfig(): Record<string, string> {
162
+ try {
163
+ if (existsSync(USER_CONFIG_FILE)) {
164
+ return JSON.parse(readFileSync(USER_CONFIG_FILE, "utf8"));
165
+ }
166
+ } catch {
167
+ // Ignore parse errors
168
+ }
169
+ return {};
170
+ }
171
+
172
+ function saveUserConfig(config: Record<string, string>): void {
173
+ mkdirSync(USER_CONFIG_DIR, { recursive: true });
174
+ writeFileSync(USER_CONFIG_FILE, JSON.stringify(config, null, 2), "utf8");
175
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apmantza/greedysearch-pi",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
4
4
  "description": "Headless multi-engine AI search (Perplexity, Bing Copilot, Google AI) via browser automation -- NO API KEYS needed. Extracts answers with sources, optional synthesis. Grounded AI answers from real browser interactions.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -49,7 +49,8 @@
49
49
  "turndown": "^7.1.2"
50
50
  },
51
51
  "peerDependencies": {
52
- "@mariozechner/pi-coding-agent": "*",
52
+ "@earendil-works/pi-coding-agent": "*",
53
+ "@earendil-works/pi-tui": "*",
53
54
  "@sinclair/typebox": "*"
54
55
  }
55
56
  }
@@ -9,18 +9,13 @@ Use `greedy_search` for live web answers.
9
9
  greedy_search({ query: "React 19 changes", depth: "standard" });
10
10
  ```
11
11
 
12
- **Params:** `query` (required), `engine`: `all`|`perplexity`|`bing`|`google`|`gemini`, `depth`: `fast`|`standard`|`deep`, `fullAnswer`, `visible`/`alwaysVisible`/`headless: false`
12
+ **Params:** `query` (required), `engine`: `all`|`perplexity`|`bing`|`google`|`gemini`, `depth`: `fast`|`standard`|`deep`
13
13
 
14
14
  **Depths:**
15
-
16
15
  - `fast`: ~15-30s, single engine, no synthesis
17
16
  - `standard`: ~30-90s, all engines + Gemini synthesis + sources
18
17
  - `deep`: ~60-180s, stronger grounding + confidence metadata
19
18
 
20
- **Captcha/blocks:** Headless by default. Bing/Perplexity auto-retry in visible mode when blocked. If human verification is needed, visible Chrome stays open — tell the user to solve it and rerun. Use `visible: true` proactively for repeated issues.
21
-
22
- **Pi commands:** `/greedy-visible`, `/greedy-status`, `/greedy-kill`, `/set-greedy-locale`
19
+ **Blocks:** Headless by default; auto-retries in visible mode. If human verification is needed, visible Chrome stays open — tell the user to solve it and rerun.
23
20
 
24
21
  **CDP safety:** Never call raw `bin/cdp.mjs`. Use `bin/cdp-greedy.mjs`, `bin/cdp-visible.mjs`, or `bin/cdp-headless.mjs`.
25
-
26
- Old `coding_task`/`deep_research` folded into `greedy_search`. Use `engine: "gemini"` for one-off opinion, `depth: "deep"` for research.