@duckmind/dm-windows-x64 0.62.1 → 0.62.9

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,344 +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
- waitForCopyButton,
17
- waitForSelector,
18
- waitForStreamComplete
19
- } from "./common.mjs";
20
- import {
21
- detectVerificationChallenge,
22
- dismissConsent,
23
- handleVerification
24
- } from "./consent.mjs";
25
- import { SELECTORS } from "./selectors.mjs";
26
- const S = SELECTORS.bing;
27
- const GLOBAL_VAR = "__bingClipboard";
28
- async function detectSignInWall(tab) {
29
- const code = `(() => {
30
- if (document.querySelector('#userInput')) return false;
31
- const links = Array.from(document.querySelectorAll('a[href], button'));
32
- const hasOAuth = links.some(el => {
33
- const h = (el.href || el.getAttribute('formaction') || '').toLowerCase();
34
- return h.includes('login.microsoftonline.com')
35
- || h.includes('appleid.apple.com')
36
- || h.includes('accounts.google.com');
37
- });
38
- return hasOAuth;
39
- })()`;
40
- const result = await cdp(["eval", tab, code]).catch(() => "false");
41
- return result === "true";
42
- }
43
- async function extractAnswer(tab, env, query = "") {
44
- await waitForCopyButton(tab, S.copyButton, { timeout: 2000 }).catch(() => null);
45
- await new Promise((r) => setTimeout(r, 800));
46
- let answer = await clickCopyAndPollClipboard(tab, 5000);
47
- let clipboardEmpty = !answer;
48
- if (!answer) {
49
- console.error("[bing] Clipboard empty, retrying copy/poll...");
50
- answer = await clickCopyAndPollClipboard(tab, 8000);
51
- clipboardEmpty = !answer;
52
- }
53
- if (!answer) {
54
- answer = await extractFromVisibleDom(tab, query);
55
- if (answer)
56
- env.fallbackUsed = "visibleDom";
57
- }
58
- if (!answer) {
59
- answer = await extractFromAccessibilityTree(tab, query);
60
- if (answer)
61
- env.fallbackUsed = "accessibilityTree";
62
- }
63
- if (!answer) {
64
- const iframeResult = await extractFromIframes(tab, env);
65
- answer = iframeResult.answer;
66
- if (answer)
67
- env.fallbackUsed = "iframeDom";
68
- }
69
- if (!answer)
70
- throw new Error("Clipboard interceptor returned empty text");
71
- env.clipboardEmpty = clipboardEmpty;
72
- const sources = parseSourcesFromMarkdown(answer);
73
- return { answer: answer.trim(), sources };
74
- }
75
- async function clickCopyAndPollClipboard(tab, timeoutMs) {
76
- const clickExpr = `(() => {
77
- window.${GLOBAL_VAR} = '';
78
- const buttons = document.querySelectorAll('${S.copyButton}');
79
- buttons[buttons.length - 1]?.click();
80
- })()`;
81
- return clickCopyAndAwaitClipboard(tab, clickExpr, GLOBAL_VAR, {
82
- timeoutMs,
83
- retryClick: timeoutMs * 2
84
- });
85
- }
86
- async function extractFromVisibleDom(tab, query = "") {
87
- try {
88
- const bodyText = await cdp([
89
- "eval",
90
- tab,
91
- "document.body?.innerText || ''"
92
- ]).catch(() => "");
93
- let answer = "";
94
- if (bodyText && bodyText.includes("Copilot said")) {
95
- const copilotSplit = bodyText.split(/Copilot said\s*/i);
96
- const afterCopilot = copilotSplit.pop() || "";
97
- answer = cleanCopilotArticleText(truncateAtBoilerplate(afterCopilot));
98
- }
99
- if (!answer) {
100
- const articlesJson = await cdp([
101
- "eval",
102
- tab,
103
- `JSON.stringify(Array.from(document.querySelectorAll('article')).map(a => a.innerText || '').filter(Boolean))`
104
- ]).catch(() => "[]");
105
- const articles = JSON.parse(articlesJson || "[]");
106
- answer = pickAnswerArticle(articles, query);
107
- }
108
- if (answer.length < 20)
109
- return "";
110
- console.error(`[bing] Visible DOM extraction succeeded (${answer.length} chars)`);
111
- return answer;
112
- } catch (e) {
113
- console.error(`[bing] Visible DOM extraction failed: ${e.message}`);
114
- return "";
115
- }
116
- }
117
- async function extractFromAccessibilityTree(tab, query = "") {
118
- try {
119
- const snap = await cdp(["snap", tab]).catch(() => "");
120
- if (!snap || await detectVerificationChallenge(tab, cdp))
121
- return "";
122
- const articleLines = [];
123
- for (const line of snap.split(`
124
- `)) {
125
- const trimmed = line.trimStart();
126
- if (!trimmed.toLowerCase().startsWith("[article]"))
127
- continue;
128
- const after = trimmed.slice("[article]".length).trimStart();
129
- if (after)
130
- articleLines.push(after);
131
- }
132
- if (articleLines.length === 0)
133
- return "";
134
- const answer = pickAnswerArticle(articleLines, query);
135
- if (answer.length < 50)
136
- return "";
137
- console.error(`[bing] Accessibility extraction succeeded (${answer.length} chars)`);
138
- return answer;
139
- } catch (e) {
140
- console.error(`[bing] Accessibility extraction failed: ${e.message}`);
141
- return "";
142
- }
143
- }
144
- function pickAnswerArticle(articles, query = "") {
145
- const normalizedQuery = normalizeForCompare(query);
146
- const candidates = articles.map((text) => cleanCopilotArticleText(text)).filter((text) => text.length >= 50).filter((text) => {
147
- if (!normalizedQuery)
148
- return true;
149
- const normalizedText = normalizeForCompare(text);
150
- return !normalizedText.includes(normalizedQuery) || text.length > query.length * 3;
151
- });
152
- return candidates.at(-1) || "";
153
- }
154
- function normalizeForCompare(text = "") {
155
- return String(text).toLocaleLowerCase().replace(/\s+/g, " ").trim();
156
- }
157
- const BOILERPLATE_MARKERS = [
158
- "Good response",
159
- "Bad response",
160
- "Share message",
161
- "Copy message",
162
- "Read aloud",
163
- "Regenerate",
164
- "Edit in a page",
165
- "Message Copilot",
166
- "Smart"
167
- ];
168
- function truncateAtBoilerplate(text) {
169
- let earliest = text.length;
170
- for (const marker of BOILERPLATE_MARKERS) {
171
- let searchFrom = 0;
172
- while (searchFrom < text.length) {
173
- const idx = text.indexOf(marker, searchFrom);
174
- if (idx === -1)
175
- break;
176
- const before = idx > 0 ? text[idx - 1] : "";
177
- const precededByWhitespace = !before || /\s/.test(before);
178
- const after = text[idx + marker.length] || "";
179
- const notFollowedByWord = !after || !/\w/.test(after);
180
- if (precededByWhitespace && notFollowedByWord) {
181
- if (idx < earliest)
182
- earliest = idx;
183
- break;
184
- }
185
- searchFrom = idx + marker.length;
186
- }
187
- }
188
- return earliest < text.length ? text.slice(0, earliest) : text;
189
- }
190
- function cleanCopilotArticleText(text = "") {
191
- return truncateAtBoilerplate(String(text).replace(/\s+/g, " ")).trim();
192
- }
193
- async function extractFromIframes(mainTab, env) {
194
- try {
195
- const hasCopyBtn = await cdp([
196
- "eval",
197
- mainTab,
198
- `!!document.querySelector('${S.copyButton}')`
199
- ]).catch(() => "false");
200
- if (hasCopyBtn === "true")
201
- return { answer: "" };
202
- if (await detectVerificationChallenge(mainTab, cdp)) {
203
- console.error("[bing] Verification challenge detected — content blocked in headless");
204
- env.blockedBy = "verification";
205
- return { answer: "" };
206
- }
207
- console.error("[bing] Copy button hidden, no Cloudflare — trying DOM extraction...");
208
- const targetsRaw = await cdp([
209
- "evalraw",
210
- mainTab,
211
- "Target.getTargets",
212
- "{}"
213
- ]);
214
- const targets = JSON.parse(targetsRaw);
215
- const targetInfos = targets.targetInfos || [];
216
- const funFrame = targetInfos.find((t) => t.type === "iframe" && t.url.includes("copilot.fun"));
217
- if (!funFrame) {
218
- console.error("[bing] No copilot.fun iframe target found");
219
- return { answer: "" };
220
- }
221
- const funTabId = funFrame.targetId.slice(0, 8);
222
- const innerText = await cdp([
223
- "eval",
224
- funTabId,
225
- `(()=>{const iframe=document.querySelector('iframe'); if(!iframe) return''; try{const doc=iframe.contentDocument||iframe.contentWindow.document; return doc?.body?.innerText?.trim()||''}catch(e){return''}})()`
226
- ]).catch(() => "");
227
- if (innerText) {
228
- console.error(`[bing] DOM extraction succeeded (${innerText.length} chars)`);
229
- return { answer: innerText };
230
- }
231
- console.error("[bing] DOM extraction returned empty — falling through to visible retry");
232
- } catch (e) {
233
- console.error(`[bing] DOM extraction failed: ${e.message}`);
234
- }
235
- return { answer: "" };
236
- }
237
- const USAGE = `Usage: node extractors/bing-copilot.mjs "<query>" [--tab <prefix>]
238
- `;
239
- async function main() {
240
- const args = await prepareArgs(process.argv.slice(2));
241
- validateQuery(args, USAGE);
242
- const { query, tabPrefix, short } = parseArgs(args);
243
- const startTime = Date.now();
244
- const mode = process.env.GREEDY_SEARCH_VISIBLE === "1" ? "visible" : "headless";
245
- const env = {
246
- engine: "bing",
247
- mode,
248
- clipboardEmpty: null,
249
- fallbackUsed: null,
250
- blockedBy: null,
251
- verificationResult: null,
252
- inputReady: null
253
- };
254
- try {
255
- if (!tabPrefix)
256
- await cdp(["list"]);
257
- const tab = await getOrOpenTab(tabPrefix);
258
- const currentUrl = await cdp(["eval", tab, "document.location.href"]).catch(() => "");
259
- let onCopilot = false;
260
- try {
261
- const host = new URL(currentUrl).hostname.toLowerCase();
262
- onCopilot = host === "copilot.microsoft.com" || host.endsWith(".copilot.microsoft.com");
263
- } catch {}
264
- if (!onCopilot) {
265
- await cdp(["nav", tab, "https://copilot.microsoft.com/"], 20000);
266
- await new Promise((r) => setTimeout(r, 600));
267
- }
268
- await dismissConsent(tab, cdp);
269
- const verifyResult = await handleVerification(tab, cdp, 1e4);
270
- env.verificationResult = verifyResult;
271
- if (verifyResult === "needs-human") {
272
- throw new Error("Copilot verification required — please solve it manually in the browser window");
273
- }
274
- if (verifyResult === "clicked") {
275
- await new Promise((r) => setTimeout(r, TIMING.afterVerify));
276
- const currentUrl = await cdp([
277
- "eval",
278
- tab,
279
- "document.location.href"
280
- ]).catch(() => "");
281
- let onCopilot = false;
282
- try {
283
- const host = new URL(currentUrl).hostname.toLowerCase();
284
- onCopilot = host === "copilot.microsoft.com" || host.endsWith(".copilot.microsoft.com");
285
- } catch {}
286
- if (!onCopilot) {
287
- await cdp(["nav", tab, "https://copilot.microsoft.com/"], 20000);
288
- await new Promise((r) => setTimeout(r, 600));
289
- await dismissConsent(tab, cdp);
290
- }
291
- }
292
- if (await detectSignInWall(tab)) {
293
- throw new Error("Copilot requires sign-in — please sign in with Microsoft, Apple, or Google in the visible browser window. Once signed in, cookies persist for future runs.");
294
- }
295
- const inputReady = await waitForSelector(tab, S.input, 15000, 500);
296
- env.inputReady = inputReady;
297
- await new Promise((r) => setTimeout(r, jitter(300)));
298
- if (!inputReady) {
299
- if (await detectSignInWall(tab)) {
300
- throw new Error("Copilot requires sign-in — please sign in with Microsoft, Apple, or Google in the visible browser window. Once signed in, cookies persist for future runs.");
301
- }
302
- throw new Error("Copilot input not found — verification may have failed or page is in unexpected state");
303
- }
304
- await injectClipboardInterceptor(tab, GLOBAL_VAR);
305
- await cdp(["click", tab, S.input]);
306
- await new Promise((r) => setTimeout(r, TIMING.postClick));
307
- await cdp(["type", tab, query]);
308
- await new Promise((r) => setTimeout(r, TIMING.postType));
309
- await cdp([
310
- "eval",
311
- tab,
312
- `document.querySelector('${S.input}')?.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true,keyCode:13})), 'ok'`
313
- ]);
314
- setTimeout(() => {
315
- handleVerification(tab, cdp, 1e4).then((v) => {
316
- if (v === "clicked") {
317
- console.error("[bing] Post-submit verification clicked");
318
- env.verificationResult = "post-submit-clicked";
319
- }
320
- }).catch(() => {});
321
- }, 2000);
322
- await waitForStreamComplete(tab, {
323
- timeout: short ? 25000 : 60000,
324
- minLength: 50,
325
- selector: "document.querySelector('article') || document.body"
326
- });
327
- const { answer, sources } = await extractAnswer(tab, env, query);
328
- if (!answer)
329
- throw new Error("No answer extracted — Copilot may not have responded");
330
- const finalUrl = await cdp(["eval", tab, "document.location.href"]).catch(() => "");
331
- env.durationMs = Date.now() - startTime;
332
- outputJson({
333
- query,
334
- url: finalUrl,
335
- answer: formatAnswer(answer, short),
336
- sources,
337
- _envelope: buildEnvelope(env)
338
- });
339
- } catch (e) {
340
- env.durationMs = Date.now() - startTime;
341
- handleError(e, buildEnvelope(env));
342
- }
343
- }
344
- main();
@@ -1,354 +0,0 @@
1
- import {
2
- buildEnvelope,
3
- cdp,
4
- clickCopyAndAwaitClipboard,
5
- formatAnswer,
6
- getOrOpenTab,
7
- handleError,
8
- injectClipboardInterceptor,
9
- jitter,
10
- logStage,
11
- outputJson,
12
- parseArgs,
13
- parseSourcesFromMarkdown,
14
- parseSourcesFromMarkdownRefStyle,
15
- prepareArgs,
16
- validateQuery,
17
- waitForSelector,
18
- waitForStreamComplete
19
- } from "./common.mjs";
20
- import { dismissConsent, handleVerification } from "./consent.mjs";
21
- const GLOBAL_VAR = "__chatgptClipboard";
22
- const PROSE_SELECTOR = "div.ProseMirror";
23
- const SEND_SELECTOR = 'button[data-testid="send-button"]';
24
- const COPY_SELECTOR = 'button[data-testid="copy-turn-action-button"]';
25
- const CHATGPT_RESPONSE_SELECTOR = String.raw`(() => {
26
- const all = document.querySelectorAll('[data-message-author-role]');
27
- let lastUserIdx = -1;
28
- for (let i = 0; i < all.length; i++) {
29
- if (all[i].getAttribute('data-message-author-role') === 'user') lastUserIdx = i;
30
- }
31
- if (lastUserIdx < 0) return null;
32
- let bestEl = null;
33
- let bestLen = 0;
34
- for (let i = lastUserIdx + 1; i < all.length; i++) {
35
- if (all[i].getAttribute('data-message-author-role') === 'assistant') {
36
- const len = (all[i].innerText || '').length;
37
- if (len > bestLen) { bestLen = len; bestEl = all[i]; }
38
- }
39
- }
40
- return bestEl;
41
- })()`;
42
- const CHATGPT_ASSISTANT_COPY_CLICK_EXPR = `(() => {
43
- window.${GLOBAL_VAR} = '';
44
- const assistant = ${CHATGPT_RESPONSE_SELECTOR};
45
- const assistantCopy = assistant?.querySelector('${COPY_SELECTOR}');
46
- if (assistantCopy) assistantCopy.click();
47
- })()`;
48
- async function typeAndSubmit(tab, query) {
49
- await cdp(["click", tab, PROSE_SELECTOR]);
50
- await new Promise((r) => setTimeout(r, jitter(200)));
51
- const typeResult = await cdp([
52
- "eval",
53
- tab,
54
- `(() => {
55
- const editor = document.querySelector('${PROSE_SELECTOR}');
56
- if (!editor) return 'no-editor';
57
- editor.focus();
58
- const ok = document.execCommand('insertText', false, ${JSON.stringify(query)});
59
- return ok ? 'ok' : 'exec-failed';
60
- })()`
61
- ], 5000);
62
- if (typeResult !== "ok") {
63
- throw new Error(`ChatGPT type failed: ${typeResult}`);
64
- }
65
- await new Promise((r) => setTimeout(r, jitter(300)));
66
- const sendCode = `
67
- (() => {
68
- const btn = document.querySelector('${SEND_SELECTOR}');
69
- if (!btn) return 'no-send';
70
- if (btn.disabled) return 'send-disabled';
71
- btn.click();
72
- return 'ok';
73
- })()
74
- `;
75
- const sendResult = await cdp(["eval", tab, sendCode]);
76
- if (sendResult === "no-send")
77
- throw new Error("ChatGPT send button not found");
78
- if (sendResult === "send-disabled")
79
- throw new Error("ChatGPT send button disabled — query was not registered");
80
- await new Promise((r) => setTimeout(r, jitter(300)));
81
- }
82
- const CHATGPT_STREAMING_EXPR = String.raw`(() => {
83
- const el = ${CHATGPT_RESPONSE_SELECTOR};
84
- return !!el?.querySelector('.streaming-animation,[data-is-streaming="true"]') ||
85
- !!document.querySelector('button[data-testid="stop-button"], button[aria-label="Stop generating"], button[aria-label*="Stop"]');
86
- })()`;
87
- async function bringToFront(tab, { required = false } = {}) {
88
- try {
89
- await cdp(["evalraw", tab, "Page.bringToFront", "{}"], 5000);
90
- return true;
91
- } catch (error) {
92
- console.error(`[chatgpt] bringToFront failed: ${error.message}`);
93
- if (required)
94
- throw error;
95
- return false;
96
- }
97
- }
98
- async function waitForResponse(tab, timeoutMs = 35000) {
99
- await bringToFront(tab, { required: true });
100
- let foregroundAttempts = 0;
101
- let foregroundInFlight = false;
102
- const keepForeground = setInterval(() => {
103
- if (foregroundInFlight)
104
- return;
105
- if (foregroundAttempts >= 8) {
106
- console.error("[chatgpt] stopping foreground keepalive after 8 attempts");
107
- clearInterval(keepForeground);
108
- return;
109
- }
110
- foregroundAttempts++;
111
- foregroundInFlight = true;
112
- bringToFront(tab).finally(() => {
113
- foregroundInFlight = false;
114
- });
115
- }, 4000);
116
- try {
117
- return await waitForStreamComplete(tab, {
118
- timeout: timeoutMs,
119
- interval: 700,
120
- stableRounds: 5,
121
- minLength: 1,
122
- selector: CHATGPT_RESPONSE_SELECTOR,
123
- isStreamingExpr: CHATGPT_STREAMING_EXPR
124
- });
125
- } finally {
126
- clearInterval(keepForeground);
127
- }
128
- }
129
- async function pollForResponseNodeSide(tab, maxMs = 15000) {
130
- const deadline = Date.now() + maxMs;
131
- let lastLen = 0;
132
- let stableRounds = 0;
133
- while (Date.now() < deadline) {
134
- const result = await cdp(["eval", tab, `${CHATGPT_RESPONSE_SELECTOR}?.innerText?.length ?? 0`], 4000).catch(() => "0");
135
- const len = parseInt(result, 10) || 0;
136
- if (len >= 1 && len === lastLen) {
137
- stableRounds++;
138
- if (stableRounds >= 3)
139
- return len;
140
- } else {
141
- lastLen = len;
142
- stableRounds = 0;
143
- }
144
- await new Promise((r) => setTimeout(r, 1200));
145
- }
146
- return lastLen;
147
- }
148
- async function extractAnswerFromDom(tab) {
149
- const raw = await cdp([
150
- "eval",
151
- tab,
152
- String.raw`
153
- (() => {
154
- const assistant = ${CHATGPT_RESPONSE_SELECTOR};
155
- if (!assistant) {
156
- const skipped = document.querySelector('[data-message-author-role="user"]')
157
- ? 'no-assistant-response'
158
- : 'no-user-message';
159
- return JSON.stringify({
160
- answer: '',
161
- sources: [],
162
- skipped,
163
- });
164
- }
165
- const streaming = !!assistant.querySelector('.streaming-animation,[data-is-streaming="true"]') ||
166
- !!document.querySelector('button[data-testid="stop-button"], button[aria-label="Stop generating"], button[aria-label*="Stop"]');
167
- const answer = (assistant.innerText || assistant.textContent || '').trim();
168
- const seen = new Set();
169
- const sources = [];
170
- for (const link of assistant.querySelectorAll('a[href]')) {
171
- const url = link.href;
172
- if (!url || seen.has(url)) continue;
173
- seen.add(url);
174
- const title = (link.innerText || link.textContent || '').replace(/\s+/g, ' ').trim();
175
- sources.push({ title, url });
176
- if (sources.length >= 10) break;
177
- }
178
- return JSON.stringify({ answer, sources, streaming });
179
- })()
180
- `
181
- ]);
182
- try {
183
- return JSON.parse(raw);
184
- } catch (error) {
185
- console.error(`[chatgpt] DOM fallback JSON parse failed (${raw.length} chars): ${error.message}`);
186
- return { answer: "", sources: [], skipped: "parse-error" };
187
- }
188
- }
189
- async function extractAnswer(tab, env) {
190
- let answer = await clickCopyAndAwaitClipboard(tab, CHATGPT_ASSISTANT_COPY_CLICK_EXPR, GLOBAL_VAR, { timeoutMs: 2600, retryClick: 600 });
191
- env.clipboardEmpty = !answer;
192
- let domFallback = null;
193
- if (!answer) {
194
- domFallback = await extractAnswerFromDom(tab);
195
- if (domFallback.streaming) {
196
- return { answer: "", sources: [], skipped: "still-streaming" };
197
- }
198
- answer = domFallback.answer;
199
- env.fallbackUsed = answer ? "dom" : null;
200
- }
201
- if (answer) {
202
- const trimmed = answer.trim();
203
- const looksLikeShortAnswer = trimmed.length >= 5 && trimmed.length <= 50 && /\s|[.,!?;:]/.test(trimmed);
204
- const looksLikeLongAnswer = trimmed.length > 50;
205
- const words = trimmed.split(/\s+/).filter(Boolean);
206
- const domainRepeats = (trimmed.match(/\b[a-z0-9-]+\.(?:com|org|net|dev|io)\b/gi) || []).length;
207
- const looksLikeCitationStub = domainRepeats >= 4 && domainRepeats >= Math.max(3, Math.floor(words.length / 3));
208
- if (!looksLikeShortAnswer && !looksLikeLongAnswer || looksLikeCitationStub) {
209
- console.error(`[chatgpt] DOM fallback answer suspicious (${trimmed.length} chars, domainRepeats=${domainRepeats}: ${JSON.stringify(trimmed.slice(0, 80))}) — returning empty for caller to retry`);
210
- env.fallbackUsed = null;
211
- return {
212
- answer: "",
213
- sources: [],
214
- skipped: looksLikeCitationStub ? "citation-stub" : "header-stub"
215
- };
216
- }
217
- }
218
- if (!answer) {
219
- return { answer: "", sources: [], skipped: "no-answer" };
220
- }
221
- const sourcesInline = parseSourcesFromMarkdown(answer);
222
- const sourcesRef = parseSourcesFromMarkdownRefStyle(answer);
223
- const sourceMap = new Map;
224
- for (const s of [
225
- ...domFallback?.sources || [],
226
- ...sourcesRef,
227
- ...sourcesInline
228
- ]) {
229
- if (s?.url && !sourceMap.has(s.url))
230
- sourceMap.set(s.url, s);
231
- }
232
- const sources = Array.from(sourceMap.values()).slice(0, 10);
233
- return { answer: answer.trim(), sources };
234
- }
235
- const USAGE = `Usage: node extractors/chatgpt.mjs "<query>" [--tab <prefix>]
236
- `;
237
- async function main() {
238
- const args = await prepareArgs(process.argv.slice(2));
239
- validateQuery(args, USAGE);
240
- const { query, tabPrefix, short } = parseArgs(args);
241
- const startTime = Date.now();
242
- const mode = process.env.GREEDY_SEARCH_VISIBLE === "1" ? "visible" : "headless";
243
- const env = {
244
- engine: "chatgpt",
245
- mode,
246
- clipboardEmpty: null,
247
- fallbackUsed: null,
248
- blockedBy: null,
249
- verificationResult: null,
250
- inputReady: null
251
- };
252
- try {
253
- if (!tabPrefix)
254
- await cdp(["list"]);
255
- const tab = await getOrOpenTab(tabPrefix);
256
- const currentUrl = await cdp(["eval", tab, "document.location.href"]).catch(() => "");
257
- let onChatGPT = false;
258
- try {
259
- onChatGPT = new URL(currentUrl).hostname.toLowerCase() === "chatgpt.com";
260
- } catch {}
261
- if (!onChatGPT) {
262
- logStage(env, "nav", startTime);
263
- await cdp(["nav", tab, "https://chatgpt.com"], 20000);
264
- await new Promise((r) => setTimeout(r, 600));
265
- }
266
- logStage(env, "consent", startTime);
267
- await dismissConsent(tab, cdp);
268
- logStage(env, "verification", startTime);
269
- const verificationResult = await handleVerification(tab, cdp, 1e4);
270
- env.verificationResult = verificationResult;
271
- if (verificationResult === "needs-human") {
272
- env.blockedBy = "cloudflare-closed-shadow-dom";
273
- throw new Error("ChatGPT is showing a Cloudflare Turnstile challenge that auto-clicking could not clear — please solve it in the visible browser window");
274
- }
275
- if (verificationResult === "clicked") {
276
- await new Promise((r) => setTimeout(r, 2500));
277
- }
278
- logStage(env, "input-wait", startTime);
279
- const inputReady = await waitForSelector(tab, PROSE_SELECTOR, 8000, 400);
280
- env.inputReady = inputReady;
281
- if (!inputReady) {
282
- const bodyText = await cdp([
283
- "eval",
284
- tab,
285
- `document.body?.innerText || ''`
286
- ]).catch(() => "");
287
- if (/sign in|log in|sign up|\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7|login/i.test(bodyText)) {
288
- throw new Error("ChatGPT requires sign-in — please sign in in the visible browser window");
289
- }
290
- throw new Error("ChatGPT input not found — page may be blocked or in unexpected state");
291
- }
292
- logStage(env, "clipboard-inject", startTime);
293
- await injectClipboardInterceptor(tab, GLOBAL_VAR);
294
- logStage(env, "type-and-submit", startTime);
295
- await typeAndSubmit(tab, query);
296
- logStage(env, "stream-wait", startTime);
297
- let asstLen = 0;
298
- try {
299
- asstLen = await waitForResponse(tab, 20000);
300
- } catch (e) {
301
- logStage(env, "stream-poll-fallback", startTime);
302
- asstLen = await pollForResponseNodeSide(tab, 15000);
303
- }
304
- env.assistantTextLen = asstLen;
305
- if (asstLen < 1) {
306
- console.error("[chatgpt] Warning: assistant response may not have completed");
307
- }
308
- logStage(env, "extract", startTime);
309
- let extractResult;
310
- for (let attempt = 0;attempt < 3; attempt++) {
311
- if (attempt > 0) {
312
- try {
313
- await waitForResponse(tab, 1e4);
314
- } catch {}
315
- }
316
- extractResult = await extractAnswer(tab, env);
317
- if (extractResult.answer)
318
- break;
319
- if (attempt < 2) {
320
- console.error(`[chatgpt] Extract attempt ${attempt + 1} returned empty, retrying in 2s...`);
321
- await new Promise((r) => setTimeout(r, 2000));
322
- }
323
- }
324
- const { answer, sources, skipped } = extractResult;
325
- if (!answer) {
326
- env.blockedBy = "no-response";
327
- env.skipped = skipped || null;
328
- let message = "ChatGPT returned no answer — assistant never responded";
329
- if (skipped === "no-user-message") {
330
- message = "ChatGPT still on homepage — query was not submitted";
331
- } else if (skipped === "no-assistant-response") {
332
- message = "ChatGPT did not return an assistant response after submit";
333
- } else if (skipped === "header-stub" || skipped === "citation-stub" || skipped === "still-streaming") {
334
- message = "ChatGPT response did not finish rendering after 3 retries — assistant never rendered the body";
335
- }
336
- throw new Error(message);
337
- }
338
- logStage(env, "done", startTime);
339
- const finalUrl = await cdp(["eval", tab, "document.location.href"]).catch(() => "https://chatgpt.com");
340
- env.durationMs = Date.now() - startTime;
341
- outputJson({
342
- query,
343
- url: finalUrl,
344
- answer: formatAnswer(answer, short),
345
- sources,
346
- _envelope: buildEnvelope(env)
347
- });
348
- } catch (e) {
349
- env.durationMs = Date.now() - startTime;
350
- console.error(`[chatgpt] error during stage '${env.lastStage || "unknown"}': ${e.message}`);
351
- handleError(e, buildEnvelope(env));
352
- }
353
- }
354
- main();