@apmantza/greedysearch-pi 1.4.1 → 1.4.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.
- package/README.md +219 -208
- package/cdp.mjs +16 -16
- package/extractors/bing-copilot.mjs +12 -21
- package/extractors/consent.mjs +10 -3
- package/extractors/gemini.mjs +12 -53
- package/extractors/google-ai.mjs +7 -10
- package/extractors/perplexity.mjs +28 -31
- package/extractors/selectors.mjs +52 -52
- package/index.ts +623 -623
- package/launch.mjs +33 -33
- package/newfeaturesideas.md +105 -0
- package/package.json +1 -1
- package/skills/greedy-search/SKILL.md +145 -145
- package/test.sh +298 -298
package/extractors/google-ai.mjs
CHANGED
|
@@ -47,17 +47,14 @@ function cdp(args, timeoutMs = 30000) {
|
|
|
47
47
|
|
|
48
48
|
async function getOrOpenTab(tabPrefix) {
|
|
49
49
|
if (tabPrefix) return tabPrefix;
|
|
50
|
-
|
|
51
|
-
if (existsSync(PAGES_CACHE)) {
|
|
52
|
-
const pages = JSON.parse(readFileSync(PAGES_CACHE, 'utf8'));
|
|
53
|
-
const existing = pages.find(p => p.url.includes('google.com'));
|
|
54
|
-
if (existing) return existing.targetId.slice(0, 8);
|
|
55
|
-
}
|
|
56
|
-
|
|
50
|
+
// Always open a fresh tab to avoid SPA navigation issues
|
|
57
51
|
const list = await cdp(['list']);
|
|
58
|
-
const
|
|
59
|
-
if (!
|
|
60
|
-
|
|
52
|
+
const anchor = list.split('\n')[0]?.slice(0, 8);
|
|
53
|
+
if (!anchor) throw new Error('No Chrome tabs found. Is Chrome running with --remote-debugging-port=9222?');
|
|
54
|
+
const raw = await cdp(['evalraw', anchor, 'Target.createTarget', '{"url":"about:blank"}']);
|
|
55
|
+
const { targetId } = JSON.parse(raw);
|
|
56
|
+
await cdp(['list']); // refresh cache
|
|
57
|
+
return targetId.slice(0, 8);
|
|
61
58
|
}
|
|
62
59
|
|
|
63
60
|
async function waitForStreamComplete(tab) {
|
|
@@ -44,21 +44,15 @@ function cdp(args, timeoutMs = 30000) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
async function getOrOpenTab(tabPrefix) {
|
|
47
|
-
// If caller specified a tab, use it
|
|
48
47
|
if (tabPrefix) return tabPrefix;
|
|
49
|
-
|
|
50
|
-
// Otherwise look for an existing Perplexity tab
|
|
51
|
-
if (existsSync(PAGES_CACHE)) {
|
|
52
|
-
const pages = JSON.parse(readFileSync(PAGES_CACHE, 'utf8'));
|
|
53
|
-
const existing = pages.find(p => p.url.includes('perplexity.ai'));
|
|
54
|
-
if (existing) return existing.targetId.slice(0, 8);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Fall back to first available tab
|
|
48
|
+
// Always open a fresh tab to avoid SPA navigation issues
|
|
58
49
|
const list = await cdp(['list']);
|
|
59
|
-
const
|
|
60
|
-
if (!
|
|
61
|
-
|
|
50
|
+
const anchor = list.split('\n')[0]?.slice(0, 8);
|
|
51
|
+
if (!anchor) throw new Error('No Chrome tabs found. Is Chrome running with --remote-debugging-port=9222?');
|
|
52
|
+
const raw = await cdp(['evalraw', anchor, 'Target.createTarget', '{"url":"about:blank"}']);
|
|
53
|
+
const { targetId } = JSON.parse(raw);
|
|
54
|
+
await cdp(['list']); // refresh cache so cdp nav can find the new tab
|
|
55
|
+
return targetId.slice(0, 8);
|
|
62
56
|
}
|
|
63
57
|
|
|
64
58
|
async function injectClipboardInterceptor(tab) {
|
|
@@ -85,16 +79,25 @@ async function injectClipboardInterceptor(tab) {
|
|
|
85
79
|
`]);
|
|
86
80
|
}
|
|
87
81
|
|
|
88
|
-
async function
|
|
82
|
+
async function waitForGenerationToFinish(tab) {
|
|
89
83
|
const deadline = Date.now() + COPY_TIMEOUT;
|
|
84
|
+
let lastLen = -1;
|
|
85
|
+
let stableCount = 0;
|
|
90
86
|
while (Date.now() < deadline) {
|
|
91
87
|
await new Promise(r => setTimeout(r, COPY_POLL_INTERVAL));
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
88
|
+
const lenStr = await cdp(['eval', tab, 'document.body.innerText.length']).catch(() => '0');
|
|
89
|
+
const currentLen = parseInt(lenStr) || 0;
|
|
90
|
+
if (currentLen > 0) {
|
|
91
|
+
if (currentLen === lastLen) {
|
|
92
|
+
stableCount++;
|
|
93
|
+
if (stableCount >= 3) return;
|
|
94
|
+
} else {
|
|
95
|
+
lastLen = currentLen;
|
|
96
|
+
stableCount = 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
96
99
|
}
|
|
97
|
-
throw new Error(`Perplexity
|
|
100
|
+
throw new Error(`Perplexity generation did not finish within ${COPY_TIMEOUT}ms`);
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
async function extractAnswer(tab) {
|
|
@@ -104,17 +107,11 @@ async function extractAnswer(tab) {
|
|
|
104
107
|
const answer = await cdp(['eval', tab, `window.__pplxClipboard || ''`]);
|
|
105
108
|
if (!answer) throw new Error('Clipboard interceptor returned empty text');
|
|
106
109
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
.filter((v, i, arr) => arr.findIndex(x => x.url === v.url) === i)
|
|
113
|
-
.slice(0, 10);
|
|
114
|
-
return JSON.stringify(sources);
|
|
115
|
-
})()
|
|
116
|
-
`]).catch(() => '[]');
|
|
117
|
-
const sources = JSON.parse(raw);
|
|
110
|
+
// Regex parse Markdown links from clipboard — robust against DOM changes
|
|
111
|
+
const sources = Array.from(answer.matchAll(/\[([^\]]+)\]\((https?:\/\/[^\s\)]+)\)/g))
|
|
112
|
+
.map(m => ({ title: m[1], url: m[2] }))
|
|
113
|
+
.filter((v, i, arr) => arr.findIndex(x => x.url === v.url) === i)
|
|
114
|
+
.slice(0, 10);
|
|
118
115
|
|
|
119
116
|
return { answer: answer.trim(), sources };
|
|
120
117
|
}
|
|
@@ -166,7 +163,7 @@ async function main() {
|
|
|
166
163
|
`document.querySelector('${S.input}')?.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true,keyCode:13})), 'ok'`
|
|
167
164
|
]);
|
|
168
165
|
|
|
169
|
-
await
|
|
166
|
+
await waitForGenerationToFinish(tab);
|
|
170
167
|
|
|
171
168
|
const { answer, sources } = await extractAnswer(tab);
|
|
172
169
|
|
package/extractors/selectors.mjs
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
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
|
-
copyButton: 'button[aria-label="Copy"]',
|
|
12
|
-
sourceItem: '[data-pplx-citation-url]',
|
|
13
|
-
sourceLink: 'a',
|
|
14
|
-
consent: '#onetrust-accept-btn-handler',
|
|
15
|
-
},
|
|
16
|
-
|
|
17
|
-
// ──────────────────────────────────────────────
|
|
18
|
-
// Bing Copilot (copilot.microsoft.com)
|
|
19
|
-
// ──────────────────────────────────────────────
|
|
20
|
-
bing: {
|
|
21
|
-
input: '#userInput',
|
|
22
|
-
copyButton: 'button[data-testid="copy-ai-message-button"]',
|
|
23
|
-
sourceLink: 'a[href^="http"][target="_blank"]',
|
|
24
|
-
sourceExclude: 'copilot.microsoft.com',
|
|
25
|
-
consent: '#onetrust-accept-btn-handler',
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
// ──────────────────────────────────────────────
|
|
29
|
-
// Google AI Mode (google.com/search?udm=50)
|
|
30
|
-
// ──────────────────────────────────────────────
|
|
31
|
-
google: {
|
|
32
|
-
answerContainer: '.pWvJNd',
|
|
33
|
-
sourceLink: 'a[href^="http"]',
|
|
34
|
-
sourceExclude: ['google.', 'gstatic', 'googleapis'],
|
|
35
|
-
sourceHeadingParent: '[data-snhf]',
|
|
36
|
-
consent: '#L2AGLb, button[jsname="b3VHJd"], .tHlp8d',
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
// ──────────────────────────────────────────────
|
|
40
|
-
// Gemini (gemini.google.com/app)
|
|
41
|
-
// ──────────────────────────────────────────────
|
|
42
|
-
gemini: {
|
|
43
|
-
input: 'rich-textarea .ql-editor',
|
|
44
|
-
copyButton: 'button[aria-label="Copy"]',
|
|
45
|
-
sendButton: 'button[aria-label*="Send"]',
|
|
46
|
-
sourcesSidebarButton: 'button.legacy-sources-sidebar-button',
|
|
47
|
-
sourcesExclude: ['gemini.google', 'gstatic', 'google.com/search'],
|
|
48
|
-
citationButtonPattern: 'button[aria-label*="citation from"]',
|
|
49
|
-
// For parsing citation aria-labels: "View source details for citation from {name}. Opens side panel."
|
|
50
|
-
citationNameRegex: /from\s+(.+?)\.\s/,
|
|
51
|
-
},
|
|
52
|
-
};
|
|
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
|
+
copyButton: 'button[aria-label="Copy"]',
|
|
12
|
+
sourceItem: '[data-pplx-citation-url]',
|
|
13
|
+
sourceLink: 'a',
|
|
14
|
+
consent: '#onetrust-accept-btn-handler',
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
// ──────────────────────────────────────────────
|
|
18
|
+
// Bing Copilot (copilot.microsoft.com)
|
|
19
|
+
// ──────────────────────────────────────────────
|
|
20
|
+
bing: {
|
|
21
|
+
input: '#userInput',
|
|
22
|
+
copyButton: 'button[data-testid="copy-ai-message-button"]',
|
|
23
|
+
sourceLink: 'a[href^="http"][target="_blank"]',
|
|
24
|
+
sourceExclude: 'copilot.microsoft.com',
|
|
25
|
+
consent: '#onetrust-accept-btn-handler',
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
// ──────────────────────────────────────────────
|
|
29
|
+
// Google AI Mode (google.com/search?udm=50)
|
|
30
|
+
// ──────────────────────────────────────────────
|
|
31
|
+
google: {
|
|
32
|
+
answerContainer: '.pWvJNd',
|
|
33
|
+
sourceLink: 'a[href^="http"]',
|
|
34
|
+
sourceExclude: ['google.', 'gstatic', 'googleapis'],
|
|
35
|
+
sourceHeadingParent: '[data-snhf]',
|
|
36
|
+
consent: '#L2AGLb, button[jsname="b3VHJd"], .tHlp8d',
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// ──────────────────────────────────────────────
|
|
40
|
+
// Gemini (gemini.google.com/app)
|
|
41
|
+
// ──────────────────────────────────────────────
|
|
42
|
+
gemini: {
|
|
43
|
+
input: 'rich-textarea .ql-editor',
|
|
44
|
+
copyButton: 'button[aria-label="Copy"]',
|
|
45
|
+
sendButton: 'button[aria-label*="Send"]',
|
|
46
|
+
sourcesSidebarButton: 'button.legacy-sources-sidebar-button',
|
|
47
|
+
sourcesExclude: ['gemini.google', 'gstatic', 'google.com/search'],
|
|
48
|
+
citationButtonPattern: 'button[aria-label*="citation from"]',
|
|
49
|
+
// For parsing citation aria-labels: "View source details for citation from {name}. Opens side panel."
|
|
50
|
+
citationNameRegex: /from\s+(.+?)\.\s/,
|
|
51
|
+
},
|
|
52
|
+
};
|