@apmantza/greedysearch-pi 1.9.0 → 1.9.2

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.
@@ -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,176 @@
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
+
17
+ import { registerGreedySearchTool } from "./src/tools/greedy-search-handler.js";
18
+
19
+ type ExtensionAPI = any;
20
+ import { cdpAvailable } from "./src/tools/shared.js";
21
+
22
+ const __dir = dirname(fileURLToPath(import.meta.url));
23
+
24
+ export default function greedySearchExtension(pi: ExtensionAPI) {
25
+ pi.on("session_start", async (_event, ctx) => {
26
+ if (!cdpAvailable(__dir)) {
27
+ ctx.ui.notify(
28
+ "GreedySearch: cdp.mjs missing from package directory — try reinstalling: pi install git:github.com/apmantza/GreedySearch-pi",
29
+ "warning",
30
+ );
31
+ }
32
+ });
33
+
34
+ // ─── greedy_search ────────────────────────────────────────────────────────
35
+ registerGreedySearchTool(pi, __dir);
36
+
37
+ // ─── GreedySearch Chrome commands ─────────────────────────────────────────
38
+ pi.registerCommand("greedy-visible", {
39
+ description:
40
+ "Launch GreedySearch Chrome in visible mode for captcha/login/cookie setup.",
41
+ handler: async (_args, ctx) => {
42
+ await runChromeCommand([], ctx, "Visible GreedySearch Chrome launched.");
43
+ },
44
+ });
45
+
46
+ pi.registerCommand("greedy-status", {
47
+ description: "Show GreedySearch Chrome status.",
48
+ handler: async (_args, ctx) => {
49
+ await runChromeCommand(["--status"], ctx);
50
+ },
51
+ });
52
+
53
+ pi.registerCommand("greedy-kill", {
54
+ description: "Stop GreedySearch Chrome.",
55
+ handler: async (_args, ctx) => {
56
+ await runChromeCommand(["--kill"], ctx, "GreedySearch Chrome stopped.");
57
+ },
58
+ });
59
+
60
+ // ─── /set-greedy-locale command ───────────────────────────────────────────
61
+ pi.registerCommand("set-greedy-locale", {
62
+ description:
63
+ "Set default locale for GreedySearch results (e.g., /set-greedy-locale de, /set-greedy-locale --clear, /set-greedy-locale --show)",
64
+ handler: async (args, ctx) => {
65
+ const arg = args.trim() || "--show";
66
+
67
+ if (arg === "--show") {
68
+ const config = loadUserConfig();
69
+ if (config.locale) {
70
+ ctx.ui.notify(`Default locale: ${config.locale}`, "info");
71
+ } else {
72
+ ctx.ui.notify("No default locale (uses: en)", "info");
73
+ }
74
+ return;
75
+ }
76
+
77
+ if (arg === "--clear") {
78
+ const config = loadUserConfig();
79
+ delete config.locale;
80
+ saveUserConfig(config);
81
+ ctx.ui.notify("Default locale cleared (now uses: en).", "info");
82
+ return;
83
+ }
84
+
85
+ // Set locale
86
+ const locale = arg.toLowerCase();
87
+ const VALID_LOCALES = [
88
+ "en",
89
+ "de",
90
+ "fr",
91
+ "es",
92
+ "it",
93
+ "pt",
94
+ "nl",
95
+ "pl",
96
+ "ru",
97
+ "ja",
98
+ "ko",
99
+ "zh",
100
+ "ar",
101
+ "hi",
102
+ "tr",
103
+ "sv",
104
+ "da",
105
+ "no",
106
+ "fi",
107
+ "cs",
108
+ "hu",
109
+ "ro",
110
+ "el",
111
+ ];
112
+
113
+ if (!VALID_LOCALES.includes(locale)) {
114
+ ctx.ui.notify(
115
+ `Invalid locale "${locale}". Valid: ${VALID_LOCALES.join(", ")}`,
116
+ "error",
117
+ );
118
+ return;
119
+ }
120
+
121
+ const config = loadUserConfig();
122
+ config.locale = locale;
123
+ saveUserConfig(config);
124
+ ctx.ui.notify(`Default locale set to: ${locale}`, "info");
125
+ },
126
+ });
127
+ }
128
+
129
+ const USER_CONFIG_DIR = join(homedir(), ".config", "greedysearch");
130
+ const USER_CONFIG_FILE = join(USER_CONFIG_DIR, "config.json");
131
+
132
+ async function runChromeCommand(
133
+ args: string[],
134
+ ctx: any,
135
+ successMessage?: string,
136
+ ): Promise<void> {
137
+ const visibleBin = join(__dir, "bin", "visible.mjs");
138
+ const { code, output } = await new Promise<{
139
+ code: number | null;
140
+ output: string;
141
+ }>((resolve) => {
142
+ const proc = spawn(process.execPath, [visibleBin, ...args], {
143
+ stdio: ["ignore", "pipe", "pipe"],
144
+ env: { ...process.env, GREEDY_SEARCH_VISIBLE: "1" },
145
+ });
146
+ let output = "";
147
+ proc.stdout.on("data", (d: Buffer) => (output += d.toString()));
148
+ proc.stderr.on("data", (d: Buffer) => (output += d.toString()));
149
+ proc.on("close", (code: number | null) => resolve({ code, output }));
150
+ });
151
+
152
+ if (code === 0) {
153
+ ctx.ui.notify((successMessage || output.trim() || "Done.").trim(), "info");
154
+ } else {
155
+ ctx.ui.notify(
156
+ output.trim() || `GreedySearch Chrome command failed (${code})`,
157
+ "error",
158
+ );
159
+ }
160
+ }
161
+
162
+ function loadUserConfig(): Record<string, string> {
163
+ try {
164
+ if (existsSync(USER_CONFIG_FILE)) {
165
+ return JSON.parse(readFileSync(USER_CONFIG_FILE, "utf8"));
166
+ }
167
+ } catch {
168
+ // Ignore parse errors
169
+ }
170
+ return {};
171
+ }
172
+
173
+ function saveUserConfig(config: Record<string, string>): void {
174
+ mkdirSync(USER_CONFIG_DIR, { recursive: true });
175
+ writeFileSync(USER_CONFIG_FILE, JSON.stringify(config, null, 2), "utf8");
176
+ }
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.2",
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": [
@@ -45,11 +45,16 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@mozilla/readability": "^0.6.0",
48
+ "@sinclair/typebox": "^0.34.48",
48
49
  "jsdom": "^24.0.0",
