@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,370 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
buildEnvelope,
|
|
3
|
-
cdp,
|
|
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
|
-
waitForSelector,
|
|
17
|
-
waitForStreamComplete
|
|
18
|
-
} from "./common.mjs";
|
|
19
|
-
import { dismissConsent, handleVerification } from "./consent.mjs";
|
|
20
|
-
import { SELECTORS } from "./selectors.mjs";
|
|
21
|
-
const S = SELECTORS.perplexity;
|
|
22
|
-
const GLOBAL_VAR = "__pplxClipboard";
|
|
23
|
-
function findCopyButtonJsExpression() {
|
|
24
|
-
return `Array.from(document.querySelectorAll('button')).filter(b => b.innerHTML.includes('#pplx-icon-copy')).pop()`;
|
|
25
|
-
}
|
|
26
|
-
async function extractAnswerFromDom(tab, env) {
|
|
27
|
-
function _looksLikeAnswerText(text) {
|
|
28
|
-
const t = (text || "").trim();
|
|
29
|
-
if (t.length > 50)
|
|
30
|
-
return true;
|
|
31
|
-
return t.length >= 5 && /\s|[.,!?;:]/.test(t);
|
|
32
|
-
}
|
|
33
|
-
const navResult = await cdp([
|
|
34
|
-
"eval",
|
|
35
|
-
tab,
|
|
36
|
-
`new Promise((resolve) => {
|
|
37
|
-
const _deadline = Date.now() + 8000;
|
|
38
|
-
function _checkNav() {
|
|
39
|
-
const url = document.location.href;
|
|
40
|
-
if (url.includes('/search/') || url.includes('/thread/') || url.match(/perplexity.ai\\/[^/]+/)) {
|
|
41
|
-
resolve('navigated');
|
|
42
|
-
} else if (Date.now() < _deadline) {
|
|
43
|
-
setTimeout(_checkNav, 300);
|
|
44
|
-
} else {
|
|
45
|
-
resolve('timeout');
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
_checkNav();
|
|
49
|
-
})`
|
|
50
|
-
], 1e4).catch(() => "timeout");
|
|
51
|
-
if (navResult === "timeout") {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
const domExtract = await cdp([
|
|
55
|
-
"eval",
|
|
56
|
-
tab,
|
|
57
|
-
`new Promise((resolve) => {
|
|
58
|
-
const _deadline = Date.now() + 5000;
|
|
59
|
-
function _looksLikeAnswerText(text) {
|
|
60
|
-
const t = (text || '').trim();
|
|
61
|
-
if (t.length > 50) return true;
|
|
62
|
-
return t.length >= 5 && /\\s|[.,!?;:]/.test(t);
|
|
63
|
-
}
|
|
64
|
-
function _tryExtract() {
|
|
65
|
-
try {
|
|
66
|
-
// Strategy 1: Find .prose block that's NOT the question
|
|
67
|
-
// and NOT in the sidebar/nav. The answer is the last .prose
|
|
68
|
-
// that contains substantial text and is in the main content area.
|
|
69
|
-
const proseBlocks = Array.from(document.querySelectorAll('.prose, [class*="prose"]'));
|
|
70
|
-
const candidates = proseBlocks.filter(el => {
|
|
71
|
-
const text = el.innerText?.trim() || '';
|
|
72
|
-
if (!_looksLikeAnswerText(text)) return false;
|
|
73
|
-
// Exclude sidebar/nav (they're usually in <nav> or <aside> or have specific classes)
|
|
74
|
-
if (el.closest('nav, aside, [role="navigation"], [class*="sidebar"], [class*="nav-"]')) return false;
|
|
75
|
-
return true;
|
|
76
|
-
});
|
|
77
|
-
if (candidates.length > 0) {
|
|
78
|
-
const last = candidates[candidates.length - 1];
|
|
79
|
-
return resolve(JSON.stringify({ answer: last.innerText.trim(), method: 'prose' }));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Strategy 2: Look for the answer container by data attributes
|
|
83
|
-
// Perplexity uses [data-testid*="answer"] or [class*="answer-content"]
|
|
84
|
-
const answerContainer = document.querySelector('[data-testid*="answer"], [class*="answer-content"], [class*="response-content"]');
|
|
85
|
-
if (answerContainer && _looksLikeAnswerText(answerContainer.innerText?.trim())) {
|
|
86
|
-
return resolve(JSON.stringify({ answer: answerContainer.innerText.trim(), method: 'answer-container' }));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Strategy 3: Find the largest text block in the main content area
|
|
90
|
-
// (not in nav/aside/sidebar), positioned after the input.
|
|
91
|
-
const input = document.querySelector('${S.input}');
|
|
92
|
-
if (!input) return resolve(null);
|
|
93
|
-
const inputRect = input.getBoundingClientRect();
|
|
94
|
-
const main = document.querySelector('main, [role="main"], [class*="main-content"]') || document.body;
|
|
95
|
-
const blocks = Array.from(main.querySelectorAll('div, article, section'))
|
|
96
|
-
.filter(d => {
|
|
97
|
-
const r = d.getBoundingClientRect();
|
|
98
|
-
if (r.top <= inputRect.bottom) return false; // not below input
|
|
99
|
-
if (r.width === 0 || r.height === 0) return false; // not visible
|
|
100
|
-
if (d.closest('nav, aside, [role="navigation"], [class*="sidebar"]')) return false; // not in nav
|
|
101
|
-
const text = d.innerText?.trim() || '';
|
|
102
|
-
return _looksLikeAnswerText(text) && d.children.length < 20;
|
|
103
|
-
})
|
|
104
|
-
.sort((a, b) => (b.innerText?.length || 0) - (a.innerText?.length || 0));
|
|
105
|
-
if (blocks.length > 0) {
|
|
106
|
-
return resolve(JSON.stringify({ answer: blocks[0].innerText.trim(), method: 'main-content' }));
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Retry if we haven't found anything yet
|
|
110
|
-
if (Date.now() < _deadline) {
|
|
111
|
-
setTimeout(_tryExtract, 400);
|
|
112
|
-
} else {
|
|
113
|
-
resolve(null);
|
|
114
|
-
}
|
|
115
|
-
} catch(e) { resolve(null); }
|
|
116
|
-
}
|
|
117
|
-
_tryExtract();
|
|
118
|
-
})`
|
|
119
|
-
], 8000).catch(() => null);
|
|
120
|
-
if (!domExtract || domExtract === "null")
|
|
121
|
-
return null;
|
|
122
|
-
try {
|
|
123
|
-
const { answer, method } = JSON.parse(domExtract);
|
|
124
|
-
if (answer && _looksLikeAnswerText(answer)) {
|
|
125
|
-
env.fallbackUsed = `dom:${method}`;
|
|
126
|
-
env.clipboardEmpty = true;
|
|
127
|
-
const sourcesExtract = await cdp([
|
|
128
|
-
"eval",
|
|
129
|
-
tab,
|
|
130
|
-
`(() => {
|
|
131
|
-
const links = Array.from(document.querySelectorAll('a[href^="https://"]'))
|
|
132
|
-
.filter(a => {
|
|
133
|
-
const href = a.href || '';
|
|
134
|
-
return !href.includes('perplexity.ai') && !href.includes('google.com') && !href.includes('gstatic');
|
|
135
|
-
})
|
|
136
|
-
.slice(0, 10)
|
|
137
|
-
.map(a => ({ title: a.innerText?.trim() || a.href, url: a.href }));
|
|
138
|
-
return JSON.stringify(links);
|
|
139
|
-
})()`
|
|
140
|
-
], 3000).catch(() => "[]");
|
|
141
|
-
let sources = [];
|
|
142
|
-
try {
|
|
143
|
-
sources = JSON.parse(sourcesExtract || "[]");
|
|
144
|
-
} catch {}
|
|
145
|
-
return { answer, sources };
|
|
146
|
-
}
|
|
147
|
-
} catch {}
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
async function extractAnswer(tab, env) {
|
|
151
|
-
const copyBtnExpr = findCopyButtonJsExpression();
|
|
152
|
-
let answer = await clickCopyAndAwaitClipboard(tab, `(${copyBtnExpr})?.click()`, GLOBAL_VAR, { timeoutMs: 2400, retryClick: 400 });
|
|
153
|
-
env.clipboardEmpty = !answer;
|
|
154
|
-
if (env.query && answer) {
|
|
155
|
-
const queryNorm = env.query.toLowerCase().trim();
|
|
156
|
-
const answerNorm = answer.toLowerCase().trim();
|
|
157
|
-
if (answerNorm === queryNorm || answer.trim().length < Math.max(20, queryNorm.length * 0.5)) {
|
|
158
|
-
console.error(`[perplexity] Clipboard contains query echo or stub (${answer.length} chars), retrying with longer wait...`);
|
|
159
|
-
env.clipboardEmpty = true;
|
|
160
|
-
answer = "";
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
if (!answer) {
|
|
164
|
-
console.error("[perplexity] Clipboard empty — trying DOM fallback...");
|
|
165
|
-
const domResult = await extractAnswerFromDom(tab, env);
|
|
166
|
-
if (domResult) {
|
|
167
|
-
console.error(`[perplexity] DOM fallback succeeded (${env.fallbackUsed})`);
|
|
168
|
-
return domResult;
|
|
169
|
-
}
|
|
170
|
-
throw new Error("Clipboard interceptor returned empty text");
|
|
171
|
-
}
|
|
172
|
-
const sources = parseSourcesFromMarkdown(answer);
|
|
173
|
-
return { answer: answer.trim(), sources };
|
|
174
|
-
}
|
|
175
|
-
const USAGE = `Usage: node extractors/perplexity.mjs "<query>" [--tab <prefix>]
|
|
176
|
-
`;
|
|
177
|
-
async function main() {
|
|
178
|
-
const args = await prepareArgs(process.argv.slice(2));
|
|
179
|
-
validateQuery(args, USAGE);
|
|
180
|
-
const { query, tabPrefix, short } = parseArgs(args);
|
|
181
|
-
const startTime = Date.now();
|
|
182
|
-
const mode = process.env.GREEDY_SEARCH_VISIBLE === "1" ? "visible" : "headless";
|
|
183
|
-
const env = {
|
|
184
|
-
engine: "perplexity",
|
|
185
|
-
mode,
|
|
186
|
-
clipboardEmpty: null,
|
|
187
|
-
fallbackUsed: null,
|
|
188
|
-
blockedBy: null,
|
|
189
|
-
verificationResult: null,
|
|
190
|
-
inputReady: null,
|
|
191
|
-
query
|
|
192
|
-
};
|
|
193
|
-
try {
|
|
194
|
-
if (!tabPrefix)
|
|
195
|
-
await cdp(["list"]);
|
|
196
|
-
const tab = await getOrOpenTab(tabPrefix);
|
|
197
|
-
const currentUrl = await cdp(["eval", tab, "document.location.href"]).catch(() => "");
|
|
198
|
-
let onPerplexity = false;
|
|
199
|
-
try {
|
|
200
|
-
const host = new URL(currentUrl).hostname.toLowerCase();
|
|
201
|
-
onPerplexity = host === "perplexity.ai" || host.endsWith(".perplexity.ai");
|
|
202
|
-
} catch {}
|
|
203
|
-
if (!onPerplexity) {
|
|
204
|
-
await cdp(["nav", tab, "https://www.perplexity.ai/"], 20000);
|
|
205
|
-
const _inputReady = await cdp([
|
|
206
|
-
"eval",
|
|
207
|
-
tab,
|
|
208
|
-
`new Promise((resolve) => {
|
|
209
|
-
const _deadline = Date.now() + 15000;
|
|
210
|
-
function _check() {
|
|
211
|
-
const input = document.querySelector('${S.input}');
|
|
212
|
-
if (input) {
|
|
213
|
-
// Force visibility on all parents up to body —
|
|
214
|
-
// Perplexity hides the first 5 wrapper DIVs until
|
|
215
|
-
// the user interacts with the page
|
|
216
|
-
let el = input;
|
|
217
|
-
while (el && el !== document.body) {
|
|
218
|
-
if (window.getComputedStyle(el).visibility === 'hidden') {
|
|
219
|
-
el.style.visibility = 'visible';
|
|
220
|
-
}
|
|
221
|
-
el = el.parentElement;
|
|
222
|
-
}
|
|
223
|
-
input.focus();
|
|
224
|
-
if (document.activeElement === input) return resolve('ready');
|
|
225
|
-
}
|
|
226
|
-
if (Date.now() < _deadline) setTimeout(_check, 500);
|
|
227
|
-
else resolve('timeout');
|
|
228
|
-
}
|
|
229
|
-
_check();
|
|
230
|
-
})`
|
|
231
|
-
], 18000).catch(() => "timeout");
|
|
232
|
-
if (_inputReady !== "ready") {
|
|
233
|
-
for (let retry = 0;retry < 2; retry++) {
|
|
234
|
-
await cdp(["nav", tab, "https://www.perplexity.ai/"], 20000);
|
|
235
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
236
|
-
const _retryReady = await cdp([
|
|
237
|
-
"eval",
|
|
238
|
-
tab,
|
|
239
|
-
`(() => {
|
|
240
|
-
const input = document.querySelector('${S.input}');
|
|
241
|
-
if (!input) return false;
|
|
242
|
-
let el = input;
|
|
243
|
-
while (el && el !== document.body) {
|
|
244
|
-
if (window.getComputedStyle(el).visibility === 'hidden') {
|
|
245
|
-
el.style.visibility = 'visible';
|
|
246
|
-
}
|
|
247
|
-
el = el.parentElement;
|
|
248
|
-
}
|
|
249
|
-
input.focus();
|
|
250
|
-
return document.activeElement === input;
|
|
251
|
-
})()`
|
|
252
|
-
], 5000).catch(() => false);
|
|
253
|
-
if (_retryReady === "true")
|
|
254
|
-
break;
|
|
255
|
-
}
|
|
256
|
-
} else {
|
|
257
|
-
await new Promise((r) => setTimeout(r, 600));
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
const verifyResult = await handleVerification(tab, cdp, 1e4);
|
|
261
|
-
env.verificationResult = verifyResult;
|
|
262
|
-
if (verifyResult === "needs-human") {
|
|
263
|
-
throw new Error("Perplexity verification required — please solve it manually in the browser window");
|
|
264
|
-
}
|
|
265
|
-
await dismissConsent(tab, cdp);
|
|
266
|
-
if (verifyResult === "clicked") {
|
|
267
|
-
await new Promise((r) => setTimeout(r, TIMING.afterVerify));
|
|
268
|
-
const postVerifyUrl = await cdp([
|
|
269
|
-
"eval",
|
|
270
|
-
tab,
|
|
271
|
-
"document.location.href"
|
|
272
|
-
]).catch(() => "");
|
|
273
|
-
let onPerplexityAfter = false;
|
|
274
|
-
try {
|
|
275
|
-
const host = new URL(postVerifyUrl).hostname.toLowerCase();
|
|
276
|
-
onPerplexityAfter = host === "perplexity.ai" || host.endsWith(".perplexity.ai");
|
|
277
|
-
} catch {}
|
|
278
|
-
if (!onPerplexityAfter) {
|
|
279
|
-
await cdp(["nav", tab, "https://www.perplexity.ai/"], 20000);
|
|
280
|
-
await new Promise((r) => setTimeout(r, 800));
|
|
281
|
-
await dismissConsent(tab, cdp);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
const inputReady = await waitForSelector(tab, S.input, 15000, 400);
|
|
285
|
-
env.inputReady = inputReady;
|
|
286
|
-
if (!inputReady) {
|
|
287
|
-
throw new Error("Perplexity input not found — page may not have loaded or is in unexpected state");
|
|
288
|
-
}
|
|
289
|
-
await new Promise((r) => setTimeout(r, jitter(300)));
|
|
290
|
-
await injectClipboardInterceptor(tab, GLOBAL_VAR);
|
|
291
|
-
await cdp(["click", tab, S.input]);
|
|
292
|
-
await new Promise((r) => setTimeout(r, jitter(400)));
|
|
293
|
-
let typeResult;
|
|
294
|
-
for (let attempt = 0;attempt < 3; attempt++) {
|
|
295
|
-
typeResult = await cdp([
|
|
296
|
-
"eval",
|
|
297
|
-
tab,
|
|
298
|
-
`(() => {
|
|
299
|
-
try {
|
|
300
|
-
const input = document.querySelector('${S.input}');
|
|
301
|
-
if (!input) return 'no-input';
|
|
302
|
-
input.focus();
|
|
303
|
-
if (document.activeElement !== input) {
|
|
304
|
-
const activeTag = document.activeElement?.tagName || 'none';
|
|
305
|
-
const activeClass = (document.activeElement?.className || '').slice(0, 80);
|
|
306
|
-
return 'not-focused:active=' + activeTag + '.' + activeClass;
|
|
307
|
-
}
|
|
308
|
-
// execCommand('insertText') dispatches the proper input
|
|
309
|
-
// event that React's onChange listens for
|
|
310
|
-
const ok = document.execCommand('insertText', false, ${JSON.stringify(query)});
|
|
311
|
-
return ok ? 'ok' : 'exec-failed';
|
|
312
|
-
} catch (e) { return 'err:' + e.message; }
|
|
313
|
-
})()`
|
|
314
|
-
], 5000);
|
|
315
|
-
if (typeResult === "ok")
|
|
316
|
-
break;
|
|
317
|
-
if (String(typeResult).startsWith("not-focused")) {
|
|
318
|
-
await cdp(["click", tab, S.input]).catch(() => {});
|
|
319
|
-
}
|
|
320
|
-
await new Promise((r) => setTimeout(r, 800));
|
|
321
|
-
}
|
|
322
|
-
if (typeResult !== "ok") {
|
|
323
|
-
throw new Error(`Perplexity type failed: ${typeResult}`);
|
|
324
|
-
}
|
|
325
|
-
await new Promise((r) => setTimeout(r, jitter(400)));
|
|
326
|
-
await cdp([
|
|
327
|
-
"eval",
|
|
328
|
-
tab,
|
|
329
|
-
`(() => {
|
|
330
|
-
const input = document.querySelector('${S.input}');
|
|
331
|
-
if (!input) return 'no-input';
|
|
332
|
-
input.focus();
|
|
333
|
-
const ev = new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true });
|
|
334
|
-
input.dispatchEvent(ev);
|
|
335
|
-
return 'ok';
|
|
336
|
-
})()`
|
|
337
|
-
]);
|
|
338
|
-
await waitForStreamComplete(tab, {
|
|
339
|
-
timeout: 20000,
|
|
340
|
-
interval: 600,
|
|
341
|
-
stableRounds: 5,
|
|
342
|
-
minLength: 50,
|
|
343
|
-
selector: `Array.from(document.querySelectorAll('.prose, [class*="prose"]')).pop() || document.querySelector('[data-testid*="answer"], [class*="answer-content"], [class*="response-content"]') || document.body`
|
|
344
|
-
});
|
|
345
|
-
if (process.env.GREEDY_SEARCH_HEADLESS === "1") {
|
|
346
|
-
const postSnap = await cdp(["snap", tab]).catch(() => "");
|
|
347
|
-
if (/\[dialog\]/i.test(postSnap) && /Pro|αναβάθμιση|upgrade/i.test(postSnap) && !/\.prose|\[article\]/i.test(postSnap)) {
|
|
348
|
-
console.error("[perplexity] Rate Limited — skipping (visible retry won't help)");
|
|
349
|
-
env.blockedBy = "rate-limit";
|
|
350
|
-
throw new Error("Rate Limited — Perplexity free search limit reached. Wait a few hours.");
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
const { answer, sources } = await extractAnswer(tab, env);
|
|
354
|
-
if (!answer)
|
|
355
|
-
throw new Error("No answer extracted — Perplexity may not have responded");
|
|
356
|
-
const finalUrl = await cdp(["eval", tab, "document.location.href"]).catch(() => "");
|
|
357
|
-
env.durationMs = Date.now() - startTime;
|
|
358
|
-
outputJson({
|
|
359
|
-
query,
|
|
360
|
-
url: finalUrl,
|
|
361
|
-
answer: formatAnswer(answer, short),
|
|
362
|
-
sources,
|
|
363
|
-
_envelope: buildEnvelope(env)
|
|
364
|
-
});
|
|
365
|
-
} catch (e) {
|
|
366
|
-
env.durationMs = Date.now() - startTime;
|
|
367
|
-
handleError(e, buildEnvelope(env));
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
main();
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export const SELECTORS = {
|
|
2
|
-
perplexity: {
|
|
3
|
-
input: "#ask-input",
|
|
4
|
-
copyButton: null,
|
|
5
|
-
sourceItem: "[data-pplx-citation-url]",
|
|
6
|
-
sourceLink: "a",
|
|
7
|
-
consent: "#onetrust-accept-btn-handler"
|
|
8
|
-
},
|
|
9
|
-
bing: {
|
|
10
|
-
input: "#userInput",
|
|
11
|
-
copyButton: 'button[data-testid="copy-ai-message-button"]',
|
|
12
|
-
sourceLink: 'a[href^="http"][target="_blank"]',
|
|
13
|
-
sourceExclude: "copilot.microsoft.com",
|
|
14
|
-
consent: "#onetrust-accept-btn-handler"
|
|
15
|
-
},
|
|
16
|
-
google: {
|
|
17
|
-
answerContainer: ".pWvJNd",
|
|
18
|
-
sourceLink: 'a[href^="http"]',
|
|
19
|
-
sourceExclude: ["google.", "gstatic", "googleapis"],
|
|
20
|
-
sourceHeadingParent: "[data-snhf]",
|
|
21
|
-
consent: '#L2AGLb, button[jsname="b3VHJd"], .tHlp8d'
|
|
22
|
-
},
|
|
23
|
-
gemini: {
|
|
24
|
-
input: "rich-textarea .ql-editor",
|
|
25
|
-
copyButton: 'button:has(mat-icon[data-mat-icon-name="copy"])',
|
|
26
|
-
sendButton: 'button:has(mat-icon[data-mat-icon-name="arrow_upward"]), [data-test-id="send-button"], .send-button',
|
|
27
|
-
sourcesSidebarButton: "button.legacy-sources-sidebar-button",
|
|
28
|
-
sourcesExclude: ["gemini.google", "gstatic", "google.com/search"],
|
|
29
|
-
citationButtonPattern: 'button[aria-label*="citation from"]',
|
|
30
|
-
citationNameRegex: /from\s{1,20}([^.]{1,200})\.\s/
|
|
31
|
-
}
|
|
32
|
-
};
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
buildEnvelope,
|
|
3
|
-
cdp,
|
|
4
|
-
formatAnswer,
|
|
5
|
-
getOrOpenTab,
|
|
6
|
-
handleError,
|
|
7
|
-
logStage,
|
|
8
|
-
outputJson,
|
|
9
|
-
parseArgs,
|
|
10
|
-
prepareArgs,
|
|
11
|
-
validateQuery,
|
|
12
|
-
waitForSelector
|
|
13
|
-
} from "./common.mjs";
|
|
14
|
-
const USAGE = `Usage: node extractors/semantic-scholar.mjs "<query>" [--tab <prefix>]
|
|
15
|
-
`;
|
|
16
|
-
const RESULT_SELECTOR = ".cl-paper-row[data-paper-id]";
|
|
17
|
-
function semanticScholarSearchUrl(query) {
|
|
18
|
-
const normalized = String(query || "").replaceAll("-", " ");
|
|
19
|
-
return `https://www.semanticscholar.org/search?q=${encodeURIComponent(normalized)}&sort=relevance`;
|
|
20
|
-
}
|
|
21
|
-
async function dismissCookieBanner(tab) {
|
|
22
|
-
await cdp([
|
|
23
|
-
"eval",
|
|
24
|
-
tab,
|
|
25
|
-
String.raw`
|
|
26
|
-
(() => {
|
|
27
|
-
const selectors = [
|
|
28
|
-
'.osano-cm-dialog__close',
|
|
29
|
-
'.osano-cm-denyAll',
|
|
30
|
-
'.osano-cm-accept-all',
|
|
31
|
-
'button[aria-label*="Close" i]',
|
|
32
|
-
];
|
|
33
|
-
for (const selector of selectors) {
|
|
34
|
-
const btn = document.querySelector(selector);
|
|
35
|
-
if (btn) { btn.click(); return selector; }
|
|
36
|
-
}
|
|
37
|
-
return null;
|
|
38
|
-
})()
|
|
39
|
-
`
|
|
40
|
-
]).catch(() => null);
|
|
41
|
-
}
|
|
42
|
-
async function extractPapers(tab, { limit = 8 } = {}) {
|
|
43
|
-
const raw = await cdp([
|
|
44
|
-
"eval",
|
|
45
|
-
tab,
|
|
46
|
-
String.raw`
|
|
47
|
-
((limit) => {
|
|
48
|
-
function clean(value) {
|
|
49
|
-
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
50
|
-
}
|
|
51
|
-
function absolutize(href) {
|
|
52
|
-
try { return new URL(href, location.href).href; } catch { return ''; }
|
|
53
|
-
}
|
|
54
|
-
function isDirectPdf(url) {
|
|
55
|
-
return /\.pdf(?:[?#]|$)/i.test(url) || /\/pdf\//i.test(url);
|
|
56
|
-
}
|
|
57
|
-
const rows = Array.from(document.querySelectorAll('.cl-paper-row[data-paper-id]')).slice(0, limit);
|
|
58
|
-
return JSON.stringify(rows.map((row, index) => {
|
|
59
|
-
const titleLink = row.querySelector('a[data-test-id="title-link"][href], a[href*="/paper/"][href]');
|
|
60
|
-
const paperUrl = absolutize(titleLink?.getAttribute('href') || '');
|
|
61
|
-
const title = clean(titleLink?.innerText || row.querySelector('.cl-paper-title')?.innerText || '');
|
|
62
|
-
const authors = Array.from(row.querySelectorAll('[data-test-id="author-list"] a, .cl-paper-authors a'))
|
|
63
|
-
.map((a) => clean(a.innerText))
|
|
64
|
-
.filter(Boolean)
|
|
65
|
-
.slice(0, 8);
|
|
66
|
-
const field = clean(row.querySelector('.cl-paper-fos')?.innerText || '');
|
|
67
|
-
const venue = clean(row.querySelector('[data-test-id="normalized-venue-link"], .cl-paper-venue')?.innerText || '');
|
|
68
|
-
const date = clean(row.querySelector('.cl-paper-pubdates')?.innerText || '');
|
|
69
|
-
const tldrNode = row.querySelector('.tldr-abstract-replacement');
|
|
70
|
-
let tldr = clean(tldrNode?.innerText || '');
|
|
71
|
-
tldr = tldr.replace(/^TLDR\s*/i, '').replace(/\s*Expand$/i, '').trim();
|
|
72
|
-
const citationNode = row.querySelector('[data-test-id="total-citations-stat"]');
|
|
73
|
-
const citationLabel = citationNode?.getAttribute('aria-label') || citationNode?.innerText || '';
|
|
74
|
-
const citationMatch = clean(citationLabel).match(/[\d,]+/);
|
|
75
|
-
const citationCount = citationMatch ? Number.parseInt(citationMatch[0].replace(/,/g, ''), 10) : null;
|
|
76
|
-
const externalLinks = Array.from(row.querySelectorAll('a[data-test-id="paper-link"][href], a.cl-paper-view-paper[href]'))
|
|
77
|
-
.map((a) => ({
|
|
78
|
-
url: absolutize(a.getAttribute('href')),
|
|
79
|
-
label: clean(a.innerText),
|
|
80
|
-
}))
|
|
81
|
-
.filter((link) => link.url);
|
|
82
|
-
const directPdf = externalLinks.find((link) => isDirectPdf(link.url));
|
|
83
|
-
const primaryExternal = directPdf || externalLinks[0] || null;
|
|
84
|
-
const sourceUrl = primaryExternal?.url || paperUrl;
|
|
85
|
-
return {
|
|
86
|
-
rank: index + 1,
|
|
87
|
-
paperId: row.getAttribute('data-paper-id') || '',
|
|
88
|
-
title,
|
|
89
|
-
url: sourceUrl,
|
|
90
|
-
semanticScholarUrl: paperUrl,
|
|
91
|
-
pdfUrl: directPdf?.url || '',
|
|
92
|
-
externalUrl: primaryExternal?.url || '',
|
|
93
|
-
externalLabel: primaryExternal?.label || '',
|
|
94
|
-
authors,
|
|
95
|
-
field,
|
|
96
|
-
venue,
|
|
97
|
-
date,
|
|
98
|
-
tldr,
|
|
99
|
-
citationCount,
|
|
100
|
-
};
|
|
101
|
-
}));
|
|
102
|
-
})(${limit})
|
|
103
|
-
`
|
|
104
|
-
]);
|
|
105
|
-
try {
|
|
106
|
-
return JSON.parse(raw);
|
|
107
|
-
} catch {
|
|
108
|
-
return [];
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
function formatPaperSummary(papers) {
|
|
112
|
-
if (!papers.length)
|
|
113
|
-
return "Semantic Scholar returned no paper results.";
|
|
114
|
-
return papers.map((paper) => {
|
|
115
|
-
const parts = [];
|
|
116
|
-
if (paper.authors?.length)
|
|
117
|
-
parts.push(paper.authors.join(", "));
|
|
118
|
-
if (paper.venue)
|
|
119
|
-
parts.push(paper.venue);
|
|
120
|
-
if (paper.date)
|
|
121
|
-
parts.push(paper.date);
|
|
122
|
-
if (Number.isFinite(paper.citationCount)) {
|
|
123
|
-
parts.push(`${paper.citationCount.toLocaleString()} citations`);
|
|
124
|
-
}
|
|
125
|
-
const meta = parts.length ? ` — ${parts.join(" · ")}` : "";
|
|
126
|
-
const tldr = paper.tldr ? `
|
|
127
|
-
TLDR: ${paper.tldr}` : "";
|
|
128
|
-
return `${paper.rank}. ${paper.title}${meta}${tldr}`;
|
|
129
|
-
}).join(`
|
|
130
|
-
|
|
131
|
-
`);
|
|
132
|
-
}
|
|
133
|
-
async function main() {
|
|
134
|
-
const args = await prepareArgs(process.argv.slice(2));
|
|
135
|
-
validateQuery(args, USAGE);
|
|
136
|
-
const { query, tabPrefix, short } = parseArgs(args);
|
|
137
|
-
const startTime = Date.now();
|
|
138
|
-
const mode = process.env.GREEDY_SEARCH_VISIBLE === "1" ? "visible" : "headless";
|
|
139
|
-
const env = {
|
|
140
|
-
engine: "semantic-scholar",
|
|
141
|
-
mode,
|
|
142
|
-
blockedBy: null,
|
|
143
|
-
verificationResult: null,
|
|
144
|
-
inputReady: null
|
|
145
|
-
};
|
|
146
|
-
try {
|
|
147
|
-
if (!tabPrefix)
|
|
148
|
-
await cdp(["list"]);
|
|
149
|
-
const tab = await getOrOpenTab(tabPrefix);
|
|
150
|
-
logStage(env, "nav", startTime);
|
|
151
|
-
await cdp(["nav", tab, semanticScholarSearchUrl(query)], 25000);
|
|
152
|
-
await new Promise((r) => setTimeout(r, 800));
|
|
153
|
-
logStage(env, "consent", startTime);
|
|
154
|
-
await dismissCookieBanner(tab);
|
|
155
|
-
logStage(env, "results-wait", startTime);
|
|
156
|
-
const inputReady = await waitForSelector(tab, RESULT_SELECTOR, 15000, 500);
|
|
157
|
-
env.inputReady = inputReady;
|
|
158
|
-
if (!inputReady) {
|
|
159
|
-
const body = await cdp([
|
|
160
|
-
"eval",
|
|
161
|
-
tab,
|
|
162
|
-
"document.body?.innerText || ''"
|
|
163
|
-
]).catch(() => "");
|
|
164
|
-
if (/captcha|cloudflare|verify|robot|blocked/i.test(body)) {
|
|
165
|
-
env.blockedBy = "verification";
|
|
166
|
-
env.verificationResult = "needs-human";
|
|
167
|
-
throw new Error("Semantic Scholar verification required — please solve it in the visible browser window");
|
|
168
|
-
}
|
|
169
|
-
throw new Error("Semantic Scholar results not found");
|
|
170
|
-
}
|
|
171
|
-
logStage(env, "extract", startTime);
|
|
172
|
-
const papers = await extractPapers(tab, { limit: short ? 5 : 8 });
|
|
173
|
-
const sources = papers.filter((paper) => paper.title && paper.url).map((paper) => ({
|
|
174
|
-
title: paper.pdfUrl ? `${paper.title} (PDF)` : paper.title,
|
|
175
|
-
url: paper.url,
|
|
176
|
-
semanticScholarUrl: paper.semanticScholarUrl,
|
|
177
|
-
paperId: paper.paperId,
|
|
178
|
-
citationCount: paper.citationCount,
|
|
179
|
-
venue: paper.venue,
|
|
180
|
-
year: paper.date
|
|
181
|
-
}));
|
|
182
|
-
const answer = formatPaperSummary(papers);
|
|
183
|
-
const durationMs = Date.now() - startTime;
|
|
184
|
-
outputJson({
|
|
185
|
-
answer: formatAnswer(answer, short),
|
|
186
|
-
sources,
|
|
187
|
-
query,
|
|
188
|
-
url: semanticScholarSearchUrl(query),
|
|
189
|
-
papers,
|
|
190
|
-
_envelope: buildEnvelope({ ...env, durationMs })
|
|
191
|
-
});
|
|
192
|
-
} catch (error) {
|
|
193
|
-
handleError(error, buildEnvelope({
|
|
194
|
-
...env,
|
|
195
|
-
durationMs: Date.now() - startTime
|
|
196
|
-
}));
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
main();
|