@duckmind/dm-windows-x64 0.62.3 → 0.63.0
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/dm.exe +0 -0
- package/extensions/.dm-extensions.json +43 -183
- package/extensions/dm-context/src/context.js +1 -2
- package/extensions/dm-context/src/index.js +1 -2
- package/extensions/{greedysearch-dm/bin/cdp.mjs → dm-cua/bin/browser-cdp.mjs} +3 -3
- package/extensions/dm-cua/bin/browser-cua.mjs +1 -1
- package/extensions/{greedysearch-dm/bin/launch.mjs → dm-cua/bin/browser-launch.mjs} +12 -11
- package/extensions/dm-cua/index.js +1 -1
- package/extensions/dm-cua/src/browser-cua-lib.mjs +17 -12
- package/extensions/{greedysearch-dm/src/search → dm-cua/src/browser-helpers}/constants.mjs +11 -11
- package/extensions/{greedysearch-dm/src/utils → dm-cua/src/browser-helpers}/node-runtime.mjs +1 -1
- package/extensions/{greedysearch-dm/src/search → dm-cua/src/browser-helpers}/port-pid.mjs +1 -1
- package/extensions/dm-ponytail/extensions/ponytail.mjs +186 -0
- package/extensions/dm-ponytail/hooks/claude-codex-hooks.json +41 -0
- package/extensions/dm-ponytail/hooks/copilot-hooks.json +21 -0
- package/extensions/dm-ponytail/hooks/ponytail-activate.js +59 -0
- package/extensions/dm-ponytail/hooks/ponytail-config.js +116 -0
- package/extensions/dm-ponytail/hooks/ponytail-instructions.js +81 -0
- package/extensions/dm-ponytail/hooks/ponytail-mode-tracker.js +96 -0
- package/extensions/dm-ponytail/hooks/ponytail-runtime.js +79 -0
- package/extensions/dm-ponytail/hooks/ponytail-statusline.ps1 +24 -0
- package/extensions/dm-ponytail/hooks/ponytail-statusline.sh +18 -0
- package/extensions/dm-ponytail/hooks/ponytail-subagent.js +50 -0
- package/extensions/dm-ponytail/hooks/qoder-hooks.json +26 -0
- package/extensions/dm-ponytail/package.json +19 -0
- package/extensions/dm-ponytail/pi-extension/index.js +186 -0
- package/extensions/dm-ponytail/pi-extension/package.json +8 -0
- package/extensions/dm-ponytail/skills/ponytail/SKILL.md +120 -0
- package/extensions/dm-ponytail/skills/ponytail-audit/SKILL.md +41 -0
- package/extensions/dm-ponytail/skills/ponytail-debt/SKILL.md +44 -0
- package/extensions/dm-ponytail/skills/ponytail-gain/SKILL.md +50 -0
- package/extensions/dm-ponytail/skills/ponytail-help/SKILL.md +71 -0
- package/extensions/dm-ponytail/skills/ponytail-review/SKILL.md +57 -0
- package/package.json +2 -6
- package/extensions/dm-caveman/extensions/caveman.js +0 -300
- package/extensions/dm-caveman/package.json +0 -20
- package/extensions/dm-goal/package.json +0 -23
- package/extensions/dm-goal/src/goal.js +0 -914
- package/extensions/greedysearch-dm/bin/cdp-greedy.mjs +0 -40
- package/extensions/greedysearch-dm/bin/cdp-headless.mjs +0 -5
- package/extensions/greedysearch-dm/bin/cdp-visible.mjs +0 -5
- package/extensions/greedysearch-dm/bin/gschrome.mjs +0 -30
- package/extensions/greedysearch-dm/bin/kill-visible.mjs +0 -7
- package/extensions/greedysearch-dm/bin/launch-visible.mjs +0 -13
- package/extensions/greedysearch-dm/bin/mcp.mjs +0 -400
- package/extensions/greedysearch-dm/bin/search.mjs +0 -635
- package/extensions/greedysearch-dm/bin/visible.mjs +0 -22
- package/extensions/greedysearch-dm/extractors/bing-copilot.mjs +0 -344
- package/extensions/greedysearch-dm/extractors/chatgpt.mjs +0 -354
- package/extensions/greedysearch-dm/extractors/common.mjs +0 -794
- package/extensions/greedysearch-dm/extractors/consensus.mjs +0 -527
- package/extensions/greedysearch-dm/extractors/consent.mjs +0 -449
- package/extensions/greedysearch-dm/extractors/gemini.mjs +0 -291
- package/extensions/greedysearch-dm/extractors/google-ai.mjs +0 -86
- package/extensions/greedysearch-dm/extractors/logically.mjs +0 -589
- package/extensions/greedysearch-dm/extractors/perplexity.mjs +0 -370
- package/extensions/greedysearch-dm/extractors/selectors.mjs +0 -32
- package/extensions/greedysearch-dm/extractors/semantic-scholar.mjs +0 -199
- package/extensions/greedysearch-dm/index.js +0 -123
- package/extensions/greedysearch-dm/package.json +0 -27
- package/extensions/greedysearch-dm/skills/greedy-search/skill.md +0 -18
- package/extensions/greedysearch-dm/src/fetcher.mjs +0 -577
- package/extensions/greedysearch-dm/src/formatters/results.js +0 -95
- package/extensions/greedysearch-dm/src/formatters/sources.js +0 -57
- package/extensions/greedysearch-dm/src/formatters/synthesis.js +0 -49
- package/extensions/greedysearch-dm/src/github.mjs +0 -228
- package/extensions/greedysearch-dm/src/reddit.mjs +0 -154
- package/extensions/greedysearch-dm/src/search/browser-lifecycle.mjs +0 -340
- package/extensions/greedysearch-dm/src/search/challenge-detect.mjs +0 -156
- package/extensions/greedysearch-dm/src/search/chrome.mjs +0 -510
- package/extensions/greedysearch-dm/src/search/defaults.mjs +0 -10
- package/extensions/greedysearch-dm/src/search/engines.mjs +0 -79
- package/extensions/greedysearch-dm/src/search/fetch-source.mjs +0 -470
- package/extensions/greedysearch-dm/src/search/file-sources.mjs +0 -31
- package/extensions/greedysearch-dm/src/search/output.mjs +0 -51
- package/extensions/greedysearch-dm/src/search/paths.mjs +0 -48
- package/extensions/greedysearch-dm/src/search/pdf.mjs +0 -64
- package/extensions/greedysearch-dm/src/search/progress.mjs +0 -109
- package/extensions/greedysearch-dm/src/search/query.mjs +0 -21
- package/extensions/greedysearch-dm/src/search/recovery.mjs +0 -49
- package/extensions/greedysearch-dm/src/search/research.mjs +0 -2255
- package/extensions/greedysearch-dm/src/search/scale-aware.mjs +0 -61
- package/extensions/greedysearch-dm/src/search/simple-research.mjs +0 -418
- package/extensions/greedysearch-dm/src/search/sources.mjs +0 -412
- package/extensions/greedysearch-dm/src/search/synthesis-runner.mjs +0 -127
- package/extensions/greedysearch-dm/src/search/synthesis.mjs +0 -202
- package/extensions/greedysearch-dm/src/tools/greedy-search-handler.js +0 -209
- package/extensions/greedysearch-dm/src/tools/shared.js +0 -226
- package/extensions/greedysearch-dm/src/types.js +0 -0
- package/extensions/greedysearch-dm/src/utils/content.mjs +0 -38
- package/extensions/greedysearch-dm/src/utils/helpers.js +0 -22
- /package/extensions/{greedysearch-dm/src/search → dm-cua/src/browser-helpers}/minimize.mjs +0 -0
- /package/extensions/{greedysearch-dm/src/utils → dm-cua/src/browser-helpers}/system-cmds.mjs +0 -0
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
cdp,
|
|
3
|
-
cdpWithInput,
|
|
4
|
-
clickCopyAndAwaitClipboard,
|
|
5
|
-
formatAnswer,
|
|
6
|
-
getOrOpenTab,
|
|
7
|
-
handleError,
|
|
8
|
-
injectClipboardInterceptor,
|
|
9
|
-
jitter,
|
|
10
|
-
outputJson,
|
|
11
|
-
parseArgs,
|
|
12
|
-
parseSourcesFromMarkdown,
|
|
13
|
-
prepareArgs,
|
|
14
|
-
TIMING,
|
|
15
|
-
validateQuery,
|
|
16
|
-
verifyInputText,
|
|
17
|
-
waitForSelector,
|
|
18
|
-
waitForStreamComplete
|
|
19
|
-
} from "./common.mjs";
|
|
20
|
-
import { ensureChrome } from "../src/search/chrome.mjs";
|
|
21
|
-
import { dismissConsent, handleVerification } from "./consent.mjs";
|
|
22
|
-
import { SELECTORS } from "./selectors.mjs";
|
|
23
|
-
const S = SELECTORS.gemini;
|
|
24
|
-
const GLOBAL_VAR = "__geminiClipboard";
|
|
25
|
-
async function typeIntoGemini(tab, text) {
|
|
26
|
-
await cdp(["click", tab, S.input]);
|
|
27
|
-
await new Promise((r) => setTimeout(r, jitter(200)));
|
|
28
|
-
const type = async () => {
|
|
29
|
-
await cdpWithInput(["type", tab, "--stdin"], text);
|
|
30
|
-
await new Promise((r) => setTimeout(r, jitter(300)));
|
|
31
|
-
};
|
|
32
|
-
const readInput = async () => {
|
|
33
|
-
const raw = await cdp([
|
|
34
|
-
"eval",
|
|
35
|
-
tab,
|
|
36
|
-
`(() => {
|
|
37
|
-
const el = document.querySelector('${S.input}');
|
|
38
|
-
return JSON.stringify(el ? el.innerText || el.textContent || '' : '');
|
|
39
|
-
})()`
|
|
40
|
-
]);
|
|
41
|
-
try {
|
|
42
|
-
return JSON.parse(raw);
|
|
43
|
-
} catch {
|
|
44
|
-
return raw;
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
const verify = async () => verifyInputText(await readInput(), text);
|
|
48
|
-
const clear = async () => {
|
|
49
|
-
await cdp([
|
|
50
|
-
"eval",
|
|
51
|
-
tab,
|
|
52
|
-
`(() => {
|
|
53
|
-
const el = document.querySelector('${S.input}');
|
|
54
|
-
if (!el) return false;
|
|
55
|
-
el.focus();
|
|
56
|
-
const range = document.createRange();
|
|
57
|
-
range.selectNodeContents(el);
|
|
58
|
-
const selection = window.getSelection();
|
|
59
|
-
selection.removeAllRanges();
|
|
60
|
-
selection.addRange(range);
|
|
61
|
-
document.execCommand('delete');
|
|
62
|
-
return true;
|
|
63
|
-
})()`
|
|
64
|
-
]);
|
|
65
|
-
};
|
|
66
|
-
await type();
|
|
67
|
-
if (await verify())
|
|
68
|
-
return;
|
|
69
|
-
await clear();
|
|
70
|
-
await type();
|
|
71
|
-
if (!await verify()) {
|
|
72
|
-
throw new Error("Gemini input field did not accept text — input verification failed");
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
async function bringToFront(tab, { required = false } = {}) {
|
|
76
|
-
try {
|
|
77
|
-
await cdp(["evalraw", tab, "Page.bringToFront", "{}"], 5000);
|
|
78
|
-
return true;
|
|
79
|
-
} catch (error) {
|
|
80
|
-
console.error(`[gemini] bringToFront failed: ${error.message}`);
|
|
81
|
-
if (required)
|
|
82
|
-
throw error;
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
async function scrollToBottom(tab) {
|
|
87
|
-
await cdp([
|
|
88
|
-
"eval",
|
|
89
|
-
tab,
|
|
90
|
-
`(function() {
|
|
91
|
-
const chat = document.querySelector('chat-window, [role="main"], main') || document.body;
|
|
92
|
-
chat.scrollTo ? chat.scrollTo({ top: chat.scrollHeight, behavior: 'smooth' }) : window.scrollTo(0, document.body.scrollHeight);
|
|
93
|
-
})()`
|
|
94
|
-
]);
|
|
95
|
-
}
|
|
96
|
-
async function extractAnswerFromDom(tab) {
|
|
97
|
-
const raw = await cdp([
|
|
98
|
-
"eval",
|
|
99
|
-
tab,
|
|
100
|
-
String.raw`
|
|
101
|
-
new Promise((resolve) => {
|
|
102
|
-
const _deadline = Date.now() + 6000;
|
|
103
|
-
function _tryExtract() {
|
|
104
|
-
const responses = Array.from(document.querySelectorAll('model-response'));
|
|
105
|
-
const resp = responses[responses.length - 1];
|
|
106
|
-
if (resp) {
|
|
107
|
-
const text = (resp.innerText || resp.textContent || '').trim();
|
|
108
|
-
const idx = text.indexOf('\n');
|
|
109
|
-
const answer = idx >= 0 ? text.slice(idx + 1).trim() : text;
|
|
110
|
-
if (answer) {
|
|
111
|
-
const seen = new Set();
|
|
112
|
-
const sources = [];
|
|
113
|
-
for (const link of resp.querySelectorAll('a[href]')) {
|
|
114
|
-
const url = link.href;
|
|
115
|
-
if (!url || seen.has(url)) continue;
|
|
116
|
-
seen.add(url);
|
|
117
|
-
const title = (link.innerText || link.textContent || '').replace(/\s+/g, ' ').trim();
|
|
118
|
-
sources.push({ title, url });
|
|
119
|
-
if (sources.length >= 10) break;
|
|
120
|
-
}
|
|
121
|
-
return resolve(JSON.stringify({ answer, sources }));
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
if (Date.now() < _deadline) {
|
|
125
|
-
setTimeout(_tryExtract, 500);
|
|
126
|
-
} else {
|
|
127
|
-
resolve(JSON.stringify({ answer: '', sources: [] }));
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
_tryExtract();
|
|
131
|
-
})
|
|
132
|
-
`
|
|
133
|
-
], 8000);
|
|
134
|
-
try {
|
|
135
|
-
return JSON.parse(raw);
|
|
136
|
-
} catch (error) {
|
|
137
|
-
console.error(`[gemini] DOM fallback JSON parse failed (${raw.length} chars): ${error.message}`);
|
|
138
|
-
return { answer: "", sources: [] };
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
const GEMINI_COPY_CLICK_EXPR = `(() => {
|
|
142
|
-
window.${GLOBAL_VAR} = '';
|
|
143
|
-
const responses = Array.from(document.querySelectorAll('model-response'));
|
|
144
|
-
const resp = responses[responses.length - 1];
|
|
145
|
-
if (!resp) return;
|
|
146
|
-
const buttons = Array.from(resp.querySelectorAll('${S.copyButton}'));
|
|
147
|
-
const nonCodeButtons = buttons.filter((btn) => !btn.closest('pre, code, code-block, .code-block, .code-container'));
|
|
148
|
-
const btn = nonCodeButtons[nonCodeButtons.length - 1] || buttons[buttons.length - 1];
|
|
149
|
-
if (btn) btn.click();
|
|
150
|
-
})()`;
|
|
151
|
-
async function extractAnswer(tab, query = "", streamWaitOk = false) {
|
|
152
|
-
const queryNorm = query.toLowerCase().trim();
|
|
153
|
-
let modelReady = false;
|
|
154
|
-
if (streamWaitOk) {
|
|
155
|
-
const ready = await cdp([
|
|
156
|
-
"eval",
|
|
157
|
-
tab,
|
|
158
|
-
String.raw`(() => {
|
|
159
|
-
const responses = Array.from(document.querySelectorAll('model-response'));
|
|
160
|
-
const r = responses[responses.length - 1];
|
|
161
|
-
return !!r && (r.innerText || '').trim().length > 60;
|
|
162
|
-
})()`
|
|
163
|
-
]).catch(() => "false");
|
|
164
|
-
modelReady = ready === "true";
|
|
165
|
-
} else {
|
|
166
|
-
const modelDeadline = Date.now() + 12000;
|
|
167
|
-
while (Date.now() < modelDeadline) {
|
|
168
|
-
const ready = await cdp([
|
|
169
|
-
"eval",
|
|
170
|
-
tab,
|
|
171
|
-
String.raw`(() => {
|
|
172
|
-
const responses = Array.from(document.querySelectorAll('model-response'));
|
|
173
|
-
const r = responses[responses.length - 1];
|
|
174
|
-
if (!r) return false;
|
|
175
|
-
const t = (r.innerText || '').trim();
|
|
176
|
-
// Must have content beyond the locale-specific label
|
|
177
|
-
// ("Gemini said" / "Το Gemini είπε" / etc.) and ideally
|
|
178
|
-
// a copy button rendered on the response.
|
|
179
|
-
return t.length > 60;
|
|
180
|
-
})()`
|
|
181
|
-
]);
|
|
182
|
-
if (ready === "true") {
|
|
183
|
-
modelReady = true;
|
|
184
|
-
break;
|
|
185
|
-
}
|
|
186
|
-
await new Promise((r) => setTimeout(r, 800));
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
if (!modelReady) {
|
|
190
|
-
console.error("[gemini] Warning: model-response did not render content");
|
|
191
|
-
}
|
|
192
|
-
let answer = await clickCopyAndAwaitClipboard(tab, GEMINI_COPY_CLICK_EXPR, GLOBAL_VAR, { timeoutMs: 600, retryClick: 600 });
|
|
193
|
-
if (answer && queryNorm && (answer.toLowerCase().trim() === queryNorm || answer.trim().length < queryNorm.length)) {
|
|
194
|
-
console.error("[gemini] Clipboard echoed query, retrying in 2s...");
|
|
195
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
196
|
-
answer = await clickCopyAndAwaitClipboard(tab, GEMINI_COPY_CLICK_EXPR, GLOBAL_VAR, { timeoutMs: 600, retryClick: 600 });
|
|
197
|
-
}
|
|
198
|
-
let domFallback = null;
|
|
199
|
-
const answerText = answer?.trim() || "";
|
|
200
|
-
const clipboardLooksEchoed = queryNorm && (answerText.toLowerCase() === queryNorm || answerText.length < queryNorm.length);
|
|
201
|
-
const clipboardLooksComplete = answerText.length > 500 && !clipboardLooksEchoed;
|
|
202
|
-
if (!clipboardLooksComplete) {
|
|
203
|
-
domFallback = await extractAnswerFromDom(tab);
|
|
204
|
-
const domExtendsClipboard = answerText && domFallback.answer.includes(answerText) && domFallback.answer.length > answerText.length * 2;
|
|
205
|
-
if (domFallback.answer && (!answerText || clipboardLooksEchoed || domExtendsClipboard)) {
|
|
206
|
-
answer = domFallback.answer;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
if (!answer) {
|
|
210
|
-
throw new Error("Gemini returned no answer — model-response never rendered content");
|
|
211
|
-
}
|
|
212
|
-
const sourcesInline = parseSourcesFromMarkdown(answer);
|
|
213
|
-
const sourceMap = new Map;
|
|
214
|
-
for (const s of [...domFallback?.sources || [], ...sourcesInline]) {
|
|
215
|
-
if (s?.url && !sourceMap.has(s.url))
|
|
216
|
-
sourceMap.set(s.url, s);
|
|
217
|
-
}
|
|
218
|
-
const sources = Array.from(sourceMap.values()).slice(0, 10);
|
|
219
|
-
return { answer: answer.trim(), sources };
|
|
220
|
-
}
|
|
221
|
-
const USAGE = `Usage: node extractors/gemini.mjs "<query>" [--tab <prefix>]
|
|
222
|
-
`;
|
|
223
|
-
async function main() {
|
|
224
|
-
const args = await prepareArgs(process.argv.slice(2));
|
|
225
|
-
validateQuery(args, USAGE);
|
|
226
|
-
const { query, tabPrefix, short } = parseArgs(args);
|
|
227
|
-
if (process.env.GREEDY_SEARCH_VISIBLE !== "1" && process.env.GREEDY_SEARCH_ALWAYS_VISIBLE !== "1") {
|
|
228
|
-
process.env.GREEDY_SEARCH_HEADLESS = "1";
|
|
229
|
-
}
|
|
230
|
-
await ensureChrome();
|
|
231
|
-
try {
|
|
232
|
-
await cdp(["list"]);
|
|
233
|
-
const tab = await getOrOpenTab(tabPrefix);
|
|
234
|
-
const currentUrl = await cdp(["eval", tab, "document.location.href"]).catch(() => "");
|
|
235
|
-
let onGemini = false;
|
|
236
|
-
try {
|
|
237
|
-
const host = new URL(currentUrl).hostname.toLowerCase();
|
|
238
|
-
onGemini = host === "gemini.google.com" || host.endsWith(".gemini.google.com");
|
|
239
|
-
} catch {}
|
|
240
|
-
if (!onGemini) {
|
|
241
|
-
await cdp(["nav", tab, "https://gemini.google.com/app"], 20000);
|
|
242
|
-
await new Promise((r) => setTimeout(r, 600));
|
|
243
|
-
}
|
|
244
|
-
await dismissConsent(tab, cdp);
|
|
245
|
-
await handleVerification(tab, cdp, 1e4);
|
|
246
|
-
await waitForSelector(tab, S.input, 8000, TIMING.inputPoll);
|
|
247
|
-
await new Promise((r) => setTimeout(r, jitter(TIMING.postClick)));
|
|
248
|
-
await bringToFront(tab, { required: true });
|
|
249
|
-
await injectClipboardInterceptor(tab, GLOBAL_VAR);
|
|
250
|
-
await typeIntoGemini(tab, query);
|
|
251
|
-
await new Promise((r) => setTimeout(r, jitter(TIMING.postType)));
|
|
252
|
-
await cdp([
|
|
253
|
-
"eval",
|
|
254
|
-
tab,
|
|
255
|
-
`document.querySelector('${S.sendButton}')?.click()`
|
|
256
|
-
]);
|
|
257
|
-
await bringToFront(tab);
|
|
258
|
-
let pollTick = 0;
|
|
259
|
-
const scrollInterval = setInterval(() => {
|
|
260
|
-
bringToFront(tab).catch(() => null);
|
|
261
|
-
if (++pollTick % 10 === 0)
|
|
262
|
-
scrollToBottom(tab).catch(() => null);
|
|
263
|
-
}, 6000);
|
|
264
|
-
let streamWaitOk = true;
|
|
265
|
-
try {
|
|
266
|
-
await waitForStreamComplete(tab, {
|
|
267
|
-
timeout: 45000,
|
|
268
|
-
stableRounds: 5,
|
|
269
|
-
minLength: 60,
|
|
270
|
-
selector: "(() => { const responses = Array.from(document.querySelectorAll('model-response')); return responses[responses.length - 1]; })()"
|
|
271
|
-
});
|
|
272
|
-
} catch {
|
|
273
|
-
streamWaitOk = false;
|
|
274
|
-
} finally {
|
|
275
|
-
clearInterval(scrollInterval);
|
|
276
|
-
}
|
|
277
|
-
const { answer, sources } = await extractAnswer(tab, query, streamWaitOk);
|
|
278
|
-
if (!answer)
|
|
279
|
-
throw new Error("No answer captured from Gemini clipboard");
|
|
280
|
-
const finalUrl = await cdp(["eval", tab, "document.location.href"]).catch(() => "https://gemini.google.com/app");
|
|
281
|
-
outputJson({
|
|
282
|
-
query,
|
|
283
|
-
url: finalUrl,
|
|
284
|
-
answer: formatAnswer(answer, short),
|
|
285
|
-
sources
|
|
286
|
-
});
|
|
287
|
-
} catch (e) {
|
|
288
|
-
handleError(e);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
main();
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
cdp,
|
|
3
|
-
formatAnswer,
|
|
4
|
-
getOrOpenTab,
|
|
5
|
-
handleError,
|
|
6
|
-
jitter,
|
|
7
|
-
outputJson,
|
|
8
|
-
parseArgs,
|
|
9
|
-
prepareArgs,
|
|
10
|
-
TIMING,
|
|
11
|
-
validateQuery,
|
|
12
|
-
waitForStreamComplete
|
|
13
|
-
} from "./common.mjs";
|
|
14
|
-
import { dismissConsent, handleVerification } from "./consent.mjs";
|
|
15
|
-
import { SELECTORS } from "./selectors.mjs";
|
|
16
|
-
const S = SELECTORS.google;
|
|
17
|
-
const MIN_ANSWER_LENGTH = 50;
|
|
18
|
-
async function extractAnswer(tab) {
|
|
19
|
-
const excludeFilter = S.sourceExclude.map((e) => `!a.href.includes('${e}')`).join(" && ");
|
|
20
|
-
const raw = await cdp([
|
|
21
|
-
"eval",
|
|
22
|
-
tab,
|
|
23
|
-
String.raw`
|
|
24
|
-
(function() {
|
|
25
|
-
var el = document.querySelector('${S.answerContainer}');
|
|
26
|
-
if (!el) return JSON.stringify({ answer: '', sources: [] });
|
|
27
|
-
var answer = el.innerText.trim();
|
|
28
|
-
var sources = Array.from(document.querySelectorAll('${S.sourceLink}'))
|
|
29
|
-
.filter(a => ${excludeFilter})
|
|
30
|
-
.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) }))
|
|
31
|
-
.filter(s => s.url && s.url.length > 10)
|
|
32
|
-
.filter((v, i, arr) => arr.findIndex(x => x.url === v.url) === i)
|
|
33
|
-
.slice(0, 10);
|
|
34
|
-
return JSON.stringify({ answer, sources });
|
|
35
|
-
})()
|
|
36
|
-
`
|
|
37
|
-
]);
|
|
38
|
-
return JSON.parse(raw);
|
|
39
|
-
}
|
|
40
|
-
const USAGE = `Usage: node extractors/google-ai.mjs "<query>" [--tab <prefix>]
|
|
41
|
-
`;
|
|
42
|
-
async function main() {
|
|
43
|
-
const args = await prepareArgs(process.argv.slice(2));
|
|
44
|
-
validateQuery(args, USAGE);
|
|
45
|
-
const { query, tabPrefix, short, locale } = parseArgs(args);
|
|
46
|
-
try {
|
|
47
|
-
if (!tabPrefix)
|
|
48
|
-
await cdp(["list"]);
|
|
49
|
-
const tab = await getOrOpenTab(tabPrefix);
|
|
50
|
-
const langParam = locale ? `&hl=${encodeURIComponent(locale)}` : "&hl=en";
|
|
51
|
-
const url = `https://www.google.com/search?q=${encodeURIComponent(query)}&udm=50${langParam}`;
|
|
52
|
-
await new Promise((r) => setTimeout(r, jitter(TIMING.postNav)));
|
|
53
|
-
await dismissConsent(tab, cdp);
|
|
54
|
-
const currentUrl = await cdp(["eval", tab, "document.location.href"]).catch(() => "");
|
|
55
|
-
if (!currentUrl.includes("google.com/search")) {
|
|
56
|
-
await cdp(["nav", tab, url], 20000);
|
|
57
|
-
await new Promise((r) => setTimeout(r, jitter(TIMING.postNav)));
|
|
58
|
-
}
|
|
59
|
-
const verifyResult = await handleVerification(tab, cdp, 1e4);
|
|
60
|
-
if (verifyResult === "needs-human")
|
|
61
|
-
throw new Error("Google verification required — could not be completed automatically");
|
|
62
|
-
if (verifyResult === "clicked" || verifyResult === "cleared-by-user") {
|
|
63
|
-
await cdp(["nav", tab, url], 20000);
|
|
64
|
-
await new Promise((r) => setTimeout(r, jitter(TIMING.postNav)));
|
|
65
|
-
}
|
|
66
|
-
await waitForStreamComplete(tab, {
|
|
67
|
-
timeout: 30000,
|
|
68
|
-
stableRounds: 5,
|
|
69
|
-
selector: `document.querySelector('${S.answerContainer}')`,
|
|
70
|
-
minLength: MIN_ANSWER_LENGTH
|
|
71
|
-
});
|
|
72
|
-
const { answer, sources } = await extractAnswer(tab);
|
|
73
|
-
if (!answer)
|
|
74
|
-
throw new Error("No answer extracted — Google AI Mode may not have responded");
|
|
75
|
-
const finalUrl = await cdp(["eval", tab, "document.location.href"]).catch(() => url);
|
|
76
|
-
outputJson({
|
|
77
|
-
query,
|
|
78
|
-
url: finalUrl,
|
|
79
|
-
answer: formatAnswer(answer, short),
|
|
80
|
-
sources
|
|
81
|
-
});
|
|
82
|
-
} catch (e) {
|
|
83
|
-
handleError(e);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
main();
|