@apmantza/greedysearch-pi 1.0.23 → 1.1.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.
@@ -108,15 +108,49 @@ async function extractAnswer(tab) {
108
108
  const answer = await cdp(['eval', tab, `window.__geminiClipboard || ''`]);
109
109
  if (!answer) throw new Error('Clipboard interceptor returned empty text');
110
110
 
111
- // Sources: links rendered in the page (best-effort; Shadow DOM may hide some)
111
+ // Click "Sources" button to open the sidebar with proper source cards
112
+ await cdp(['eval', tab, `
113
+ (function() {
114
+ var btn = document.querySelector('button.legacy-sources-sidebar-button, button.mdс-button--outline');
115
+ if (!btn) btn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.trim() === 'Sources');
116
+ if (btn) { btn.click(); return 'clicked'; }
117
+ return 'not-found';
118
+ })()
119
+ `]).catch(() => 'not-found');
120
+
121
+ // Wait for the sources sidebar to populate
122
+ await new Promise(r => setTimeout(r, 1500));
123
+
124
+ // Extract sources from the sidebar panel (has proper URLs + titles)
112
125
  const raw = await cdp(['eval', tab, `
113
126
  (function() {
114
- var sources = Array.from(document.querySelectorAll('a[href^="http"]'))
127
+ // Find the Sources sidebar container by heading
128
+ var headings = Array.from(document.querySelectorAll('h1, h2, h3, [class*="header"]'));
129
+ var sourceHeading = headings.find(h => h.innerText?.trim() === 'Sources');
130
+ if (sourceHeading) {
131
+ var container = sourceHeading.closest('.container') || sourceHeading.parentElement;
132
+ var links = Array.from(container.querySelectorAll('a[href^="http"]'))
133
+ .map(a => ({ url: a.href.split('#')[0], title: a.innerText?.trim().split('\\n')[0] || '' }))
134
+ .filter(s => s.url && !s.url.includes('gemini.google') && !s.url.includes('gstatic') && !s.url.includes('google.com/search'))
135
+ .filter((v, i, arr) => arr.findIndex(x => x.url === v.url) === i)
136
+ .slice(0, 8);
137
+ return JSON.stringify(links);
138
+ }
139
+ // Fallback: inline source cards with aria-labels
140
+ var cards = Array.from(document.querySelectorAll('button[aria-label*="citation from"]'));
141
+ if (cards.length) {
142
+ return JSON.stringify(cards.map(b => {
143
+ var label = b.getAttribute('aria-label') || '';
144
+ var name = label.match(/from\\s+(.+?)\\.\\s/)?.[1] || label;
145
+ return { url: '', title: name };
146
+ }));
147
+ }
148
+ // Last resort: page-wide links (may include footer junk)
149
+ return JSON.stringify(Array.from(document.querySelectorAll('a[href^="http"]'))
115
150
  .map(a => ({ url: a.href.split('#')[0], title: a.innerText?.trim().split('\\n')[0] || '' }))
116
151
  .filter(s => s.url && !s.url.includes('gemini.google') && !s.url.includes('gstatic') && !s.url.includes('google.com/search'))
117
152
  .filter((v, i, arr) => arr.findIndex(x => x.url === v.url) === i)
118
- .slice(0, 8);
119
- return JSON.stringify(sources);
153
+ .slice(0, 8));
120
154
  })()
121
155
  `]).catch(() => '[]');
122
156
  const sources = JSON.parse(raw);
package/index.ts CHANGED
@@ -22,7 +22,7 @@ function cdpAvailable(): boolean {
22
22
 
23
23
  function runSearch(engine: string, query: string, flags: string[] = []): Promise<Record<string, unknown>> {
24
24
  return new Promise((resolve, reject) => {
25
- const proc = spawn("node", [__dir + "/search.mjs", engine, ...flags, query], {
25
+ const proc = spawn("node", [__dir + "/search.mjs", engine, "--inline", ...flags, query], {
26
26
  stdio: ["ignore", "pipe", "pipe"],
27
27
  });
28
28
  let out = "";
@@ -137,9 +137,13 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
137
137
  description: 'When true and engine is "all", deduplicates sources across engines and feeds them to Gemini for a single grounded synthesis. Adds ~30s but saves tokens and improves answer quality.',
138
138
  default: false,
139
139
  })),
140
+ fullAnswer: Type.Optional(Type.Boolean({
141
+ description: 'When true, returns the complete answer instead of a truncated preview (default: false, answers are shortened to ~300 chars to save tokens).',
142
+ default: false,
143
+ })),
140
144
  }),
141
145
  execute: async (_toolCallId, params) => {
142
- const { query, engine = "all", synthesize = false } = params as { query: string; engine: string; synthesize?: boolean };
146
+ const { query, engine = "all", synthesize = false, fullAnswer = false } = params as { query: string; engine: string; synthesize?: boolean; fullAnswer?: boolean };
143
147
 
144
148
  if (!cdpAvailable()) {
145
149
  return {
@@ -150,6 +154,7 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
150
154
 
151
155
  const flags: string[] = [];
152
156
  if (synthesize && engine === "all") flags.push("--synthesize");
157
+ if (fullAnswer) flags.push("--full");
153
158
 
154
159
  let data: Record<string, unknown>;
155
160
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apmantza/greedysearch-pi",
3
- "version": "1.0.23",
3
+ "version": "1.1.0",
4
4
  "description": "Pi extension: search Perplexity, Bing Copilot, and Google AI in parallel with optional Gemini synthesis — grounded AI answers, not just links",
5
5
  "type": "module",
6
6
  "keywords": [
package/search.mjs CHANGED
@@ -22,7 +22,7 @@
22
22
  import { spawn } from 'child_process';
23
23
  import { fileURLToPath } from 'url';
24
24
  import { join, dirname } from 'path';
25
- import { readFileSync, existsSync, writeFileSync } from 'fs';
25
+ import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
26
26
  import { tmpdir, homedir } from 'os';
27
27
  import http from 'http';
28
28
 
@@ -41,6 +41,8 @@ const ENGINES = {
41
41
  b: 'bing-copilot.mjs',
42
42
  google: 'google-ai.mjs',
43
43
  g: 'google-ai.mjs',
44
+ gemini: 'gemini.mjs',
45
+ gem: 'gemini.mjs',
44
46
  stackoverflow: 'stackoverflow-ai.mjs',
45
47
  so: 'stackoverflow-ai.mjs',
46
48
  stack: 'stackoverflow-ai.mjs',
@@ -52,6 +54,7 @@ const ENGINE_DOMAINS = {
52
54
  perplexity: 'perplexity.ai',
53
55
  bing: 'copilot.microsoft.com',
54
56
  google: 'google.com',
57
+ gemini: 'gemini.google.com',
55
58
  stackoverflow: 'stackoverflow.com',
56
59
  };
57
60
 
@@ -155,7 +158,7 @@ async function fetchTopSource(url) {
155
158
  (function(){
156
159
  var el = document.querySelector('article, [role="main"], main, .post-content, .article-body, #content, .content');
157
160
  var text = (el || document.body).innerText;
158
- return text.replace(/\\s+/g, ' ').trim().slice(0, 1500);
161
+ return text.replace(/\\s+/g, ' ').trim();
159
162
  })()
160
163
  `]);
161
164
  return { url, content };
@@ -214,7 +217,7 @@ async function synthesizeWithGemini(query, results) {
214
217
  if (r?.error) {
215
218
  prompt += `## ${engine} (failed)\nError: ${r.error}\n\n`;
216
219
  } else if (r?.answer) {
217
- prompt += `## ${engine}\n${r.answer.slice(0, 2000)}\n\n`;
220
+ prompt += `## ${engine}\n${r.answer}\n\n`;
218
221
  }
219
222
  }
220
223
 
@@ -248,13 +251,41 @@ async function synthesizeWithGemini(query, results) {
248
251
  });
249
252
  }
250
253
 
251
- function writeOutput(data, outFile) {
254
+ function slugify(query) {
255
+ return query.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 60);
256
+ }
257
+
258
+ function resultsDir() {
259
+ const dir = join(__dir, 'results');
260
+ mkdirSync(dir, { recursive: true });
261
+ return dir;
262
+ }
263
+
264
+ function writeOutput(data, outFile, { inline = false, synthesize = false, query = '' } = {}) {
252
265
  const json = JSON.stringify(data, null, 2) + '\n';
266
+
253
267
  if (outFile) {
254
268
  writeFileSync(outFile, json, 'utf8');
255
269
  process.stderr.write(`Results written to ${outFile}\n`);
256
- } else {
270
+ return;
271
+ }
272
+
273
+ if (inline) {
257
274
  process.stdout.write(json);
275
+ return;
276
+ }
277
+
278
+ const ts = new Date().toISOString().replace('T', '_').replace(/[:.]/g, '-').slice(0, 19);
279
+ const slug = slugify(query);
280
+ const base = join(resultsDir(), `${ts}_${slug}`);
281
+
282
+ writeFileSync(`${base}.json`, json, 'utf8');
283
+
284
+ if (synthesize && data._synthesis?.answer) {
285
+ writeFileSync(`${base}-synthesis.md`, data._synthesis.answer, 'utf8');
286
+ process.stdout.write(`${base}-synthesis.md\n`);
287
+ } else {
288
+ process.stdout.write(`${base}.json\n`);
258
289
  }
259
290
  }
260
291
 
@@ -332,6 +363,7 @@ async function main() {
332
363
  const short = !full; // brief by default; --full opts into complete answers
333
364
  const fetchSource = args.includes('--fetch-top-source');
334
365
  const synthesize = args.includes('--synthesize');
366
+ const inline = args.includes('--inline');
335
367
  const outIdx = args.indexOf('--out');
336
368
  const outFile = outIdx !== -1 ? args[outIdx + 1] : null;
337
369
  const rest = args.filter((a, i) =>
@@ -339,6 +371,7 @@ async function main() {
339
371
  a !== '--short' && // keep accepting --short for back-compat
340
372
  a !== '--fetch-top-source' &&
341
373
  a !== '--synthesize' &&
374
+ a !== '--inline' &&
342
375
  a !== '--out' &&
343
376
  (outIdx === -1 || i !== outIdx + 1)
344
377
  );
@@ -410,7 +443,7 @@ async function main() {
410
443
  if (top) out._topSource = await fetchTopSource(top.url);
411
444
  }
412
445
 
413
- writeOutput(out, outFile);
446
+ writeOutput(out, outFile, { inline, synthesize, query });
414
447
  return;
415
448
  }
416
449
 
@@ -425,7 +458,7 @@ async function main() {
425
458
  if (fetchSource && result.sources?.length > 0) {
426
459
  result.topSource = await fetchTopSource(result.sources[0].url);
427
460
  }
428
- writeOutput(result, outFile);
461
+ writeOutput(result, outFile, { inline, synthesize, query });
429
462
  } catch (e) {
430
463
  process.stderr.write(`Error: ${e.message}\n`);
431
464
  process.exit(1);