@apmantza/greedysearch-pi 1.6.3 → 1.6.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.6.4 (2026-04-02)
4
+
5
+ ### Fixes
6
+ - **Gemini scroll-to-bottom** — Changed from small random jitter scrolls to actual bottom-of-page scrolls every ~6 seconds while waiting for the copy button. This ensures lazy-loaded content is triggered and the full answer is captured.
7
+ - **Restored missing files** — `.mjs` source files (extractors, search.mjs, launch.mjs, etc.) were incorrectly removed in v1.6.2 cleanup; now properly tracked again.
8
+
9
+ ## v1.6.3 (2026-04-02)
10
+
11
+ ### Fixes
12
+ - **Debug output removed** — Cleaned up stderr passthrough that was causing CDP connection issues in some environments.
13
+
3
14
  ## v1.6.2 (2026-04-01)
4
15
 
5
16
  ### Fixes
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/index.ts CHANGED
@@ -1,421 +1,421 @@
1
- /**
2
- * GreedySearch Pi Extension
3
- *
4
- * Adds a `greedy_search` tool to Pi that fans out queries to Perplexity,
5
- * Bing Copilot, and Google AI in parallel, returning synthesized AI answers.
6
- *
7
- * Reports streaming progress as each engine completes.
8
- * Requires Chrome to be running (or it auto-launches a dedicated instance).
9
- */
10
-
11
- import { spawn } from "node:child_process";
12
- import { existsSync } from "node:fs";
13
- import { dirname, join } from "node:path";
14
- import { fileURLToPath } from "node:url";
15
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
16
- import { Type } from "@sinclair/typebox";
17
-
18
- // Formatters extracted to reduce file complexity
19
- import { formatCodingTask } from "./src/formatters/coding.js";
20
- import { formatResults, formatDeepResearch } from "./src/formatters/results.js";
21
-
22
- const __dir = dirname(fileURLToPath(import.meta.url));
23
-
24
- const ALL_ENGINES = ["perplexity", "bing", "google"] as const;
25
-
26
- function cdpAvailable(): boolean {
27
- return existsSync(join(__dir, "cdp.mjs"));
28
- }
29
-
30
- function runSearch(
31
- engine: string,
32
- query: string,
33
- flags: string[] = [],
34
- signal?: AbortSignal,
35
- onProgress?: (engine: string, status: "done" | "error") => void,
36
- ): Promise<Record<string, unknown>> {
37
- return new Promise((resolve, reject) => {
38
- const proc = spawn(
39
- "node",
40
- [`${__dir}/search.mjs`, engine, "--inline", ...flags, query],
41
- {
42
- stdio: ["ignore", "pipe", "pipe"],
43
- },
44
- );
45
- let out = "";
46
- let err = "";
47
-
48
- const onAbort = () => {
49
- proc.kill("SIGTERM");
50
- reject(new Error("Aborted"));
51
- };
52
- signal?.addEventListener("abort", onAbort, { once: true });
53
-
54
- // Watch stderr for progress events (PROGRESS:engine:done|error)
55
- proc.stderr.on("data", (d: Buffer) => {
56
- err += d;
57
- const lines = d.toString().split("\n");
58
- for (const line of lines) {
59
- const match = line.match(/^PROGRESS:(\w+):(done|error)$/);
60
- if (match && onProgress) {
61
- onProgress(match[1], match[2] as "done" | "error");
62
- }
63
- }
64
- });
65
-
66
- proc.stdout.on("data", (d: Buffer) => (out += d));
67
- proc.on("close", (code: number) => {
68
- signal?.removeEventListener("abort", onAbort);
69
- if (code !== 0) {
70
- reject(new Error(err.trim() || `search.mjs exited with code ${code}`));
71
- } else {
72
- try {
73
- resolve(JSON.parse(out.trim()));
74
- } catch {
75
- reject(
76
- new Error(`Invalid JSON from search.mjs: ${out.slice(0, 200)}`),
77
- );
78
- }
79
- }
80
- });
81
- });
82
- }
83
-
84
- export default function greedySearchExtension(pi: ExtensionAPI) {
85
- pi.on("session_start", async (_event, ctx) => {
86
- if (!cdpAvailable()) {
87
- ctx.ui.notify(
88
- "GreedySearch: cdp.mjs missing from package directory — try reinstalling: pi install git:github.com/apmantza/GreedySearch-pi",
89
- "warning",
90
- );
91
- }
92
- });
93
-
94
- pi.registerTool({
95
- name: "greedy_search",
96
- label: "Greedy Search",
97
- description:
98
- "WEB SEARCH ONLY — searches live web via Perplexity, Bing Copilot, and Google AI in parallel. " +
99
- "Optionally synthesizes results with Gemini, deduplicates sources by consensus. " +
100
- "Use for: library docs, recent framework changes, error messages, best practices, current events. " +
101
- "Reports streaming progress as each engine completes.",
102
- promptSnippet: "Multi-engine AI web search with streaming progress",
103
- parameters: Type.Object({
104
- query: Type.String({ description: "The search query" }),
105
- engine: Type.Union(
106
- [
107
- Type.Literal("all"),
108
- Type.Literal("perplexity"),
109
- Type.Literal("bing"),
110
- Type.Literal("google"),
111
- Type.Literal("gemini"),
112
- Type.Literal("gem"),
113
- ],
114
- {
115
- description:
116
- 'Engine to use. "all" fans out to Perplexity, Bing, and Google in parallel (default).',
117
- default: "all",
118
- },
119
- ),
120
- depth: Type.Union(
121
- [Type.Literal("fast"), Type.Literal("standard"), Type.Literal("deep")],
122
- {
123
- description:
124
- "Search depth: fast (single engine, ~15-30s), standard (3 engines + synthesis, ~30-90s), deep (3 engines + source fetching + synthesis + confidence, ~60-180s). Default: standard.",
125
- default: "standard",
126
- },
127
- ),
128
- fullAnswer: Type.Optional(
129
- Type.Boolean({
130
- description:
131
- "When true, returns the complete answer instead of a truncated preview (default: false, answers are shortened to ~300 chars to save tokens).",
132
- default: false,
133
- }),
134
- ),
135
- }),
136
- execute: async (_toolCallId, params, signal, onUpdate) => {
137
- const {
138
- query,
139
- engine = "all",
140
- depth = "standard",
141
- fullAnswer: fullAnswerParam,
142
- } = params as {
143
- query: string;
144
- engine: string;
145
- depth?: "fast" | "standard" | "deep";
146
- fullAnswer?: boolean;
147
- };
148
-
149
- if (!cdpAvailable()) {
150
- return {
151
- content: [
152
- {
153
- type: "text",
154
- text: "cdp.mjs missing — try reinstalling: pi install git:github.com/apmantza/GreedySearch-pi",
155
- },
156
- ],
157
- details: {} as { raw?: Record<string, unknown> },
158
- };
159
- }
160
-
161
- const flags: string[] = [];
162
- // Default to full answer for single-engine queries (unless explicitly set to false)
163
- // For multi-engine, default to truncated to save tokens during synthesis
164
- const fullAnswer = fullAnswerParam ?? (engine !== "all");
165
- if (fullAnswer) flags.push("--full");
166
- if (depth === "deep") flags.push("--deep");
167
- else if (depth === "standard" && engine === "all") flags.push("--synthesize");
168
-
169
- const completed = new Set<string>();
170
-
171
- const onProgress = (eng: string, _status: "done" | "error") => {
172
- completed.add(eng);
173
- const parts: string[] = [];
174
- for (const e of ALL_ENGINES) {
175
- if (completed.has(e)) parts.push(`✅ ${e} done`);
176
- else parts.push(`⏳ ${e}`);
177
- }
178
- if (depth !== "fast" && completed.size >= 3)
179
- parts.push("🔄 synthesizing");
180
-
181
- onUpdate?.({
182
- content: [
183
- { type: "text", text: `**Searching...** ${parts.join(" · ")}` },
184
- ],
185
- details: { _progress: true },
186
- } as any);
187
- };
188
-
189
- try {
190
- const data = await runSearch(
191
- engine,
192
- query,
193
- flags,
194
- signal,
195
- engine === "all" ? onProgress : undefined,
196
- );
197
- const text = formatResults(engine, data);
198
- return {
199
- content: [{ type: "text", text: text || "No results returned." }],
200
- details: { raw: data },
201
- };
202
- } catch (e) {
203
- const msg = e instanceof Error ? e.message : String(e);
204
- return {
205
- content: [{ type: "text", text: `Search failed: ${msg}` }],
206
- details: {} as { raw?: Record<string, unknown> },
207
- };
208
- }
209
- },
210
- });
211
-
212
- // ─── deep_research ─────────────────────────────────────────────────────────
213
- pi.registerTool({
214
- name: "deep_research",
215
- label: "Deep Research (legacy)",
216
- description:
217
- "DEPRECATED — Use greedy_search with depth: 'deep' instead. " +
218
- "Comprehensive multi-engine research with source fetching and synthesis.",
219
- promptSnippet: "Deep multi-engine research (legacy alias to greedy_search)",
220
- parameters: Type.Object({
221
- query: Type.String({ description: "The research question" }),
222
- }),
223
- execute: async (_toolCallId, params, signal, onUpdate) => {
224
- const { query } = params as { query: string };
225
-
226
- if (!cdpAvailable()) {
227
- return {
228
- content: [
229
- { type: "text", text: "cdp.mjs missing — try reinstalling." },
230
- ],
231
- details: {} as { raw?: Record<string, unknown> },
232
- };
233
- }
234
-
235
- const completed = new Set<string>();
236
-
237
- const onProgress = (eng: string, _status: "done" | "error") => {
238
- completed.add(eng);
239
- const parts: string[] = [];
240
- for (const e of ALL_ENGINES) {
241
- if (completed.has(e)) parts.push(`✅ ${e}`);
242
- else parts.push(`⏳ ${e}`);
243
- }
244
- if (completed.size >= 3) parts.push("🔄 synthesizing");
245
-
246
- onUpdate?.({
247
- content: [
248
- { type: "text", text: `**Researching...** ${parts.join(" · ")}` },
249
- ],
250
- details: { _progress: true },
251
- } as any);
252
- };
253
-
254
- try {
255
- const data = await runSearch(
256
- "all",
257
- query,
258
- ["--deep"],
259
- signal,
260
- onProgress,
261
- );
262
- const text = formatDeepResearch(data);
263
- return {
264
- content: [{ type: "text", text: text || "No results returned." }],
265
- details: { raw: data },
266
- };
267
- } catch (e) {
268
- const msg = e instanceof Error ? e.message : String(e);
269
- return {
270
- content: [{ type: "text", text: `Deep research failed: ${msg}` }],
271
- details: {} as { raw?: Record<string, unknown> },
272
- };
273
- }
274
- },
275
- });
276
-
277
- // ─── coding_task ───────────────────────────────────────────────────────────
278
- pi.registerTool({
279
- name: "coding_task",
280
- label: "Coding Task",
281
- description:
282
- "Delegate a coding task to Gemini and/or Copilot via browser automation. " +
283
- "Returns extracted code blocks and explanations. Supports multiple modes: " +
284
- "'code' (write/modify code), 'review' (senior engineer code review), " +
285
- "'plan' (architect risk assessment), 'test' (edge case testing), " +
286
- "'debug' (fresh-eyes root cause analysis). " +
287
- "Best for getting a 'second opinion' on hard problems, debugging tricky issues, " +
288
- "or risk-assessing major refactors. Use engine 'all' for both perspectives.",
289
- promptSnippet: "Browser-based coding assistant with Gemini and Copilot",
290
- parameters: Type.Object({
291
- task: Type.String({ description: "The coding task or question" }),
292
- engine: Type.Union(
293
- [Type.Literal("all"), Type.Literal("gemini"), Type.Literal("copilot")],
294
- {
295
- description:
296
- 'Engine to use. "all" runs both Gemini and Copilot in parallel.',
297
- default: "gemini",
298
- },
299
- ),
300
- mode: Type.Union(
301
- [
302
- Type.Literal("code"),
303
- Type.Literal("review"),
304
- Type.Literal("plan"),
305
- Type.Literal("test"),
306
- Type.Literal("debug"),
307
- ],
308
- {
309
- description:
310
- "Task mode: code (default), review (code review), plan (architect review), test (write tests), debug (root cause analysis)",
311
- default: "code",
312
- },
313
- ),
314
- context: Type.Optional(
315
- Type.String({
316
- description: "Optional code context/snippet to include with the task",
317
- }),
318
- ),
319
- }),
320
- execute: async (_toolCallId, params, signal, onUpdate) => {
321
- const {
322
- task,
323
- engine = "gemini",
324
- mode = "code",
325
- context,
326
- } = params as {
327
- task: string;
328
- engine: string;
329
- mode: string;
330
- context?: string;
331
- };
332
-
333
- if (!cdpAvailable()) {
334
- return {
335
- content: [
336
- { type: "text", text: "cdp.mjs missing — try reinstalling." },
337
- ],
338
- details: {} as { raw?: Record<string, unknown> },
339
- };
340
- }
341
-
342
- const flags: string[] = ["--engine", engine, "--mode", mode];
343
- if (context) flags.push("--context", context);
344
-
345
- try {
346
- onUpdate?.({
347
- content: [
348
- {
349
- type: "text",
350
- text: `**Coding task...** 🔄 ${engine === "all" ? "Gemini + Copilot" : engine} (${mode} mode)`,
351
- },
352
- ],
353
- details: { _progress: true },
354
- } as any);
355
-
356
- const data = await new Promise<Record<string, unknown>>(
357
- (resolve, reject) => {
358
- const proc = spawn(
359
- "node",
360
- [`${__dir}/coding-task.mjs`, task, ...flags],
361
- {
362
- stdio: ["ignore", "pipe", "pipe"],
363
- },
364
- );
365
- let out = "";
366
- let err = "";
367
-
368
- const onAbort = () => {
369
- proc.kill("SIGTERM");
370
- reject(new Error("Aborted"));
371
- };
372
- signal?.addEventListener("abort", onAbort, { once: true });
373
-
374
- proc.stdout.on("data", (d: Buffer) => (out += d));
375
- proc.stderr.on("data", (d: Buffer) => {
376
- err += d;
377
- });
378
- proc.on("close", (code: number) => {
379
- signal?.removeEventListener("abort", onAbort);
380
- if (code !== 0) {
381
- reject(
382
- new Error(
383
- err.trim() || `coding-task.mjs exited with code ${code}`,
384
- ),
385
- );
386
- } else {
387
- try {
388
- resolve(JSON.parse(out.trim()));
389
- } catch {
390
- reject(
391
- new Error(
392
- `Invalid JSON from coding-task.mjs: ${out.slice(0, 200)}`,
393
- ),
394
- );
395
- }
396
- }
397
- });
398
-
399
- // Timeout after 3 minutes
400
- setTimeout(() => {
401
- proc.kill("SIGTERM");
402
- reject(new Error("Coding task timed out after 180s"));
403
- }, 180000);
404
- },
405
- );
406
-
407
- const text = formatCodingTask(data);
408
- return {
409
- content: [{ type: "text", text: text || "No response." }],
410
- details: { raw: data },
411
- };
412
- } catch (e) {
413
- const msg = e instanceof Error ? e.message : String(e);
414
- return {
415
- content: [{ type: "text", text: `Coding task failed: ${msg}` }],
416
- details: {} as { raw?: Record<string, unknown> },
417
- };
418
- }
419
- },
420
- });
421
- }
1
+ /**
2
+ * GreedySearch Pi Extension
3
+ *
4
+ * Adds a `greedy_search` tool to Pi that fans out queries to Perplexity,
5
+ * Bing Copilot, and Google AI in parallel, returning synthesized AI answers.
6
+ *
7
+ * Reports streaming progress as each engine completes.
8
+ * Requires Chrome to be running (or it auto-launches a dedicated instance).
9
+ */
10
+
11
+ import { spawn } from "node:child_process";
12
+ import { existsSync } from "node:fs";
13
+ import { dirname, join } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
16
+ import { Type } from "@sinclair/typebox";
17
+
18
+ // Formatters extracted to reduce file complexity
19
+ import { formatCodingTask } from "./src/formatters/coding.js";
20
+ import { formatResults, formatDeepResearch } from "./src/formatters/results.js";
21
+
22
+ const __dir = dirname(fileURLToPath(import.meta.url));
23
+
24
+ const ALL_ENGINES = ["perplexity", "bing", "google"] as const;
25
+
26
+ function cdpAvailable(): boolean {
27
+ return existsSync(join(__dir, "cdp.mjs"));
28
+ }
29
+
30
+ function runSearch(
31
+ engine: string,
32
+ query: string,
33
+ flags: string[] = [],
34
+ signal?: AbortSignal,
35
+ onProgress?: (engine: string, status: "done" | "error") => void,
36
+ ): Promise<Record<string, unknown>> {
37
+ return new Promise((resolve, reject) => {
38
+ const proc = spawn(
39
+ "node",
40
+ [`${__dir}/search.mjs`, engine, "--inline", ...flags, query],
41
+ {
42
+ stdio: ["ignore", "pipe", "pipe"],
43
+ },
44
+ );
45
+ let out = "";
46
+ let err = "";
47
+
48
+ const onAbort = () => {
49
+ proc.kill("SIGTERM");
50
+ reject(new Error("Aborted"));
51
+ };
52
+ signal?.addEventListener("abort", onAbort, { once: true });
53
+
54
+ // Watch stderr for progress events (PROGRESS:engine:done|error)
55
+ proc.stderr.on("data", (d: Buffer) => {
56
+ err += d;
57
+ const lines = d.toString().split("\n");
58
+ for (const line of lines) {
59
+ const match = line.match(/^PROGRESS:(\w+):(done|error)$/);
60
+ if (match && onProgress) {
61
+ onProgress(match[1], match[2] as "done" | "error");
62
+ }
63
+ }
64
+ });
65
+
66
+ proc.stdout.on("data", (d: Buffer) => (out += d));
67
+ proc.on("close", (code: number) => {
68
+ signal?.removeEventListener("abort", onAbort);
69
+ if (code !== 0) {
70
+ reject(new Error(err.trim() || `search.mjs exited with code ${code}`));
71
+ } else {
72
+ try {
73
+ resolve(JSON.parse(out.trim()));
74
+ } catch {
75
+ reject(
76
+ new Error(`Invalid JSON from search.mjs: ${out.slice(0, 200)}`),
77
+ );
78
+ }
79
+ }
80
+ });
81
+ });
82
+ }
83
+
84
+ export default function greedySearchExtension(pi: ExtensionAPI) {
85
+ pi.on("session_start", async (_event, ctx) => {
86
+ if (!cdpAvailable()) {
87
+ ctx.ui.notify(
88
+ "GreedySearch: cdp.mjs missing from package directory — try reinstalling: pi install git:github.com/apmantza/GreedySearch-pi",
89
+ "warning",
90
+ );
91
+ }
92
+ });
93
+
94
+ pi.registerTool({
95
+ name: "greedy_search",
96
+ label: "Greedy Search",
97
+ description:
98
+ "WEB SEARCH ONLY — searches live web via Perplexity, Bing Copilot, and Google AI in parallel. " +
99
+ "Optionally synthesizes results with Gemini, deduplicates sources by consensus. " +
100
+ "Use for: library docs, recent framework changes, error messages, best practices, current events. " +
101
+ "Reports streaming progress as each engine completes.",
102
+ promptSnippet: "Multi-engine AI web search with streaming progress",
103
+ parameters: Type.Object({
104
+ query: Type.String({ description: "The search query" }),
105
+ engine: Type.Union(
106
+ [
107
+ Type.Literal("all"),
108
+ Type.Literal("perplexity"),
109
+ Type.Literal("bing"),
110
+ Type.Literal("google"),
111
+ Type.Literal("gemini"),
112
+ Type.Literal("gem"),
113
+ ],
114
+ {
115
+ description:
116
+ 'Engine to use. "all" fans out to Perplexity, Bing, and Google in parallel (default).',
117
+ default: "all",
118
+ },
119
+ ),
120
+ depth: Type.Union(
121
+ [Type.Literal("fast"), Type.Literal("standard"), Type.Literal("deep")],
122
+ {
123
+ description:
124
+ "Search depth: fast (single engine, ~15-30s), standard (3 engines + synthesis, ~30-90s), deep (3 engines + source fetching + synthesis + confidence, ~60-180s). Default: standard.",
125
+ default: "standard",
126
+ },
127
+ ),
128
+ fullAnswer: Type.Optional(
129
+ Type.Boolean({
130
+ description:
131
+ "When true, returns the complete answer instead of a truncated preview (default: false, answers are shortened to ~300 chars to save tokens).",
132
+ default: false,
133
+ }),
134
+ ),
135
+ }),
136
+ execute: async (_toolCallId, params, signal, onUpdate) => {
137
+ const {
138
+ query,
139
+ engine = "all",
140
+ depth = "standard",
141
+ fullAnswer: fullAnswerParam,
142
+ } = params as {
143
+ query: string;
144
+ engine: string;
145
+ depth?: "fast" | "standard" | "deep";
146
+ fullAnswer?: boolean;
147
+ };
148
+
149
+ if (!cdpAvailable()) {
150
+ return {
151
+ content: [
152
+ {
153
+ type: "text",
154
+ text: "cdp.mjs missing — try reinstalling: pi install git:github.com/apmantza/GreedySearch-pi",
155
+ },
156
+ ],
157
+ details: {} as { raw?: Record<string, unknown> },
158
+ };
159
+ }
160
+
161
+ const flags: string[] = [];
162
+ // Default to full answer for single-engine queries (unless explicitly set to false)
163
+ // For multi-engine, default to truncated to save tokens during synthesis
164
+ const fullAnswer = fullAnswerParam ?? (engine !== "all");
165
+ if (fullAnswer) flags.push("--full");
166
+ if (depth === "deep") flags.push("--deep");
167
+ else if (depth === "standard" && engine === "all") flags.push("--synthesize");
168
+
169
+ const completed = new Set<string>();
170
+
171
+ const onProgress = (eng: string, _status: "done" | "error") => {
172
+ completed.add(eng);
173
+ const parts: string[] = [];
174
+ for (const e of ALL_ENGINES) {
175
+ if (completed.has(e)) parts.push(`✅ ${e} done`);
176
+ else parts.push(`⏳ ${e}`);
177
+ }
178
+ if (depth !== "fast" && completed.size >= 3)
179
+ parts.push("🔄 synthesizing");
180
+
181
+ onUpdate?.({
182
+ content: [
183
+ { type: "text", text: `**Searching...** ${parts.join(" · ")}` },
184
+ ],
185
+ details: { _progress: true },
186
+ } as any);
187
+ };
188
+
189
+ try {
190
+ const data = await runSearch(
191
+ engine,
192
+ query,
193
+ flags,
194
+ signal,
195
+ engine === "all" ? onProgress : undefined,
196
+ );
197
+ const text = formatResults(engine, data);
198
+ return {
199
+ content: [{ type: "text", text: text || "No results returned." }],
200
+ details: { raw: data },
201
+ };
202
+ } catch (e) {
203
+ const msg = e instanceof Error ? e.message : String(e);
204
+ return {
205
+ content: [{ type: "text", text: `Search failed: ${msg}` }],
206
+ details: {} as { raw?: Record<string, unknown> },
207
+ };
208
+ }
209
+ },
210
+ });
211
+
212
+ // ─── deep_research ─────────────────────────────────────────────────────────
213
+ pi.registerTool({
214
+ name: "deep_research",
215
+ label: "Deep Research (legacy)",
216
+ description:
217
+ "DEPRECATED — Use greedy_search with depth: 'deep' instead. " +
218
+ "Comprehensive multi-engine research with source fetching and synthesis.",
219
+ promptSnippet: "Deep multi-engine research (legacy alias to greedy_search)",
220
+ parameters: Type.Object({
221
+ query: Type.String({ description: "The research question" }),
222
+ }),
223
+ execute: async (_toolCallId, params, signal, onUpdate) => {
224
+ const { query } = params as { query: string };
225
+
226
+ if (!cdpAvailable()) {
227
+ return {
228
+ content: [
229
+ { type: "text", text: "cdp.mjs missing — try reinstalling." },
230
+ ],
231
+ details: {} as { raw?: Record<string, unknown> },
232
+ };
233
+ }
234
+
235
+ const completed = new Set<string>();
236
+
237
+ const onProgress = (eng: string, _status: "done" | "error") => {
238
+ completed.add(eng);
239
+ const parts: string[] = [];
240
+ for (const e of ALL_ENGINES) {
241
+ if (completed.has(e)) parts.push(`✅ ${e}`);
242
+ else parts.push(`⏳ ${e}`);
243
+ }
244
+ if (completed.size >= 3) parts.push("🔄 synthesizing");
245
+
246
+ onUpdate?.({
247
+ content: [
248
+ { type: "text", text: `**Researching...** ${parts.join(" · ")}` },
249
+ ],
250
+ details: { _progress: true },
251
+ } as any);
252
+ };
253
+
254
+ try {
255
+ const data = await runSearch(
256
+ "all",
257
+ query,
258
+ ["--deep"],
259
+ signal,
260
+ onProgress,
261
+ );
262
+ const text = formatDeepResearch(data);
263
+ return {
264
+ content: [{ type: "text", text: text || "No results returned." }],
265
+ details: { raw: data },
266
+ };
267
+ } catch (e) {
268
+ const msg = e instanceof Error ? e.message : String(e);
269
+ return {
270
+ content: [{ type: "text", text: `Deep research failed: ${msg}` }],
271
+ details: {} as { raw?: Record<string, unknown> },
272
+ };
273
+ }
274
+ },
275
+ });
276
+
277
+ // ─── coding_task ───────────────────────────────────────────────────────────
278
+ pi.registerTool({
279
+ name: "coding_task",
280
+ label: "Coding Task",
281
+ description:
282
+ "Delegate a coding task to Gemini and/or Copilot via browser automation. " +
283
+ "Returns extracted code blocks and explanations. Supports multiple modes: " +
284
+ "'code' (write/modify code), 'review' (senior engineer code review), " +
285
+ "'plan' (architect risk assessment), 'test' (edge case testing), " +
286
+ "'debug' (fresh-eyes root cause analysis). " +
287
+ "Best for getting a 'second opinion' on hard problems, debugging tricky issues, " +
288
+ "or risk-assessing major refactors. Use engine 'all' for both perspectives.",
289
+ promptSnippet: "Browser-based coding assistant with Gemini and Copilot",
290
+ parameters: Type.Object({
291
+ task: Type.String({ description: "The coding task or question" }),
292
+ engine: Type.Union(
293
+ [Type.Literal("all"), Type.Literal("gemini"), Type.Literal("copilot")],
294
+ {
295
+ description:
296
+ 'Engine to use. "all" runs both Gemini and Copilot in parallel.',
297
+ default: "gemini",
298
+ },
299
+ ),
300
+ mode: Type.Union(
301
+ [
302
+ Type.Literal("code"),
303
+ Type.Literal("review"),
304
+ Type.Literal("plan"),
305
+ Type.Literal("test"),
306
+ Type.Literal("debug"),
307
+ ],
308
+ {
309
+ description:
310
+ "Task mode: code (default), review (code review), plan (architect review), test (write tests), debug (root cause analysis)",
311
+ default: "code",
312
+ },
313
+ ),
314
+ context: Type.Optional(
315
+ Type.String({
316
+ description: "Optional code context/snippet to include with the task",
317
+ }),
318
+ ),
319
+ }),
320
+ execute: async (_toolCallId, params, signal, onUpdate) => {
321
+ const {
322
+ task,
323
+ engine = "gemini",
324
+ mode = "code",
325
+ context,
326
+ } = params as {
327
+ task: string;
328
+ engine: string;
329
+ mode: string;
330
+ context?: string;
331
+ };
332
+
333
+ if (!cdpAvailable()) {
334
+ return {
335
+ content: [
336
+ { type: "text", text: "cdp.mjs missing — try reinstalling." },
337
+ ],
338
+ details: {} as { raw?: Record<string, unknown> },
339
+ };
340
+ }
341
+
342
+ const flags: string[] = ["--engine", engine, "--mode", mode];
343
+ if (context) flags.push("--context", context);
344
+
345
+ try {
346
+ onUpdate?.({
347
+ content: [
348
+ {
349
+ type: "text",
350
+ text: `**Coding task...** 🔄 ${engine === "all" ? "Gemini + Copilot" : engine} (${mode} mode)`,
351
+ },
352
+ ],
353
+ details: { _progress: true },
354
+ } as any);
355
+
356
+ const data = await new Promise<Record<string, unknown>>(
357
+ (resolve, reject) => {
358
+ const proc = spawn(
359
+ "node",
360
+ [`${__dir}/coding-task.mjs`, task, ...flags],
361
+ {
362
+ stdio: ["ignore", "pipe", "pipe"],
363
+ },
364
+ );
365
+ let out = "";
366
+ let err = "";
367
+
368
+ const onAbort = () => {
369
+ proc.kill("SIGTERM");
370
+ reject(new Error("Aborted"));
371
+ };
372
+ signal?.addEventListener("abort", onAbort, { once: true });
373
+
374
+ proc.stdout.on("data", (d: Buffer) => (out += d));
375
+ proc.stderr.on("data", (d: Buffer) => {
376
+ err += d;
377
+ });
378
+ proc.on("close", (code: number) => {
379
+ signal?.removeEventListener("abort", onAbort);
380
+ if (code !== 0) {
381
+ reject(
382
+ new Error(
383
+ err.trim() || `coding-task.mjs exited with code ${code}`,
384
+ ),
385
+ );
386
+ } else {
387
+ try {
388
+ resolve(JSON.parse(out.trim()));
389
+ } catch {
390
+ reject(
391
+ new Error(
392
+ `Invalid JSON from coding-task.mjs: ${out.slice(0, 200)}`,
393
+ ),
394
+ );
395
+ }
396
+ }
397
+ });
398
+
399
+ // Timeout after 3 minutes
400
+ setTimeout(() => {
401
+ proc.kill("SIGTERM");
402
+ reject(new Error("Coding task timed out after 180s"));
403
+ }, 180000);
404
+ },
405
+ );
406
+
407
+ const text = formatCodingTask(data);
408
+ return {
409
+ content: [{ type: "text", text: text || "No response." }],
410
+ details: { raw: data },
411
+ };
412
+ } catch (e) {
413
+ const msg = e instanceof Error ? e.message : String(e);
414
+ return {
415
+ content: [{ type: "text", text: `Coding task failed: ${msg}` }],
416
+ details: {} as { raw?: Record<string, unknown> },
417
+ };
418
+ }
419
+ },
420
+ });
421
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apmantza/greedysearch-pi",
3
- "version": "1.6.3",
3
+ "version": "1.6.5",
4
4
  "description": "Pi extension: multi-engine AI search (Perplexity, Bing Copilot, Google AI) via browser automation — NO API KEYS needed. Extracts answers with sources, optional Gemini synthesis. Grounded AI answers from real browser interactions.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -10,6 +10,7 @@
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/apmantza/GreedySearch-pi.git"
12
12
  },
