@apmantza/greedysearch-pi 1.1.1 → 1.1.3

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.
Files changed (3) hide show
  1. package/index.ts +222 -177
  2. package/package.json +1 -1
  3. package/search.mjs +11 -1
package/index.ts CHANGED
@@ -1,177 +1,222 @@
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
- * Requires Chrome to be running (or it auto-launches a dedicated instance).
8
- */
9
-
10
- import { spawn } from "node:child_process";
11
- import { existsSync } from "node:fs";
12
- import { join, dirname } from "node:path";
13
- import { fileURLToPath } from "node:url";
14
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
15
- import { Type } from "@sinclair/typebox";
16
-
17
- const __dir = dirname(fileURLToPath(import.meta.url));
18
-
19
- function cdpAvailable(): boolean {
20
- return existsSync(join(__dir, "cdp.mjs"));
21
- }
22
-
23
- function runSearch(engine: string, query: string, flags: string[] = []): Promise<Record<string, unknown>> {
24
- return new Promise((resolve, reject) => {
25
- const proc = spawn("node", [__dir + "/search.mjs", engine, "--inline", ...flags, query], {
26
- stdio: ["ignore", "pipe", "pipe"],
27
- });
28
- let out = "";
29
- let err = "";
30
- proc.stdout.on("data", (d: Buffer) => (out += d));
31
- proc.stderr.on("data", (d: Buffer) => (err += d));
32
- proc.on("close", (code: number) => {
33
- if (code !== 0) {
34
- reject(new Error(err.trim() || `search.mjs exited with code ${code}`));
35
- } else {
36
- try {
37
- resolve(JSON.parse(out.trim()));
38
- } catch {
39
- reject(new Error(`Invalid JSON from search.mjs: ${out.slice(0, 200)}`));
40
- }
41
- }
42
- });
43
- });
44
- }
45
-
46
- function formatResults(engine: string, data: Record<string, unknown>): string {
47
- const lines: string[] = [];
48
-
49
- if (engine === "all") {
50
- // Synthesized output: prefer _synthesis + _sources
51
- const synthesis = data._synthesis as Record<string, unknown> | undefined;
52
- const dedupedSources = data._sources as Array<Record<string, unknown>> | undefined;
53
- if (synthesis?.answer) {
54
- lines.push("## Synthesis");
55
- lines.push(String(synthesis.answer));
56
- if (dedupedSources?.length) {
57
- lines.push("\n**Top sources by consensus:**");
58
- for (const s of dedupedSources.slice(0, 6)) {
59
- const engines = (s.engines as string[]) || [];
60
- lines.push(`- [${s.title || s.url}](${s.url}) [${engines.length}/3]`);
61
- }
62
- }
63
- lines.push("\n---\n*Synthesized from Perplexity, Bing Copilot, and Google AI*");
64
- return lines.join("\n").trim();
65
- }
66
-
67
- // Standard output: per-engine answers
68
- for (const [eng, result] of Object.entries(data)) {
69
- if (eng.startsWith("_")) continue;
70
- lines.push(`\n## ${eng.charAt(0).toUpperCase() + eng.slice(1)}`);
71
- const r = result as Record<string, unknown>;
72
- if (r.error) {
73
- lines.push(`Error: ${r.error}`);
74
- } else {
75
- if (r.answer) lines.push(String(r.answer));
76
- if (Array.isArray(r.sources) && r.sources.length > 0) {
77
- lines.push("\nSources:");
78
- for (const s of r.sources.slice(0, 3)) {
79
- const src = s as Record<string, string>;
80
- lines.push(`- [${src.title || src.url}](${src.url})`);
81
- }
82
- }
83
- }
84
- }
85
- } else {
86
- if (data.error) {
87
- lines.push(`Error: ${data.error}`);
88
- } else {
89
- if (data.answer) lines.push(String(data.answer));
90
- if (Array.isArray(data.sources) && data.sources.length > 0) {
91
- lines.push("\nSources:");
92
- for (const s of data.sources.slice(0, 5)) {
93
- const src = s as Record<string, string>;
94
- lines.push(`- [${src.title || src.url}](${src.url})`);
95
- }
96
- }
97
- }
98
- }
99
-
100
- return lines.join("\n").trim();
101
- }
102
-
103
- export default function greedySearchExtension(pi: ExtensionAPI) {
104
- pi.on("session_start", async (_event, ctx) => {
105
- if (!cdpAvailable()) {
106
- ctx.ui.notify(
107
- "GreedySearch: cdp.mjs missing from package directory — try reinstalling: pi install git:github.com/apmantza/GreedySearch-pi",
108
- "warning",
109
- );
110
- }
111
- });
112
-
113
- pi.registerTool({
114
- name: "greedy_search",
115
- label: "Greedy Search",
116
- description:
117
- "Search the web using AI-powered engines (Perplexity, Bing Copilot, Google AI) in parallel. " +
118
- "Optionally synthesize results with Gemini deduplicates sources by consensus and returns one grounded answer. " +
119
- "Use for current information, library docs, error messages, best practices, or any question where training data may be stale.",
120
- parameters: Type.Object({
121
- query: Type.String({ description: "The search query" }),
122
- engine: Type.Union(
123
- [
124
- Type.Literal("all"),
125
- Type.Literal("perplexity"),
126
- Type.Literal("bing"),
127
- Type.Literal("google"),
128
- Type.Literal("gemini"),
129
- Type.Literal("gem"),
130
- ],
131
- {
132
- description: 'Engine to use. "all" fans out to Perplexity, Bing, and Google in parallel (default).',
133
- default: "all",
134
- },
135
- ),
136
- synthesize: Type.Optional(Type.Boolean({
137
- description: 'When true and engine is "all", deduplicates sources across engines and feeds them to Gemini for a single grounded synthesis. Adds ~30s but saves tokens and improves answer quality.',
138
- default: false,
139
- })),
140
- fullAnswer: Type.Optional(Type.Boolean({
141
- description: 'When true, returns the complete answer instead of a truncated preview (default: false, answers are shortened to ~300 chars to save tokens).',
142
- default: false,
143
- })),
144
- }),
145
- execute: async (_toolCallId, params) => {
146
- const { query, engine = "all", synthesize = false, fullAnswer = false } = params as { query: string; engine: string; synthesize?: boolean; fullAnswer?: boolean };
147
-
148
- if (!cdpAvailable()) {
149
- return {
150
- content: [{ type: "text", text: "cdp.mjs missing — try reinstalling: pi install git:github.com/apmantza/GreedySearch-pi" }],
151
- details: {} as { raw?: Record<string, unknown> },
152
- };
153
- }
154
-
155
- const flags: string[] = [];
156
- if (synthesize && engine === "all") flags.push("--synthesize");
157
- if (fullAnswer) flags.push("--full");
158
-
159
- let data: Record<string, unknown>;
160
- try {
161
- data = await runSearch(engine, query, flags);
162
- } catch (e) {
163
- const msg = e instanceof Error ? e.message : String(e);
164
- return {
165
- content: [{ type: "text", text: `Search failed: ${msg}` }],
166
- details: {} as { raw?: Record<string, unknown> },
167
- };
168
- }
169
-
170
- const text = formatResults(engine, data);
171
- return {
172
- content: [{ type: "text", text: text || "No results returned." }],
173
- details: { raw: data } as { raw?: Record<string, unknown> },
174
- };
175
- },
176
- });
177
- }
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 { join, dirname } 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
+ const __dir = dirname(fileURLToPath(import.meta.url));
19
+
20
+ const ALL_ENGINES = ["perplexity", "bing", "google"] as const;
21
+
22
+ function cdpAvailable(): boolean {
23
+ return existsSync(join(__dir, "cdp.mjs"));
24
+ }
25
+
26
+ function runSearch(
27
+ engine: string,
28
+ query: string,
29
+ flags: string[] = [],
30
+ signal?: AbortSignal,
31
+ onProgress?: (engine: string, status: "done" | "error") => void,
32
+ ): Promise<Record<string, unknown>> {
33
+ return new Promise((resolve, reject) => {
34
+ const proc = spawn("node", [__dir + "/search.mjs", engine, "--inline", ...flags, query], {
35
+ stdio: ["ignore", "pipe", "pipe"],
36
+ });
37
+ let out = "";
38
+ let err = "";
39
+
40
+ const onAbort = () => { proc.kill("SIGTERM"); reject(new Error("Aborted")); };
41
+ signal?.addEventListener("abort", onAbort, { once: true });
42
+
43
+ // Watch stderr for progress events (PROGRESS:engine:done|error)
44
+ proc.stderr.on("data", (d: Buffer) => {
45
+ err += d;
46
+ const lines = d.toString().split("\n");
47
+ for (const line of lines) {
48
+ const match = line.match(/^PROGRESS:(\w+):(done|error)$/);
49
+ if (match && onProgress) {
50
+ onProgress(match[1], match[2] as "done" | "error");
51
+ }
52
+ }
53
+ });
54
+
55
+ proc.stdout.on("data", (d: Buffer) => (out += d));
56
+ proc.on("close", (code: number) => {
57
+ signal?.removeEventListener("abort", onAbort);
58
+ if (code !== 0) {
59
+ reject(new Error(err.trim() || `search.mjs exited with code ${code}`));
60
+ } else {
61
+ try {
62
+ resolve(JSON.parse(out.trim()));
63
+ } catch {
64
+ reject(new Error(`Invalid JSON from search.mjs: ${out.slice(0, 200)}`));
65
+ }
66
+ }
67
+ });
68
+ });
69
+ }
70
+
71
+ function formatResults(engine: string, data: Record<string, unknown>): string {
72
+ const lines: string[] = [];
73
+
74
+ if (engine === "all") {
75
+ // Synthesized output: prefer _synthesis + _sources
76
+ const synthesis = data._synthesis as Record<string, unknown> | undefined;
77
+ const dedupedSources = data._sources as Array<Record<string, unknown>> | undefined;
78
+ if (synthesis?.answer) {
79
+ lines.push("## Synthesis");
80
+ lines.push(String(synthesis.answer));
81
+ if (dedupedSources?.length) {
82
+ lines.push("\n**Top sources by consensus:**");
83
+ for (const s of dedupedSources.slice(0, 6)) {
84
+ const engines = (s.engines as string[]) || [];
85
+ lines.push(`- [${s.title || s.url}](${s.url}) [${engines.length}/3]`);
86
+ }
87
+ }
88
+ lines.push("\n---\n*Synthesized from Perplexity, Bing Copilot, and Google AI*");
89
+ return lines.join("\n").trim();
90
+ }
91
+
92
+ // Standard output: per-engine answers
93
+ for (const [eng, result] of Object.entries(data)) {
94
+ if (eng.startsWith("_")) continue;
95
+ lines.push(`\n## ${eng.charAt(0).toUpperCase() + eng.slice(1)}`);
96
+ const r = result as Record<string, unknown>;
97
+ if (r.error) {
98
+ lines.push(`Error: ${r.error}`);
99
+ } else {
100
+ if (r.answer) lines.push(String(r.answer));
101
+ if (Array.isArray(r.sources) && r.sources.length > 0) {
102
+ lines.push("\nSources:");
103
+ for (const s of r.sources.slice(0, 3)) {
104
+ const src = s as Record<string, string>;
105
+ lines.push(`- [${src.title || src.url}](${src.url})`);
106
+ }
107
+ }
108
+ }
109
+ }
110
+ } else {
111
+ if (data.error) {
112
+ lines.push(`Error: ${data.error}`);
113
+ } else {
114
+ if (data.answer) lines.push(String(data.answer));
115
+ if (Array.isArray(data.sources) && data.sources.length > 0) {
116
+ lines.push("\nSources:");
117
+ for (const s of data.sources.slice(0, 5)) {
118
+ const src = s as Record<string, string>;
119
+ lines.push(`- [${src.title || src.url}](${src.url})`);
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ return lines.join("\n").trim();
126
+ }
127
+
128
+ export default function greedySearchExtension(pi: ExtensionAPI) {
129
+ pi.on("session_start", async (_event, ctx) => {
130
+ if (!cdpAvailable()) {
131
+ ctx.ui.notify(
132
+ "GreedySearch: cdp.mjs missing from package directory try reinstalling: pi install git:github.com/apmantza/GreedySearch-pi",
133
+ "warning",
134
+ );
135
+ }
136
+ });
137
+
138
+ pi.registerTool({
139
+ name: "greedy_search",
140
+ label: "Greedy Search",
141
+ description:
142
+ "Search the web using AI-powered engines (Perplexity, Bing Copilot, Google AI) in parallel. " +
143
+ "Optionally synthesize results with Gemini — deduplicates sources by consensus and returns one grounded answer. " +
144
+ "Reports streaming progress as each engine completes. " +
145
+ "Use for current information, library docs, error messages, best practices, or any question where training data may be stale.",
146
+ promptSnippet: "Multi-engine AI web search with streaming progress",
147
+ parameters: Type.Object({
148
+ query: Type.String({ description: "The search query" }),
149
+ engine: Type.Union(
150
+ [
151
+ Type.Literal("all"),
152
+ Type.Literal("perplexity"),
153
+ Type.Literal("bing"),
154
+ Type.Literal("google"),
155
+ Type.Literal("gemini"),
156
+ Type.Literal("gem"),
157
+ ],
158
+ {
159
+ description: 'Engine to use. "all" fans out to Perplexity, Bing, and Google in parallel (default).',
160
+ default: "all",
161
+ },
162
+ ),
163
+ synthesize: Type.Optional(Type.Boolean({
164
+ 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.',
165
+ default: false,
166
+ })),
167
+ fullAnswer: Type.Optional(Type.Boolean({
168
+ description: 'When true, returns the complete answer instead of a truncated preview (default: false, answers are shortened to ~300 chars to save tokens).',
169
+ default: false,
170
+ })),
171
+ }),
172
+ execute: async (_toolCallId, params, signal, onUpdate) => {
173
+ const { query, engine = "all", synthesize = false, fullAnswer = false } = params as {
174
+ query: string; engine: string; synthesize?: boolean; fullAnswer?: boolean;
175
+ };
176
+
177
+ if (!cdpAvailable()) {
178
+ return {
179
+ content: [{ type: "text", text: "cdp.mjs missing — try reinstalling: pi install git:github.com/apmantza/GreedySearch-pi" }],
180
+ details: {} as { raw?: Record<string, unknown> },
181
+ };
182
+ }
183
+
184
+ const flags: string[] = [];
185
+ if (fullAnswer) flags.push("--full");
186
+ if (synthesize && engine === "all") flags.push("--synthesize");
187
+
188
+ // Track progress for "all" engine mode
189
+ const completed = new Set<string>();
190
+
191
+ const onProgress = (eng: string, status: "done" | "error") => {
192
+ completed.add(eng);
193
+ const parts: string[] = [];
194
+ for (const e of ALL_ENGINES) {
195
+ if (completed.has(e)) parts.push(`✅ ${e} done`);
196
+ else parts.push(`⏳ ${e}`);
197
+ }
198
+ if (synthesize && completed.size >= 3) parts.push("🔄 synthesizing");
199
+
200
+ onUpdate?.({
201
+ content: [{ type: "text", text: `**Searching...** ${parts.join(" · ")}` }],
202
+ details: { _progress: true },
203
+ } as any);
204
+ };
205
+
206
+ try {
207
+ const data = await runSearch(engine, query, flags, signal, engine === "all" ? onProgress : undefined);
208
+ const text = formatResults(engine, data);
209
+ return {
210
+ content: [{ type: "text", text: text || "No results returned." }],
211
+ details: { raw: data },
212
+ };
213
+ } catch (e) {
214
+ const msg = e instanceof Error ? e.message : String(e);
215
+ return {
216
+ content: [{ type: "text", text: `Search failed: ${msg}` }],
217
+ details: {} as { raw?: Record<string, unknown> },
218
+ };
219
+ }
220
+ },
221
+ });
222
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apmantza/greedysearch-pi",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Pi extension: search Perplexity, Bing Copilot, and Google AI in parallel with optional Gemini synthesis — grounded AI answers, not just links",
5
5
  "type": "module",
6
6
  "keywords": [
package/search.mjs CHANGED
@@ -405,7 +405,15 @@ async function main() {
405
405
  // All tabs assigned — run extractors in parallel
406
406
  const results = await Promise.allSettled(
407
407
  ALL_ENGINES.map((e, i) =>
408
- runExtractor(ENGINES[e], query, tabs[i], short).then(r => ({ engine: e, ...r }))
408
+ runExtractor(ENGINES[e], query, tabs[i], short)
409
+ .then(r => {
410
+ process.stderr.write(`PROGRESS:${e}:done\n`);
411
+ return { engine: e, ...r };
412
+ })
413
+ .catch(err => {
414
+ process.stderr.write(`PROGRESS:${e}:error\n`);
415
+ throw err;
416
+ })
409
417
  )
410
418
  );
411
419
 
@@ -424,6 +432,7 @@ async function main() {
424
432
 
425
433
  // Synthesize with Gemini if requested
426
434
  if (synthesize) {
435
+ process.stderr.write('PROGRESS:synthesis:start\n');
427
436
  process.stderr.write('[greedysearch] Synthesizing results with Gemini...\n');
428
437
  try {
429
438
  const synthesis = await synthesizeWithGemini(query, out);
@@ -432,6 +441,7 @@ async function main() {
432
441
  sources: synthesis.sources || [],
433
442
  synthesized: true,
434
443
  };
444
+ process.stderr.write('PROGRESS:synthesis:done\n');
435
445
  } catch (e) {
436
446
  process.stderr.write(`[greedysearch] Synthesis failed: ${e.message}\n`);
437
447
  out._synthesis = { error: e.message, synthesized: false };