@apmantza/greedysearch-pi 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -7
- package/cdp.mjs +16 -15
- package/index.ts +414 -13
- package/launch.mjs +33 -77
- package/newfeaturesideas.md +105 -0
- package/package.json +1 -1
- package/search.mjs +600 -110
- package/skills/greedy-search/SKILL.md +76 -3
package/README.md
CHANGED
|
@@ -4,12 +4,13 @@ Pi extension that adds a `greedy_search` tool — fans out queries to Perplexity
|
|
|
4
4
|
|
|
5
5
|
Forked from [GreedySearch-claude](https://github.com/apmantza/GreedySearch-claude).
|
|
6
6
|
|
|
7
|
-
## What's New (v1.
|
|
7
|
+
## What's New (v1.4.0)
|
|
8
8
|
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
9
|
+
- **Grounded synthesis** — Gemini now receives a normalized source registry with stable source IDs, agreement summaries, caveats, and cited claims
|
|
10
|
+
- **Real deep research** — top sources are fetched before synthesis so deep research answers are grounded in fetched evidence, not just engine summaries
|
|
11
|
+
- **Richer source metadata** — source output now includes canonical URLs, domains, source types, per-engine attribution, and confidence metadata
|
|
12
|
+
- **Cleaner tab lifecycle** — temporary Perplexity, Bing, and Google tabs are closed after each fan-out search, and synthesis finishes on the Gemini tab
|
|
13
|
+
- **Isolated Chrome targeting** — GreedySearch now refuses to fall back to your normal Chrome session, preventing stray remote-debugging prompts
|
|
13
14
|
|
|
14
15
|
## Install
|
|
15
16
|
|
|
@@ -69,7 +70,15 @@ For complex research questions, use `synthesize: true` with `engine: "all"`:
|
|
|
69
70
|
greedy_search({ query: "best auth patterns for SaaS in 2026", engine: "all", synthesize: true })
|
|
70
71
|
```
|
|
71
72
|
|
|
72
|
-
This deduplicates sources across engines and feeds
|
|
73
|
+
This deduplicates sources across engines, builds a normalized source registry, and feeds that context to Gemini for one clean synthesized answer. Adds ~30s but now returns agreement summaries, caveats, key claims, and better-labeled top sources.
|
|
74
|
+
|
|
75
|
+
For the most grounded mode, use deep research from the CLI:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
node search.mjs all "best auth patterns for SaaS in 2026" --deep-research
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Deep research fetches top source pages before synthesis and reports source confidence metadata such as agreement level, fetched-source success rate, and source mix.
|
|
73
82
|
|
|
74
83
|
**Use synthesis when:**
|
|
75
84
|
- You need one definitive answer, not multiple perspectives
|
|
@@ -112,7 +121,7 @@ greedy_search({ query: "Error: Cannot find module 'react-dom/client' Next.js 15"
|
|
|
112
121
|
|
|
113
122
|
## Requirements
|
|
114
123
|
|
|
115
|
-
- **Chrome** — must be installed. The extension auto-launches a dedicated Chrome instance on port 9222
|
|
124
|
+
- **Chrome** — must be installed. The extension auto-launches a dedicated Chrome instance on port 9222 with its own isolated profile and DevTools port file, separate from your main browser session.
|
|
116
125
|
- **Node.js 22+** — for built-in `fetch` and WebSocket support.
|
|
117
126
|
|
|
118
127
|
## Setup (first time)
|
package/cdp.mjs
CHANGED
|
@@ -37,21 +37,22 @@ function getDevToolsActivePortPath() {
|
|
|
37
37
|
return join(homedir(), '.config', 'google-chrome', 'DevToolsActivePort');
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function getWsUrl() {
|
|
41
|
-
// If CDP_PROFILE_DIR is set (by search.mjs), prefer that profile's port file
|
|
42
|
-
// so GreedySearch targets its own Chrome, not the user's main session.
|
|
43
|
-
const profileDir = process.env.CDP_PROFILE_DIR;
|
|
44
|
-
if (profileDir) {
|
|
45
|
-
const p = profileDir.replace(/\\/g, '/') + '/DevToolsActivePort';
|
|
46
|
-
if (existsSync(p)) {
|
|
47
|
-
const lines = readFileSync(p, 'utf8').trim().split('\n');
|
|
48
|
-
return `ws://127.0.0.1:${lines[0]}${lines[1]}`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
}
|
|
40
|
+
function getWsUrl() {
|
|
41
|
+
// If CDP_PROFILE_DIR is set (by search.mjs), prefer that profile's port file
|
|
42
|
+
// so GreedySearch targets its own Chrome, not the user's main session.
|
|
43
|
+
const profileDir = process.env.CDP_PROFILE_DIR;
|
|
44
|
+
if (profileDir) {
|
|
45
|
+
const p = profileDir.replace(/\\/g, '/') + '/DevToolsActivePort';
|
|
46
|
+
if (existsSync(p)) {
|
|
47
|
+
const lines = readFileSync(p, 'utf8').trim().split('\n');
|
|
48
|
+
return `ws://127.0.0.1:${lines[0]}${lines[1]}`;
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`GreedySearch DevToolsActivePort not found at ${p}. Refusing to fall back to the main Chrome session.`);
|
|
51
|
+
}
|
|
52
|
+
const portFile = getDevToolsActivePortPath();
|
|
53
|
+
const lines = readFileSync(portFile, 'utf8').trim().split('\n');
|
|
54
|
+
return `ws://127.0.0.1:${lines[0]}${lines[1]}`;
|
|
55
|
+
}
|
|
55
56
|
|
|
56
57
|
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
57
58
|
|
package/index.ts
CHANGED
|
@@ -68,31 +68,164 @@ function runSearch(
|
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function formatEngineName(engine: string): string {
|
|
72
|
+
if (engine === "bing") return "Bing Copilot";
|
|
73
|
+
if (engine === "google") return "Google AI";
|
|
74
|
+
return engine.charAt(0).toUpperCase() + engine.slice(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function humanizeSourceType(sourceType: string): string {
|
|
78
|
+
if (!sourceType) return "";
|
|
79
|
+
if (sourceType === "official-docs") return "official docs";
|
|
80
|
+
return sourceType.replace(/-/g, " ");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sourceUrl(source: Record<string, unknown>): string {
|
|
84
|
+
return String(source.displayUrl || source.canonicalUrl || source.url || "");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function sourceLabel(source: Record<string, unknown>): string {
|
|
88
|
+
return String(source.title || source.domain || sourceUrl(source) || "Untitled source");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function sourceConsensus(source: Record<string, unknown>): number {
|
|
92
|
+
if (typeof source.engineCount === "number") return source.engineCount;
|
|
93
|
+
const engines = Array.isArray(source.engines) ? (source.engines as string[]) : [];
|
|
94
|
+
return engines.length;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatAgreementLevel(level: string): string {
|
|
98
|
+
if (!level) return "Mixed";
|
|
99
|
+
return level.charAt(0).toUpperCase() + level.slice(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getSourceMap(sources: Array<Record<string, unknown>>): Map<string, Record<string, unknown>> {
|
|
103
|
+
return new Map(
|
|
104
|
+
sources
|
|
105
|
+
.map((source) => [String(source.id || ""), source] as const)
|
|
106
|
+
.filter(([id]) => id),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function formatSourceLine(source: Record<string, unknown>): string {
|
|
111
|
+
const id = String(source.id || "?");
|
|
112
|
+
const url = sourceUrl(source);
|
|
113
|
+
const title = sourceLabel(source);
|
|
114
|
+
const domain = String(source.domain || "");
|
|
115
|
+
const engines = Array.isArray(source.engines) ? (source.engines as string[]) : [];
|
|
116
|
+
const consensus = sourceConsensus(source);
|
|
117
|
+
const typeLabel = humanizeSourceType(String(source.sourceType || ""));
|
|
118
|
+
const fetch = source.fetch as Record<string, unknown> | undefined;
|
|
119
|
+
const fetchStatus = fetch?.ok ? `fetched ${fetch.status || 200}` : fetch?.attempted ? "fetch failed" : "";
|
|
120
|
+
const pieces = [
|
|
121
|
+
`${id} - [${title}](${url})`,
|
|
122
|
+
domain,
|
|
123
|
+
typeLabel,
|
|
124
|
+
engines.length ? `cited by ${engines.map(formatEngineName).join(", ")} (${consensus}/3)` : `${consensus}/3`,
|
|
125
|
+
fetchStatus,
|
|
126
|
+
].filter(Boolean);
|
|
127
|
+
return `- ${pieces.join(" - ")}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function renderSourceEvidence(lines: string[], source: Record<string, unknown>): void {
|
|
131
|
+
const fetch = source.fetch as Record<string, unknown> | undefined;
|
|
132
|
+
if (!fetch?.attempted) return;
|
|
133
|
+
|
|
134
|
+
const snippet = String(fetch.snippet || "").trim();
|
|
135
|
+
const lastModified = String(fetch.lastModified || "").trim();
|
|
136
|
+
if (snippet) lines.push(` Evidence: ${snippet}`);
|
|
137
|
+
if (lastModified) lines.push(` Last-Modified: ${lastModified}`);
|
|
138
|
+
if (fetch.error) lines.push(` Fetch error: ${String(fetch.error)}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function pickSources(
|
|
142
|
+
sources: Array<Record<string, unknown>>,
|
|
143
|
+
recommendedIds: string[] = [],
|
|
144
|
+
max = 6,
|
|
145
|
+
): Array<Record<string, unknown>> {
|
|
146
|
+
if (!sources.length) return [];
|
|
147
|
+
const sourceMap = getSourceMap(sources);
|
|
148
|
+
const recommended = recommendedIds
|
|
149
|
+
.map((id) => sourceMap.get(id))
|
|
150
|
+
.filter((source): source is Record<string, unknown> => Boolean(source));
|
|
151
|
+
if (recommended.length > 0) return recommended.slice(0, max);
|
|
152
|
+
return sources.slice(0, max);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function renderSynthesis(
|
|
156
|
+
lines: string[],
|
|
157
|
+
synthesis: Record<string, unknown>,
|
|
158
|
+
sources: Array<Record<string, unknown>>,
|
|
159
|
+
maxSources = 6,
|
|
160
|
+
): void {
|
|
161
|
+
if (synthesis.answer) {
|
|
162
|
+
lines.push("## Answer");
|
|
163
|
+
lines.push(String(synthesis.answer));
|
|
164
|
+
lines.push("");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const agreement = synthesis.agreement as Record<string, unknown> | undefined;
|
|
168
|
+
const agreementSummary = String(agreement?.summary || "").trim();
|
|
169
|
+
const agreementLevel = String(agreement?.level || "").trim();
|
|
170
|
+
if (agreementSummary || agreementLevel) {
|
|
171
|
+
lines.push("## Consensus");
|
|
172
|
+
lines.push(`- ${formatAgreementLevel(agreementLevel)}${agreementSummary ? ` - ${agreementSummary}` : ""}`);
|
|
173
|
+
lines.push("");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const differences = Array.isArray(synthesis.differences) ? (synthesis.differences as string[]) : [];
|
|
177
|
+
if (differences.length > 0) {
|
|
178
|
+
lines.push("## Where Engines Differ");
|
|
179
|
+
for (const difference of differences) lines.push(`- ${difference}`);
|
|
180
|
+
lines.push("");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const caveats = Array.isArray(synthesis.caveats) ? (synthesis.caveats as string[]) : [];
|
|
184
|
+
if (caveats.length > 0) {
|
|
185
|
+
lines.push("## Caveats");
|
|
186
|
+
for (const caveat of caveats) lines.push(`- ${caveat}`);
|
|
187
|
+
lines.push("");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const claims = Array.isArray(synthesis.claims)
|
|
191
|
+
? (synthesis.claims as Array<Record<string, unknown>>)
|
|
192
|
+
: [];
|
|
193
|
+
if (claims.length > 0) {
|
|
194
|
+
lines.push("## Key Claims");
|
|
195
|
+
for (const claim of claims) {
|
|
196
|
+
const sourceIds = Array.isArray(claim.sourceIds) ? (claim.sourceIds as string[]) : [];
|
|
197
|
+
const support = String(claim.support || "moderate");
|
|
198
|
+
lines.push(`- ${String(claim.claim || "")} [${support}${sourceIds.length ? `; ${sourceIds.join(", ")}` : ""}]`);
|
|
199
|
+
}
|
|
200
|
+
lines.push("");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const recommendedIds = Array.isArray(synthesis.recommendedSources)
|
|
204
|
+
? (synthesis.recommendedSources as string[])
|
|
205
|
+
: [];
|
|
206
|
+
const topSources = pickSources(sources, recommendedIds, maxSources);
|
|
207
|
+
if (topSources.length > 0) {
|
|
208
|
+
lines.push("## Top Sources");
|
|
209
|
+
for (const source of topSources) lines.push(formatSourceLine(source));
|
|
210
|
+
lines.push("");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
71
214
|
function formatResults(engine: string, data: Record<string, unknown>): string {
|
|
72
215
|
const lines: string[] = [];
|
|
73
216
|
|
|
74
217
|
if (engine === "all") {
|
|
75
|
-
// Synthesized output: prefer _synthesis + _sources
|
|
76
218
|
const synthesis = data._synthesis as Record<string, unknown> | undefined;
|
|
77
219
|
const dedupedSources = data._sources as Array<Record<string, unknown>> | undefined;
|
|
78
220
|
if (synthesis?.answer) {
|
|
79
|
-
lines
|
|
80
|
-
lines.push(
|
|
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*");
|
|
221
|
+
renderSynthesis(lines, synthesis, dedupedSources || [], 6);
|
|
222
|
+
lines.push("*Synthesized from Perplexity, Bing Copilot, and Google AI*\n");
|
|
89
223
|
return lines.join("\n").trim();
|
|
90
224
|
}
|
|
91
225
|
|
|
92
|
-
// Standard output: per-engine answers
|
|
93
226
|
for (const [eng, result] of Object.entries(data)) {
|
|
94
227
|
if (eng.startsWith("_")) continue;
|
|
95
|
-
lines.push(`\n## ${
|
|
228
|
+
lines.push(`\n## ${formatEngineName(eng)}`);
|
|
96
229
|
const r = result as Record<string, unknown>;
|
|
97
230
|
if (r.error) {
|
|
98
231
|
lines.push(`Error: ${r.error}`);
|
|
@@ -125,6 +258,107 @@ function formatResults(engine: string, data: Record<string, unknown>): string {
|
|
|
125
258
|
return lines.join("\n").trim();
|
|
126
259
|
}
|
|
127
260
|
|
|
261
|
+
function formatDeepResearch(data: Record<string, unknown>): string {
|
|
262
|
+
const lines: string[] = [];
|
|
263
|
+
const confidence = data._confidence as Record<string, unknown> | undefined;
|
|
264
|
+
const dedupedSources = data._sources as Array<Record<string, unknown>> | undefined;
|
|
265
|
+
const synthesis = data._synthesis as Record<string, unknown> | undefined;
|
|
266
|
+
|
|
267
|
+
lines.push("# Deep Research Report\n");
|
|
268
|
+
|
|
269
|
+
if (confidence) {
|
|
270
|
+
const enginesResponded = (confidence.enginesResponded as string[]) || [];
|
|
271
|
+
const enginesFailed = (confidence.enginesFailed as string[]) || [];
|
|
272
|
+
const agreementLevel = String(confidence.agreementLevel || "mixed");
|
|
273
|
+
const firstPartySourceCount = Number(confidence.firstPartySourceCount || 0);
|
|
274
|
+
const sourceTypeBreakdown = confidence.sourceTypeBreakdown as Record<string, number> | undefined;
|
|
275
|
+
|
|
276
|
+
lines.push("## Confidence\n");
|
|
277
|
+
lines.push(`- Agreement: ${formatAgreementLevel(agreementLevel)}`);
|
|
278
|
+
lines.push(`- Engines responded: ${enginesResponded.map(formatEngineName).join(", ") || "none"}`);
|
|
279
|
+
if (enginesFailed.length > 0) {
|
|
280
|
+
lines.push(`- Engines failed: ${enginesFailed.map(formatEngineName).join(", ")}`);
|
|
281
|
+
}
|
|
282
|
+
lines.push(`- Top source consensus: ${confidence.topSourceConsensus || 0}/3 engines`);
|
|
283
|
+
lines.push(`- Total unique sources: ${confidence.sourcesCount || 0}`);
|
|
284
|
+
lines.push(`- Official sources: ${confidence.officialSourceCount || 0}`);
|
|
285
|
+
lines.push(`- First-party sources: ${firstPartySourceCount}`);
|
|
286
|
+
lines.push(`- Fetch success rate: ${confidence.fetchedSourceSuccessRate || 0}`);
|
|
287
|
+
if (sourceTypeBreakdown && Object.keys(sourceTypeBreakdown).length > 0) {
|
|
288
|
+
lines.push(`- Source mix: ${Object.entries(sourceTypeBreakdown).map(([type, count]) => `${humanizeSourceType(type)} ${count}`).join(", ")}`);
|
|
289
|
+
}
|
|
290
|
+
lines.push("");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (synthesis?.answer) renderSynthesis(lines, synthesis, dedupedSources || [], 8);
|
|
294
|
+
|
|
295
|
+
lines.push("## Engine Perspectives\n");
|
|
296
|
+
for (const engine of ["perplexity", "bing", "google"]) {
|
|
297
|
+
const r = data[engine] as Record<string, unknown> | undefined;
|
|
298
|
+
if (!r) continue;
|
|
299
|
+
lines.push(`### ${formatEngineName(engine)}`);
|
|
300
|
+
if (r.error) {
|
|
301
|
+
lines.push(`⚠️ Error: ${r.error}`);
|
|
302
|
+
} else if (r.answer) {
|
|
303
|
+
lines.push(String(r.answer).slice(0, 2000));
|
|
304
|
+
}
|
|
305
|
+
lines.push("");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (dedupedSources && dedupedSources.length > 0) {
|
|
309
|
+
lines.push("## Source Registry\n");
|
|
310
|
+
for (const source of dedupedSources) {
|
|
311
|
+
lines.push(formatSourceLine(source));
|
|
312
|
+
renderSourceEvidence(lines, source);
|
|
313
|
+
}
|
|
314
|
+
lines.push("");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return lines.join("\n").trim();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function formatCodingTask(data: Record<string, unknown> | Record<string, Record<string, unknown>>): string {
|
|
321
|
+
const lines: string[] = [];
|
|
322
|
+
|
|
323
|
+
// Check if it's multi-engine result
|
|
324
|
+
const hasMultipleEngines = "gemini" in data || "copilot" in data;
|
|
325
|
+
|
|
326
|
+
if (hasMultipleEngines) {
|
|
327
|
+
// Multi-engine result
|
|
328
|
+
for (const [engineName, result] of Object.entries(data)) {
|
|
329
|
+
const r = result as Record<string, unknown>;
|
|
330
|
+
lines.push(`## ${engineName.charAt(0).toUpperCase() + engineName.slice(1)}\n`);
|
|
331
|
+
|
|
332
|
+
if (r.error) {
|
|
333
|
+
lines.push(`⚠️ Error: ${r.error}\n`);
|
|
334
|
+
} else {
|
|
335
|
+
if (r.explanation) lines.push(String(r.explanation));
|
|
336
|
+
if (Array.isArray(r.code) && r.code.length > 0) {
|
|
337
|
+
for (const block of r.code) {
|
|
338
|
+
const b = block as { language: string; code: string };
|
|
339
|
+
lines.push(`\n\`\`\`${b.language}\n${b.code}\n\`\`\`\n`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (r.url) lines.push(`*Source: ${r.url}*`);
|
|
343
|
+
}
|
|
344
|
+
lines.push("");
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
// Single engine result
|
|
348
|
+
const r = data as Record<string, unknown>;
|
|
349
|
+
if (r.explanation) lines.push(String(r.explanation));
|
|
350
|
+
if (Array.isArray(r.code) && r.code.length > 0) {
|
|
351
|
+
for (const block of r.code) {
|
|
352
|
+
const b = block as { language: string; code: string };
|
|
353
|
+
lines.push(`\n\`\`\`${b.language}\n${b.code}\n\`\`\`\n`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (r.url) lines.push(`*Source: ${r.url}*`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return lines.join("\n").trim();
|
|
360
|
+
}
|
|
361
|
+
|
|
128
362
|
export default function greedySearchExtension(pi: ExtensionAPI) {
|
|
129
363
|
pi.on("session_start", async (_event, ctx) => {
|
|
130
364
|
if (!cdpAvailable()) {
|
|
@@ -219,4 +453,171 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
|
|
|
219
453
|
}
|
|
220
454
|
},
|
|
221
455
|
});
|
|
456
|
+
|
|
457
|
+
// ─── deep_research ─────────────────────────────────────────────────────────
|
|
458
|
+
pi.registerTool({
|
|
459
|
+
name: "deep_research",
|
|
460
|
+
label: "Deep Research",
|
|
461
|
+
description:
|
|
462
|
+
"Comprehensive multi-engine research with source fetching and synthesis. " +
|
|
463
|
+
"Runs Perplexity, Bing Copilot, and Google AI in parallel with full answers, " +
|
|
464
|
+
"deduplicates and ranks sources by consensus, fetches content from top sources, " +
|
|
465
|
+
"and synthesizes via Gemini. Returns a structured research document with confidence scores. " +
|
|
466
|
+
"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",
|
|
468
|
+
parameters: Type.Object({
|
|
469
|
+
query: Type.String({ description: "The research question" }),
|
|
470
|
+
}),
|
|
471
|
+
execute: async (_toolCallId, params, signal, onUpdate) => {
|
|
472
|
+
const { query } = params as { query: string };
|
|
473
|
+
|
|
474
|
+
if (!cdpAvailable()) {
|
|
475
|
+
return {
|
|
476
|
+
content: [{ type: "text", text: "cdp.mjs missing — try reinstalling." }],
|
|
477
|
+
details: {} as { raw?: Record<string, unknown> },
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const completed = new Set<string>();
|
|
482
|
+
|
|
483
|
+
const onProgress = (eng: string, status: "done" | "error") => {
|
|
484
|
+
completed.add(eng);
|
|
485
|
+
const parts: string[] = [];
|
|
486
|
+
for (const e of ALL_ENGINES) {
|
|
487
|
+
if (completed.has(e)) parts.push(`✅ ${e}`);
|
|
488
|
+
else parts.push(`⏳ ${e}`);
|
|
489
|
+
}
|
|
490
|
+
if (completed.size >= 3) parts.push("🔄 synthesizing");
|
|
491
|
+
|
|
492
|
+
onUpdate?.({
|
|
493
|
+
content: [{ type: "text", text: `**Researching...** ${parts.join(" · ")}` }],
|
|
494
|
+
details: { _progress: true },
|
|
495
|
+
} as any);
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
// Run deep research (includes full answers, synthesis, and source fetching)
|
|
500
|
+
const data = await runSearch("all", query, ["--deep-research"], signal, onProgress);
|
|
501
|
+
const text = formatDeepResearch(data);
|
|
502
|
+
return {
|
|
503
|
+
content: [{ type: "text", text: text || "No results returned." }],
|
|
504
|
+
details: { raw: data },
|
|
505
|
+
};
|
|
506
|
+
} catch (e) {
|
|
507
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
508
|
+
return {
|
|
509
|
+
content: [{ type: "text", text: `Deep research failed: ${msg}` }],
|
|
510
|
+
details: {} as { raw?: Record<string, unknown> },
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// ─── coding_task ───────────────────────────────────────────────────────────
|
|
517
|
+
pi.registerTool({
|
|
518
|
+
name: "coding_task",
|
|
519
|
+
label: "Coding Task",
|
|
520
|
+
description:
|
|
521
|
+
"Delegate a coding task to Gemini and/or Copilot via browser automation. " +
|
|
522
|
+
"Returns extracted code blocks and explanations. Supports multiple modes: " +
|
|
523
|
+
"'code' (write/modify code), 'review' (senior engineer code review), " +
|
|
524
|
+
"'plan' (architect risk assessment), 'test' (edge case testing), " +
|
|
525
|
+
"'debug' (fresh-eyes root cause analysis). " +
|
|
526
|
+
"Best for getting a 'second opinion' on hard problems, debugging tricky issues, " +
|
|
527
|
+
"or risk-assessing major refactors. Use engine 'all' for both perspectives.",
|
|
528
|
+
promptSnippet: "Browser-based coding assistant with Gemini and Copilot",
|
|
529
|
+
parameters: Type.Object({
|
|
530
|
+
task: Type.String({ description: "The coding task or question" }),
|
|
531
|
+
engine: Type.Union(
|
|
532
|
+
[
|
|
533
|
+
Type.Literal("all"),
|
|
534
|
+
Type.Literal("gemini"),
|
|
535
|
+
Type.Literal("copilot"),
|
|
536
|
+
],
|
|
537
|
+
{
|
|
538
|
+
description: 'Engine to use. "all" runs both Gemini and Copilot in parallel.',
|
|
539
|
+
default: "gemini",
|
|
540
|
+
},
|
|
541
|
+
),
|
|
542
|
+
mode: Type.Union(
|
|
543
|
+
[
|
|
544
|
+
Type.Literal("code"),
|
|
545
|
+
Type.Literal("review"),
|
|
546
|
+
Type.Literal("plan"),
|
|
547
|
+
Type.Literal("test"),
|
|
548
|
+
Type.Literal("debug"),
|
|
549
|
+
],
|
|
550
|
+
{
|
|
551
|
+
description: "Task mode: code (default), review (code review), plan (architect review), test (write tests), debug (root cause analysis)",
|
|
552
|
+
default: "code",
|
|
553
|
+
},
|
|
554
|
+
),
|
|
555
|
+
context: Type.Optional(Type.String({
|
|
556
|
+
description: "Optional code context/snippet to include with the task",
|
|
557
|
+
})),
|
|
558
|
+
}),
|
|
559
|
+
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;
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
if (!cdpAvailable()) {
|
|
565
|
+
return {
|
|
566
|
+
content: [{ type: "text", text: "cdp.mjs missing — try reinstalling." }],
|
|
567
|
+
details: {} as { raw?: Record<string, unknown> },
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const flags: string[] = ["--engine", engine, "--mode", mode];
|
|
572
|
+
if (context) flags.push("--context", context);
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
onUpdate?.({
|
|
576
|
+
content: [{ type: "text", text: `**Coding task...** 🔄 ${engine === "all" ? "Gemini + Copilot" : engine} (${mode} mode)` }],
|
|
577
|
+
details: { _progress: true },
|
|
578
|
+
} as any);
|
|
579
|
+
|
|
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)}`));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// Timeout after 3 minutes
|
|
606
|
+
setTimeout(() => { proc.kill("SIGTERM"); reject(new Error("Coding task timed out after 180s")); }, 180000);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
const text = formatCodingTask(data);
|
|
610
|
+
return {
|
|
611
|
+
content: [{ type: "text", text: text || "No response." }],
|
|
612
|
+
details: { raw: data },
|
|
613
|
+
};
|
|
614
|
+
} catch (e) {
|
|
615
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
616
|
+
return {
|
|
617
|
+
content: [{ type: "text", text: `Coding task failed: ${msg}` }],
|
|
618
|
+
details: {} as { raw?: Record<string, unknown> },
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
});
|
|
222
623
|
}
|