@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.
@@ -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 firstLine = list.split('\n')[0];
59
- if (!firstLine) throw new Error('No Chrome tabs found. Is Chrome running with --remote-debugging-port=9222?');
60
- return firstLine.slice(0, 8);
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 firstLine = list.split('\n')[0];
60
- if (!firstLine) throw new Error('No Chrome tabs found. Is Chrome running with --remote-debugging-port=9222?');
61
- return firstLine.slice(0, 8);
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 waitForCopyButton(tab) {
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 found = await cdp(['eval', tab,
93
- `!!document.querySelector('${S.copyButton}')`
94
- ]).catch(() => 'false');
95
- if (found === 'true') return;
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 copy button did not appear within ${COPY_TIMEOUT}ms`);
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
- const raw = await cdp(['eval', tab, `
108
- (function() {
109
- var sources = Array.from(document.querySelectorAll('${S.sourceItem}'))
110
- .map(el => ({ url: el.getAttribute('data-pplx-citation-url'), title: el.querySelector('${S.sourceLink}')?.innerText?.trim() || '' }))
111
- .filter(s => s.url)
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 waitForCopyButton(tab);
166
+ await waitForGenerationToFinish(tab);
170
167
 
171
168
  const { answer, sources } = await extractAnswer(tab);
172
169
 
@@ -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
+ };