@apmantza/greedysearch-pi 1.0.15 → 1.0.17
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/extractors/bing-copilot.mjs +46 -40
- package/extractors/gemini.mjs +54 -47
- package/extractors/perplexity.mjs +48 -39
- package/package.json +1 -1
|
@@ -17,10 +17,8 @@ import { dismissConsent, handleVerification } from './consent.mjs';
|
|
|
17
17
|
const CDP = join(homedir(), '.claude', 'skills', 'chrome-cdp', 'scripts', 'cdp.mjs');
|
|
18
18
|
const PAGES_CACHE = `${tmpdir().replace(/\\/g, '/')}/cdp-pages.json`;
|
|
19
19
|
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const STREAM_TIMEOUT = 60000; // bail out after 60s
|
|
23
|
-
const MIN_ANSWER_LENGTH = 50; // don't accept trivial answers
|
|
20
|
+
const COPY_POLL_INTERVAL = 700;
|
|
21
|
+
const COPY_TIMEOUT = 60000;
|
|
24
22
|
|
|
25
23
|
// ---------------------------------------------------------------------------
|
|
26
24
|
|
|
@@ -55,55 +53,62 @@ async function getOrOpenTab(tabPrefix) {
|
|
|
55
53
|
return firstLine.slice(0, 8);
|
|
56
54
|
}
|
|
57
55
|
|
|
58
|
-
async function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
async function injectClipboardInterceptor(tab) {
|
|
57
|
+
await cdp(['eval', tab, `
|
|
58
|
+
window.__bingClipboard = null;
|
|
59
|
+
const _origWriteText = navigator.clipboard.writeText.bind(navigator.clipboard);
|
|
60
|
+
navigator.clipboard.writeText = function(text) {
|
|
61
|
+
window.__bingClipboard = text;
|
|
62
|
+
return _origWriteText(text);
|
|
63
|
+
};
|
|
64
|
+
const _origWrite = navigator.clipboard.write.bind(navigator.clipboard);
|
|
65
|
+
navigator.clipboard.write = async function(items) {
|
|
66
|
+
try {
|
|
67
|
+
for (const item of items) {
|
|
68
|
+
if (item.types && item.types.includes('text/plain')) {
|
|
69
|
+
const blob = await item.getType('text/plain');
|
|
70
|
+
window.__bingClipboard = await blob.text();
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch(e) {}
|
|
75
|
+
return _origWrite(items);
|
|
76
|
+
};
|
|
77
|
+
`]);
|
|
78
|
+
}
|
|
62
79
|
|
|
80
|
+
async function waitForCopyButton(tab) {
|
|
81
|
+
const deadline = Date.now() + COPY_TIMEOUT;
|
|
63
82
|
while (Date.now() < deadline) {
|
|
64
|
-
await new Promise(r => setTimeout(r,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
var items = Array.from(document.querySelectorAll('[class*="ai-message-item"]'));
|
|
70
|
-
var filled = items.filter(el => (el.innerText?.length || 0) > 0);
|
|
71
|
-
var last = filled[filled.length - 1];
|
|
72
|
-
return (last?.innerText?.length || 0) + '';
|
|
73
|
-
})()`
|
|
74
|
-
]).catch(() => '0');
|
|
75
|
-
|
|
76
|
-
const len = parseInt(lenStr) || 0;
|
|
77
|
-
|
|
78
|
-
if (len >= MIN_ANSWER_LENGTH && len === lastLen) {
|
|
79
|
-
stableCount++;
|
|
80
|
-
if (stableCount >= STREAM_STABLE_ROUNDS) return len;
|
|
81
|
-
} else {
|
|
82
|
-
stableCount = 0;
|
|
83
|
-
lastLen = len;
|
|
84
|
-
}
|
|
83
|
+
await new Promise(r => setTimeout(r, COPY_POLL_INTERVAL));
|
|
84
|
+
const found = await cdp(['eval', tab,
|
|
85
|
+
`!!document.querySelector('button[data-testid="copy-ai-message-button"]')`
|
|
86
|
+
]).catch(() => 'false');
|
|
87
|
+
if (found === 'true') return;
|
|
85
88
|
}
|
|
86
|
-
|
|
87
|
-
if (lastLen >= MIN_ANSWER_LENGTH) return lastLen;
|
|
88
|
-
throw new Error(`Copilot answer did not stabilise within ${STREAM_TIMEOUT}ms`);
|
|
89
|
+
throw new Error(`Copilot copy button did not appear within ${COPY_TIMEOUT}ms`);
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
async function extractAnswer(tab) {
|
|
93
|
+
await cdp(['eval', tab, `document.querySelector('button[data-testid="copy-ai-message-button"]')?.click()`]);
|
|
94
|
+
await new Promise(r => setTimeout(r, 400));
|
|
95
|
+
|
|
96
|
+
const answer = await cdp(['eval', tab, `window.__bingClipboard || ''`]);
|
|
97
|
+
if (!answer) throw new Error('Clipboard interceptor returned empty text');
|
|
98
|
+
|
|
92
99
|
const raw = await cdp(['eval', tab, `
|
|
93
100
|
(function() {
|
|
94
|
-
var items = Array.from(document.querySelectorAll('[class*="ai-message-item"]'));
|
|
95
|
-
var el = items.filter(e => (e.innerText?.length || 0) > 0).pop();
|
|
96
|
-
if (!el) return JSON.stringify({ answer: '', sources: [] });
|
|
97
|
-
var answer = el.innerText.trim();
|
|
98
101
|
var sources = Array.from(document.querySelectorAll('a[href^="http"][target="_blank"]'))
|
|
99
102
|
.map(a => ({ url: a.href, title: a.innerText?.trim().split('\\n')[0] || a.title || '' }))
|
|
100
103
|
.filter(s => s.url && !s.url.includes('copilot.microsoft.com'))
|
|
101
104
|
.filter((v, i, arr) => arr.findIndex(x => x.url === v.url) === i)
|
|
102
105
|
.slice(0, 10);
|
|
103
|
-
return JSON.stringify(
|
|
106
|
+
return JSON.stringify(sources);
|
|
104
107
|
})()
|
|
105
|
-
`]);
|
|
106
|
-
|
|
108
|
+
`]).catch(() => '[]');
|
|
109
|
+
const sources = JSON.parse(raw);
|
|
110
|
+
|
|
111
|
+
return { answer: answer.trim(), sources };
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
// ---------------------------------------------------------------------------
|
|
@@ -142,6 +147,7 @@ async function main() {
|
|
|
142
147
|
}
|
|
143
148
|
await new Promise(r => setTimeout(r, 300));
|
|
144
149
|
|
|
150
|
+
await injectClipboardInterceptor(tab);
|
|
145
151
|
// Find input and type query
|
|
146
152
|
await cdp(['click', tab, '#userInput']);
|
|
147
153
|
await new Promise(r => setTimeout(r, 400));
|
|
@@ -153,7 +159,7 @@ async function main() {
|
|
|
153
159
|
`document.querySelector('#userInput')?.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true,keyCode:13})), 'ok'`
|
|
154
160
|
]);
|
|
155
161
|
|
|
156
|
-
await
|
|
162
|
+
await waitForCopyButton(tab);
|
|
157
163
|
|
|
158
164
|
const { answer, sources } = await extractAnswer(tab);
|
|
159
165
|
if (!answer) throw new Error('No answer extracted — Copilot may not have responded');
|
package/extractors/gemini.mjs
CHANGED
|
@@ -10,19 +10,17 @@
|
|
|
10
10
|
|
|
11
11
|
import { readFileSync, existsSync } from 'fs';
|
|
12
12
|
import { spawn } from 'child_process';
|
|
13
|
-
import { tmpdir } from 'os';
|
|
13
|
+
import { tmpdir, homedir } from 'os';
|
|
14
14
|
import { join, dirname } from 'path';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
16
|
import { dismissConsent, handleVerification } from './consent.mjs';
|
|
17
17
|
|
|
18
18
|
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
const CDP = join(
|
|
19
|
+
const CDP = join(homedir(), '.claude', 'skills', 'chrome-cdp', 'scripts', 'cdp.mjs');
|
|
20
20
|
const PAGES_CACHE = `${tmpdir().replace(/\\/g, '/')}/cdp-pages.json`;
|
|
21
21
|
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const STREAM_TIMEOUT = 60000;
|
|
25
|
-
const MIN_ANSWER_LENGTH = 20;
|
|
22
|
+
const COPY_POLL_INTERVAL = 600;
|
|
23
|
+
const COPY_TIMEOUT = 120000; // wait up to 2 min for copy button to appear
|
|
26
24
|
|
|
27
25
|
// ---------------------------------------------------------------------------
|
|
28
26
|
|
|
@@ -64,58 +62,66 @@ async function typeIntoGemini(tab, text) {
|
|
|
64
62
|
`]);
|
|
65
63
|
}
|
|
66
64
|
|
|
67
|
-
async function
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
65
|
+
async function injectClipboardInterceptor(tab) {
|
|
66
|
+
// Override both clipboard APIs — Gemini uses clipboard.write(ClipboardItem) for rich copy.
|
|
67
|
+
await cdp(['eval', tab, `
|
|
68
|
+
window.__geminiClipboard = null;
|
|
69
|
+
const _origWriteText = navigator.clipboard.writeText.bind(navigator.clipboard);
|
|
70
|
+
navigator.clipboard.writeText = function(text) {
|
|
71
|
+
window.__geminiClipboard = text;
|
|
72
|
+
return _origWriteText(text);
|
|
73
|
+
};
|
|
74
|
+
const _origWrite = navigator.clipboard.write.bind(navigator.clipboard);
|
|
75
|
+
navigator.clipboard.write = async function(items) {
|
|
76
|
+
try {
|
|
77
|
+
for (const item of items) {
|
|
78
|
+
if (item.types && item.types.includes('text/plain')) {
|
|
79
|
+
const blob = await item.getType('text/plain');
|
|
80
|
+
window.__geminiClipboard = await blob.text();
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch(e) {}
|
|
85
|
+
return _origWrite(items);
|
|
86
|
+
};
|
|
87
|
+
`]);
|
|
88
|
+
}
|
|
73
89
|
|
|
90
|
+
async function waitForCopyButton(tab) {
|
|
91
|
+
// The "Copy response" button appears only after streaming is complete.
|
|
92
|
+
const deadline = Date.now() + COPY_TIMEOUT;
|
|
74
93
|
while (Date.now() < deadline) {
|
|
75
|
-
await new Promise(r => setTimeout(r,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
`!!document.querySelector('button[aria-label*="Stop"]')`
|
|
94
|
+
await new Promise(r => setTimeout(r, COPY_POLL_INTERVAL));
|
|
95
|
+
const found = await cdp(['eval', tab,
|
|
96
|
+
`!!document.querySelector('button[aria-label="Copy"]')`
|
|
79
97
|
]).catch(() => 'false');
|
|
80
|
-
|
|
81
|
-
if (stopVisible === 'true') {
|
|
82
|
-
streamingStarted = true;
|
|
83
|
-
} else if (streamingStarted) {
|
|
84
|
-
// Stop button gone — streaming finished. Confirm with stable text length.
|
|
85
|
-
const lenStr = await cdp(['eval', tab,
|
|
86
|
-
`(function(){var els=document.querySelectorAll('model-response .markdown');var last=els[els.length-1];return (last?.innerText?.length||0)+''})()`
|
|
87
|
-
]).catch(() => '0');
|
|
88
|
-
const len = parseInt(lenStr) || 0;
|
|
89
|
-
if (len >= MIN_ANSWER_LENGTH && len === lastLen) {
|
|
90
|
-
stableCount++;
|
|
91
|
-
if (stableCount >= STREAM_STABLE_ROUNDS) return len;
|
|
92
|
-
} else {
|
|
93
|
-
stableCount = 0;
|
|
94
|
-
lastLen = len;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
98
|
+
if (found === 'true') return;
|
|
97
99
|
}
|
|
98
|
-
|
|
99
|
-
if (lastLen >= MIN_ANSWER_LENGTH) return lastLen;
|
|
100
|
-
throw new Error(`Gemini answer did not stabilise within ${STREAM_TIMEOUT}ms`);
|
|
100
|
+
throw new Error(`Gemini copy button did not appear within ${COPY_TIMEOUT}ms`);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
async function extractAnswer(tab) {
|
|
104
|
+
// Click copy button → our interceptor captures the text.
|
|
105
|
+
await cdp(['eval', tab, `document.querySelector('button[aria-label="Copy"]')?.click()`]);
|
|
106
|
+
await new Promise(r => setTimeout(r, 400));
|
|
107
|
+
|
|
108
|
+
const answer = await cdp(['eval', tab, `window.__geminiClipboard || ''`]);
|
|
109
|
+
if (!answer) throw new Error('Clipboard interceptor returned empty text');
|
|
110
|
+
|
|
111
|
+
// Sources: links rendered in the page (best-effort; Shadow DOM may hide some)
|
|
104
112
|
const raw = await cdp(['eval', tab, `
|
|
105
113
|
(function() {
|
|
106
|
-
var
|
|
107
|
-
var last = els[els.length - 1];
|
|
108
|
-
if (!last) return JSON.stringify({ answer: '', sources: [] });
|
|
109
|
-
var answer = last.innerText.trim();
|
|
110
|
-
var sources = Array.from(document.querySelectorAll('model-response a[href^="http"]'))
|
|
114
|
+
var sources = Array.from(document.querySelectorAll('a[href^="http"]'))
|
|
111
115
|
.map(a => ({ url: a.href.split('#')[0], title: a.innerText?.trim().split('\\n')[0] || '' }))
|
|
112
|
-
.filter(s => s.url && !s.url.includes('gemini.google') && !s.url.includes('gstatic'))
|
|
116
|
+
.filter(s => s.url && !s.url.includes('gemini.google') && !s.url.includes('gstatic') && !s.url.includes('google.com/search'))
|
|
113
117
|
.filter((v, i, arr) => arr.findIndex(x => x.url === v.url) === i)
|
|
114
118
|
.slice(0, 8);
|
|
115
|
-
return JSON.stringify(
|
|
119
|
+
return JSON.stringify(sources);
|
|
116
120
|
})()
|
|
117
|
-
`]);
|
|
118
|
-
|
|
121
|
+
`]).catch(() => '[]');
|
|
122
|
+
const sources = JSON.parse(raw);
|
|
123
|
+
|
|
124
|
+
return { answer: answer.trim(), sources };
|
|
119
125
|
}
|
|
120
126
|
|
|
121
127
|
// ---------------------------------------------------------------------------
|
|
@@ -154,15 +160,16 @@ async function main() {
|
|
|
154
160
|
}
|
|
155
161
|
await new Promise(r => setTimeout(r, 300));
|
|
156
162
|
|
|
163
|
+
await injectClipboardInterceptor(tab);
|
|
157
164
|
await typeIntoGemini(tab, query);
|
|
158
165
|
await new Promise(r => setTimeout(r, 400));
|
|
159
166
|
|
|
160
167
|
await cdp(['eval', tab, `document.querySelector('button[aria-label*="Send"]')?.click()`]);
|
|
161
168
|
|
|
162
|
-
await
|
|
169
|
+
await waitForCopyButton(tab);
|
|
163
170
|
|
|
164
171
|
const { answer, sources } = await extractAnswer(tab);
|
|
165
|
-
if (!answer) throw new Error('No answer
|
|
172
|
+
if (!answer) throw new Error('No answer captured from Gemini clipboard');
|
|
166
173
|
const out = short ? answer.slice(0, 300).replace(/\s+\S*$/, '') + '…' : answer;
|
|
167
174
|
|
|
168
175
|
const finalUrl = await cdp(['eval', tab, 'document.location.href']).catch(() => 'https://gemini.google.com/app');
|
|
@@ -11,17 +11,14 @@
|
|
|
11
11
|
import { readFileSync, existsSync } from 'fs';
|
|
12
12
|
import { spawn } from 'child_process';
|
|
13
13
|
import { tmpdir, homedir } from 'os';
|
|
14
|
-
import { join
|
|
15
|
-
import { fileURLToPath } from 'url';
|
|
14
|
+
import { join } from 'path';
|
|
16
15
|
import { dismissConsent } from './consent.mjs';
|
|
17
16
|
|
|
18
|
-
const CDP = join(
|
|
17
|
+
const CDP = join(homedir(), '.claude', 'skills', 'chrome-cdp', 'scripts', 'cdp.mjs');
|
|
19
18
|
const PAGES_CACHE = `${tmpdir().replace(/\\/g, '/')}/cdp-pages.json`;
|
|
20
19
|
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const STREAM_TIMEOUT = 30000; // bail out after 30s regardless
|
|
24
|
-
const MIN_ANSWER_LENGTH = 50; // don't accept trivial answers
|
|
20
|
+
const COPY_POLL_INTERVAL = 600;
|
|
21
|
+
const COPY_TIMEOUT = 30000;
|
|
25
22
|
|
|
26
23
|
// ---------------------------------------------------------------------------
|
|
27
24
|
|
|
@@ -59,49 +56,62 @@ async function getOrOpenTab(tabPrefix) {
|
|
|
59
56
|
return firstLine.slice(0, 8);
|
|
60
57
|
}
|
|
61
58
|
|
|
62
|
-
async function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
async function injectClipboardInterceptor(tab) {
|
|
60
|
+
await cdp(['eval', tab, `
|
|
61
|
+
window.__pplxClipboard = null;
|
|
62
|
+
const _origWriteText = navigator.clipboard.writeText.bind(navigator.clipboard);
|
|
63
|
+
navigator.clipboard.writeText = function(text) {
|
|
64
|
+
window.__pplxClipboard = text;
|
|
65
|
+
return _origWriteText(text);
|
|
66
|
+
};
|
|
67
|
+
const _origWrite = navigator.clipboard.write.bind(navigator.clipboard);
|
|
68
|
+
navigator.clipboard.write = async function(items) {
|
|
69
|
+
try {
|
|
70
|
+
for (const item of items) {
|
|
71
|
+
if (item.types && item.types.includes('text/plain')) {
|
|
72
|
+
const blob = await item.getType('text/plain');
|
|
73
|
+
window.__pplxClipboard = await blob.text();
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch(e) {}
|
|
78
|
+
return _origWrite(items);
|
|
79
|
+
};
|
|
80
|
+
`]);
|
|
81
|
+
}
|
|
66
82
|
|
|
83
|
+
async function waitForCopyButton(tab) {
|
|
84
|
+
const deadline = Date.now() + COPY_TIMEOUT;
|
|
67
85
|
while (Date.now() < deadline) {
|
|
68
|
-
await new Promise(r => setTimeout(r,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const len = parseInt(lenStr) || 0;
|
|
75
|
-
|
|
76
|
-
if (len >= MIN_ANSWER_LENGTH && len === lastLen) {
|
|
77
|
-
stableCount++;
|
|
78
|
-
if (stableCount >= STREAM_STABLE_ROUNDS) return len;
|
|
79
|
-
} else {
|
|
80
|
-
stableCount = 0;
|
|
81
|
-
lastLen = len;
|
|
82
|
-
}
|
|
86
|
+
await new Promise(r => setTimeout(r, COPY_POLL_INTERVAL));
|
|
87
|
+
const found = await cdp(['eval', tab,
|
|
88
|
+
`!!document.querySelector('button[aria-label="Copy"]')`
|
|
89
|
+
]).catch(() => 'false');
|
|
90
|
+
if (found === 'true') return;
|
|
83
91
|
}
|
|
84
|
-
|
|
85
|
-
// Timeout — return whatever we have if it meets minimum length
|
|
86
|
-
if (lastLen >= MIN_ANSWER_LENGTH) return lastLen;
|
|
87
|
-
throw new Error(`Perplexity answer did not stabilise within ${STREAM_TIMEOUT}ms`);
|
|
92
|
+
throw new Error(`Perplexity copy button did not appear within ${COPY_TIMEOUT}ms`);
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
async function extractAnswer(tab) {
|
|
96
|
+
await cdp(['eval', tab, `document.querySelector('button[aria-label="Copy"]')?.click()`]);
|
|
97
|
+
await new Promise(r => setTimeout(r, 400));
|
|
98
|
+
|
|
99
|
+
const answer = await cdp(['eval', tab, `window.__pplxClipboard || ''`]);
|
|
100
|
+
if (!answer) throw new Error('Clipboard interceptor returned empty text');
|
|
101
|
+
|
|
91
102
|
const raw = await cdp(['eval', tab, `
|
|
92
103
|
(function() {
|
|
93
|
-
var prose = document.querySelector('.prose');
|
|
94
|
-
if (!prose) return JSON.stringify({ answer: '', sources: [] });
|
|
95
|
-
var answer = prose.innerText.trim();
|
|
96
104
|
var sources = Array.from(document.querySelectorAll('[data-pplx-citation-url]'))
|
|
97
105
|
.map(el => ({ url: el.getAttribute('data-pplx-citation-url'), title: el.querySelector('a')?.innerText?.trim() || '' }))
|
|
98
106
|
.filter(s => s.url)
|
|
99
107
|
.filter((v, i, arr) => arr.findIndex(x => x.url === v.url) === i)
|
|
100
108
|
.slice(0, 10);
|
|
101
|
-
return JSON.stringify(
|
|
109
|
+
return JSON.stringify(sources);
|
|
102
110
|
})()
|
|
103
|
-
`]);
|
|
104
|
-
|
|
111
|
+
`]).catch(() => '[]');
|
|
112
|
+
const sources = JSON.parse(raw);
|
|
113
|
+
|
|
114
|
+
return { answer: answer.trim(), sources };
|
|
105
115
|
}
|
|
106
116
|
|
|
107
117
|
// ---------------------------------------------------------------------------
|
|
@@ -141,6 +151,7 @@ async function main() {
|
|
|
141
151
|
}
|
|
142
152
|
await new Promise(r => setTimeout(r, 300));
|
|
143
153
|
|
|
154
|
+
await injectClipboardInterceptor(tab);
|
|
144
155
|
await cdp(['click', tab, '#ask-input']);
|
|
145
156
|
await new Promise(r => setTimeout(r, 400));
|
|
146
157
|
await cdp(['type', tab, query]);
|
|
@@ -150,10 +161,8 @@ async function main() {
|
|
|
150
161
|
`document.querySelector('#ask-input')?.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true,keyCode:13})), 'ok'`
|
|
151
162
|
]);
|
|
152
163
|
|
|
153
|
-
|
|
154
|
-
await waitForStreamComplete(tab);
|
|
164
|
+
await waitForCopyButton(tab);
|
|
155
165
|
|
|
156
|
-
// Extract
|
|
157
166
|
const { answer, sources } = await extractAnswer(tab);
|
|
158
167
|
|
|
159
168
|
if (!answer) throw new Error('No answer extracted — Perplexity may not have responded');
|
package/package.json
CHANGED