@apmantza/greedysearch-pi 1.8.2 → 1.8.4

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,146 +1,146 @@
1
- #!/usr/bin/env node
2
-
3
- // extractors/gemini.mjs
4
- // Navigate gemini.google.com/app, submit query, wait for answer, return clean answer + sources.
5
- //
6
- // Usage:
7
- // node extractors/gemini.mjs "<query>" [--tab <prefix>]
8
- //
9
- // Output (stdout): JSON { answer, sources, query, url }
10
- // Errors go to stderr only — stdout is always clean JSON for piping.
11
-
12
- import {
13
- cdp,
14
- formatAnswer,
15
- getOrOpenTab,
16
- handleError,
17
- injectClipboardInterceptor,
18
- outputJson,
19
- parseArgs,
20
- parseSourcesFromMarkdown,
21
- TIMING,
22
- validateQuery,
23
- waitForCopyButton,
24
- } from "./common.mjs";
25
- import { dismissConsent, handleVerification } from "./consent.mjs";
26
- import { SELECTORS } from "./selectors.mjs";
27
-
28
- const S = SELECTORS.gemini;
29
- const GLOBAL_VAR = "__geminiClipboard";
30
-
31
- // ============================================================================
32
- // Gemini-specific helpers
33
- // ============================================================================
34
-
35
- async function typeIntoGemini(tab, text) {
36
- await cdp([
37
- "eval",
38
- tab,
39
- `
40
- (function(t) {
41
- var el = document.querySelector('${S.input}');
42
- if (!el) return false;
43
- el.focus();
44
- document.execCommand('insertText', false, t);
45
- return true;
46
- })(${JSON.stringify(text)})
47
- `,
48
- ]);
49
- }
50
-
51
- async function scrollToBottom(tab) {
52
- await cdp([
53
- "eval",
54
- tab,
55
- `(function() {
56
- const chat = document.querySelector('chat-window, [role="main"], main') || document.body;
57
- chat.scrollTo ? chat.scrollTo({ top: chat.scrollHeight, behavior: 'smooth' }) : window.scrollTo(0, document.body.scrollHeight);
58
- })()`,
59
- ]);
60
- }
61
-
62
- async function extractAnswer(tab) {
63
- await cdp([
64
- "eval",
65
- tab,
66
- `document.querySelector('${S.copyButton}')?.click()`,
67
- ]);
68
- await new Promise((r) => setTimeout(r, 400));
69
-
70
- const answer = await cdp(["eval", tab, `window.${GLOBAL_VAR} || ''`]);
71
- if (!answer) throw new Error("Clipboard interceptor returned empty text");
72
-
73
- const sources = parseSourcesFromMarkdown(answer);
74
- return { answer: answer.trim(), sources };
75
- }
76
-
77
- // ============================================================================
78
- // Main
79
- // ============================================================================
80
-
81
- const USAGE = 'Usage: node extractors/gemini.mjs "<query>" [--tab <prefix>]\n';
82
-
83
- async function main() {
84
- const args = process.argv.slice(2);
85
- validateQuery(args, USAGE);
86
-
87
- const { query, tabPrefix, short } = parseArgs(args);
88
-
89
- try {
90
- await cdp(["list"]);
91
- const tab = await getOrOpenTab(tabPrefix);
92
-
93
- // Each search = fresh conversation
94
- await cdp(["nav", tab, "https://gemini.google.com/app"], 35000);
95
- await new Promise((r) => setTimeout(r, TIMING.postNavSlow));
96
- await dismissConsent(tab, cdp);
97
- await handleVerification(tab, cdp, 60000);
98
-
99
- // Wait for input to be ready
100
- const deadline = Date.now() + 10000;
101
- while (Date.now() < deadline) {
102
- const ready = await cdp([
103
- "eval",
104
- tab,
105
- `!!document.querySelector('${S.input}')`,
106
- ]).catch(() => "false");
107
- if (ready === "true") break;
108
- await new Promise((r) => setTimeout(r, TIMING.inputPoll));
109
- }
110
- await new Promise((r) => setTimeout(r, TIMING.postClick));
111
-
112
- await injectClipboardInterceptor(tab, GLOBAL_VAR);
113
- await typeIntoGemini(tab, query);
114
- await new Promise((r) => setTimeout(r, TIMING.postType));
115
-
116
- await cdp([
117
- "eval",
118
- tab,
119
- `document.querySelector('${S.sendButton}')?.click()`,
120
- ]);
121
-
122
- // Scroll to bottom every ~6s while waiting to trigger lazy-loaded content
123
- await waitForCopyButton(tab, S.copyButton, {
124
- timeout: 120000,
125
- onPoll: (tick) =>
126
- tick % 10 === 0 ? scrollToBottom(tab) : Promise.resolve(),
127
- });
128
-
129
- const { answer, sources } = await extractAnswer(tab);
130
- if (!answer) throw new Error("No answer captured from Gemini clipboard");
131
-
132
- const finalUrl = await cdp(["eval", tab, "document.location.href"]).catch(
133
- () => "https://gemini.google.com/app",
134
- );
135
- outputJson({
136
- query,
137
- url: finalUrl,
138
- answer: formatAnswer(answer, short),
139
- sources,
140
- });
141
- } catch (e) {
142
- handleError(e);
143
- }
144
- }
145
-
146
- main();
1
+ #!/usr/bin/env node
2
+
3
+ // extractors/gemini.mjs
4
+ // Navigate gemini.google.com/app, submit query, wait for answer, return clean answer + sources.
5
+ //
6
+ // Usage:
7
+ // node extractors/gemini.mjs "<query>" [--tab <prefix>]
8
+ //
9
+ // Output (stdout): JSON { answer, sources, query, url }
10
+ // Errors go to stderr only — stdout is always clean JSON for piping.
11
+
12
+ import {
13
+ cdp,
14
+ formatAnswer,
15
+ getOrOpenTab,
16
+ handleError,
17
+ injectClipboardInterceptor,
18
+ outputJson,
19
+ parseArgs,
20
+ parseSourcesFromMarkdown,
21
+ TIMING,
22
+ validateQuery,
23
+ waitForCopyButton,
24
+ } from "./common.mjs";
25
+ import { dismissConsent, handleVerification } from "./consent.mjs";
26
+ import { SELECTORS } from "./selectors.mjs";
27
+
28
+ const S = SELECTORS.gemini;
29
+ const GLOBAL_VAR = "__geminiClipboard";
30
+
31
+ // ============================================================================
32
+ // Gemini-specific helpers
33
+ // ============================================================================
34
+
35
+ async function typeIntoGemini(tab, text) {
36
+ await cdp([
37
+ "eval",
38
+ tab,
39
+ `
40
+ (function(t) {
41
+ var el = document.querySelector('${S.input}');
42
+ if (!el) return false;
43
+ el.focus();
44
+ document.execCommand('insertText', false, t);
45
+ return true;
46
+ })(${JSON.stringify(text)})
47
+ `,
48
+ ]);
49
+ }
50
+
51
+ async function scrollToBottom(tab) {
52
+ await cdp([
53
+ "eval",
54
+ tab,
55
+ `(function() {
56
+ const chat = document.querySelector('chat-window, [role="main"], main') || document.body;
57
+ chat.scrollTo ? chat.scrollTo({ top: chat.scrollHeight, behavior: 'smooth' }) : window.scrollTo(0, document.body.scrollHeight);
58
+ })()`,
59
+ ]);
60
+ }
61
+
62
+ async function extractAnswer(tab) {
63
+ await cdp([
64
+ "eval",
65
+ tab,
66
+ `document.querySelector('${S.copyButton}')?.click()`,
67
+ ]);
68
+ await new Promise((r) => setTimeout(r, 400));
69
+
70
+ const answer = await cdp(["eval", tab, `window.${GLOBAL_VAR} || ''`]);
71
+ if (!answer) throw new Error("Clipboard interceptor returned empty text");
72
+
73
+ const sources = parseSourcesFromMarkdown(answer);
74
+ return { answer: answer.trim(), sources };
75
+ }
76
+
77
+ // ============================================================================
78
+ // Main
79
+ // ============================================================================
80
+
81
+ const USAGE = 'Usage: node extractors/gemini.mjs "<query>" [--tab <prefix>]\n';
82
+
83
+ async function main() {
84
+ const args = process.argv.slice(2);
85
+ validateQuery(args, USAGE);
86
+
87
+ const { query, tabPrefix, short } = parseArgs(args);
88
+
89
+ try {
90
+ await cdp(["list"]);
91
+ const tab = await getOrOpenTab(tabPrefix);
92
+
93
+ // Each search = fresh conversation
94
+ await cdp(["nav", tab, "https://gemini.google.com/app"], 35000);
95
+ await new Promise((r) => setTimeout(r, TIMING.postNavSlow));
96
+ await dismissConsent(tab, cdp);
97
+ await handleVerification(tab, cdp, 60000);
98
+
99
+ // Wait for input to be ready
100
+ const deadline = Date.now() + 10000;
101
+ while (Date.now() < deadline) {
102
+ const ready = await cdp([
103
+ "eval",
104
+ tab,
105
+ `!!document.querySelector('${S.input}')`,
106
+ ]).catch(() => "false");
107
+ if (ready === "true") break;
108
+ await new Promise((r) => setTimeout(r, TIMING.inputPoll));
109
+ }
110
+ await new Promise((r) => setTimeout(r, TIMING.postClick));
111
+
112
+ await injectClipboardInterceptor(tab, GLOBAL_VAR);
113
+ await typeIntoGemini(tab, query);
114
+ await new Promise((r) => setTimeout(r, TIMING.postType));
115
+
116
+ await cdp([
117
+ "eval",
118
+ tab,
119
+ `document.querySelector('${S.sendButton}')?.click()`,
120
+ ]);
121
+
122
+ // Scroll to bottom every ~6s while waiting to trigger lazy-loaded content
123
+ await waitForCopyButton(tab, S.copyButton, {
124
+ timeout: 120000,
125
+ onPoll: (tick) =>
126
+ tick % 10 === 0 ? scrollToBottom(tab) : Promise.resolve(),
127
+ });
128
+
129
+ const { answer, sources } = await extractAnswer(tab);
130
+ if (!answer) throw new Error("No answer captured from Gemini clipboard");
131
+
132
+ const finalUrl = await cdp(["eval", tab, "document.location.href"]).catch(
133
+ () => "https://gemini.google.com/app",
134
+ );
135
+ outputJson({
136
+ query,
137
+ url: finalUrl,
138
+ answer: formatAnswer(answer, short),
139
+ sources,
140
+ });
141
+ } catch (e) {
142
+ handleError(e);
143
+ }
144
+ }
145
+
146
+ main();
@@ -1,125 +1,125 @@
1
- #!/usr/bin/env node
2
-
3
- // extractors/google-ai.mjs
4
- // Navigate Google AI Mode (udm=50), wait for answer, return clean answer + sources.
5
- //
6
- // Usage:
7
- // node extractors/google-ai.mjs "<query>" [--tab <prefix>]
8
- //
9
- // Output (stdout): JSON { answer, sources, query, url }
10
- // Errors go to stderr only — stdout is always clean JSON for piping.
11
-
12
- import {
13
- cdp,
14
- formatAnswer,
15
- getOrOpenTab,
16
- handleError,
17
- outputJson,
18
- parseArgs,
19
- TIMING,
20
- validateQuery,
21
- waitForStreamComplete,
22
- } from "./common.mjs";
23
- import { dismissConsent, handleVerification } from "./consent.mjs";
24
- import { SELECTORS } from "./selectors.mjs";
25
-
26
- const S = SELECTORS.google;
27
-
28
- const MIN_ANSWER_LENGTH = 50;
29
-
30
- async function extractAnswer(tab) {
31
- const excludeFilter = S.sourceExclude
32
- .map((e) => `!a.href.includes('${e}')`)
33
- .join(" && ");
34
- const raw = await cdp([
35
- "eval",
36
- tab,
37
- `
38
- (function() {
39
- var el = document.querySelector('${S.answerContainer}');
40
- if (!el) return JSON.stringify({ answer: '', sources: [] });
41
- var answer = el.innerText.trim();
42
- var sources = Array.from(document.querySelectorAll('${S.sourceLink}'))
43
- .filter(a => ${excludeFilter})
44
- .map(a => ({ url: a.href.split('#')[0], title: (a.closest('${S.sourceHeadingParent}')?.querySelector('h3, [role=heading]')?.innerText || a.innerText?.trim().split('\\n')[0] || '').slice(0, 100) }))
45
- .filter(s => s.url && s.url.length > 10)
46
- .filter((v, i, arr) => arr.findIndex(x => x.url === v.url) === i)
47
- .slice(0, 10);
48
- return JSON.stringify({ answer, sources });
49
- })()
50
- `,
51
- ]);
52
- return JSON.parse(raw);
53
- }
54
-
55
- // ============================================================================
56
- // Main
57
- // ============================================================================
58
-
59
- const USAGE =
60
- 'Usage: node extractors/google-ai.mjs "<query>" [--tab <prefix>]\n';
61
-
62
- async function main() {
63
- const args = process.argv.slice(2);
64
- validateQuery(args, USAGE);
65
-
66
- const { query, tabPrefix, short, locale } = parseArgs(args);
67
-
68
- try {
69
- await cdp(["list"]);
70
- const tab = await getOrOpenTab(tabPrefix);
71
-
72
- // Build URL with language parameter (default to English)
73
- const langParam = locale ? `&hl=${encodeURIComponent(locale)}` : "&hl=en";
74
- const url = `https://www.google.com/search?q=${encodeURIComponent(query)}&udm=50${langParam}`;
75
- await new Promise((r) => setTimeout(r, TIMING.postNav));
76
- await dismissConsent(tab, cdp);
77
-
78
- // If consent redirected us away, navigate back
79
- const currentUrl = await cdp(["eval", tab, "document.location.href"]).catch(
80
- () => "",
81
- );
82
- if (!currentUrl.includes("google.com/search")) {
83
- await cdp(["nav", tab, url], 35000);
84
- await new Promise((r) => setTimeout(r, TIMING.postNav));
85
- }
86
-
87
- // Handle "verify you're human" — auto-click simple buttons, wait for user on hard CAPTCHA
88
- const verifyResult = await handleVerification(tab, cdp, 60000);
89
- if (verifyResult === "needs-human")
90
- throw new Error(
91
- "Google verification required — could not be completed automatically",
92
- );
93
- if (verifyResult === "clicked" || verifyResult === "cleared-by-user") {
94
- // Re-navigate to the search URL after verification
95
- await cdp(["nav", tab, url], 35000);
96
- await new Promise((r) => setTimeout(r, TIMING.postNav));
97
- }
98
-
99
- await waitForStreamComplete(tab, {
100
- timeout: 45000,
101
- selector: `document.querySelector('${S.answerContainer}')`,
102
- minLength: MIN_ANSWER_LENGTH,
103
- });
104
-
105
- const { answer, sources } = await extractAnswer(tab);
106
- if (!answer)
107
- throw new Error(
108
- "No answer extracted — Google AI Mode may not have responded",
109
- );
110
-
111
- const finalUrl = await cdp(["eval", tab, "document.location.href"]).catch(
112
- () => url,
113
- );
114
- outputJson({
115
- query,
116
- url: finalUrl,
117
- answer: formatAnswer(answer, short),
118
- sources,
119
- });
120
- } catch (e) {
121
- handleError(e);
122
- }
123
- }
124
-
125
- main();
1
+ #!/usr/bin/env node
2
+
3
+ // extractors/google-ai.mjs
4
+ // Navigate Google AI Mode (udm=50), wait for answer, return clean answer + sources.
5
+ //
6
+ // Usage:
7
+ // node extractors/google-ai.mjs "<query>" [--tab <prefix>]
8
+ //
9
+ // Output (stdout): JSON { answer, sources, query, url }
10
+ // Errors go to stderr only — stdout is always clean JSON for piping.
11
+
12
+ import {
13
+ cdp,
14
+ formatAnswer,
15
+ getOrOpenTab,
16
+ handleError,
17
+ outputJson,
18
+ parseArgs,
19
+ TIMING,
20
+ validateQuery,
21
+ waitForStreamComplete,
22
+ } from "./common.mjs";
23
+ import { dismissConsent, handleVerification } from "./consent.mjs";
24
+ import { SELECTORS } from "./selectors.mjs";
25
+
26
+ const S = SELECTORS.google;
27
+
28
+ const MIN_ANSWER_LENGTH = 50;
29
+
30
+ async function extractAnswer(tab) {
31
+ const excludeFilter = S.sourceExclude
32
+ .map((e) => `!a.href.includes('${e}')`)
33
+ .join(" && ");
34
+ const raw = await cdp([
35
+ "eval",
36
+ tab,
37
+ `
38
+ (function() {
39
+ var el = document.querySelector('${S.answerContainer}');
40
+ if (!el) return JSON.stringify({ answer: '', sources: [] });
41
+ var answer = el.innerText.trim();
42
+ var sources = Array.from(document.querySelectorAll('${S.sourceLink}'))
43
+ .filter(a => ${excludeFilter})
44
+ .map(a => ({ url: a.href.split('#')[0], title: (a.closest('${S.sourceHeadingParent}')?.querySelector('h3, [role=heading]')?.innerText || a.innerText?.trim().split('\\n')[0] || '').slice(0, 100) }))
45
+ .filter(s => s.url && s.url.length > 10)
46
+ .filter((v, i, arr) => arr.findIndex(x => x.url === v.url) === i)
47
+ .slice(0, 10);
48
+ return JSON.stringify({ answer, sources });
49
+ })()
50
+ `,
51
+ ]);
52
+ return JSON.parse(raw);
53
+ }
54
+
55
+ // ============================================================================
56
+ // Main
57
+ // ============================================================================
58
+
59
+ const USAGE =
60
+ 'Usage: node extractors/google-ai.mjs "<query>" [--tab <prefix>]\n';
61
+
62
+ async function main() {
63
+ const args = process.argv.slice(2);
64
+ validateQuery(args, USAGE);
65
+
66
+ const { query, tabPrefix, short, locale } = parseArgs(args);
67
+
68
+ try {
69
+ await cdp(["list"]);
70
+ const tab = await getOrOpenTab(tabPrefix);
71
+
72
+ // Build URL with language parameter (default to English)
73
+ const langParam = locale ? `&hl=${encodeURIComponent(locale)}` : "&hl=en";
74
+ const url = `https://www.google.com/search?q=${encodeURIComponent(query)}&udm=50${langParam}`;
75
+ await new Promise((r) => setTimeout(r, TIMING.postNav));
76
+ await dismissConsent(tab, cdp);
77
+
78
+ // If consent redirected us away, navigate back
79
+ const currentUrl = await cdp(["eval", tab, "document.location.href"]).catch(
80
+ () => "",
81
+ );
82
+ if (!currentUrl.includes("google.com/search")) {
83
+ await cdp(["nav", tab, url], 35000);
84
+ await new Promise((r) => setTimeout(r, TIMING.postNav));
85
+ }
86
+
87
+ // Handle "verify you're human" — auto-click simple buttons, wait for user on hard CAPTCHA
88
+ const verifyResult = await handleVerification(tab, cdp, 60000);
89
+ if (verifyResult === "needs-human")
90
+ throw new Error(
91
+ "Google verification required — could not be completed automatically",
92
+ );
93
+ if (verifyResult === "clicked" || verifyResult === "cleared-by-user") {
94
+ // Re-navigate to the search URL after verification
95
+ await cdp(["nav", tab, url], 35000);
96
+ await new Promise((r) => setTimeout(r, TIMING.postNav));
97
+ }
98
+
99
+ await waitForStreamComplete(tab, {
100
+ timeout: 45000,
101
+ selector: `document.querySelector('${S.answerContainer}')`,
102
+ minLength: MIN_ANSWER_LENGTH,
103
+ });
104
+
105
+ const { answer, sources } = await extractAnswer(tab);
106
+ if (!answer)
107
+ throw new Error(
108
+ "No answer extracted — Google AI Mode may not have responded",
109
+ );
110
+
111
+ const finalUrl = await cdp(["eval", tab, "document.location.href"]).catch(
112
+ () => url,
113
+ );
114
+ outputJson({
115
+ query,
116
+ url: finalUrl,
117
+ answer: formatAnswer(answer, short),
118
+ sources,
119
+ });
120
+ } catch (e) {
121
+ handleError(e);
122
+ }
123
+ }
124
+
125
+ main();