13
+ "author": "Apostolos Mantzaris",
13
14
  "license": "MIT",
14
15
  "files": [
15
16
  "index.ts",
@@ -1,207 +1,207 @@
1
- /**
2
- * Search results formatters
3
- * Extracted from index.ts
4
- */
5
-
6
- import { formatEngineName, humanizeSourceType } from "../utils/helpers.js";
7
- import { renderSynthesis } from "./synthesis.js";
8
- import { formatSourceLine, renderSourceEvidence } from "./sources.js";
9
-
10
- /**
11
- * Format search results based on engine type
12
- */
13
- export function formatResults(
14
- engine: string,
15
- data: Record<string, unknown>,
16
- ): string {
17
- const lines: string[] = [];
18
-
19
- if (engine === "all") {
20
- return formatAllEnginesResult(data, lines);
21
- }
22
-
23
- return formatSingleEngineResult(data, lines);
24
- }
25
-
26
- /**
27
- * Format multi-engine results with synthesis
28
- */
29
- function formatAllEnginesResult(
30
- data: Record<string, unknown>,
31
- lines: string[],
32
- ): string {
33
- const synthesis = data._synthesis as Record<string, unknown> | undefined;
34
- const dedupedSources = data._sources as
35
- | Array<Record<string, unknown>>
36
- | undefined;
37
-
38
- // If we have a synthesis answer, render it
39
- if (synthesis?.answer) {
40
- renderSynthesis(lines, synthesis, dedupedSources || [], 6);
41
- lines.push("*Synthesized from Perplexity, Bing Copilot, and Google AI*\n");
42
- return lines.join("\n").trim();
43
- }
44
-
45
- // Fallback: render individual engine results
46
- for (const [eng, result] of Object.entries(data)) {
47
- if (eng.startsWith("_")) continue;
48
- lines.push(`\n## ${formatEngineName(eng)}`);
49
- formatEngineResult(result as Record<string, unknown>, lines, 3);
50
- }
51
-
52
- return lines.join("\n").trim();
53
- }
54
-
55
- /**
56
- * Format single engine result
57
- */
58
- function formatSingleEngineResult(
59
- data: Record<string, unknown>,
60
- lines: string[],
61
- ): string {
62
- formatEngineResult(data, lines, 5);
63
- return lines.join("\n").trim();
64
- }
65
-
66
- /**
67
- * Format a single engine's result (answer + sources)
68
- */
69
- function formatEngineResult(
70
- data: Record<string, unknown>,
71
- lines: string[],
72
- maxSources: number,
73
- ): void {
74
- if (data.error) {
75
- lines.push(`Error: ${data.error}`);
76
- return;
77
- }
78
-
79
- if (data.answer) {
80
- lines.push(String(data.answer));
81
- }
82
-
83
- const sources = data.sources as Array<Record<string, string>> | undefined;
84
- if (Array.isArray(sources) && sources.length > 0) {
85
- lines.push("\nSources:");
86
- for (const s of sources.slice(0, maxSources)) {
87
- lines.push(`- [${s.title || s.url}](${s.url})`);
88
- }
89
- }
90
- }
91
-
92
- /**
93
- * Format deep research results with confidence metrics
94
- */
95
- export function formatDeepResearch(data: Record<string, unknown>): string {
96
- const lines: string[] = [];
97
- const confidence = data._confidence as Record<string, unknown> | undefined;
98
- const dedupedSources = data._sources as
99
- | Array<Record<string, unknown>>
100
- | undefined;
101
- const synthesis = data._synthesis as Record<string, unknown> | undefined;
102
-
103
- lines.push("# Deep Research Report\n");
104
-
105
- if (confidence) {
106
- formatConfidenceSection(lines, confidence);
107
- }
108
-
109
- if (synthesis?.answer) {
110
- renderSynthesis(lines, synthesis, dedupedSources || [], 8);
111
- }
112
-
113
- formatEnginePerspectives(lines, data);
114
- formatSourceRegistry(lines, dedupedSources || []);
115
-
116
- return lines.join("\n").trim();
117
- }
118
-
119
- /**
120
- * Format confidence section with metrics
121
- */
122
- function formatConfidenceSection(
123
- lines: string[],
124
- confidence: Record<string, unknown>,
125
- ): void {
126
- const enginesResponded = (confidence.enginesResponded as string[]) || [];
127
- const enginesFailed = (confidence.enginesFailed as string[]) || [];
128
- const agreementLevel = String(confidence.agreementLevel || "mixed");
129
- const firstPartySourceCount = Number(confidence.firstPartySourceCount || 0);
130
- const sourceTypeBreakdown = confidence.sourceTypeBreakdown as
131
- | Record<string, number>
132
- | undefined;
133
-
134
- lines.push("## Confidence\n");
135
- lines.push(`- Agreement: ${formatEngineName(agreementLevel)}`);
136
- lines.push(
137
- `- Engines responded: ${enginesResponded.map(formatEngineName).join(", ") || "none"}`,
138
- );
139
-
140
- if (enginesFailed.length > 0) {
141
- lines.push(
142
- `- Engines failed: ${enginesFailed.map(formatEngineName).join(", ")}`,
143
- );
144
- }
145
-
146
- lines.push(
147
- `- Top source consensus: ${confidence.topSourceConsensus || 0}/3 engines`,
148
- );
149
- lines.push(`- Total unique sources: ${confidence.sourcesCount || 0}`);
150
- lines.push(`- Official sources: ${confidence.officialSourceCount || 0}`);
151
- lines.push(`- First-party sources: ${firstPartySourceCount}`);
152
- lines.push(
153
- `- Fetch success rate: ${confidence.fetchedSourceSuccessRate || 0}`,
154
- );
155
-
156
- if (sourceTypeBreakdown && Object.keys(sourceTypeBreakdown).length > 0) {
157
- lines.push(
158
- `- Source mix: ${Object.entries(sourceTypeBreakdown)
159
- .map(([type, count]) => `${humanizeSourceType(type)} ${count}`)
160
- .join(", ")}`,
161
- );
162
- }
163
-
164
- lines.push("");
165
- }
166
-
167
- /**
168
- * Format engine perspectives section
169
- */
170
- function formatEnginePerspectives(
171
- lines: string[],
172
- data: Record<string, unknown>,
173
- ): void {
174
- lines.push("## Engine Perspectives\n");
175
-
176
- for (const engine of ["perplexity", "bing", "google"]) {
177
- const r = data[engine] as Record<string, unknown> | undefined;
178
- if (!r) continue;
179
-
180
- lines.push(`### ${formatEngineName(engine)}`);
181
-
182
- if (r.error) {
183
- lines.push(`⚠️ Error: ${r.error}`);
184
- } else if (r.answer) {
185
- lines.push(String(r.answer).slice(0, 2000));
186
- }
187
-
188
- lines.push("");
189
- }
190
- }
191
-
192
- /**
193
- * Format source registry section
194
- */
195
- function formatSourceRegistry(
196
- lines: string[],
197
- sources: Array<Record<string, unknown>>,
198
- ): void {
199
- if (sources.length === 0) return;
200
-
201
- lines.push("## Source Registry\n");
202
- for (const source of sources) {
203
- lines.push(formatSourceLine(source));
204
- renderSourceEvidence(lines, source);
205
- }
206
- lines.push("");
207
- }
1
+ /**
2
+ * Search results formatters
3
+ * Extracted from index.ts
4
+ */
5
+
6
+ import { formatEngineName, humanizeSourceType } from "../utils/helpers.js";
7
+ import { renderSynthesis } from "./synthesis.js";
8
+ import { formatSourceLine, renderSourceEvidence } from "./sources.js";
9
+
10
+ /**
11
+ * Format search results based on engine type
12
+ */
13
+ export function formatResults(
14
+ engine: string,
15
+ data: Record<string, unknown>,
16
+ ): string {
17
+ const lines: string[] = [];
18
+
19
+ if (engine === "all") {
20
+ return formatAllEnginesResult(data, lines);
21
+ }
22
+
23
+ return formatSingleEngineResult(data, lines);
24
+ }
25
+
26
+ /**
27
+ * Format multi-engine results with synthesis
28
+ */
29
+ function formatAllEnginesResult(
30
+ data: Record<string, unknown>,
31
+ lines: string[],
32
+ ): string {
33
+ const synthesis = data._synthesis as Record<string, unknown> | undefined;
34
+ const dedupedSources = data._sources as
35
+ | Array<Record<string, unknown>>
36
+ | undefined;
37
+
38
+ // If we have a synthesis answer, render it
39
+ if (synthesis?.answer) {
40
+ renderSynthesis(lines, synthesis, dedupedSources || [], 6);
41
+ lines.push("*Synthesized from Perplexity, Bing Copilot, and Google AI*\n");
42
+ return lines.join("\n").trim();
43
+ }
44
+
45
+ // Fallback: render individual engine results
46
+ for (const [eng, result] of Object.entries(data)) {
47
+ if (eng.startsWith("_")) continue;
48
+ lines.push(`\n## ${formatEngineName(eng)}`);
49
+ formatEngineResult(result as Record<string, unknown>, lines, 3);
50
+ }
51
+
52
+ return lines.join("\n").trim();
53
+ }
54
+
55
+ /**
56
+ * Format single engine result
57
+ */
58
+ function formatSingleEngineResult(
59
+ data: Record<string, unknown>,
60
+ lines: string[],
61
+ ): string {
62
+ formatEngineResult(data, lines, 5);
63
+ return lines.join("\n").trim();
64
+ }
65
+
66
+ /**
67
+ * Format a single engine's result (answer + sources)
68
+ */
69
+ function formatEngineResult(
70
+ data: Record<string, unknown>,
71
+ lines: string[],
72
+ maxSources: number,
73
+ ): void {
74
+ if (data.error) {
75
+ lines.push(`Error: ${data.error}`);
76
+ return;
77
+ }
78
+
79
+ if (data.answer) {
80
+ lines.push(String(data.answer));
81
+ }
82
+
83
+ const sources = data.sources as Array<Record<string, string>> | undefined;
84
+ if (Array.isArray(sources) && sources.length > 0) {
85
+ lines.push("\nSources:");
86
+ for (const s of sources.slice(0, maxSources)) {
87
+ lines.push(`- [${s.title || s.url}](${s.url})`);
88
+ }
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Format deep research results with confidence metrics
94
+ */
95
+ export function formatDeepResearch(data: Record<string, unknown>): string {
96
+ const lines: string[] = [];
97
+ const confidence = data._confidence as Record<string, unknown> | undefined;
98
+ const dedupedSources = data._sources as
99
+ | Array<Record<string, unknown>>
100
+ | undefined;
101
+ const synthesis = data._synthesis as Record<string, unknown> | undefined;
102
+
103
+ lines.push("# Deep Research Report\n");
104
+
105
+ if (confidence) {
106
+ formatConfidenceSection(lines, confidence);
107
+ }
108
+
109
+ if (synthesis?.answer) {
110
+ renderSynthesis(lines, synthesis, dedupedSources || [], 8);
111
+ }
112
+
113
+ formatEnginePerspectives(lines, data);
114
+ formatSourceRegistry(lines, dedupedSources || []);
115
+
116
+ return lines.join("\n").trim();
117
+ }
118
+
119
+ /**
120
+ * Format confidence section with metrics
121
+ */
122
+ function formatConfidenceSection(
123
+ lines: string[],
124
+ confidence: Record<string, unknown>,
125
+ ): void {
126
+ const enginesResponded = (confidence.enginesResponded as string[]) || [];
127
+ const enginesFailed = (confidence.enginesFailed as string[]) || [];
128
+ const agreementLevel = String(confidence.agreementLevel || "mixed");
129
+ const firstPartySourceCount = Number(confidence.firstPartySourceCount || 0);
130
+ const sourceTypeBreakdown = confidence.sourceTypeBreakdown as
131
+ | Record<string, number>
132
+ | undefined;
133
+
134
+ lines.push("## Confidence\n");
135
+ lines.push(`- Agreement: ${formatEngineName(agreementLevel)}`);
136
+ lines.push(
137
+ `- Engines responded: ${enginesResponded.map(formatEngineName).join(", ") || "none"}`,
138
+ );
139
+
140
+ if (enginesFailed.length > 0) {
141
+ lines.push(
142
+ `- Engines failed: ${enginesFailed.map(formatEngineName).join(", ")}`,
143
+ );
144
+ }
145
+
146
+ lines.push(
147
+ `- Top source consensus: ${confidence.topSourceConsensus || 0}/3 engines`,
148
+ );
149
+ lines.push(`- Total unique sources: ${confidence.sourcesCount || 0}`);
150
+ lines.push(`- Official sources: ${confidence.officialSourceCount || 0}`);
151
+ lines.push(`- First-party sources: ${firstPartySourceCount}`);
152
+ lines.push(
153
+ `- Fetch success rate: ${confidence.fetchedSourceSuccessRate || 0}`,
154
+ );
155
+
156
+ if (sourceTypeBreakdown && Object.keys(sourceTypeBreakdown).length > 0) {
157
+ lines.push(
158
+ `- Source mix: ${Object.entries(sourceTypeBreakdown)
159
+ .map(([type, count]) => `${humanizeSourceType(type)} ${count}`)
160
+ .join(", ")}`,
161
+ );
162
+ }
163
+
164
+ lines.push("");
165
+ }
166
+
167
+ /**
168
+ * Format engine perspectives section
169
+ */
170
+ function formatEnginePerspectives(
171
+ lines: string[],
172
+ data: Record<string, unknown>,
173
+ ): void {
174
+ lines.push("## Engine Perspectives\n");
175
+
176
+ for (const engine of ["perplexity", "bing", "google"]) {
177
+ const r = data[engine] as Record<string, unknown> | undefined;
178
+ if (!r) continue;
179
+
180
+ lines.push(`### ${formatEngineName(engine)}`);
181
+
182
+ if (r.error) {
183
+ lines.push(`⚠️ Error: ${r.error}`);
184
+ } else if (r.answer) {
185
+ lines.push(String(r.answer).slice(0, 2000));
186
+ }
187
+
188
+ lines.push("");
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Format source registry section
194
+ */
195
+ function formatSourceRegistry(
196
+ lines: string[],
197
+ sources: Array<Record<string, unknown>>,
198
+ ): void {
199
+ if (sources.length === 0) return;
200
+
201
+ lines.push("## Source Registry\n");
202
+ for (const source of sources) {
203
+ lines.push(formatSourceLine(source));
204
+ renderSourceEvidence(lines, source);
205
+ }
206
+ lines.push("");
207
+ }