@apmantza/greedysearch-pi 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +208 -195
- package/cdp.mjs +1 -0
- package/extractors/bing-copilot.mjs +204 -204
- package/extractors/consent.mjs +248 -248
- package/extractors/google-ai.mjs +165 -165
- package/extractors/perplexity.mjs +184 -184
- package/extractors/selectors.mjs +52 -52
- package/index.ts +623 -507
- package/launch.mjs +7 -51
- package/package.json +2 -2
- package/search.mjs +997 -609
- package/skills/greedy-search/SKILL.md +145 -145
- package/test.sh +298 -298
|
@@ -1,204 +1,204 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// extractors/bing-copilot.mjs
|
|
3
|
-
// Navigate copilot.microsoft.com, wait for answer to complete, return clean answer + sources.
|
|
4
|
-
//
|
|
5
|
-
// Usage:
|
|
6
|
-
// node extractors/bing-copilot.mjs "<query>" [--tab <prefix>]
|
|
7
|
-
//
|
|
8
|
-
// Output (stdout): JSON { answer, sources, query, url }
|
|
9
|
-
// Errors go to stderr only — stdout is always clean JSON for piping.
|
|
10
|
-
|
|
11
|
-
import { readFileSync, existsSync } from 'fs';
|
|
12
|
-
import { spawn } from 'child_process';
|
|
13
|
-
import { tmpdir } from 'os';
|
|
14
|
-
import { join, dirname } from 'path';
|
|
15
|
-
import { fileURLToPath } from 'url';
|
|
16
|
-
import { dismissConsent, handleVerification } from './consent.mjs';
|
|
17
|
-
import { SELECTORS } from './selectors.mjs';
|
|
18
|
-
|
|
19
|
-
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
20
|
-
const CDP = join(__dir, '..', 'cdp.mjs');
|
|
21
|
-
const PAGES_CACHE = `${tmpdir().replace(/\\/g, '/')}/cdp-pages.json`;
|
|
22
|
-
|
|
23
|
-
const COPY_POLL_INTERVAL = 700;
|
|
24
|
-
const COPY_TIMEOUT = 60000;
|
|
25
|
-
|
|
26
|
-
const S = SELECTORS.bing;
|
|
27
|
-
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
|
|
30
|
-
function cdp(args, timeoutMs = 30000) {
|
|
31
|
-
return new Promise((resolve, reject) => {
|
|
32
|
-
const proc = spawn('node', [CDP, ...args], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
33
|
-
let out = '';
|
|
34
|
-
let err = '';
|
|
35
|
-
proc.stdout.on('data', d => out += d);
|
|
36
|
-
proc.stderr.on('data', d => err += d);
|
|
37
|
-
const timer = setTimeout(() => { proc.kill(); reject(new Error(`cdp timeout: ${args[0]}`)); }, timeoutMs);
|
|
38
|
-
proc.on('close', code => {
|
|
39
|
-
clearTimeout(timer);
|
|
40
|
-
if (code !== 0) reject(new Error(err.trim() || `cdp exit ${code}`));
|
|
41
|
-
else resolve(out.trim());
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function getOrOpenTab(tabPrefix) {
|
|
47
|
-
if (tabPrefix) return tabPrefix;
|
|
48
|
-
|
|
49
|
-
if (existsSync(PAGES_CACHE)) {
|
|
50
|
-
const pages = JSON.parse(readFileSync(PAGES_CACHE, 'utf8'));
|
|
51
|
-
const existing = pages.find(p => p.url.includes('copilot.microsoft.com'));
|
|
52
|
-
if (existing) return existing.targetId.slice(0, 8);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const list = await cdp(['list']);
|
|
56
|
-
const firstLine = list.split('\n')[0];
|
|
57
|
-
if (!firstLine) throw new Error('No Chrome tabs found. Is Chrome running with --remote-debugging-port=9222?');
|
|
58
|
-
return firstLine.slice(0, 8);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function injectClipboardInterceptor(tab) {
|
|
62
|
-
await cdp(['eval', tab, `
|
|
63
|
-
window.__bingClipboard = null;
|
|
64
|
-
const _origWriteText = navigator.clipboard.writeText.bind(navigator.clipboard);
|
|
65
|
-
navigator.clipboard.writeText = function(text) {
|
|
66
|
-
window.__bingClipboard = text;
|
|
67
|
-
return _origWriteText(text);
|
|
68
|
-
};
|
|
69
|
-
const _origWrite = navigator.clipboard.write.bind(navigator.clipboard);
|
|
70
|
-
navigator.clipboard.write = async function(items) {
|
|
71
|
-
try {
|
|
72
|
-
for (const item of items) {
|
|
73
|
-
if (item.types && item.types.includes('text/plain')) {
|
|
74
|
-
const blob = await item.getType('text/plain');
|
|
75
|
-
window.__bingClipboard = await blob.text();
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
} catch(e) {}
|
|
80
|
-
return _origWrite(items);
|
|
81
|
-
};
|
|
82
|
-
`]);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function waitForCopyButton(tab) {
|
|
86
|
-
const deadline = Date.now() + COPY_TIMEOUT;
|
|
87
|
-
while (Date.now() < deadline) {
|
|
88
|
-
await new Promise(r => setTimeout(r, COPY_POLL_INTERVAL));
|
|
89
|
-
const found = await cdp(['eval', tab,
|
|
90
|
-
`!!document.querySelector('${S.copyButton}')`
|
|
91
|
-
]).catch(() => 'false');
|
|
92
|
-
if (found === 'true') return;
|
|
93
|
-
}
|
|
94
|
-
throw new Error(`Copilot copy button did not appear within ${COPY_TIMEOUT}ms`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function extractAnswer(tab) {
|
|
98
|
-
await cdp(['eval', tab, `document.querySelector('${S.copyButton}')?.click()`]);
|
|
99
|
-
await new Promise(r => setTimeout(r, 400));
|
|
100
|
-
|
|
101
|
-
const answer = await cdp(['eval', tab, `window.__bingClipboard || ''`]);
|
|
102
|
-
if (!answer) throw new Error('Clipboard interceptor returned empty text');
|
|
103
|
-
|
|
104
|
-
const raw = await cdp(['eval', tab, `
|
|
105
|
-
(function() {
|
|
106
|
-
var sources = Array.from(document.querySelectorAll('${S.sourceLink}'))
|
|
107
|
-
.map(a => ({ url: a.href, title: a.innerText?.trim().split('\\n')[0] || a.title || '' }))
|
|
108
|
-
.filter(s => s.url && !s.url.includes('${S.sourceExclude}'))
|
|
109
|
-
.filter((v, i, arr) => arr.findIndex(x => x.url === v.url) === i)
|
|
110
|
-
.slice(0, 10);
|
|
111
|
-
return JSON.stringify(sources);
|
|
112
|
-
})()
|
|
113
|
-
`]).catch(() => '[]');
|
|
114
|
-
const sources = JSON.parse(raw);
|
|
115
|
-
|
|
116
|
-
return { answer: answer.trim(), sources };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ---------------------------------------------------------------------------
|
|
120
|
-
|
|
121
|
-
async function main() {
|
|
122
|
-
const args = process.argv.slice(2);
|
|
123
|
-
if (!args.length || args[0] === '--help') {
|
|
124
|
-
process.stderr.write('Usage: node extractors/bing-copilot.mjs "<query>" [--tab <prefix>]\n');
|
|
125
|
-
process.exit(1);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const short = args.includes('--short');
|
|
129
|
-
const rest = args.filter(a => a !== '--short');
|
|
130
|
-
const tabFlagIdx = rest.indexOf('--tab');
|
|
131
|
-
const tabPrefix = tabFlagIdx !== -1 ? rest[tabFlagIdx + 1] : null;
|
|
132
|
-
const query = tabFlagIdx !== -1
|
|
133
|
-
? rest.filter((_, i) => i !== tabFlagIdx && i !== tabFlagIdx + 1).join(' ')
|
|
134
|
-
: rest.join(' ');
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
await cdp(['list']);
|
|
138
|
-
const tab = await getOrOpenTab(tabPrefix);
|
|
139
|
-
|
|
140
|
-
// Navigate to Copilot homepage and use the chat input
|
|
141
|
-
await cdp(['nav', tab, 'https://copilot.microsoft.com/'], 35000);
|
|
142
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
143
|
-
await dismissConsent(tab, cdp);
|
|
144
|
-
|
|
145
|
-
// Handle verification challenges (Cloudflare Turnstile, Microsoft auth, etc.)
|
|
146
|
-
const verifyResult = await handleVerification(tab, cdp, 90000);
|
|
147
|
-
if (verifyResult === 'needs-human') {
|
|
148
|
-
throw new Error('Copilot verification required — please solve it manually in the browser window');
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// After verification, page may have redirected or reloaded — wait for it to settle
|
|
152
|
-
if (verifyResult === 'clicked') {
|
|
153
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
154
|
-
|
|
155
|
-
// Re-navigate if we got redirected
|
|
156
|
-
const currentUrl = await cdp(['eval', tab, 'document.location.href']).catch(() => '');
|
|
157
|
-
if (!currentUrl.includes('copilot.microsoft.com')) {
|
|
158
|
-
await cdp(['nav', tab, 'https://copilot.microsoft.com/'], 35000);
|
|
159
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
160
|
-
await dismissConsent(tab, cdp);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Wait for React app to mount input (up to 15s, longer after verification)
|
|
165
|
-
const inputDeadline = Date.now() + 15000;
|
|
166
|
-
while (Date.now() < inputDeadline) {
|
|
167
|
-
const found = await cdp(['eval', tab, `!!document.querySelector('${S.input}')`]).catch(() => 'false');
|
|
168
|
-
if (found === 'true') break;
|
|
169
|
-
await new Promise(r => setTimeout(r, 500));
|
|
170
|
-
}
|
|
171
|
-
await new Promise(r => setTimeout(r, 300));
|
|
172
|
-
|
|
173
|
-
// Verify input is actually there before proceeding
|
|
174
|
-
const inputReady = await cdp(['eval', tab, `!!document.querySelector('${S.input}')`]).catch(() => 'false');
|
|
175
|
-
if (inputReady !== 'true') {
|
|
176
|
-
throw new Error('Copilot input not found — verification may have failed or page is in unexpected state');
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
await injectClipboardInterceptor(tab);
|
|
180
|
-
await cdp(['click', tab, S.input]);
|
|
181
|
-
await new Promise(r => setTimeout(r, 400));
|
|
182
|
-
await cdp(['type', tab, query]);
|
|
183
|
-
await new Promise(r => setTimeout(r, 400));
|
|
184
|
-
|
|
185
|
-
// Submit with Enter (most reliable across locales and Chrome instances)
|
|
186
|
-
await cdp(['eval', tab,
|
|
187
|
-
`document.querySelector('${S.input}')?.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true,keyCode:13})), 'ok'`
|
|
188
|
-
]);
|
|
189
|
-
|
|
190
|
-
await waitForCopyButton(tab);
|
|
191
|
-
|
|
192
|
-
const { answer, sources } = await extractAnswer(tab);
|
|
193
|
-
if (!answer) throw new Error('No answer extracted — Copilot may not have responded');
|
|
194
|
-
const out = short ? answer.slice(0, 300).replace(/\s+\S*$/, '') + '…' : answer;
|
|
195
|
-
|
|
196
|
-
const finalUrl = await cdp(['eval', tab, 'document.location.href']).catch(() => '');
|
|
197
|
-
process.stdout.write(JSON.stringify({ query, url: finalUrl, answer: out, sources }, null, 2) + '\n');
|
|
198
|
-
} catch (e) {
|
|
199
|
-
process.stderr.write(`Error: ${e.message}\n`);
|
|
200
|
-
process.exit(1);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
main();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// extractors/bing-copilot.mjs
|
|
3
|
+
// Navigate copilot.microsoft.com, wait for answer to complete, return clean answer + sources.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// node extractors/bing-copilot.mjs "<query>" [--tab <prefix>]
|
|
7
|
+
//
|
|
8
|
+
// Output (stdout): JSON { answer, sources, query, url }
|
|
9
|
+
// Errors go to stderr only — stdout is always clean JSON for piping.
|
|
10
|
+
|
|
11
|
+
import { readFileSync, existsSync } from 'fs';
|
|
12
|
+
import { spawn } from 'child_process';
|
|
13
|
+
import { tmpdir } from 'os';
|
|
14
|
+
import { join, dirname } from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
import { dismissConsent, handleVerification } from './consent.mjs';
|
|
17
|
+
import { SELECTORS } from './selectors.mjs';
|
|
18
|
+
|
|
19
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const CDP = join(__dir, '..', 'cdp.mjs');
|
|
21
|
+
const PAGES_CACHE = `${tmpdir().replace(/\\/g, '/')}/cdp-pages.json`;
|
|
22
|
+
|
|
23
|
+
const COPY_POLL_INTERVAL = 700;
|
|
24
|
+
const COPY_TIMEOUT = 60000;
|
|
25
|
+
|
|
26
|
+
const S = SELECTORS.bing;
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
function cdp(args, timeoutMs = 30000) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const proc = spawn('node', [CDP, ...args], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
33
|
+
let out = '';
|
|
34
|
+
let err = '';
|
|
35
|
+
proc.stdout.on('data', d => out += d);
|
|
36
|
+
proc.stderr.on('data', d => err += d);
|
|
37
|
+
const timer = setTimeout(() => { proc.kill(); reject(new Error(`cdp timeout: ${args[0]}`)); }, timeoutMs);
|
|
38
|
+
proc.on('close', code => {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
if (code !== 0) reject(new Error(err.trim() || `cdp exit ${code}`));
|
|
41
|
+
else resolve(out.trim());
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function getOrOpenTab(tabPrefix) {
|
|
47
|
+
if (tabPrefix) return tabPrefix;
|
|
48
|
+
|
|
49
|
+
if (existsSync(PAGES_CACHE)) {
|
|
50
|
+
const pages = JSON.parse(readFileSync(PAGES_CACHE, 'utf8'));
|
|
51
|
+
const existing = pages.find(p => p.url.includes('copilot.microsoft.com'));
|
|
52
|
+
if (existing) return existing.targetId.slice(0, 8);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const list = await cdp(['list']);
|
|
56
|
+
const firstLine = list.split('\n')[0];
|
|
57
|
+
if (!firstLine) throw new Error('No Chrome tabs found. Is Chrome running with --remote-debugging-port=9222?');
|
|
58
|
+
return firstLine.slice(0, 8);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function injectClipboardInterceptor(tab) {
|
|
62
|
+
await cdp(['eval', tab, `
|
|
63
|
+
window.__bingClipboard = null;
|
|
64
|
+
const _origWriteText = navigator.clipboard.writeText.bind(navigator.clipboard);
|
|
65
|
+
navigator.clipboard.writeText = function(text) {
|
|
66
|
+
window.__bingClipboard = text;
|
|
67
|
+
return _origWriteText(text);
|
|
68
|
+
};
|
|
69
|
+
const _origWrite = navigator.clipboard.write.bind(navigator.clipboard);
|
|
70
|
+
navigator.clipboard.write = async function(items) {
|
|
71
|
+
try {
|
|
72
|
+
for (const item of items) {
|
|
73
|
+
if (item.types && item.types.includes('text/plain')) {
|
|
74
|
+
const blob = await item.getType('text/plain');
|
|
75
|
+
window.__bingClipboard = await blob.text();
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch(e) {}
|
|
80
|
+
return _origWrite(items);
|
|
81
|
+
};
|
|
82
|
+
`]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function waitForCopyButton(tab) {
|
|
86
|
+
const deadline = Date.now() + COPY_TIMEOUT;
|
|
87
|
+
while (Date.now() < deadline) {
|
|
88
|
+
await new Promise(r => setTimeout(r, COPY_POLL_INTERVAL));
|
|
89
|
+
const found = await cdp(['eval', tab,
|
|
90
|
+
`!!document.querySelector('${S.copyButton}')`
|
|
91
|
+
]).catch(() => 'false');
|
|
92
|
+
if (found === 'true') return;
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`Copilot copy button did not appear within ${COPY_TIMEOUT}ms`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function extractAnswer(tab) {
|
|
98
|
+
await cdp(['eval', tab, `document.querySelector('${S.copyButton}')?.click()`]);
|
|
99
|
+
await new Promise(r => setTimeout(r, 400));
|
|
100
|
+
|
|
101
|
+
const answer = await cdp(['eval', tab, `window.__bingClipboard || ''`]);
|
|
102
|
+
if (!answer) throw new Error('Clipboard interceptor returned empty text');
|
|
103
|
+
|
|
104
|
+
const raw = await cdp(['eval', tab, `
|
|
105
|
+
(function() {
|
|
106
|
+
var sources = Array.from(document.querySelectorAll('${S.sourceLink}'))
|
|
107
|
+
.map(a => ({ url: a.href, title: a.innerText?.trim().split('\\n')[0] || a.title || '' }))
|
|
108
|
+
.filter(s => s.url && !s.url.includes('${S.sourceExclude}'))
|
|
109
|
+
.filter((v, i, arr) => arr.findIndex(x => x.url === v.url) === i)
|
|
110
|
+
.slice(0, 10);
|
|
111
|
+
return JSON.stringify(sources);
|
|
112
|
+
})()
|
|
113
|
+
`]).catch(() => '[]');
|
|
114
|
+
const sources = JSON.parse(raw);
|
|
115
|
+
|
|
116
|
+
return { answer: answer.trim(), sources };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
async function main() {
|
|
122
|
+
const args = process.argv.slice(2);
|
|
123
|
+
if (!args.length || args[0] === '--help') {
|
|
124
|
+
process.stderr.write('Usage: node extractors/bing-copilot.mjs "<query>" [--tab <prefix>]\n');
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const short = args.includes('--short');
|
|
129
|
+
const rest = args.filter(a => a !== '--short');
|
|
130
|
+
const tabFlagIdx = rest.indexOf('--tab');
|
|
131
|
+
const tabPrefix = tabFlagIdx !== -1 ? rest[tabFlagIdx + 1] : null;
|
|
132
|
+
const query = tabFlagIdx !== -1
|
|
133
|
+
? rest.filter((_, i) => i !== tabFlagIdx && i !== tabFlagIdx + 1).join(' ')
|
|
134
|
+
: rest.join(' ');
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
await cdp(['list']);
|
|
138
|
+
const tab = await getOrOpenTab(tabPrefix);
|
|
139
|
+
|
|
140
|
+
// Navigate to Copilot homepage and use the chat input
|
|
141
|
+
await cdp(['nav', tab, 'https://copilot.microsoft.com/'], 35000);
|
|
142
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
143
|
+
await dismissConsent(tab, cdp);
|
|
144
|
+
|
|
145
|
+
// Handle verification challenges (Cloudflare Turnstile, Microsoft auth, etc.)
|
|
146
|
+
const verifyResult = await handleVerification(tab, cdp, 90000);
|
|
147
|
+
if (verifyResult === 'needs-human') {
|
|
148
|
+
throw new Error('Copilot verification required — please solve it manually in the browser window');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// After verification, page may have redirected or reloaded — wait for it to settle
|
|
152
|
+
if (verifyResult === 'clicked') {
|
|
153
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
154
|
+
|
|
155
|
+
// Re-navigate if we got redirected
|
|
156
|
+
const currentUrl = await cdp(['eval', tab, 'document.location.href']).catch(() => '');
|
|
157
|
+
if (!currentUrl.includes('copilot.microsoft.com')) {
|
|
158
|
+
await cdp(['nav', tab, 'https://copilot.microsoft.com/'], 35000);
|
|
159
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
160
|
+
await dismissConsent(tab, cdp);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Wait for React app to mount input (up to 15s, longer after verification)
|
|
165
|
+
const inputDeadline = Date.now() + 15000;
|
|
166
|
+
while (Date.now() < inputDeadline) {
|
|
167
|
+
const found = await cdp(['eval', tab, `!!document.querySelector('${S.input}')`]).catch(() => 'false');
|
|
168
|
+
if (found === 'true') break;
|
|
169
|
+
await new Promise(r => setTimeout(r, 500));
|
|
170
|
+
}
|
|
171
|
+
await new Promise(r => setTimeout(r, 300));
|
|
172
|
+
|
|
173
|
+
// Verify input is actually there before proceeding
|
|
174
|
+
const inputReady = await cdp(['eval', tab, `!!document.querySelector('${S.input}')`]).catch(() => 'false');
|
|
175
|
+
if (inputReady !== 'true') {
|
|
176
|
+
throw new Error('Copilot input not found — verification may have failed or page is in unexpected state');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
await injectClipboardInterceptor(tab);
|
|
180
|
+
await cdp(['click', tab, S.input]);
|
|
181
|
+
await new Promise(r => setTimeout(r, 400));
|
|
182
|
+
await cdp(['type', tab, query]);
|
|
183
|
+
await new Promise(r => setTimeout(r, 400));
|
|
184
|
+
|
|
185
|
+
// Submit with Enter (most reliable across locales and Chrome instances)
|
|
186
|
+
await cdp(['eval', tab,
|
|
187
|
+
`document.querySelector('${S.input}')?.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true,keyCode:13})), 'ok'`
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
await waitForCopyButton(tab);
|
|
191
|
+
|
|
192
|
+
const { answer, sources } = await extractAnswer(tab);
|
|
193
|
+
if (!answer) throw new Error('No answer extracted — Copilot may not have responded');
|
|
194
|
+
const out = short ? answer.slice(0, 300).replace(/\s+\S*$/, '') + '…' : answer;
|
|
195
|
+
|
|
196
|
+
const finalUrl = await cdp(['eval', tab, 'document.location.href']).catch(() => '');
|
|
197
|
+
process.stdout.write(JSON.stringify({ query, url: finalUrl, answer: out, sources }, null, 2) + '\n');
|
|
198
|
+
} catch (e) {
|
|
199
|
+
process.stderr.write(`Error: ${e.message}\n`);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
main();
|