49
50
  "turndown": "^7.1.2"
50
51
  },
51
52
  "peerDependencies": {
52
- "@mariozechner/pi-coding-agent": "*",
53
- "@sinclair/typebox": "*"
53
+ "@earendil-works/pi-coding-agent": "*"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "@earendil-works/pi-coding-agent": {
57
+ "optional": true
58
+ }
54
59
  }
55
60
  }
@@ -1,26 +1,12 @@
1
1
  ---
2
2
  name: greedy-search
3
- description: Live web search via Perplexity, Bing, Google AI, and Gemini. Use for current docs, recent errors/framework changes, dependency choices, or stale-knowledge questions. NOT for codebase search.
3
+ description: Web search via Perplexity, Bing, Google AI & Gemini. Current docs, recent changes, dependency choices. NOT codebase search.
4
4
  ---
5
5
 
6
- Use `greedy_search` for live web answers.
6
+ `greedy_search({ query, engine: "all"|"perplexity"|"bing"|"google"|"gemini", depth: "fast"|"standard"|"deep"|"research", breadth: 1-5, iterations: 1-3, maxSources: 3-12, visible: bool })`
7
7
 
8
- ```js
9
- greedy_search({ query: "React 19 changes", depth: "standard" });
10
- ```
8
+ **Depth:** `fast`(15-30s, no synthesis) · `standard`(30-90s, all+synthesis+sources) · `deep`(60-180s, stronger grounding) · `research`(slowest, iterative planning+follow-ups+learning extraction; uses breadth/iterations/maxSources)
11
9
 
12
- **Params:** `query` (required), `engine`: `all`|`perplexity`|`bing`|`google`|`gemini`, `depth`: `fast`|`standard`|`deep`, `fullAnswer`, `visible`/`alwaysVisible`/`headless: false`
10
+ **Auto-recovery:** Headless default. Bing/Perplexity auto-retry visible on CF block. Manual CAPTCHA → visible stays open; solve then rerun.
13
11
 
14
- **Depths:**
15
-
16
- - `fast`: ~15-30s, single engine, no synthesis
17
- - `standard`: ~30-90s, all engines + Gemini synthesis + sources
18
- - `deep`: ~60-180s, stronger grounding + confidence metadata
19
-
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`
23
-
24
- **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.
12
+ **CDP safety:** Use `bin/cdp-greedy.mjs` only. Never raw `bin/cdp.mjs`.