@apmantza/greedysearch-pi 1.4.2 → 1.5.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.
@@ -3,50 +3,50 @@
3
3
  // Update selectors here when a site changes its UI.
4
4
 
5
5
  export const SELECTORS = {
6
- // ──────────────────────────────────────────────
7
- // Perplexity (perplexity.ai)
8
- // ──────────────────────────────────────────────
9
- perplexity: {
10
- input: '#ask-input',
11
- copyButton: 'button[aria-label="Copy"]',
12
- sourceItem: '[data-pplx-citation-url]',
13
- sourceLink: 'a',
14
- consent: '#onetrust-accept-btn-handler',
15
- },
6
+ // ──────────────────────────────────────────────
7
+ // Perplexity (perplexity.ai)
8
+ // ──────────────────────────────────────────────
9
+ perplexity: {
10
+ input: "#ask-input",
11
+ copyButton: 'button[aria-label="Copy"]',
12
+ sourceItem: "[data-pplx-citation-url]",
13
+ sourceLink: "a",
14
+ consent: "#onetrust-accept-btn-handler",
15
+ },
16
16
 
17
- // ──────────────────────────────────────────────
18
- // Bing Copilot (copilot.microsoft.com)
19
- // ──────────────────────────────────────────────
20
- bing: {
21
- input: '#userInput',
22
- copyButton: 'button[data-testid="copy-ai-message-button"]',
23
- sourceLink: 'a[href^="http"][target="_blank"]',
24
- sourceExclude: 'copilot.microsoft.com',
25
- consent: '#onetrust-accept-btn-handler',
26
- },
17
+ // ──────────────────────────────────────────────
18
+ // Bing Copilot (copilot.microsoft.com)
19
+ // ──────────────────────────────────────────────
20
+ bing: {
21
+ input: "#userInput",
22
+ copyButton: 'button[data-testid="copy-ai-message-button"]',
23
+ sourceLink: 'a[href^="http"][target="_blank"]',
24
+ sourceExclude: "copilot.microsoft.com",
25
+ consent: "#onetrust-accept-btn-handler",
26
+ },
27
27
 
28
- // ──────────────────────────────────────────────
29
- // Google AI Mode (google.com/search?udm=50)
30
- // ──────────────────────────────────────────────
31
- google: {
32
- answerContainer: '.pWvJNd',
33
- sourceLink: 'a[href^="http"]',
34
- sourceExclude: ['google.', 'gstatic', 'googleapis'],
35
- sourceHeadingParent: '[data-snhf]',
36
- consent: '#L2AGLb, button[jsname="b3VHJd"], .tHlp8d',
37
- },
28
+ // ──────────────────────────────────────────────
29
+ // Google AI Mode (google.com/search?udm=50)
30
+ // ──────────────────────────────────────────────
31
+ google: {
32
+ answerContainer: ".pWvJNd",
33
+ sourceLink: 'a[href^="http"]',
34
+ sourceExclude: ["google.", "gstatic", "googleapis"],
35
+ sourceHeadingParent: "[data-snhf]",
36
+ consent: '#L2AGLb, button[jsname="b3VHJd"], .tHlp8d',
37
+ },
38
38
 
39
- // ──────────────────────────────────────────────
40
- // Gemini (gemini.google.com/app)
41
- // ──────────────────────────────────────────────
42
- gemini: {
43
- input: 'rich-textarea .ql-editor',
44
- copyButton: 'button[aria-label="Copy"]',
45
- sendButton: 'button[aria-label*="Send"]',
46
- sourcesSidebarButton: 'button.legacy-sources-sidebar-button',
47
- sourcesExclude: ['gemini.google', 'gstatic', 'google.com/search'],
48
- citationButtonPattern: 'button[aria-label*="citation from"]',
49
- // For parsing citation aria-labels: "View source details for citation from {name}. Opens side panel."
50
- citationNameRegex: /from\s+(.+?)\.\s/,
51
- },
39
+ // ──────────────────────────────────────────────
40
+ // Gemini (gemini.google.com/app)
41
+ // ──────────────────────────────────────────────
42
+ gemini: {
43
+ input: "rich-textarea .ql-editor",
44
+ copyButton: 'button[aria-label="Copy"]',
45
+ sendButton: 'button[aria-label*="Send"]',
46
+ sourcesSidebarButton: "button.legacy-sources-sidebar-button",
47
+ sourcesExclude: ["gemini.google", "gstatic", "google.com/search"],
48
+ citationButtonPattern: 'button[aria-label*="citation from"]',
49
+ // For parsing citation aria-labels: "View source details for citation from {name}. Opens side panel."
50
+ citationNameRegex: /from\s+(.+?)\.\s/,
51
+ },
52
52
  };
