@apmantza/greedysearch-pi 1.2.0 → 1.3.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.
package/index.ts CHANGED
@@ -125,6 +125,124 @@ function formatResults(engine: string, data: Record<string, unknown>): string {
125
125
  return lines.join("\n").trim();
126
126
  }
127
127
 
128
+ function formatDeepResearch(data: Record<string, unknown>): string {
129
+ const lines: string[] = [];
130
+ const confidence = data._confidence as Record<string, unknown> | undefined;
131
+ const fetchedSources = data._fetchedSources as Array<Record<string, unknown>> | undefined;
132
+ const dedupedSources = data._sources as Array<Record<string, unknown>> | undefined;
133
+
134
+ lines.push("# Deep Research Report\n");
135
+
136
+ // Confidence summary
137
+ if (confidence) {
138
+ const enginesResponded = (confidence.enginesResponded as string[]) || [];
139
+ const enginesFailed = (confidence.enginesFailed as string[]) || [];
140
+ const consensusScore = confidence.consensusScore || 0;
141
+
142
+ lines.push("## Confidence\n");
143
+ lines.push(`- **Engines responded:** ${enginesResponded.join(", ") || "none"}`);
144
+ if (enginesFailed.length > 0) {
145
+ lines.push(`- **Engines failed:** ${enginesFailed.join(", ")}`);
146
+ }
147
+ lines.push(`- **Top source consensus:** ${consensusScore}/3 engines`);
148
+ lines.push(`- **Total unique sources:** ${confidence.sourcesCount || 0}`);
149
+ lines.push("");
150
+ }
151
+
152
+ // Per-engine answers
153
+ lines.push("## Findings\n");
154
+ for (const engine of ["perplexity", "bing", "google"]) {
155
+ const r = data[engine] as Record<string, unknown> | undefined;
156
+ if (!r) continue;
157
+ lines.push(`### ${engine.charAt(0).toUpperCase() + engine.slice(1)}`);
158
+ if (r.error) {
159
+ lines.push(`⚠️ Error: ${r.error}`);
160
+ } else if (r.answer) {
161
+ lines.push(String(r.answer).slice(0, 2000));
162
+ }
163
+ lines.push("");
164
+ }
165
+
166
+ // Synthesis
167
+ const synthesis = data._synthesis as Record<string, unknown> | undefined;
168
+ if (synthesis?.answer) {
169
+ lines.push("## Synthesized Answer\n");
170
+ lines.push(String(synthesis.answer));
171
+ lines.push("");
172
+ }
173
+
174
+ // Deduplicated sources by consensus
175
+ if (dedupedSources && dedupedSources.length > 0) {
176
+ lines.push("## Sources (Ranked by Consensus)\n");
177
+ for (const s of dedupedSources) {
178
+ const engines = (s.engines as string[]) || [];
179
+ const consensus = engines.length;
180
+ lines.push(`- **[${consensus}/3]** [${s.title || "Untitled"}](${s.url})`);
181
+ }
182
+ lines.push("");
183
+ }
184
+
185
+ // Fetched source content
186
+ if (fetchedSources && fetchedSources.length > 0) {
187
+ lines.push("## Source Content (Top Matches)\n");
188
+ for (const fs of fetchedSources) {
189
+ lines.push(`### ${fs.title || fs.url}`);
190
+ lines.push(`*Source: ${fs.url}*`);
191
+ lines.push("");
192
+ if (fs.content) {
193
+ lines.push(String(fs.content).slice(0, 3000));
194
+ } else if (fs.error) {
195
+ lines.push(`⚠️ Could not fetch: ${fs.error}`);
196
+ }
197
+ lines.push("\n---\n");
198
+ }
199
+ }
200
+
201
+ return lines.join("\n").trim();
202
+ }
203
+
204
+ function formatCodingTask(data: Record<string, unknown> | Record<string, Record<string, unknown>>): string {
205
+ const lines: string[] = [];
206
+
207
+ // Check if it's multi-engine result
208
+ const hasMultipleEngines = "gemini" in data || "copilot" in data;
209
+
210
+ if (hasMultipleEngines) {
211
+ // Multi-engine result
212
+ for (const [engineName, result] of Object.entries(data)) {
213
+ const r = result as Record<string, unknown>;
214
+ lines.push(`## ${engineName.charAt(0).toUpperCase() + engineName.slice(1)}\n`);
215
+
216
+ if (r.error) {
217
+ lines.push(`⚠️ Error: ${r.error}\n`);
218
+ } else {
219
+ if (r.explanation) lines.push(String(r.explanation));
220
+ if (Array.isArray(r.code) && r.code.length > 0) {
221
+ for (const block of r.code) {
222
+ const b = block as { language: string; code: string };
223
+ lines.push(`\n\`\`\`${b.language}\n${b.code}\n\`\`\`\n`);
224
+ }
225
+ }
226
+ if (r.url) lines.push(`*Source: ${r.url}*`);
227
+ }
228
+ lines.push("");
229
+ }
230
+ } else {
231
+ // Single engine result
232
+ const r = data as Record<string, unknown>;
233
+ if (r.explanation) lines.push(String(r.explanation));
234
+ if (Array.isArray(r.code) && r.code.length > 0) {
235
+ for (const block of r.code) {
236
+ const b = block as { language: string; code: string };
237
+ lines.push(`\n\`\`\`${b.language}\n${b.code}\n\`\`\`\n`);
238
+ }
239
+ }
240
+ if (r.url) lines.push(`*Source: ${r.url}*`);
241
+ }
242
+
243
+ return lines.join("\n").trim();
244
+ }
245
+
128
246
  export default function greedySearchExtension(pi: ExtensionAPI) {
129
247
  pi.on("session_start", async (_event, ctx) => {
130
248
  if (!cdpAvailable()) {
@@ -219,4 +337,171 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
219
337
  }
220
338
  },
221
339
  });
