@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.
Files changed (93) hide show
  1. package/dm.exe +0 -0
  2. package/extensions/.dm-extensions.json +43 -183
  3. package/extensions/dm-context/src/context.js +1 -2
  4. package/extensions/dm-context/src/index.js +1 -2
  5. package/extensions/{greedysearch-dm/bin/cdp.mjs → dm-cua/bin/browser-cdp.mjs} +3 -3
  6. package/extensions/dm-cua/bin/browser-cua.mjs +1 -1
  7. package/extensions/{greedysearch-dm/bin/launch.mjs → dm-cua/bin/browser-launch.mjs} +12 -11
  8. package/extensions/dm-cua/index.js +1 -1
  9. package/extensions/dm-cua/src/browser-cua-lib.mjs +17 -12
  10. package/extensions/{greedysearch-dm/src/search → dm-cua/src/browser-helpers}/constants.mjs +11 -11
  11. package/extensions/{greedysearch-dm/src/utils → dm-cua/src/browser-helpers}/node-runtime.mjs +1 -1
  12. package/extensions/{greedysearch-dm/src/search → dm-cua/src/browser-helpers}/port-pid.mjs +1 -1
  13. package/extensions/dm-ponytail/extensions/ponytail.mjs +186 -0
  14. package/extensions/dm-ponytail/hooks/claude-codex-hooks.json +41 -0
  15. package/extensions/dm-ponytail/hooks/copilot-hooks.json +21 -0
  16. package/extensions/dm-ponytail/hooks/ponytail-activate.js +59 -0
  17. package/extensions/dm-ponytail/hooks/ponytail-config.js +116 -0
  18. package/extensions/dm-ponytail/hooks/ponytail-instructions.js +81 -0
  19. package/extensions/dm-ponytail/hooks/ponytail-mode-tracker.js +96 -0
  20. package/extensions/dm-ponytail/hooks/ponytail-runtime.js +79 -0
  21. package/extensions/dm-ponytail/hooks/ponytail-statusline.ps1 +24 -0
  22. package/extensions/dm-ponytail/hooks/ponytail-statusline.sh +18 -0
  23. package/extensions/dm-ponytail/hooks/ponytail-subagent.js +50 -0
  24. package/extensions/dm-ponytail/hooks/qoder-hooks.json +26 -0
  25. package/extensions/dm-ponytail/package.json +19 -0
  26. package/extensions/dm-ponytail/pi-extension/index.js +186 -0
  27. package/extensions/dm-ponytail/pi-extension/package.json +8 -0
  28. package/extensions/dm-ponytail/skills/ponytail/SKILL.md +120 -0
  29. package/extensions/dm-ponytail/skills/ponytail-audit/SKILL.md +41 -0
  30. package/extensions/dm-ponytail/skills/ponytail-debt/SKILL.md +44 -0
  31. package/extensions/dm-ponytail/skills/ponytail-gain/SKILL.md +50 -0
  32. package/extensions/dm-ponytail/skills/ponytail-help/SKILL.md +71 -0
  33. package/extensions/dm-ponytail/skills/ponytail-review/SKILL.md +57 -0
  34. package/package.json +2 -6
  35. package/extensions/dm-caveman/extensions/caveman.js +0 -300
  36. package/extensions/dm-caveman/package.json +0 -20
  37. package/extensions/dm-goal/package.json +0 -23
  38. package/extensions/dm-goal/src/goal.js +0 -914
  39. package/extensions/greedysearch-dm/bin/cdp-greedy.mjs +0 -40
  40. package/extensions/greedysearch-dm/bin/cdp-headless.mjs +0 -5
  41. package/extensions/greedysearch-dm/bin/cdp-visible.mjs +0 -5
  42. package/extensions/greedysearch-dm/bin/gschrome.mjs +0 -30
  43. package/extensions/greedysearch-dm/bin/kill-visible.mjs +0 -7
  44. package/extensions/greedysearch-dm/bin/launch-visible.mjs +0 -13
  45. package/extensions/greedysearch-dm/bin/mcp.mjs +0 -400
  46. package/extensions/greedysearch-dm/bin/search.mjs +0 -635
  47. package/extensions/greedysearch-dm/bin/visible.mjs +0 -22
  48. package/extensions/greedysearch-dm/extractors/bing-copilot.mjs +0 -344
  49. package/extensions/greedysearch-dm/extractors/chatgpt.mjs +0 -354
  50. package/extensions/greedysearch-dm/extractors/common.mjs +0 -794
  51. package/extensions/greedysearch-dm/extractors/consensus.mjs +0 -527
  52. package/extensions/greedysearch-dm/extractors/consent.mjs +0 -449
  53. package/extensions/greedysearch-dm/extractors/gemini.mjs +0 -291
  54. package/extensions/greedysearch-dm/extractors/google-ai.mjs +0 -86
  55. package/extensions/greedysearch-dm/extractors/logically.mjs +0 -589
  56. package/extensions/greedysearch-dm/extractors/perplexity.mjs +0 -370
  57. package/extensions/greedysearch-dm/extractors/selectors.mjs +0 -32
  58. package/extensions/greedysearch-dm/extractors/semantic-scholar.mjs +0 -199
  59. package/extensions/greedysearch-dm/index.js +0 -123
  60. package/extensions/greedysearch-dm/package.json +0 -27
  61. package/extensions/greedysearch-dm/skills/greedy-search/skill.md +0 -18
  62. package/extensions/greedysearch-dm/src/fetcher.mjs +0 -577
  63. package/extensions/greedysearch-dm/src/formatters/results.js +0 -95
  64. package/extensions/greedysearch-dm/src/formatters/sources.js +0 -57
  65. package/extensions/greedysearch-dm/src/formatters/synthesis.js +0 -49
  66. package/extensions/greedysearch-dm/src/github.mjs +0 -228
  67. package/extensions/greedysearch-dm/src/reddit.mjs +0 -154
  68. package/extensions/greedysearch-dm/src/search/browser-lifecycle.mjs +0 -340
  69. package/extensions/greedysearch-dm/src/search/challenge-detect.mjs +0 -156
  70. package/extensions/greedysearch-dm/src/search/chrome.mjs +0 -510
  71. package/extensions/greedysearch-dm/src/search/defaults.mjs +0 -10
  72. package/extensions/greedysearch-dm/src/search/engines.mjs +0 -79
  73. package/extensions/greedysearch-dm/src/search/fetch-source.mjs +0 -470
  74. package/extensions/greedysearch-dm/src/search/file-sources.mjs +0 -31
  75. package/extensions/greedysearch-dm/src/search/output.mjs +0 -51
  76. package/extensions/greedysearch-dm/src/search/paths.mjs +0 -48
  77. package/extensions/greedysearch-dm/src/search/pdf.mjs +0 -64
  78. package/extensions/greedysearch-dm/src/search/progress.mjs +0 -109
  79. package/extensions/greedysearch-dm/src/search/query.mjs +0 -21
  80. package/extensions/greedysearch-dm/src/search/recovery.mjs +0 -49
  81. package/extensions/greedysearch-dm/src/search/research.mjs +0 -2255
  82. package/extensions/greedysearch-dm/src/search/scale-aware.mjs +0 -61
  83. package/extensions/greedysearch-dm/src/search/simple-research.mjs +0 -418
  84. package/extensions/greedysearch-dm/src/search/sources.mjs +0 -412
  85. package/extensions/greedysearch-dm/src/search/synthesis-runner.mjs +0 -127
  86. package/extensions/greedysearch-dm/src/search/synthesis.mjs +0 -202
  87. package/extensions/greedysearch-dm/src/tools/greedy-search-handler.js +0 -209
  88. package/extensions/greedysearch-dm/src/tools/shared.js +0 -226
  89. package/extensions/greedysearch-dm/src/types.js +0 -0
  90. package/extensions/greedysearch-dm/src/utils/content.mjs +0 -38
  91. package/extensions/greedysearch-dm/src/utils/helpers.js +0 -22
  92. /package/extensions/{greedysearch-dm/src/search → dm-cua/src/browser-helpers}/minimize.mjs +0 -0
  93. /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();