@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,589 +0,0 @@
1
- import {
2
- buildEnvelope,
3
- cdp,
4
- formatAnswer,
5
- getOrOpenTab,
6
- handleError,
7
- jitter,
8
- logStage,
9
- outputJson,
10
- parseArgs,
11
- prepareArgs,
12
- TIMING,
13
- validateQuery,
14
- verifyInputText,
15
- waitForSelector
16
- } from "./common.mjs";
17
- import { readFileSync } from "node:fs";
18
- import { tmpdir } from "node:os";
19
- import { ensureChrome } from "../src/search/chrome.mjs";
20
- const START_URL = "https://logically.app/research-assistant/";
21
- const SELECTORS = {
22
- input: 'div.ProseMirror[contenteditable="true"]',
23
- submitButton: '.chat-control button[class*="MuiButton-black"], button[type="submit"]',
24
- answerContainer: '#last-message .chat-content, [class*="chat-content"]',
25
- citationSpan: '#last-message .chat-content span[title], [class*="chat-content"] span[title]'
26
- };
27
- async function startNewChat(tab) {
28
- const code = `(() => {
29
- const buttons = Array.from(document.querySelectorAll('button'));
30
- const create = buttons.find((b) => (b.innerText || '').trim() === 'Create');
31
- if (!create) return 'missing';
32
- create.click();
33
- return 'clicked';
34
- })()`;
35
- return cdp(["eval", tab, code], 5000).catch(() => "error");
36
- }
37
- async function detectLoginWall(tab) {
38
- const code = `(() => {
39
- const url = document.location.href || '';
40
- if (url.includes('/login') || url.includes('/signup') || url.includes('/sign-up')) return true;
41
- const modalText = Array.from(document.querySelectorAll('[role="dialog"], .MuiModal-root'))
42
- .map((el) => el.innerText || '')
43
- .join('
44
- ');
45
- return /create a free account|continue with google|have an account[?] log in|log in|sign up/i.test(modalText);
46
- })()`;
47
- return await cdp(["eval", tab, code], 5000).catch(() => "false") === "true";
48
- }
49
- async function activateTab(tab) {
50
- try {
51
- await cdp(["list"]);
52
- const cachePath = `${tmpdir().replaceAll("\\\\", "/")}/cdp-pages.json`;
53
- const pages = JSON.parse(readFileSync(cachePath, "utf8"));
54
- const fullTargetId = pages.find((p) => p.targetId?.startsWith(tab))?.targetId;
55
- if (fullTargetId) {
56
- await cdp([
57
- "browse",
58
- tab,
59
- "Target.activateTarget",
60
- JSON.stringify({ targetId: fullTargetId })
61
- ]).catch(() => null);
62
- await new Promise((r) => setTimeout(r, 250));
63
- }
64
- } catch {}
65
- }
66
- async function typeIntoLogically(tab, text) {
67
- await activateTab(tab);
68
- const pointRaw = await cdp([
69
- "eval",
70
- tab,
71
- `(() => {
72
- const inputs = Array.from(document.querySelectorAll('${SELECTORS.input}'));
73
- const input = inputs.find((el) => {
74
- const r = el.getBoundingClientRect();
75
- return r.width > 20 && r.height > 5;
76
- });
77
- if (!input) return '';
78
- input.scrollIntoView({ block: 'center', inline: 'center' });
79
- const r = input.getBoundingClientRect();
80
- return JSON.stringify({ x: Math.round(r.left + Math.min(80, r.width / 2)), y: Math.round(r.top + r.height / 2) });
81
- })()`
82
- ]);
83
- if (!pointRaw)
84
- throw new Error("Logically visible input not found");
85
- let point;
86
- try {
87
- point = JSON.parse(pointRaw);
88
- } catch {
89
- throw new Error("Logically input coordinates were invalid");
90
- }
91
- await cdp(["click", tab, SELECTORS.input]).catch(() => null);
92
- await new Promise((r) => setTimeout(r, jitter(120)));
93
- await cdp(["clickxy", tab, String(point.x), String(point.y)]);
94
- await new Promise((r) => setTimeout(r, jitter(TIMING.postClick)));
95
- const type = async () => {
96
- const typeResult = await cdp([
97
- "eval",
98
- tab,
99
- `(() => {
100
- const ed = document.querySelector('${SELECTORS.input}');
101
- if (!ed) return 'no-input';
102
- ed.focus();
103
- if (!ed.editor || !ed.editor.commands) {
104
- // Fallback to execCommand for non-tiptap editors
105
- const ok = document.execCommand('insertText', false, ${JSON.stringify(text)});
106
- return ok ? 'ok' : 'exec-failed';
107
- }
108
- const ok = ed.editor.commands.insertContent(${JSON.stringify(text)});
109
- return ok ? 'ok' : 'tiptap-failed';
110
- })()`
111
- ], 5000);
112
- if (typeResult !== "ok") {
113
- throw new Error(`Logically type failed: ${typeResult}`);
114
- }
115
- await new Promise((r) => setTimeout(r, jitter(TIMING.postType)));
116
- };
117
- const readInput = async () => {
118
- const raw = await cdp([
119
- "eval",
120
- tab,
121
- `JSON.stringify(Array.from(document.querySelectorAll('${SELECTORS.input}')).map((el) => el.innerText || el.textContent || ''))`
122
- ]);
123
- try {
124
- return JSON.parse(raw);
125
- } catch {
126
- return [];
127
- }
128
- };
129
- const verify = async () => {
130
- const contents = await readInput();
131
- return contents.some((content) => verifyInputText(content, text));
132
- };
133
- const clear = async () => {
134
- await cdp([
135
- "eval",
136
- tab,
137
- `(() => {
138
- const ed = document.querySelector('${SELECTORS.input}');
139
- if (!ed) return false;
140
- ed.focus();
141
- if (ed.editor?.commands?.clearContent) {
142
- ed.editor.commands.clearContent();
143
- return true;
144
- }
145
- const range = document.createRange();
146
- range.selectNodeContents(ed);
147
- const selection = window.getSelection();
148
- selection.removeAllRanges();
149
- selection.addRange(range);
150
- document.execCommand('delete');
151
- return true;
152
- })()`
153
- ]);
154
- };
155
- await type();
156
- if (await verify())
157
- return;
158
- await clear();
159
- await type();
160
- if (!await verify()) {
161
- throw new Error("Logically input did not accept text — input verification failed");
162
- }
163
- }
164
- async function waitForLogicallyAnswer(tab, timeoutMs = 90000) {
165
- const code = String.raw`
166
- new Promise((resolve, reject) => {
167
- const deadline = Date.now() + ${timeoutMs};
168
- let last = '';
169
- let stable = 0;
170
- function poll() {
171
- try {
172
- const answer = document.querySelector('${SELECTORS.answerContainer}');
173
- const text = (answer?.innerText || '').trim();
174
- const body = document.body.innerText || '';
175
- const stillGenerating = /Generating answer|Thinking\.\.\.|Searching the internet|Discovered \d+/.test(body) && text.length < 200;
176
- if (text.length >= 200 && !stillGenerating) {
177
- if (text === last) stable += 1;
178
- else { last = text; stable = 0; }
179
- if (stable >= 3) { resolve(text.length); return; }
180
- }
181
- if (Date.now() < deadline) setTimeout(poll, 700 + Math.random() * 250);
182
- else reject(new Error('Logically answer did not stabilise within ${timeoutMs}ms'));
183
- } catch (e) { reject(e); }
184
- }
185
- poll();
186
- })`;
187
- return cdp(["eval", tab, code], timeoutMs + 1e4);
188
- }
189
- async function extractAnswer(tab) {
190
- const code = `(() => {
191
- const el = document.querySelector('${SELECTORS.answerContainer}');
192
- if (!el) return JSON.stringify({ answer: '', answerHtml: '' });
193
- const clone = el.cloneNode(true);
194
- clone.querySelectorAll('svg').forEach((n) => n.remove());
195
- clone.querySelectorAll('span[width], span[height]').forEach((n) => {
196
- if (!(n.innerText || '').trim()) n.remove();
197
- });
198
- return JSON.stringify({
199
- answer: (el.innerText || '').trim(),
200
- answerHtml: clone.innerHTML,
201
- });
202
- })()`;
203
- try {
204
- return JSON.parse(await cdp(["eval", tab, code], 1e4));
205
- } catch {
206
- return { answer: "", answerHtml: "" };
207
- }
208
- }
209
- async function extractFullCitationSources(tab) {
210
- const code = String.raw`
211
- (async () => {
212
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
213
-
214
- function clickElement(el) {
215
- el.scrollIntoView({ block: 'center', inline: 'center' });
216
- for (const type of ['pointerdown', 'mousedown', 'mouseup', 'click']) {
217
- el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window }));
218
- }
219
- }
220
-
221
- function citationCardCount(root) {
222
- return Array.from(root.querySelectorAll('div')).filter((el) => {
223
- const text = (el.innerText || '').trim();
224
- return /^(Academic|Web)\n\n/.test(text) && text.includes('\nView');
225
- }).length;
226
- }
227
-
228
- function findFullCitationsPopover() {
229
- return Array.from(document.querySelectorAll('.MuiPopover-root, .MuiModal-root, .MuiDrawer-root, [role="dialog"]'))
230
- .find((el) => {
231
- const text = el.innerText || '';
232
- // Visible mode includes the tab header. Headless sometimes omits the
233
- // header and renders only a scrollable card list in a popover with no id.
234
- if (/Academic \(\d+\)|Web \(\d+\)/.test(text)) return true;
235
- if (el.id?.startsWith('citation-')) return false;
236
- return citationCardCount(el) > 0;
237
- });
238
- }
239
-
240
- function waitForFullCitationsPopover() {
241
- return new Promise((resolve) => {
242
- const deadline = Date.now() + 5000;
243
- function poll() {
244
- const pop = findFullCitationsPopover();
245
- if (pop) { resolve(pop); return; }
246
- if (Date.now() < deadline) setTimeout(poll, 100);
247
- else resolve(null);
248
- }
249
- poll();
250
- });
251
- }
252
-
253
- function linesOf(el) {
254
- return (el?.innerText || '').split(/\n+/).map((s) => s.trim()).filter(Boolean);
255
- }
256
-
257
- function textAfter(label, text) {
258
- const re = new RegExp(label + ':\\s*([^\\n]+)', 'i');
259
- return text.match(re)?.[1]?.trim() || '';
260
- }
261
-
262
- function parseAcademicCard(card, idx) {
263
- const lines = linesOf(card);
264
- const text = lines.join('\n');
265
- const citationCount = Number((lines[1] || '').match(/\d+/)?.[0] || '') || null;
266
- return {
267
- type: 'academic',
268
- citationIndex: idx + 1,
269
- url: '',
270
- title: lines[2] || '',
271
- authors: lines[3] || textAfter('Authors', text),
272
- venue: lines[4] || textAfter('Journal', text),
273
- publicationDate: textAfter('Publication Date', text),
274
- citationCount,
275
- references: Number(text.match(/References:\s*(\d+)/i)?.[1] || '') || null,
276
- fieldsOfStudy: textAfter('Fields of Study', text),
277
- publicationTypes: textAfter('Publication Types', text),
278
- snippet: lines.find((line) => /^(TLDR|Abstract):/i.test(line)) || '',
279
- };
280
- }
281
-
282
- function isUrlBlockAnchor(a) {
283
- let n = a;
284
- for (let i = 0; n && i < 5; i++, n = n.parentElement) {
285
- if (/^URL:\s*/.test((n.innerText || '').trim())) return true;
286
- }
287
- return false;
288
- }
289
-
290
- function parseWebAnchor(a, idx) {
291
- let card = a;
292
- for (let i = 0; card && i < 10; i++, card = card.parentElement) {
293
- const text = card.innerText || '';
294
- if (/\nUpdated:\s*/.test(text) && /\nURL:\s*/.test(text)) break;
295
- }
296
- const lines = linesOf(card || a.parentElement);
297
- const url = a.href || '';
298
- let title = lines.find((line) => line && !/^URL:|^Updated:|^Add to library$|^View$/.test(line) && line !== a.innerText) || '';
299
- if (/^[\w.-]+\.[a-z]{2,}$/i.test(title)) title = lines[1] || title;
300
- return {
301
- type: 'web',
302
- citationIndex: idx + 1,
303
- url,
304
- title: title || a.innerText || url,
305
- domain: (() => { try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return ''; } })(),
306
- updated: textAfter('Updated', lines.join('\n')),
307
- snippet: lines.filter((line) => !/^URL:|^Updated:|^Add to library$|^View$/.test(line)).slice(0, 8).join('\n'),
308
- };
309
- }
310
-
311
- const btn = Array.from(document.querySelectorAll('button'))
312
- .find((b) => /Citations\s*\n*\s*\(\d+\)/i.test(b.innerText || ''));
313
- if (!btn) return JSON.stringify({ sources: [], summary: { expectedTotal: 0, academic: 0, web: 0, reason: 'button-not-found' } });
314
- const buttonTotal = Number((btn.innerText || '').match(/\((\d+)\)/)?.[1] || '0');
315
- // Inline citation popovers are independent MUI modals and can remain mounted
316
- // after we click answer citations. Remove those stale id-bearing roots before
317
- // opening the full Citations popover so headless mode does not keep focus in
318
- // old inline popovers.
319
- document.querySelectorAll('.MuiPopover-root[id^="citation-"]').forEach((el) => el.remove());
320
- clickElement(btn);
321
- const pop = await waitForFullCitationsPopover();
322
- if (!pop) return JSON.stringify({ sources: [], summary: { expectedTotal: buttonTotal, academic: 0, web: 0, reason: 'popover-not-found' } });
323
-
324
- const header = pop.innerText || '';
325
- const academicExpected = Number(header.match(/Academic \((\d+)\)/)?.[1] || '0');
326
- const webExpected = Number(header.match(/Web \((\d+)\)/)?.[1] || '0');
327
- const allDivs = Array.from(pop.querySelectorAll('div'));
328
- const academicCandidates = allDivs.filter((el) => {
329
- const text = (el.innerText || '').trim();
330
- return /^Academic\n\n/.test(text) && text.includes('\nView');
331
- });
332
- const academicCards = academicCandidates.filter((el) =>
333
- !academicCandidates.some((other) => other !== el && el.contains(other))
334
- );
335
- const academicSources = academicCards.map(parseAcademicCard).filter((s) => s.title);
336
-
337
- const webAnchors = Array.from(pop.querySelectorAll('a[href]')).filter(isUrlBlockAnchor);
338
- const webSources = webAnchors.map(parseWebAnchor).filter((s) => s.url);
339
-
340
- return JSON.stringify({
341
- sources: [...academicSources, ...webSources],
342
- summary: {
343
- expectedTotal: academicExpected + webExpected || buttonTotal,
344
- academicExpected,
345
- webExpected,
346
- academicCaptured: academicSources.length,
347
- webCaptured: webSources.length,
348
- },
349
- });
350
- })()`;
351
- const raw = await cdp(["eval", tab, code], 20000);
352
- try {
353
- return JSON.parse(raw);
354
- } catch {
355
- return { sources: [], summary: { expectedTotal: 0, reason: "invalid-json" } };
356
- }
357
- }
358
- async function extractCitationSources(tab) {
359
- const code = String.raw`
360
- (async () => {
361
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
362
- const answer = document.querySelector('${SELECTORS.answerContainer}');
363
- if (!answer) return JSON.stringify([]);
364
-
365
- function parseUrlFromId(id) {
366
- const m = String(id || '').match(/^citation-(https?:\/\/.+)-(\d+)$/);
367
- return m ? { url: m[1], index: Number(m[2]) } : { url: '', index: null };
368
- }
369
-
370
- function scrapePopover() {
371
- const pop = document.querySelector('.MuiPopover-root[id^="citation-"]');
372
- if (!pop) return null;
373
- const lines = (pop.innerText || '').split(/\n+/).map((s) => s.trim()).filter(Boolean);
374
- const posIdx = lines.findIndex((line) => /^\d+\s+of\s+\d+$/i.test(line));
375
- const { url, index } = parseUrlFromId(pop.id);
376
- const citationCountLine = lines.find((line) => /^\d+\s+citations?$/i.test(line));
377
- return {
378
- url,
379
- index,
380
- type: lines[0] || '',
381
- citationCount: citationCountLine ? Number(citationCountLine.match(/\d+/)?.[0]) : null,
382
- position: posIdx !== -1 ? lines[posIdx] : '',
383
- title: posIdx !== -1 ? (lines[posIdx + 1] || '') : '',
384
- authors: posIdx !== -1 ? (lines[posIdx + 2] || '') : '',
385
- venue: posIdx !== -1 ? (lines[posIdx + 3] || '') : '',
386
- snippet: lines.find((line) => /^(TLDR|Abstract|Snippet|Description):/i.test(line)) || '',
387
- };
388
- }
389
-
390
- async function waitForPopover(previousId = '') {
391
- const deadline = Date.now() + 4000;
392
- while (Date.now() < deadline) {
393
- const pop = document.querySelector('.MuiPopover-root[id^="citation-"]');
394
- if (pop && pop.id !== previousId) return pop;
395
- await sleep(100);
396
- }
397
- return document.querySelector('.MuiPopover-root[id^="citation-"]');
398
- }
399
-
400
- function clickElement(el) {
401
- el.scrollIntoView({ block: 'center', inline: 'center' });
402
- for (const type of ['pointerdown', 'mousedown', 'mouseup', 'click']) {
403
- el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window }));
404
- }
405
- }
406
-
407
- const spans = Array.from(answer.querySelectorAll('span[title]'))
408
- .filter((s) => (s.innerText || s.title || '').trim());
409
- const groups = [];
410
- const seenGroups = new Set();
411
- for (const span of spans) {
412
- const text = (span.innerText || '').trim();
413
- const plus = Number(text.match(/\(\+(\d+)\)/)?.[1] || '0');
414
- const key = String(span.title) + '::' + String(plus);
415
- if (seenGroups.has(key)) continue;
416
- seenGroups.add(key);
417
- groups.push({ span, title: span.title, count: Math.min(plus + 1, 12) });
418
- if (groups.length >= 25) break;
419
- }
420
-
421
- const sources = [];
422
- const seenUrls = new Set();
423
- for (const group of groups) {
424
- const previous = document.querySelector('.MuiPopover-root[id^="citation-"]')?.id || '';
425
- clickElement(group.span);
426
- await waitForPopover(previous);
427
- for (let i = 0; i < group.count; i++) {
428
- await sleep(150);
429
- const src = scrapePopover();
430
- if (src) {
431
- if (!src.title) src.title = group.title;
432
- if (src.url && !seenUrls.has(src.url)) {
433
- seenUrls.add(src.url);
434
- sources.push(src);
435
- } else if (!src.url) {
436
- const key = 'title:' + String(src.title || '');
437
- if (!seenUrls.has(key)) { seenUrls.add(key); sources.push(src); }
438
- }
439
- }
440
- if (i < group.count - 1) {
441
- const pop = document.querySelector('.MuiPopover-root[id^="citation-"]');
442
- const before = pop?.id || '';
443
- const buttons = Array.from(pop?.querySelectorAll('button') || []);
444
- const next = buttons.find((b) => !b.disabled && !(b.innerText || '').trim());
445
- if (!next) break;
446
- next.click();
447
- await waitForPopover(before);
448
- }
449
- }
450
- }
451
-
452
- if (sources.length === 0) {
453
- for (const span of spans) {
454
- const title = span.title || (span.innerText || '').trim();
455
- const key = 'title:' + String(title || '');
456
- if (title && !seenUrls.has(key)) {
457
- seenUrls.add(key);
458
- sources.push({ title, url: '', type: 'citation-label' });
459
- }
460
- }
461
- }
462
- return JSON.stringify(sources);
463
- })()`;
464
- const raw = await cdp(["eval", tab, code], 45000);
465
- try {
466
- return JSON.parse(raw);
467
- } catch {
468
- return [];
469
- }
470
- }
471
- const USAGE = `Usage: node extractors/logically.mjs "<query>" [--tab <prefix>]
472
- `;
473
- async function main() {
474
- const args = await prepareArgs(process.argv.slice(2));
475
- validateQuery(args, USAGE);
476
- const { query, tabPrefix, short } = parseArgs(args);
477
- const startTime = Date.now();
478
- const mode = process.env.GREEDY_SEARCH_VISIBLE === "1" ? "visible" : "headless";
479
- const env = {
480
- engine: "logically",
481
- mode,
482
- clipboardEmpty: null,
483
- fallbackUsed: null,
484
- blockedBy: null,
485
- verificationResult: null,
486
- inputReady: null
487
- };
488
- try {
489
- if (process.env.GREEDY_SEARCH_VISIBLE !== "1" && process.env.GREEDY_SEARCH_ALWAYS_VISIBLE !== "1") {
490
- process.env.GREEDY_SEARCH_HEADLESS = "1";
491
- }
492
- await ensureChrome();
493
- if (!tabPrefix)
494
- await cdp(["list"]);
495
- const tab = await getOrOpenTab(tabPrefix);
496
- const currentUrl = await cdp(["eval", tab, "document.location.href"]).catch(() => "");
497
- let onLogically = false;
498
- try {
499
- const host = new URL(currentUrl).hostname.toLowerCase();
500
- onLogically = host === "logically.app" || host.endsWith(".logically.app");
501
- } catch {}
502
- if (!onLogically) {
503
- logStage(env, "nav", startTime);
504
- await cdp(["nav", tab, START_URL], 25000);
505
- await new Promise((r) => setTimeout(r, 900));
506
- }
507
- logStage(env, "new-chat", startTime);
508
- await startNewChat(tab);
509
- await new Promise((r) => setTimeout(r, 700));
510
- logStage(env, "input-wait", startTime);
511
- const inputReady = await waitForSelector(tab, SELECTORS.input, 20000, 400);
512
- env.inputReady = inputReady;
513
- if (!inputReady) {
514
- throw new Error("Logically input not found — page may not have loaded or is in unexpected state");
515
- }
516
- if (process.env.GREEDY_SEARCH_HEADLESS === "1") {
517
- const quotaResult = await cdp([
518
- "eval",
519
- tab,
520
- `(() => {
521
- const text = document.body?.innerText || '';
522
- const m = text.match(/Chat messages\\s*\\n?\\s*(\\d+)\\s*\\/\\s*(\\d+)/i);
523
- if (m && parseInt(m[1], 10) >= parseInt(m[2], 10)) {
524
- return 'quota-exceeded';
525
- }
526
- // Also check if Create button is disabled
527
- const createBtn = Array.from(document.querySelectorAll('button')).find(
528
- (b) => b.innerText.trim() === 'Create',
529
- );
530
- if (createBtn && createBtn.disabled) return 'create-disabled';
531
- return 'ok';
532
- })()`
533
- ], 5000).catch(() => "ok");
534
- if (quotaResult === "quota-exceeded" || quotaResult === "create-disabled") {
535
- console.error("[logically] Rate Limited — skipping (visible retry won't help)");
536
- env.blockedBy = "rate-limit";
537
- throw new Error("Rate Limited — Logically free message quota exhausted. Wait until reset.");
538
- }
539
- }
540
- logStage(env, "type-and-submit", startTime);
541
- await typeIntoLogically(tab, query);
542
- const submitted = await cdp([
543
- "eval",
544
- tab,
545
- `(() => { const btn = document.querySelector('${SELECTORS.submitButton}'); if (!btn) return 'missing'; btn.click(); return 'clicked'; })()`
546
- ]);
547
- if (submitted !== "clicked")
548
- throw new Error("Logically submit button not found");
549
- await new Promise((r) => setTimeout(r, 1500));
550
- if (await detectLoginWall(tab)) {
551
- env.blockedBy = "signin";
552
- env.verificationResult = "needs-human";
553
- throw new Error("Logically login required — please log in or create a free account in the visible browser window. Once signed in, cookies persist for future runs.");
554
- }
555
- logStage(env, "answer-wait", startTime);
556
- await waitForLogicallyAnswer(tab, 90000);
557
- logStage(env, "extract-answer-html", startTime);
558
- const { answer, answerHtml } = await extractAnswer(tab);
559
- if (!answer)
560
- throw new Error("No answer extracted — Logically may not have responded");
561
- logStage(env, "extract-full-citations", startTime);
562
- const fullCitations = await extractFullCitationSources(tab);
563
- let inlineSources = [];
564
- let sources = fullCitations.sources?.length ? fullCitations.sources : null;
565
- if (!sources) {
566
- logStage(env, "extract-inline-citations", startTime);
567
- inlineSources = await extractCitationSources(tab);
568
- sources = inlineSources;
569
- }
570
- const finalUrl = await cdp(["eval", tab, "document.location.href"]).catch(() => "");
571
- env.durationMs = Date.now() - startTime;
572
- env.sourcePath = fullCitations.sources?.length ? "citations-popover" : "inline-citation-popovers";
573
- logStage(env, "done", startTime);
574
- outputJson({
575
- query,
576
- url: finalUrl,
577
- answer: formatAnswer(answer, short),
578
- answerHtml,
579
- sources,
580
- inlineSources,
581
- citationSummary: fullCitations.summary,
582
- _envelope: buildEnvelope(env)
583
- });
584
- } catch (e) {
585
- env.durationMs = Date.now() - startTime;
586
- handleError(e, buildEnvelope(env));
587
- }
588
- }
589
- main();