340
+
341
+ // ─── deep_research ─────────────────────────────────────────────────────────
342
+ pi.registerTool({
343
+ name: "deep_research",
344
+ label: "Deep Research",
345
+ description:
346
+ "Comprehensive multi-engine research with source fetching and synthesis. " +
347
+ "Runs Perplexity, Bing Copilot, and Google AI in parallel with full answers, " +
348
+ "deduplicates and ranks sources by consensus, fetches content from top sources, " +
349
+ "and synthesizes via Gemini. Returns a structured research document with confidence scores. " +
350
+ "Use for architecture decisions, library comparisons, best practices, or any research where the answer matters.",
351
+ promptSnippet: "Deep multi-engine research with source deduplication and synthesis",
352
+ parameters: Type.Object({
353
+ query: Type.String({ description: "The research question" }),
354
+ }),
355
+ execute: async (_toolCallId, params, signal, onUpdate) => {
356
+ const { query } = params as { query: string };
357
+
358
+ if (!cdpAvailable()) {
359
+ return {
360
+ content: [{ type: "text", text: "cdp.mjs missing — try reinstalling." }],
361
+ details: {} as { raw?: Record<string, unknown> },
362
+ };
363
+ }
364
+
365
+ const completed = new Set<string>();
366
+
367
+ const onProgress = (eng: string, status: "done" | "error") => {
368
+ completed.add(eng);
369
+ const parts: string[] = [];
370
+ for (const e of ALL_ENGINES) {
371
+ if (completed.has(e)) parts.push(`✅ ${e}`);
372
+ else parts.push(`⏳ ${e}`);
373
+ }
374
+ if (completed.size >= 3) parts.push("🔄 synthesizing");
375
+
376
+ onUpdate?.({
377
+ content: [{ type: "text", text: `**Researching...** ${parts.join(" · ")}` }],
378
+ details: { _progress: true },
379
+ } as any);
380
+ };
381
+
382
+ try {
383
+ // Run deep research (includes full answers, synthesis, and source fetching)
384
+ const data = await runSearch("all", query, ["--deep-research"], signal, onProgress);
385
+ const text = formatDeepResearch(data);
386
+ return {
387
+ content: [{ type: "text", text: text || "No results returned." }],
388
+ details: { raw: data },
389
+ };
390
+ } catch (e) {
391
+ const msg = e instanceof Error ? e.message : String(e);
392
+ return {
393
+ content: [{ type: "text", text: `Deep research failed: ${msg}` }],
394
+ details: {} as { raw?: Record<string, unknown> },
395
+ };
396
+ }
397
+ },
398
+ });
399
+
400
+ // ─── coding_task ───────────────────────────────────────────────────────────
401
+ pi.registerTool({
402
+ name: "coding_task",
403
+ label: "Coding Task",
404
+ description:
405
+ "Delegate a coding task to Gemini and/or Copilot via browser automation. " +
406
+ "Returns extracted code blocks and explanations. Supports multiple modes: " +
407
+ "'code' (write/modify code), 'review' (senior engineer code review), " +
408
+ "'plan' (architect risk assessment), 'test' (edge case testing), " +
409
+ "'debug' (fresh-eyes root cause analysis). " +
410
+ "Best for getting a 'second opinion' on hard problems, debugging tricky issues, " +
411
+ "or risk-assessing major refactors. Use engine 'all' for both perspectives.",
412
+ promptSnippet: "Browser-based coding assistant with Gemini and Copilot",
413
+ parameters: Type.Object({
414
+ task: Type.String({ description: "The coding task or question" }),
415
+ engine: Type.Union(
416
+ [
417
+ Type.Literal("all"),
418
+ Type.Literal("gemini"),
419
+ Type.Literal("copilot"),
420
+ ],
421
+ {
422
+ description: 'Engine to use. "all" runs both Gemini and Copilot in parallel.',
423
+ default: "gemini",
424
+ },
425
+ ),
426
+ mode: Type.Union(
427
+ [
428
+ Type.Literal("code"),
429
+ Type.Literal("review"),
430
+ Type.Literal("plan"),
431
+ Type.Literal("test"),
432
+ Type.Literal("debug"),
433
+ ],
434
+ {
435
+ description: "Task mode: code (default), review (code review), plan (architect review), test (write tests), debug (root cause analysis)",
436
+ default: "code",
437
+ },
438
+ ),
439
+ context: Type.Optional(Type.String({
440
+ description: "Optional code context/snippet to include with the task",
441
+ })),
442
+ }),
443
+ execute: async (_toolCallId, params, signal, onUpdate) => {
444
+ const { task, engine = "gemini", mode = "code", context } = params as {
445
+ task: string; engine: string; mode: string; context?: string;
446
+ };
447
+
448
+ if (!cdpAvailable()) {
449
+ return {
450
+ content: [{ type: "text", text: "cdp.mjs missing — try reinstalling." }],
451
+ details: {} as { raw?: Record<string, unknown> },
452
+ };
453
+ }
454
+
455
+ const flags: string[] = ["--engine", engine, "--mode", mode];
456
+ if (context) flags.push("--context", context);
457
+
458
+ try {
459
+ onUpdate?.({
460
+ content: [{ type: "text", text: `**Coding task...** 🔄 ${engine === "all" ? "Gemini + Copilot" : engine} (${mode} mode)` }],
461
+ details: { _progress: true },
462
+ } as any);
463
+
464
+ const data = await new Promise<Record<string, unknown>>((resolve, reject) => {
465
+ const proc = spawn("node", [__dir + "/coding-task.mjs", task, ...flags], {
466
+ stdio: ["ignore", "pipe", "pipe"],
467
+ });
468
+ let out = "";
469
+ let err = "";
470
+
471
+ const onAbort = () => { proc.kill("SIGTERM"); reject(new Error("Aborted")); };
472
+ signal?.addEventListener("abort", onAbort, { once: true });
473
+
474
+ proc.stdout.on("data", (d: Buffer) => (out += d));
475
+ proc.stderr.on("data", (d: Buffer) => { err += d; });
476
+ proc.on("close", (code: number) => {
477
+ signal?.removeEventListener("abort", onAbort);
478
+ if (code !== 0) {
479
+ reject(new Error(err.trim() || `coding-task.mjs exited with code ${code}`));
480
+ } else {
481
+ try {
482
+ resolve(JSON.parse(out.trim()));
483
+ } catch {
484
+ reject(new Error(`Invalid JSON from coding-task.mjs: ${out.slice(0, 200)}`));
485
+ }
486
+ }
487
+ });
488
+
489
+ // Timeout after 3 minutes
490
+ setTimeout(() => { proc.kill("SIGTERM"); reject(new Error("Coding task timed out after 180s")); }, 180000);
491
+ });
492
+
493
+ const text = formatCodingTask(data);
494
+ return {
495
+ content: [{ type: "text", text: text || "No response." }],
496
+ details: { raw: data },
497
+ };
498
+ } catch (e) {
499
+ const msg = e instanceof Error ? e.message : String(e);
500
+ return {
501
+ content: [{ type: "text", text: `Coding task failed: ${msg}` }],
502
+ details: {} as { raw?: Record<string, unknown> },
503
+ };
504
+ }
505
+ },
506
+ });
222
507
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apmantza/greedysearch-pi",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Pi extension: browser-automation tool that searches Perplexity, Bing Copilot, and Google AI in parallel, extracts answers and sources via CDP, with optional Gemini synthesis — grounded AI answers from real browser interactions.",
5
5
  "type": "module",