package/index.ts CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  import { spawn } from "node:child_process";
12
12
  import { existsSync } from "node:fs";
13
- import { join, dirname } from "node:path";
13
+ import { dirname, join } from "node:path";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
16
16
  import { Type } from "@sinclair/typebox";
@@ -31,13 +31,20 @@ function runSearch(
31
31
  onProgress?: (engine: string, status: "done" | "error") => void,
32
32
  ): Promise<Record<string, unknown>> {
33
33
  return new Promise((resolve, reject) => {
34
- const proc = spawn("node", [__dir + "/search.mjs", engine, "--inline", ...flags, query], {
35
- stdio: ["ignore", "pipe", "pipe"],
36
- });
34
+ const proc = spawn(
35
+ "node",
36
+ [`${__dir}/search.mjs`, engine, "--inline", ...flags, query],
37
+ {
38
+ stdio: ["ignore", "pipe", "pipe"],
39
+ },
40
+ );
37
41
  let out = "";
38
42
  let err = "";
39
43
 
40
- const onAbort = () => { proc.kill("SIGTERM"); reject(new Error("Aborted")); };
44
+ const onAbort = () => {
45
+ proc.kill("SIGTERM");
46
+ reject(new Error("Aborted"));
47
+ };
41
48
  signal?.addEventListener("abort", onAbort, { once: true });
42
49
 
43
50
  // Watch stderr for progress events (PROGRESS:engine:done|error)
@@ -61,7 +68,9 @@ function runSearch(
61
68
  try {
62
69
  resolve(JSON.parse(out.trim()));
63
70
  } catch {
64
- reject(new Error(`Invalid JSON from search.mjs: ${out.slice(0, 200)}`));
71
+ reject(
72
+ new Error(`Invalid JSON from search.mjs: ${out.slice(0, 200)}`),
73
+ );
65
74
  }
66
75
  }
67
76
  });
@@ -85,12 +94,16 @@ function sourceUrl(source: Record<string, unknown>): string {
85
94
  }
86
95
 
87
96
  function sourceLabel(source: Record<string, unknown>): string {
88
- return String(source.title || source.domain || sourceUrl(source) || "Untitled source");
97
+ return String(
98
+ source.title || source.domain || sourceUrl(source) || "Untitled source",
99
+ );
89
100
  }
90
101
 
91
102
  function sourceConsensus(source: Record<string, unknown>): number {
92
103
  if (typeof source.engineCount === "number") return source.engineCount;
93
- const engines = Array.isArray(source.engines) ? (source.engines as string[]) : [];
104
+ const engines = Array.isArray(source.engines)
105
+ ? (source.engines as string[])
106
+ : [];
94
107
  return engines.length;
95
108
  }
96
109
 
@@ -99,7 +112,9 @@ function formatAgreementLevel(level: string): string {
99
112
  return level.charAt(0).toUpperCase() + level.slice(1);
100
113
  }
101
114
 
102
- function getSourceMap(sources: Array<Record<string, unknown>>): Map<string, Record<string, unknown>> {
115
+ function getSourceMap(
116
+ sources: Array<Record<string, unknown>>,
117
+ ): Map<string, Record<string, unknown>> {
103
118
  return new Map(
104
119
  sources
105
120
  .map((source) => [String(source.id || ""), source] as const)
@@ -112,22 +127,33 @@ function formatSourceLine(source: Record<string, unknown>): string {
112
127
  const url = sourceUrl(source);
113
128
  const title = sourceLabel(source);
114
129
  const domain = String(source.domain || "");
115
- const engines = Array.isArray(source.engines) ? (source.engines as string[]) : [];
130
+ const engines = Array.isArray(source.engines)
131
+ ? (source.engines as string[])
132
+ : [];
116
133
  const consensus = sourceConsensus(source);
117
134
  const typeLabel = humanizeSourceType(String(source.sourceType || ""));
118
135
  const fetch = source.fetch as Record<string, unknown> | undefined;
119
- const fetchStatus = fetch?.ok ? `fetched ${fetch.status || 200}` : fetch?.attempted ? "fetch failed" : "";
136
+ const fetchStatus = fetch?.ok
137
+ ? `fetched ${fetch.status || 200}`
138
+ : fetch?.attempted
139
+ ? "fetch failed"
140
+ : "";
120
141
  const pieces = [
121
142
  `${id} - [${title}](${url})`,
122
143
  domain,
123
144
  typeLabel,
124
- engines.length ? `cited by ${engines.map(formatEngineName).join(", ")} (${consensus}/3)` : `${consensus}/3`,
145
+ engines.length
146
+ ? `cited by ${engines.map(formatEngineName).join(", ")} (${consensus}/3)`
147
+ : `${consensus}/3`,
125
148
  fetchStatus,
126
149
  ].filter(Boolean);
127
150
  return `- ${pieces.join(" - ")}`;
128
151
  }
129
152
 
130
- function renderSourceEvidence(lines: string[], source: Record<string, unknown>): void {
153
+ function renderSourceEvidence(
154
+ lines: string[],
155
+ source: Record<string, unknown>,
156
+ ): void {
131
157
  const fetch = source.fetch as Record<string, unknown> | undefined;
132
158
  if (!fetch?.attempted) return;
133
159
 
@@ -169,18 +195,24 @@ function renderSynthesis(
169
195
  const agreementLevel = String(agreement?.level || "").trim();
170
196
  if (agreementSummary || agreementLevel) {
171
197
  lines.push("## Consensus");
172
- lines.push(`- ${formatAgreementLevel(agreementLevel)}${agreementSummary ? ` - ${agreementSummary}` : ""}`);
198
+ lines.push(
199
+ `- ${formatAgreementLevel(agreementLevel)}${agreementSummary ? ` - ${agreementSummary}` : ""}`,
200
+ );
173
201
  lines.push("");
174
202
  }
175
203
 
176
- const differences = Array.isArray(synthesis.differences) ? (synthesis.differences as string[]) : [];
204
+ const differences = Array.isArray(synthesis.differences)
205
+ ? (synthesis.differences as string[])
206
+ : [];
177
207
  if (differences.length > 0) {
178
208
  lines.push("## Where Engines Differ");
179
209
  for (const difference of differences) lines.push(`- ${difference}`);
180
210
  lines.push("");
181
211
  }
182
212
 
183
- const caveats = Array.isArray(synthesis.caveats) ? (synthesis.caveats as string[]) : [];
213
+ const caveats = Array.isArray(synthesis.caveats)
214
+ ? (synthesis.caveats as string[])
215
+ : [];
184
216
  if (caveats.length > 0) {
185
217
  lines.push("## Caveats");
186
218
  for (const caveat of caveats) lines.push(`- ${caveat}`);
@@ -193,9 +225,13 @@ function renderSynthesis(
193
225
  if (claims.length > 0) {
194
226
  lines.push("## Key Claims");
195
227
  for (const claim of claims) {
196
- const sourceIds = Array.isArray(claim.sourceIds) ? (claim.sourceIds as string[]) : [];
228
+ const sourceIds = Array.isArray(claim.sourceIds)
229
+ ? (claim.sourceIds as string[])
230
+ : [];
197
231
  const support = String(claim.support || "moderate");
198
- lines.push(`- ${String(claim.claim || "")} [${support}${sourceIds.length ? `; ${sourceIds.join(", ")}` : ""}]`);
232
+ lines.push(
233
+ `- ${String(claim.claim || "")} [${support}${sourceIds.length ? `; ${sourceIds.join(", ")}` : ""}]`,
234
+ );
199
235
  }
200
236
  lines.push("");
201
237
  }
@@ -216,10 +252,14 @@ function formatResults(engine: string, data: Record<string, unknown>): string {
216
252
 
217
253
  if (engine === "all") {
218
254
  const synthesis = data._synthesis as Record<string, unknown> | undefined;
219
- const dedupedSources = data._sources as Array<Record<string, unknown>> | undefined;
255
+ const dedupedSources = data._sources as
256
+ | Array<Record<string, unknown>>
257
+ | undefined;
220
258
  if (synthesis?.answer) {
221
259
  renderSynthesis(lines, synthesis, dedupedSources || [], 6);
222
- lines.push("*Synthesized from Perplexity, Bing Copilot, and Google AI*\n");
260
+ lines.push(
261
+ "*Synthesized from Perplexity, Bing Copilot, and Google AI*\n",
262
+ );
223
263
  return lines.join("\n").trim();
224
264
  }
225
265
 
@@ -261,7 +301,9 @@ function formatResults(engine: string, data: Record<string, unknown>): string {
261
301
  function formatDeepResearch(data: Record<string, unknown>): string {
262
302
  const lines: string[] = [];
263
303
  const confidence = data._confidence as Record<string, unknown> | undefined;
264
- const dedupedSources = data._sources as Array<Record<string, unknown>> | undefined;
304
+ const dedupedSources = data._sources as
305
+ | Array<Record<string, unknown>>
306
+ | undefined;
265
307
  const synthesis = data._synthesis as Record<string, unknown> | undefined;
266
308
 
267
309
  lines.push("# Deep Research Report\n");
@@ -271,26 +313,41 @@ function formatDeepResearch(data: Record<string, unknown>): string {
271
313
  const enginesFailed = (confidence.enginesFailed as string[]) || [];
272
314
  const agreementLevel = String(confidence.agreementLevel || "mixed");
273
315
  const firstPartySourceCount = Number(confidence.firstPartySourceCount || 0);
274
- const sourceTypeBreakdown = confidence.sourceTypeBreakdown as Record<string, number> | undefined;
316
+ const sourceTypeBreakdown = confidence.sourceTypeBreakdown as
317
+ | Record<string, number>
318
+ | undefined;
275
319
 
276
320
  lines.push("## Confidence\n");
277
321
  lines.push(`- Agreement: ${formatAgreementLevel(agreementLevel)}`);
278
- lines.push(`- Engines responded: ${enginesResponded.map(formatEngineName).join(", ") || "none"}`);
322
+ lines.push(
323
+ `- Engines responded: ${enginesResponded.map(formatEngineName).join(", ") || "none"}`,
324
+ );
279
325
  if (enginesFailed.length > 0) {
280
- lines.push(`- Engines failed: ${enginesFailed.map(formatEngineName).join(", ")}`);
326
+ lines.push(
327
+ `- Engines failed: ${enginesFailed.map(formatEngineName).join(", ")}`,
328
+ );
281
329
  }
282
- lines.push(`- Top source consensus: ${confidence.topSourceConsensus || 0}/3 engines`);
330
+ lines.push(
331
+ `- Top source consensus: ${confidence.topSourceConsensus || 0}/3 engines`,
332
+ );
283
333
  lines.push(`- Total unique sources: ${confidence.sourcesCount || 0}`);
284
334
  lines.push(`- Official sources: ${confidence.officialSourceCount || 0}`);
285
335
  lines.push(`- First-party sources: ${firstPartySourceCount}`);
286
- lines.push(`- Fetch success rate: ${confidence.fetchedSourceSuccessRate || 0}`);
336
+ lines.push(
337
+ `- Fetch success rate: ${confidence.fetchedSourceSuccessRate || 0}`,
338
+ );
287
339
  if (sourceTypeBreakdown && Object.keys(sourceTypeBreakdown).length > 0) {
288
- lines.push(`- Source mix: ${Object.entries(sourceTypeBreakdown).map(([type, count]) => `${humanizeSourceType(type)} ${count}`).join(", ")}`);
340
+ lines.push(
341
+ `- Source mix: ${Object.entries(sourceTypeBreakdown)
342
+ .map(([type, count]) => `${humanizeSourceType(type)} ${count}`)
343
+ .join(", ")}`,
344
+ );
289
345
  }
290
346
  lines.push("");
291
347
  }
292
348
 
293
- if (synthesis?.answer) renderSynthesis(lines, synthesis, dedupedSources || [], 8);
349
+ if (synthesis?.answer)
350
+ renderSynthesis(lines, synthesis, dedupedSources || [], 8);
294
351
 
295
352
  lines.push("## Engine Perspectives\n");
296
353
  for (const engine of ["perplexity", "bing", "google"]) {
@@ -317,18 +374,22 @@ function formatDeepResearch(data: Record<string, unknown>): string {
317
374
  return lines.join("\n").trim();
318
375
  }
319
376
 
320
- function formatCodingTask(data: Record<string, unknown> | Record<string, Record<string, unknown>>): string {
377
+ function formatCodingTask(
378
+ data: Record<string, unknown> | Record<string, Record<string, unknown>>,
379
+ ): string {
321
380
  const lines: string[] = [];
322
381
 
323
382
  // Check if it's multi-engine result
324
383
  const hasMultipleEngines = "gemini" in data || "copilot" in data;
325
-
384
+
326
385
  if (hasMultipleEngines) {
327
386
  // Multi-engine result
328
387
  for (const [engineName, result] of Object.entries(data)) {
329
388
  const r = result as Record<string, unknown>;
330
- lines.push(`## ${engineName.charAt(0).toUpperCase() + engineName.slice(1)}\n`);
331
-
389
+ lines.push(
390
+ `## ${engineName.charAt(0).toUpperCase() + engineName.slice(1)}\n`,
391
+ );
392
+
332
393
  if (r.error) {
333
394
  lines.push(`⚠️ Error: ${r.error}\n`);
334
395
  } else {
@@ -390,27 +451,47 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
390
451
  Type.Literal("gem"),
391
452
  ],
392
453
  {
393
- description: 'Engine to use. "all" fans out to Perplexity, Bing, and Google in parallel (default).',
454
+ description:
455
+ 'Engine to use. "all" fans out to Perplexity, Bing, and Google in parallel (default).',
394
456
  default: "all",
395
457
  },
396
458
  ),
397
- synthesize: Type.Optional(Type.Boolean({
398
- 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.',
399
- default: false,
400
- })),
401
- fullAnswer: Type.Optional(Type.Boolean({
402
- description: 'When true, returns the complete answer instead of a truncated preview (default: false, answers are shortened to ~300 chars to save tokens).',
403
- default: false,
404
- })),
459
+ synthesize: Type.Optional(
460
+ Type.Boolean({
461
+ description:
462
+ '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.',
463
+ default: false,
464
+ }),
465
+ ),
466
+ fullAnswer: Type.Optional(
467
+ Type.Boolean({
468
+ description:
469
+ "When true, returns the complete answer instead of a truncated preview (default: false, answers are shortened to ~300 chars to save tokens).",
470
+ default: false,
471
+ }),
472
+ ),
405
473
  }),
406
474
  execute: async (_toolCallId, params, signal, onUpdate) => {
407
- const { query, engine = "all", synthesize = false, fullAnswer = false } = params as {
408
- query: string; engine: string; synthesize?: boolean; fullAnswer?: boolean;
475
+ const {
476
+ query,
477
+ engine = "all",
478
+ synthesize = false,
479
+ fullAnswer = false,
480
+ } = params as {
481
+ query: string;
482
+ engine: string;
483
+ synthesize?: boolean;
484
+ fullAnswer?: boolean;
409
485
  };
410
486
 
411
487
  if (!cdpAvailable()) {
412
488
  return {
413
- content: [{ type: "text", text: "cdp.mjs missing — try reinstalling: pi install git:github.com/apmantza/GreedySearch-pi" }],
489
+ content: [
490
+ {
491
+ type: "text",
492
+ text: "cdp.mjs missing — try reinstalling: pi install git:github.com/apmantza/GreedySearch-pi",
493
+ },
494
+ ],
414
495
  details: {} as { raw?: Record<string, unknown> },
415
496
  };
416
497
  }
@@ -422,7 +503,7 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
422
503
  // Track progress for "all" engine mode
423
504
  const completed = new Set<string>();
424
505
 
425
- const onProgress = (eng: string, status: "done" | "error") => {
506
+ const onProgress = (eng: string, _status: "done" | "error") => {
426
507
  completed.add(eng);
427
508
  const parts: string[] = [];
428
509
  for (const e of ALL_ENGINES) {
@@ -432,13 +513,21 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
432
513
  if (synthesize && completed.size >= 3) parts.push("🔄 synthesizing");
433
514
 
434
515
  onUpdate?.({
435
- content: [{ type: "text", text: `**Searching...** ${parts.join(" · ")}` }],
516
+ content: [
517
+ { type: "text", text: `**Searching...** ${parts.join(" · ")}` },
518
+ ],
436
519
  details: { _progress: true },
437
520
  } as any);
438
521
  };
439
522
 
440
523
  try {
441
- const data = await runSearch(engine, query, flags, signal, engine === "all" ? onProgress : undefined);
524
+ const data = await runSearch(
525
+ engine,
526
+ query,
527
+ flags,
528
+ signal,
529
+ engine === "all" ? onProgress : undefined,
530
+ );
442
531
  const text = formatResults(engine, data);
443
532
  return {
444
533
  content: [{ type: "text", text: text || "No results returned." }],
@@ -464,7 +553,8 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
464
553
  "deduplicates and ranks sources by consensus, fetches content from top sources, " +
465
554
  "and synthesizes via Gemini. Returns a structured research document with confidence scores. " +
466
555
  "Use for architecture decisions, library comparisons, best practices, or any research where the answer matters.",
467
- promptSnippet: "Deep multi-engine research with source deduplication and synthesis",
556
+ promptSnippet:
557
+ "Deep multi-engine research with source deduplication and synthesis",
468
558
  parameters: Type.Object({
469
559
  query: Type.String({ description: "The research question" }),
470
560
  }),
@@ -473,14 +563,16 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
473
563
 
474
564
  if (!cdpAvailable()) {
475
565
  return {
476
- content: [{ type: "text", text: "cdp.mjs missing — try reinstalling." }],
566
+ content: [
567
+ { type: "text", text: "cdp.mjs missing — try reinstalling." },
568
+ ],
477
569
  details: {} as { raw?: Record<string, unknown> },
478
570
  };
479
571
  }
480
572
 
481
573
  const completed = new Set<string>();
482
574
 
483
- const onProgress = (eng: string, status: "done" | "error") => {
575
+ const onProgress = (eng: string, _status: "done" | "error") => {
484
576
  completed.add(eng);
485
577
  const parts: string[] = [];
486
578
  for (const e of ALL_ENGINES) {
@@ -490,14 +582,22 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
490
582
  if (completed.size >= 3) parts.push("🔄 synthesizing");
491
583
 
492
584
  onUpdate?.({
493
- content: [{ type: "text", text: `**Researching...** ${parts.join(" · ")}` }],
585
+ content: [
586
+ { type: "text", text: `**Researching...** ${parts.join(" · ")}` },
587
+ ],
494
588
  details: { _progress: true },
495
589
  } as any);
496
590
  };
497
591
 
498
592
  try {
499
593
  // Run deep research (includes full answers, synthesis, and source fetching)
500
- const data = await runSearch("all", query, ["--deep-research"], signal, onProgress);
594
+ const data = await runSearch(
595
+ "all",
596
+ query,
597
+ ["--deep-research"],
598
+ signal,
599
+ onProgress,
600
+ );
501
601
  const text = formatDeepResearch(data);
502
602
  return {
503
603
  content: [{ type: "text", text: text || "No results returned." }],
@@ -529,13 +629,10 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
529
629
  parameters: Type.Object({
530
630
  task: Type.String({ description: "The coding task or question" }),
531
631
  engine: Type.Union(
532
- [
533
- Type.Literal("all"),
534
- Type.Literal("gemini"),
535
- Type.Literal("copilot"),
536
- ],
632
+ [Type.Literal("all"), Type.Literal("gemini"), Type.Literal("copilot")],
537
633
  {
538
- description: 'Engine to use. "all" runs both Gemini and Copilot in parallel.',
634
+ description:
635
+ 'Engine to use. "all" runs both Gemini and Copilot in parallel.',
539
636
  default: "gemini",
540
637
  },
541
638
  ),
@@ -548,22 +645,35 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
548
645
  Type.Literal("debug"),
549
646
  ],
550
647
  {
551
- description: "Task mode: code (default), review (code review), plan (architect review), test (write tests), debug (root cause analysis)",
648
+ description:
649
+ "Task mode: code (default), review (code review), plan (architect review), test (write tests), debug (root cause analysis)",
552
650
  default: "code",
553
651
  },
554
652
  ),
555
- context: Type.Optional(Type.String({
556
- description: "Optional code context/snippet to include with the task",
557
- })),
653
+ context: Type.Optional(
654
+ Type.String({
655
+ description: "Optional code context/snippet to include with the task",
656
+ }),
657
+ ),
558
658
  }),
559
659
  execute: async (_toolCallId, params, signal, onUpdate) => {
560
- const { task, engine = "gemini", mode = "code", context } = params as {
561
- task: string; engine: string; mode: string; context?: string;
660
+ const {
661
+ task,
662
+ engine = "gemini",
663
+ mode = "code",
664
+ context,
665
+ } = params as {
666
+ task: string;
667
+ engine: string;
668
+ mode: string;
669
+ context?: string;
562
670
  };
563
671
 
564
672
  if (!cdpAvailable()) {
565
673
  return {
566
- content: [{ type: "text", text: "cdp.mjs missing — try reinstalling." }],
674
+ content: [
675
+ { type: "text", text: "cdp.mjs missing — try reinstalling." },
676
+ ],
567
677
  details: {} as { raw?: Record<string, unknown> },
568
678
  };
569
679
  }
@@ -573,38 +683,65 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
573
683
 
574
684
  try {
575
685
  onUpdate?.({
576
- content: [{ type: "text", text: `**Coding task...** 🔄 ${engine === "all" ? "Gemini + Copilot" : engine} (${mode} mode)` }],
686
+ content: [
687
+ {
688
+ type: "text",
689
+ text: `**Coding task...** 🔄 ${engine === "all" ? "Gemini + Copilot" : engine} (${mode} mode)`,
690
+ },
691
+ ],
577
692
  details: { _progress: true },
578
693
  } as any);
579
694
 
580
- const data = await new Promise<Record<string, unknown>>((resolve, reject) => {
581
- const proc = spawn("node", [__dir + "/coding-task.mjs", task, ...flags], {
582
- stdio: ["ignore", "pipe", "pipe"],
583
- });
584
- let out = "";
585
- let err = "";
586
-
587
- const onAbort = () => { proc.kill("SIGTERM"); reject(new Error("Aborted")); };
588
- signal?.addEventListener("abort", onAbort, { once: true });
589
-
590
- proc.stdout.on("data", (d: Buffer) => (out += d));
591
- proc.stderr.on("data", (d: Buffer) => { err += d; });
592
- proc.on("close", (code: number) => {
593
- signal?.removeEventListener("abort", onAbort);
594
- if (code !== 0) {
595
- reject(new Error(err.trim() || `coding-task.mjs exited with code ${code}`));
596
- } else {
597
- try {
598
- resolve(JSON.parse(out.trim()));
599
- } catch {
600
- reject(new Error(`Invalid JSON from coding-task.mjs: ${out.slice(0, 200)}`));
695
+ const data = await new Promise<Record<string, unknown>>(
696
+ (resolve, reject) => {
697
+ const proc = spawn(
698
+ "node",
699
+ [`${__dir}/coding-task.mjs`, task, ...flags],
700
+ {
701
+ stdio: ["ignore", "pipe", "pipe"],
702
+ },
703
+ );
704
+ let out = "";
705
+ let err = "";
706
+
707
+ const onAbort = () => {
708
+ proc.kill("SIGTERM");
709
+ reject(new Error("Aborted"));
710
+ };
711
+ signal?.addEventListener("abort", onAbort, { once: true });
712
+
713
+ proc.stdout.on("data", (d: Buffer) => (out += d));
714
+ proc.stderr.on("data", (d: Buffer) => {
715
+ err += d;
716
+ });
717
+ proc.on("close", (code: number) => {
718
+ signal?.removeEventListener("abort", onAbort);
719
+ if (code !== 0) {
720
+ reject(
721
+ new Error(
722
+ err.trim() || `coding-task.mjs exited with code ${code}`,
723
+ ),
724
+ );
725
+ } else {
726
+ try {
727
+ resolve(JSON.parse(out.trim()));
728
+ } catch {
729
+ reject(
730
+ new Error(
731
+ `Invalid JSON from coding-task.mjs: ${out.slice(0, 200)}`,
732
+ ),
733
+ );
734
+ }
601
735
  }
602
- }
603
- });
604
-
605
- // Timeout after 3 minutes
606
- setTimeout(() => { proc.kill("SIGTERM"); reject(new Error("Coding task timed out after 180s")); }, 180000);
607
- });
736
+ });
737
+
738
+ // Timeout after 3 minutes
739
+ setTimeout(() => {
740
+ proc.kill("SIGTERM");
741
+ reject(new Error("Coding task timed out after 180s"));
742
+ }, 180000);
743
+ },
744
+ );
608
745
 
609
746
  const text = formatCodingTask(data);
610
747
  return {