@apmantza/greedysearch-pi 1.0.0 → 1.0.2

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.
@@ -6,7 +6,13 @@
6
6
  "Read(//tmp/**)",
7
7
  "Bash(for f:*)",
8
8
  "Bash(mkdir:*)",
9
- "Bash(node:*)"
9
+ "Bash(node:*)",
10
+ "Bash(git add:*)",
11
+ "Bash(uv:*)",
12
+ "Bash(.venv/Scripts/fig-ocr.exe --help)",
13
+ "Bash(.venv/Scripts/fig-ocr.exe tui:*)",
14
+ "Bash(tree:*)",
15
+ "Bash(find:*)"
10
16
  ]
11
17
  }
12
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apmantza/greedysearch-pi",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Pi extension: search Perplexity, Bing Copilot, and Google AI simultaneously — synthesized AI answers, not just links",
5
5
  "type": "module",
6
6
  "keywords": [
package/search.mjs CHANGED
@@ -1,297 +1,308 @@
1
- #!/usr/bin/env node
2
- // search.mjs — unified CLI for GreedySearch extractors
3
- //
4
- // Usage:
5
- // node search.mjs <engine> "<query>"
6
- // node search.mjs all "<query>"
7
- //
8
- // Engines:
9
- // perplexity | pplx | p
10
- // bing | copilot | b
11
- // google | g
12
- // stackoverflow | so | stack
13
- // all — fan-out to all engines in parallel
14
- //
15
- // Output: JSON to stdout, errors to stderr
16
- //
17
- // Examples:
18
- // node search.mjs p "what is memoization"
19
- // node search.mjs so "node.js event loop explained"
20
- // node search.mjs all "how does TCP congestion control work"
21
-
22
- import { spawn } from 'child_process';
23
- import { fileURLToPath } from 'url';
24
- import { join, dirname } from 'path';
25
- import { readFileSync, existsSync, writeFileSync } from 'fs';
26
- import { tmpdir } from 'os';
27
-
28
- const __dir = dirname(fileURLToPath(import.meta.url));
29
- // Pi installs chrome-cdp-skill to ~/.pi/agent/git/...; fall back to Claude Code path
30
- // Always use the bundled Windows-compatible cdp.mjs
31
- const CDP = join(__dir, 'cdp.mjs');
32
- const PAGES_CACHE = `${tmpdir().replace(/\\/g, '/')}/cdp-pages.json`;
33
-
34
- const ENGINES = {
35
- perplexity: 'perplexity.mjs',
36
- pplx: 'perplexity.mjs',
37
- p: 'perplexity.mjs',
38
- bing: 'bing-copilot.mjs',
39
- copilot: 'bing-copilot.mjs',
40
- b: 'bing-copilot.mjs',
41
- google: 'google-ai.mjs',
42
- g: 'google-ai.mjs',
43
- stackoverflow: 'stackoverflow-ai.mjs',
44
- so: 'stackoverflow-ai.mjs',
45
- stack: 'stackoverflow-ai.mjs',
46
- };
47
-
48
- const ALL_ENGINES = ['perplexity', 'bing', 'google']; // stackoverflow: disabled until polling fix
49
-
50
- const ENGINE_DOMAINS = {
51
- perplexity: 'perplexity.ai',
52
- bing: 'copilot.microsoft.com',
53
- google: 'google.com',
54
- stackoverflow: 'stackoverflow.com',
55
- };
56
-
57
- function getTabFromCache(engine) {
58
- try {
59
- if (!existsSync(PAGES_CACHE)) return null;
60
- const pages = JSON.parse(readFileSync(PAGES_CACHE, 'utf8'));
61
- const found = pages.find(p => p.url.includes(ENGINE_DOMAINS[engine]));
62
- return found ? found.targetId.slice(0, 8) : null;
63
- } catch { return null; }
64
- }
65
-
66
- function cdp(args, timeoutMs = 15000) {
67
- return new Promise((resolve, reject) => {
68
- const proc = spawn('node', [CDP, ...args], { stdio: ['ignore', 'pipe', 'pipe'] });
69
- let out = '', err = '';
70
- proc.stdout.on('data', d => out += d);
71
- proc.stderr.on('data', d => err += d);
72
- const t = setTimeout(() => { proc.kill(); reject(new Error(`cdp timeout: ${args[0]}`)); }, timeoutMs);
73
- proc.on('close', code => {
74
- clearTimeout(t);
75
- if (code !== 0) reject(new Error(err.trim() || `cdp exit ${code}`));
76
- else resolve(out.trim());
77
- });
78
- });
79
- }
80
-
81
- async function getAnyTab() {
82
- const list = await cdp(['list']);
83
- const first = list.split('\n')[0];
84
- if (!first) throw new Error('No Chrome tabs found');
85
- return first.slice(0, 8);
86
- }
87
-
88
- async function getOrReuseBlankTab() {
89
- // Reuse an existing about:blank tab rather than always creating a new one
90
- const listOut = await cdp(['list']);
91
- const lines = listOut.split('\n').filter(Boolean);
92
- for (const line of lines) {
93
- if (line.includes('about:blank')) {
94
- return line.slice(0, 8); // prefix of the blank tab's targetId
95
- }
96
- }
97
- // No blank tab — open a new one
98
- const anchor = await getAnyTab();
99
- const raw = await cdp(['evalraw', anchor, 'Target.createTarget', '{"url":"about:blank"}']);
100
- const { targetId } = JSON.parse(raw);
101
- return targetId;
102
- }
103
-
104
- async function openNewTab() {
105
- const anchor = await getAnyTab();
106
- const raw = await cdp(['evalraw', anchor, 'Target.createTarget', '{"url":"about:blank"}']);
107
- const { targetId } = JSON.parse(raw);
108
- return targetId;
109
- }
110
-
111
- async function closeTab(targetId) {
112
- try {
113
- const anchor = await getAnyTab();
114
- await cdp(['evalraw', anchor, 'Target.closeTarget', JSON.stringify({ targetId })]);
115
- } catch { /* best-effort */ }
116
- }
117
-
118
- function runExtractor(script, query, tabPrefix = null, short = false) {
119
- const extraArgs = [
120
- ...(tabPrefix ? ['--tab', tabPrefix] : []),
121
- ...(short ? ['--short'] : []),
122
- ];
123
- return new Promise((resolve, reject) => {
124
- const proc = spawn('node', [join(__dir, 'extractors', script), query, ...extraArgs], {
125
- stdio: ['ignore', 'pipe', 'pipe'],
126
- });
127
- let out = '';
128
- let err = '';
129
- proc.stdout.on('data', d => out += d);
130
- proc.stderr.on('data', d => err += d);
131
- proc.on('close', code => {
132
- if (code !== 0) reject(new Error(err.trim() || `extractor exit ${code}`));
133
- else {
134
- try { resolve(JSON.parse(out.trim())); }
135
- catch { reject(new Error(`bad JSON from ${script}: ${out.slice(0, 100)}`)); }
136
- }
137
- });
138
- });
139
- }
140
-
141
-
142
- async function fetchTopSource(url) {
143
- const tab = await openNewTab();
144
- await cdp(['list']); // refresh cache so the new tab is findable
145
- try {
146
- await cdp(['nav', tab, url], 30000);
147
- await new Promise(r => setTimeout(r, 1500));
148
- const content = await cdp(['eval', tab, `
149
- (function(){
150
- var el = document.querySelector('article, [role="main"], main, .post-content, .article-body, #content, .content');
151
- var text = (el || document.body).innerText;
152
- return text.replace(/\\s+/g, ' ').trim().slice(0, 1500);
153
- })()
154
- `]);
155
- return { url, content };
156
- } catch (e) {
157
- return { url, content: null, error: e.message };
158
- } finally {
159
- await closeTab(tab);
160
- }
161
- }
162
-
163
- function pickTopSource(out) {
164
- for (const engine of ['perplexity', 'google', 'bing']) {
165
- const r = out[engine];
166
- if (r?.sources?.length > 0) return r.sources[0];
167
- }
168
- return null;
169
- }
170
-
171
- function writeOutput(data, outFile) {
172
- const json = JSON.stringify(data, null, 2) + '\n';
173
- if (outFile) {
174
- writeFileSync(outFile, json, 'utf8');
175
- process.stderr.write(`Results written to ${outFile}\n`);
176
- } else {
177
- process.stdout.write(json);
178
- }
179
- }
180
-
181
- async function ensureChrome() {
182
- try {
183
- await cdp(['list'], 3000);
184
- } catch {
185
- process.stderr.write('Chrome not running auto-launching GreedySearch Chrome...\n');
186
- await new Promise((resolve, reject) => {
187
- const proc = spawn('node', [join(__dir, 'launch.mjs')], { stdio: 'inherit' });
188
- proc.on('close', code => code === 0 ? resolve() : reject(new Error('launch.mjs failed')));
189
- });
190
- }
191
- }
192
-
193
- async function main() {
194
- const args = process.argv.slice(2);
195
- if (args.length < 2 || args[0] === '--help') {
196
- process.stderr.write([
197
- 'Usage: node search.mjs <engine> "<query>"',
198
- '',
199
- 'Engines: perplexity (p), bing (b), google (g), stackoverflow (so), all',
200
- '',
201
- 'Examples:',
202
- ' node search.mjs p "what is memoization"',
203
- ' node search.mjs so "node.js event loop explained"',
204
- ' node search.mjs all "TCP congestion control"',
205
- ].join('\n') + '\n');
206
- process.exit(1);
207
- }
208
-
209
- await ensureChrome();
210
-
211
- const short = args.includes('--short');
212
- const fetchSource = args.includes('--fetch-top-source');
213
- const outIdx = args.indexOf('--out');
214
- const outFile = outIdx !== -1 ? args[outIdx + 1] : null;
215
- const rest = args.filter((a, i) =>
216
- a !== '--short' &&
217
- a !== '--fetch-top-source' &&
218
- a !== '--out' &&
219
- (outIdx === -1 || i !== outIdx + 1)
220
- );
221
- const engine = rest[0].toLowerCase();
222
- const query = rest.slice(1).join(' ');
223
-
224
- if (engine === 'all') {
225
- await cdp(['list']); // refresh pages cache
226
-
227
- // Assign tabs: reuse existing engine tabs from cache, open new ones only where needed.
228
- // Track opened tabs separately so we only close what we created.
229
- const tabs = [];
230
- const openedTabs = [];
231
- let blankReused = false;
232
-
233
- for (const e of ALL_ENGINES) {
234
- const existing = getTabFromCache(e);
235
- if (existing) {
236
- tabs.push(existing);
237
- } else if (!blankReused) {
238
- const tab = await getOrReuseBlankTab();
239
- tabs.push(tab);
240
- openedTabs.push(tab);
241
- blankReused = true;
242
- } else {
243
- await new Promise(r => setTimeout(r, 500));
244
- const tab = await openNewTab();
245
- tabs.push(tab);
246
- openedTabs.push(tab);
247
- }
248
- }
249
-
250
- // All tabs assigned — run extractors in parallel
251
- const results = await Promise.allSettled(
252
- ALL_ENGINES.map((e, i) =>
253
- runExtractor(ENGINES[e], query, tabs[i], short).then(r => ({ engine: e, ...r }))
254
- )
255
- );
256
-
257
- // Close only tabs we opened (not pre-existing ones)
258
- await Promise.allSettled(openedTabs.map(closeTab));
259
-
260
- const out = {};
261
- for (let i = 0; i < results.length; i++) {
262
- const r = results[i];
263
- if (r.status === 'fulfilled') {
264
- out[r.value.engine] = r.value;
265
- } else {
266
- out[ALL_ENGINES[i]] = { error: r.reason?.message || 'unknown error' };
267
- }
268
- }
269
-
270
- if (fetchSource) {
271
- const top = pickTopSource(out);
272
- if (top) out._topSource = await fetchTopSource(top.url);
273
- }
274
-
275
- writeOutput(out, outFile);
276
- return;
277
- }
278
-
279
- const script = ENGINES[engine];
280
- if (!script) {
281
- process.stderr.write(`Unknown engine: "${engine}"\nAvailable: ${Object.keys(ENGINES).join(', ')}\n`);
282
- process.exit(1);
283
- }
284
-
285
- try {
286
- const result = await runExtractor(script, query, null, short);
287
- if (fetchSource && result.sources?.length > 0) {
288
- result.topSource = await fetchTopSource(result.sources[0].url);
289
- }
290
- writeOutput(result, outFile);
291
- } catch (e) {
292
- process.stderr.write(`Error: ${e.message}\n`);
293
- process.exit(1);
294
- }
295
- }
296
-
297
- main();
1
+ #!/usr/bin/env node
2
+ // search.mjs — unified CLI for GreedySearch extractors
3
+ //
4
+ // Usage:
5
+ // node search.mjs <engine> "<query>"
6
+ // node search.mjs all "<query>"
7
+ //
8
+ // Engines:
9
+ // perplexity | pplx | p
10
+ // bing | copilot | b
11
+ // google | g
12
+ // stackoverflow | so | stack
13
+ // all — fan-out to all engines in parallel
14
+ //
15
+ // Output: JSON to stdout, errors to stderr
16
+ //
17
+ // Examples:
18
+ // node search.mjs p "what is memoization"
19
+ // node search.mjs so "node.js event loop explained"
20
+ // node search.mjs all "how does TCP congestion control work"
21
+
22
+ import { spawn } from 'child_process';
23
+ import { fileURLToPath } from 'url';
24
+ import { join, dirname } from 'path';
25
+ import { readFileSync, existsSync, writeFileSync } from 'fs';
26
+ import { tmpdir } from 'os';
27
+ import http from 'http';
28
+
29
+ const __dir = dirname(fileURLToPath(import.meta.url));
30
+ // Pi installs chrome-cdp-skill to ~/.pi/agent/git/...; fall back to Claude Code path
31
+ // Always use the bundled Windows-compatible cdp.mjs
32
+ const CDP = join(__dir, 'cdp.mjs');
33
+ const PAGES_CACHE = `${tmpdir().replace(/\\/g, '/')}/cdp-pages.json`;
34
+
35
+ const ENGINES = {
36
+ perplexity: 'perplexity.mjs',
37
+ pplx: 'perplexity.mjs',
38
+ p: 'perplexity.mjs',
39
+ bing: 'bing-copilot.mjs',
40
+ copilot: 'bing-copilot.mjs',
41
+ b: 'bing-copilot.mjs',
42
+ google: 'google-ai.mjs',
43
+ g: 'google-ai.mjs',
44
+ stackoverflow: 'stackoverflow-ai.mjs',
45
+ so: 'stackoverflow-ai.mjs',
46
+ stack: 'stackoverflow-ai.mjs',
47
+ };
48
+
49
+ const ALL_ENGINES = ['perplexity', 'bing', 'google']; // stackoverflow: disabled until polling fix
50
+
51
+ const ENGINE_DOMAINS = {
52
+ perplexity: 'perplexity.ai',
53
+ bing: 'copilot.microsoft.com',
54
+ google: 'google.com',
55
+ stackoverflow: 'stackoverflow.com',
56
+ };
57
+
58
+ function getTabFromCache(engine) {
59
+ try {
60
+ if (!existsSync(PAGES_CACHE)) return null;
61
+ const pages = JSON.parse(readFileSync(PAGES_CACHE, 'utf8'));
62
+ const found = pages.find(p => p.url.includes(ENGINE_DOMAINS[engine]));
63
+ return found ? found.targetId.slice(0, 8) : null;
64
+ } catch { return null; }
65
+ }
66
+
67
+ function cdp(args, timeoutMs = 15000) {
68
+ return new Promise((resolve, reject) => {
69
+ const proc = spawn('node', [CDP, ...args], { stdio: ['ignore', 'pipe', 'pipe'] });
70
+ let out = '', err = '';
71
+ proc.stdout.on('data', d => out += d);
72
+ proc.stderr.on('data', d => err += d);
73
+ const t = setTimeout(() => { proc.kill(); reject(new Error(`cdp timeout: ${args[0]}`)); }, timeoutMs);
74
+ proc.on('close', code => {
75
+ clearTimeout(t);
76
+ if (code !== 0) reject(new Error(err.trim() || `cdp exit ${code}`));
77
+ else resolve(out.trim());
78
+ });
79
+ });
80
+ }
81
+
82
+ async function getAnyTab() {
83
+ const list = await cdp(['list']);
84
+ const first = list.split('\n')[0];
85
+ if (!first) throw new Error('No Chrome tabs found');
86
+ return first.slice(0, 8);
87
+ }
88
+
89
+ async function getOrReuseBlankTab() {
90
+ // Reuse an existing about:blank tab rather than always creating a new one
91
+ const listOut = await cdp(['list']);
92
+ const lines = listOut.split('\n').filter(Boolean);
93
+ for (const line of lines) {
94
+ if (line.includes('about:blank')) {
95
+ return line.slice(0, 8); // prefix of the blank tab's targetId
96
+ }
97
+ }
98
+ // No blank tab — open a new one
99
+ const anchor = await getAnyTab();
100
+ const raw = await cdp(['evalraw', anchor, 'Target.createTarget', '{"url":"about:blank"}']);
101
+ const { targetId } = JSON.parse(raw);
102
+ return targetId;
103
+ }
104
+
105
+ async function openNewTab() {
106
+ const anchor = await getAnyTab();
107
+ const raw = await cdp(['evalraw', anchor, 'Target.createTarget', '{"url":"about:blank"}']);
108
+ const { targetId } = JSON.parse(raw);
109
+ return targetId;
110
+ }
111
+
112
+ async function closeTab(targetId) {
113
+ try {
114
+ const anchor = await getAnyTab();
115
+ await cdp(['evalraw', anchor, 'Target.closeTarget', JSON.stringify({ targetId })]);
116
+ } catch { /* best-effort */ }
117
+ }
118
+
119
+ function runExtractor(script, query, tabPrefix = null, short = false) {
120
+ const extraArgs = [
121
+ ...(tabPrefix ? ['--tab', tabPrefix] : []),
122
+ ...(short ? ['--short'] : []),
123
+ ];
124
+ return new Promise((resolve, reject) => {
125
+ const proc = spawn('node', [join(__dir, 'extractors', script), query, ...extraArgs], {
126
+ stdio: ['ignore', 'pipe', 'pipe'],
127
+ });
128
+ let out = '';
129
+ let err = '';
130
+ proc.stdout.on('data', d => out += d);
131
+ proc.stderr.on('data', d => err += d);
132
+ proc.on('close', code => {
133
+ if (code !== 0) reject(new Error(err.trim() || `extractor exit ${code}`));
134
+ else {
135
+ try { resolve(JSON.parse(out.trim())); }
136
+ catch { reject(new Error(`bad JSON from ${script}: ${out.slice(0, 100)}`)); }
137
+ }
138
+ });
139
+ });
140
+ }
141
+
142
+
143
+ async function fetchTopSource(url) {
144
+ const tab = await openNewTab();
145
+ await cdp(['list']); // refresh cache so the new tab is findable
146
+ try {
147
+ await cdp(['nav', tab, url], 30000);
148
+ await new Promise(r => setTimeout(r, 1500));
149
+ const content = await cdp(['eval', tab, `
150
+ (function(){
151
+ var el = document.querySelector('article, [role="main"], main, .post-content, .article-body, #content, .content');
152
+ var text = (el || document.body).innerText;
153
+ return text.replace(/\\s+/g, ' ').trim().slice(0, 1500);
154
+ })()
155
+ `]);
156
+ return { url, content };
157
+ } catch (e) {
158
+ return { url, content: null, error: e.message };
159
+ } finally {
160
+ await closeTab(tab);
161
+ }
162
+ }
163
+
164
+ function pickTopSource(out) {
165
+ for (const engine of ['perplexity', 'google', 'bing']) {
166
+ const r = out[engine];
167
+ if (r?.sources?.length > 0) return r.sources[0];
168
+ }
169
+ return null;
170
+ }
171
+
172
+ function writeOutput(data, outFile) {
173
+ const json = JSON.stringify(data, null, 2) + '\n';
174
+ if (outFile) {
175
+ writeFileSync(outFile, json, 'utf8');
176
+ process.stderr.write(`Results written to ${outFile}\n`);
177
+ } else {
178
+ process.stdout.write(json);
179
+ }
180
+ }
181
+
182
+ const GREEDY_PORT = 9223;
183
+
184
+ function isGreedySearchChromeUp() {
185
+ return new Promise(resolve => {
186
+ const req = http.get(`http://localhost:${GREEDY_PORT}/json/version`, res => {
187
+ resolve(res.statusCode === 200);
188
+ res.resume();
189
+ });
190
+ req.on('error', () => resolve(false));
191
+ req.setTimeout(1500, () => { req.destroy(); resolve(false); });
192
+ });
193
+ }
194
+
195
+ async function ensureChrome() {
196
+ if (await isGreedySearchChromeUp()) return;
197
+ process.stderr.write('GreedySearch Chrome not running — auto-launching...\n');
198
+ await new Promise((resolve, reject) => {
199
+ const proc = spawn('node', [join(__dir, 'launch.mjs')], { stdio: ['ignore', process.stderr, process.stderr] });
200
+ proc.on('close', code => code === 0 ? resolve() : reject(new Error('launch.mjs failed')));
201
+ });
202
+ }
203
+
204
+ async function main() {
205
+ const args = process.argv.slice(2);
206
+ if (args.length < 2 || args[0] === '--help') {
207
+ process.stderr.write([
208
+ 'Usage: node search.mjs <engine> "<query>"',
209
+ '',
210
+ 'Engines: perplexity (p), bing (b), google (g), stackoverflow (so), all',
211
+ '',
212
+ 'Examples:',
213
+ ' node search.mjs p "what is memoization"',
214
+ ' node search.mjs so "node.js event loop explained"',
215
+ ' node search.mjs all "TCP congestion control"',
216
+ ].join('\n') + '\n');
217
+ process.exit(1);
218
+ }
219
+
220
+ await ensureChrome();
221
+
222
+ const short = args.includes('--short');
223
+ const fetchSource = args.includes('--fetch-top-source');
224
+ const outIdx = args.indexOf('--out');
225
+ const outFile = outIdx !== -1 ? args[outIdx + 1] : null;
226
+ const rest = args.filter((a, i) =>
227
+ a !== '--short' &&
228
+ a !== '--fetch-top-source' &&
229
+ a !== '--out' &&
230
+ (outIdx === -1 || i !== outIdx + 1)
231
+ );
232
+ const engine = rest[0].toLowerCase();
233
+ const query = rest.slice(1).join(' ');
234
+
235
+ if (engine === 'all') {
236
+ await cdp(['list']); // refresh pages cache
237
+
238
+ // Assign tabs: reuse existing engine tabs from cache, open new ones only where needed.
239
+ // Track opened tabs separately so we only close what we created.
240
+ const tabs = [];
241
+ const openedTabs = [];
242
+ let blankReused = false;
243
+
244
+ for (const e of ALL_ENGINES) {
245
+ const existing = getTabFromCache(e);
246
+ if (existing) {
247
+ tabs.push(existing);
248
+ } else if (!blankReused) {
249
+ const tab = await getOrReuseBlankTab();
250
+ tabs.push(tab);
251
+ openedTabs.push(tab);
252
+ blankReused = true;
253
+ } else {
254
+ await new Promise(r => setTimeout(r, 500));
255
+ const tab = await openNewTab();
256
+ tabs.push(tab);
257
+ openedTabs.push(tab);
258
+ }
259
+ }
260
+
261
+ // All tabs assigned run extractors in parallel
262
+ const results = await Promise.allSettled(
263
+ ALL_ENGINES.map((e, i) =>
264
+ runExtractor(ENGINES[e], query, tabs[i], short).then(r => ({ engine: e, ...r }))
265
+ )
266
+ );
267
+
268
+ // Close only tabs we opened (not pre-existing ones)
269
+ await Promise.allSettled(openedTabs.map(closeTab));
270
+
271
+ const out = {};
272
+ for (let i = 0; i < results.length; i++) {
273
+ const r = results[i];
274
+ if (r.status === 'fulfilled') {
275
+ out[r.value.engine] = r.value;
276
+ } else {
277
+ out[ALL_ENGINES[i]] = { error: r.reason?.message || 'unknown error' };
278
+ }
279
+ }
280
+
281
+ if (fetchSource) {
282
+ const top = pickTopSource(out);
283
+ if (top) out._topSource = await fetchTopSource(top.url);
284
+ }
285
+
286
+ writeOutput(out, outFile);
287
+ return;
288
+ }
289
+
290
+ const script = ENGINES[engine];
291
+ if (!script) {
292
+ process.stderr.write(`Unknown engine: "${engine}"\nAvailable: ${Object.keys(ENGINES).join(', ')}\n`);
293
+ process.exit(1);
294
+ }
295
+
296
+ try {
297
+ const result = await runExtractor(script, query, null, short);
298
+ if (fetchSource && result.sources?.length > 0) {
299
+ result.topSource = await fetchTopSource(result.sources[0].url);
300
+ }
301
+ writeOutput(result, outFile);
302
+ } catch (e) {
303
+ process.stderr.write(`Error: ${e.message}\n`);
304
+ process.exit(1);
305
+ }
306
+ }
307
+
308
+ main();