6
6
  "keywords": [
package/search.mjs CHANGED
@@ -165,6 +165,75 @@ async function fetchTopSource(url) {
165
165
  }
166
166
  }
167
167
 
168
+ async function fetchSourceContent(url, maxChars = 5000) {
169
+ try {
170
+ const controller = new AbortController();
171
+ const timeout = setTimeout(() => controller.abort(), 15000);
172
+
173
+ const res = await fetch(url, {
174
+ signal: controller.signal,
175
+ headers: {
176
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
177
+ 'Accept': 'text/html,application/xhtml+xml',
178
+ 'Accept-Language': 'en-US,en;q=0.9',
179
+ },
180
+ });
181
+ clearTimeout(timeout);
182
+
183
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
184
+
185
+ const html = await res.text();
186
+
187
+ // Simple HTML extraction - remove tags and extract text
188
+ const content = html
189
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
190
+ .replace(/<style[\s\S]*?<\/style>/gi, '')
191
+ .replace(/<nav[\s\S]*?<\/nav>/gi, '')
192
+ .replace(/<header[\s\S]*?<\/header>/gi, '')
193
+ .replace(/<footer[\s\S]*?<\/footer>/gi, '')
194
+ .replace(/<[^>]+>/g, ' ')
195
+ .replace(/&[a-z]+;/gi, ' ')
196
+ .replace(/\s+/g, ' ')
197
+ .trim()
198
+ .slice(0, maxChars);
199
+
200
+ // Extract title
201
+ const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
202
+ const title = titleMatch ? titleMatch[1].trim() : '';
203
+
204
+ return { url, title, content };
205
+ } catch (e) {
206
+ return { url, title: '', content: null, error: e.message };
207
+ }
208
+ }
209
+
210
+ async function fetchMultipleSources(sources, maxSources = 5, maxChars = 5000) {
211
+ process.stderr.write(`[greedysearch] Fetching content from ${Math.min(sources.length, maxSources)} sources...\n`);
212
+
213
+ // Fetch sources sequentially (CDP doesn't handle parallel tab operations well)
214
+ const toFetch = sources.slice(0, maxSources);
215
+ const fetched = [];
216
+
217
+ for (let i = 0; i < toFetch.length; i++) {
218
+ const s = toFetch[i];
219
+ process.stderr.write(`[greedysearch] Fetching ${i + 1}/${toFetch.length}: ${s.url.slice(0, 60)}...\n`);
220
+ try {
221
+ const result = await fetchSourceContent(s.url, maxChars);
222
+ if (result.content && result.content.length > 100) {
223
+ fetched.push(result);
224
+ process.stderr.write(`[greedysearch] ✓ Got ${result.content.length} chars\n`);
225
+ } else {
226
+ process.stderr.write(`[greedysearch] ✗ Empty or too short\n`);
227
+ }
228
+ } catch (e) {
229
+ process.stderr.write(`[greedysearch] ✗ Failed: ${e.message.slice(0, 80)}\n`);
230
+ }
231
+ process.stderr.write(`PROGRESS:fetch:${i + 1}/${toFetch.length}\n`);
232
+ }
233
+
234
+ return fetched;
235
+ }
236
+
168
237
  function pickTopSource(out) {
169
238
  for (const engine of ['perplexity', 'google', 'bing']) {
170
239
  const r = out[engine];
@@ -234,8 +303,8 @@ async function synthesizeWithGemini(query, results) {
234
303
  proc.stderr.on('data', d => err += d);
235
304
  const t = setTimeout(() => {
236
305
  proc.kill();
237
- reject(new Error('Gemini synthesis timed out after 120s'));
238
- }, 120000);
306
+ reject(new Error('Gemini synthesis timed out after 180s'));
307
+ }, 180000);
239
308
  proc.on('close', code => {
240
309
  clearTimeout(t);
241
310
  if (code !== 0) reject(new Error(err.trim() || 'gemini extractor failed'));
@@ -388,28 +457,37 @@ async function main() {
388
457
  '',
389
458
  'Engines: perplexity (p), bing (b), google (g), gemini (gem), all',
390
459
  '',
460
+ 'Flags:',
461
+ ' --full Return complete answers (~3000+ chars)',
462
+ ' --synthesize Synthesize results via Gemini (adds ~30s)',
463
+ ' --deep-research Full research: full answers + source fetching + synthesis',
464
+ ' --fetch-top-source Fetch content from top source',
465
+ ' --inline Output JSON to stdout (for piping)',
466
+ '',
391
467
  'Examples:',
392
468
  ' node search.mjs p "what is memoization"',
393
- ' node search.mjs so "node.js event loop explained"',
394
469
  ' node search.mjs all "TCP congestion control"',
470
+ ' node search.mjs all "RAG vs fine-tuning" --deep-research',
395
471
  ].join('\n') + '\n');
396
472
  process.exit(1);
397
473
  }
398
474
 
399
475
  await ensureChrome();
400
476
 
401
- const full = args.includes('--full');
402
- const short = !full; // brief by default; --full opts into complete answers
403
- const fetchSource = args.includes('--fetch-top-source');
404
- const synthesize = args.includes('--synthesize');
405
- const inline = args.includes('--inline');
406
- const outIdx = args.indexOf('--out');
407
- const outFile = outIdx !== -1 ? args[outIdx + 1] : null;
408
- const rest = args.filter((a, i) =>
477
+ const full = args.includes('--full') || args.includes('--deep-research');
478
+ const short = !full;
479
+ const fetchSource = args.includes('--fetch-top-source');
480
+ const synthesize = args.includes('--synthesize') || args.includes('--deep-research');
481
+ const deepResearch = args.includes('--deep-research');
482
+ const inline = args.includes('--inline');
483
+ const outIdx = args.indexOf('--out');
484
+ const outFile = outIdx !== -1 ? args[outIdx + 1] : null;
485
+ const rest = args.filter((a, i) =>
409
486
  a !== '--full' &&
410
- a !== '--short' && // keep accepting --short for back-compat
487
+ a !== '--short' &&
411
488
  a !== '--fetch-top-source' &&
412
489
  a !== '--synthesize' &&
490
+ a !== '--deep-research' &&
413
491
  a !== '--inline' &&
414
492
  a !== '--out' &&
415
493
  (outIdx === -1 || i !== outIdx + 1)
@@ -481,6 +559,31 @@ async function main() {
481
559
  if (top) out._topSource = await fetchTopSource(top.url);
482
560
  }
483
561
 
562
+ // Deep research mode: fetch top sources and return structured document
563
+ if (deepResearch) {
564
+ process.stderr.write('PROGRESS:deep-research:start\n');
565
+
566
+ // Get top sources by consensus
567
+ const topSources = out._sources || [];
568
+
569
+ if (topSources.length > 0) {
570
+ // Fetch content from top sources
571
+ out._fetchedSources = await fetchMultipleSources(topSources, 5, 8000);
572
+ process.stderr.write('PROGRESS:deep-research:done\n');
573
+ } else {
574
+ out._fetchedSources = [];
575
+ process.stderr.write('PROGRESS:deep-research:no-sources\n');
576
+ }
577
+
578
+ // Build confidence scores
579
+ out._confidence = {
580
+ sourcesCount: topSources.length,
581
+ consensusScore: topSources.length > 0 ? topSources[0]?.engines?.length || 0 : 0,
582
+ enginesResponded: ALL_ENGINES.filter(e => out[e]?.answer && !out[e]?.error),
583
+ enginesFailed: ALL_ENGINES.filter(e => out[e]?.error),
584
+ };
585
+ }
586
+
484
587
  writeOutput(out, outFile, { inline, synthesize, query });
485
588
  return;
486
589
  }
@@ -1,11 +1,84 @@
1
1
  ---
2
2
  name: greedy-search
3
- description: Multi-engine AI web search — Perplexity, Bing Copilot, Google AI in parallel with optional Gemini synthesis. Use for high-quality research where training data may be stale or single-engine results are insufficient.
3
+ description: Multi-engine AI web search — greedy_search, deep_research, and coding_task. Use for high-quality research where training data may be stale or single-engine results are insufficient.
4
4
  ---
5
5
 
6
- # Greedy Search
6
+ # GreedySearch Tools
7
7
 
8
- Use `greedy_search` when you need high-quality, multi-perspective answers from the web.
8
+ ## Tool Overview
9
+
10
+ | Tool | Speed | Use for |
11
+ |------|-------|---------|
12
+ | `greedy_search` | 15-90s | Quick lookups, comparisons, debugging errors |
13
+ | `deep_research` | 60-120s | Architecture decisions, thorough research, source-backed answers |
14
+ | `coding_task` | 60-180s | Second opinions on code, reviews, debugging tricky issues |
15
+
16
+ ## When to Use Which
17
+
18
+ - **`greedy_search`** — Default. Fast enough for most things. Use when you need current info.
19
+ - **`deep_research`** — When the answer *matters*. Gives you a structured document with confidence scores, deduplicated sources ranked by consensus, Gemini synthesis, AND actual content from top sources.
20
+ - **`coding_task`** — When you need a "second opinion" on hard problems. Best for `debug` and `plan` modes on tricky issues.
21
+
22
+ ---
23
+
24
+ # greedy_search
25
+
26
+ Multi-engine AI web search with streaming progress.
27
+
28
+ ```greedy_search({ query: "what changed in React 19", engine: "all" })```
29
+
30
+ | Parameter | Type | Default | Description |
31
+ |-----------|------|---------|-------------|
32
+ | `query` | string | required | The search question |
33
+ | `engine` | string | `"all"` | `all`, `perplexity`, `bing`, `google`, `gemini` |
34
+ | `synthesize` | boolean | `false` | Synthesize via Gemini |
35
+ | `fullAnswer` | boolean | `false` | Complete answer vs ~300 char summary |
36
+
37
+ **When to use:** Quick lookups, error messages, comparing tools, "what's new in X".
38
+
39
+ ---
40
+
41
+ # deep_research
42
+
43
+ Comprehensive research with source fetching and synthesis. Returns a structured document.
44
+
45
+ ```deep_research({ query: "RAG vs fine-tuning for production" })```
46
+
47
+ Returns:
48
+ - Full answers from all 3 engines (Perplexity, Bing, Google)
49
+ - Gemini synthesis combining all perspectives
50
+ - Deduplicated sources ranked by consensus (3/3 > 2/3 > 1/3)
51
+ - Fetched content from top 5 sources (no CDP — uses native fetch)
52
+ - Confidence metadata (which engines responded, consensus score)
53
+
54
+ **When to use:** Architecture decisions, "which library should I use", research for a writeup, anything where you need source-backed confidence.
55
+
56
+ ---
57
+
58
+ # coding_task
59
+
60
+ Browser-based coding assistant using Gemini and/or Copilot.
61
+
62
+ ```coding_task({ task: "debug this race condition", mode: "debug", engine: "all" })```
63
+
64
+ | Parameter | Type | Default | Description |
65
+ |-----------|------|---------|-------------|
66
+ | `task` | string | required | The coding task/question |
67
+ | `engine` | string | `"gemini"` | `gemini`, `copilot`, or `all` |
68
+ | `mode` | string | `"code"` | See modes below |
69
+ | `context` | string | — | Code snippet to include |
70
+
71
+ **Modes:**
72
+
73
+ | Mode | Use when |
74
+ |------|----------|
75
+ | `debug` | Stuck on a tricky bug. Fresh eyes catch different failure modes. |
76
+ | `plan` | About to refactor something big. Gemini plays devil's advocate. |
77
+ | `review` | Code review before merge. High-stakes code benefits from second opinion. |
78
+ | `test` | Need edge cases the author missed. |
79
+ | `code` | Just need the code written (but you can probably do this yourself faster). |
80
+
81
+ **When to use:** Debugging tricky issues, planning major refactors, security-critical reviews. **Skip for** simple code generation — you're faster.
9
82
 
10
83
  ## Greedy Search vs Built-in Web Search
11
84
 
package/test.sh CHANGED
@@ -25,9 +25,14 @@ NC='\033[0m'
25
25
 
26
26
  PASS=0
27
27
  FAIL=0
28
+ FAILURES=() # Array to store failure details for report
28
29
 
29
30
  pass() { PASS=$((PASS+1)); echo -e " ${GREEN}✓${NC} $1"; }
30
- fail() { FAIL=$((FAIL+1)); echo -e " ${RED}✗${NC} $1"; }
31
+ fail() {
32
+ FAIL=$((FAIL+1));
33
+ echo -e " ${RED}✗${NC} $1"
34
+ FAILURES+=("$1")
35
+ }
31
36
 
32
37
  check_no_errors() {
33
38
  local file="$1"
@@ -73,7 +78,7 @@ echo -e "\n${YELLOW}═══ GreedySearch Test Suite ═══${NC}\n"
73
78
  if [[ "$1" != "parallel" ]]; then
74
79
  echo "Test 1: Single engine mode"
75
80
 
76
- for engine in perplexity bing google; do
81
+ for engine in perplexity bing google gemini; do
77
82
  outfile="$RESULTS_DIR/single_${engine}.json"
78
83
  node search.mjs "$engine" "explain $engine attention mechanism" --out "$outfile" 2>/dev/null
79
84
  if [[ $? -eq 0 && -f "$outfile" ]]; then
@@ -212,10 +217,82 @@ if [[ "$1" != "parallel" && "$1" != "quick" ]]; then
212
217
  fi
213
218
 
214
219
  # ─────────────────────────────────────────────────────────
220
+ # Generate test report
221
+ REPORT_FILE="$RESULTS_DIR/REPORT.md"
222
+
223
+ cat > "$REPORT_FILE" << EOF
224
+ # GreedySearch Test Report
225
+
226
+ **Date:** $(date)
227
+ **Test run:** $RESULTS_DIR
228
+
229
+ ## Summary
230
+
231
+ | Result | Count |
232
+ |--------|-------|
233
+ | ✅ Passed | $PASS |
234
+ | ❌ Failed | $FAIL |
235
+ | Total | $((PASS + FAIL)) |
236
+
237
+ ## Failures
238
+
239
+ EOF
240
+
241
+ if [[ ${#FAILURES[@]} -eq 0 ]]; then
242
+ echo "No failures — all tests passed! 🎉" >> "$REPORT_FILE"
243
+ else
244
+ for i in "${!FAILURES[@]}"; do
245
+ echo "$((i+1)). ${FAILURES[$i]}" >> "$REPORT_FILE"
246
+ done
247
+
248
+ cat >> "$REPORT_FILE" << 'EOF'
249
+
250
+ ## Common Issues
251
+
252
+ ### Bing Copilot "copy button did not appear"
253
+ This usually means:
254
+ - **Verification challenge appeared** — Cloudflare Turnstile or Microsoft auth
255
+ - **Page didn't load** — network issue or Copilot slow to respond
256
+ - **UI changed** — selector no longer matches Copilot's DOM
257
+
258
+ To debug: check the result JSON file for the full error message.
259
+
260
+ ### Google "verification required"
261
+ Google sometimes shows CAPTCHAs that can't be auto-solved.
262
+ Manual intervention required in the Chrome window.
263
+
264
+ ### Perplexity "Clipboard interceptor returned empty text"
265
+ Perplexity's UI may have changed. Check if the copy button selector still works.
266
+
267
+ EOF
268
+ fi
269
+
270
+ cat >> "$REPORT_FILE" << EOF
271
+
272
+ ## Result Files
273
+
274
+ \`\`\`
275
+ $(ls -la "$RESULTS_DIR"/*.json 2>/dev/null | awk '{print $NF}' | xargs -I{} basename {})
276
+ \`\`\`
277
+
278
+ ---
279
+ *Generated by test.sh*
280
+ EOF
281
+
215
282
  echo -e "\n${YELLOW}═══ Results ═══${NC}"
216
283
  echo -e " ${GREEN}Passed: $PASS${NC}"
217
284
  [[ $FAIL -gt 0 ]] && echo -e " ${RED}Failed: $FAIL${NC}" || echo " Failed: 0"
218
285
  echo " Results in: $RESULTS_DIR"
286
+ echo " Report: $REPORT_FILE"
219
287
  echo ""
220
288
 
289
+ # Print failure details to console too
290
+ if [[ ${#FAILURES[@]} -gt 0 ]]; then
291
+ echo -e "${RED}Failures:${NC}"
292
+ for f in "${FAILURES[@]}"; do
293
+ echo -e " ${RED}•${NC} $f"
294
+ done
295
+ echo ""
296
+ fi
297
+
221
298
  [[ $FAIL -eq 0 ]] && exit 0 || exit 1