@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.
- package/index.ts +222 -177
- package/package.json +1 -1
- 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
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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.
|
|
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)
|
|
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 };
|