@apmantza/greedysearch-pi 1.5.1 → 1.6.1
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 +33 -0
- package/README.md +46 -30
- package/extractors/perplexity.mjs +2 -0
- package/index.ts +29 -368
- package/package.json +1 -1
- package/search.mjs +35 -12
- package/skills/greedy-search/SKILL.md +69 -26
- package/src/formatters/coding.ts +68 -0
- package/src/formatters/results.ts +207 -0
- package/src/formatters/sources.ts +116 -0
- package/src/formatters/synthesis.ts +100 -0
- package/src/utils/helpers.ts +40 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.6.1 (2026-03-31)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- **Single-engine full answers by default** — when using `engine: "perplexity"`, `engine: "bing"`, `engine: "google"`, or `engine: "gemini"`, the full answer is now returned by default instead of truncated previews. Multi-engine (`engine: "all"`) still uses truncated previews (~300 chars) to save tokens during synthesis. Explicit `fullAnswer: true/false` always overrides.
|
|
7
|
+
|
|
8
|
+
### Code Quality
|
|
9
|
+
- **Major refactoring** — extracted 438 lines from `index.ts` (856 → 418 lines) into modular formatters:
|
|
10
|
+
- `src/formatters/coding.ts` — coding task formatting
|
|
11
|
+
- `src/formatters/results.ts` — search and deep research formatting
|
|
12
|
+
- `src/formatters/sources.ts` — source utilities (URL, label, consensus, formatting)
|
|
13
|
+
- `src/formatters/synthesis.ts` — synthesis rendering
|
|
14
|
+
- `src/utils/helpers.ts` — shared formatting utilities
|
|
15
|
+
- **Complexity reduced** — cognitive complexity dropped from 360 to ~60, maintainability index improved from 11.2 to ~40+
|
|
16
|
+
- **Eliminated code duplication** — removed 6 duplicate blocks, consolidated 4+ single-use helper functions
|
|
17
|
+
|
|
18
|
+
### Documentation
|
|
19
|
+
- Clarified `greedy_search` is WEB SEARCH ONLY — removed "NOT for codebase search" from tool description (still in skill documentation)
|
|
20
|
+
|
|
21
|
+
## v1.6.0 (2026-03-29)
|
|
22
|
+
|
|
23
|
+
### Breaking Changes (Backward Compatible)
|
|
24
|
+
- **Merged deep_research into greedy_search** — new `depth` parameter with three levels:
|
|
25
|
+
- `fast`: single engine (~15-30s)
|
|
26
|
+
- `standard`: 3 engines + synthesis (~30-90s, default for `engine: "all"`)
|
|
27
|
+
- `deep`: 3 engines + source fetching + synthesis + confidence (~60-180s)
|
|
28
|
+
- **Simpler mental model** — one tool with clear speed/quality tradeoffs instead of separate tools with overlapping flags
|
|
29
|
+
- **Deprecated flags still work** — `--synthesize` maps to `depth: "standard"`, `--deep-research` maps to `depth: "deep"`
|
|
30
|
+
- **deep_research tool aliased** — still works, calls `greedy_search` with `depth: "deep"`
|
|
31
|
+
|
|
32
|
+
### Documentation
|
|
33
|
+
- Updated README with new `depth` parameter and examples
|
|
34
|
+
- Updated skill documentation (SKILL.md) to reflect simplified API
|
|
35
|
+
|
|
3
36
|
## v1.5.1 (2026-03-29)
|
|
4
37
|
|
|
5
38
|
- **Fixed npm package** — added `.pi-lens/` and test files to `.npmignore` to reduce package size
|
package/README.md
CHANGED
|
@@ -24,10 +24,10 @@ pi install git:github.com/apmantza/GreedySearch-pi
|
|
|
24
24
|
|
|
25
25
|
## Quick Start
|
|
26
26
|
|
|
27
|
-
Once installed, Pi gains a `greedy_search` tool
|
|
27
|
+
Once installed, Pi gains a `greedy_search` tool with three depth levels.
|
|
28
28
|
|
|
29
29
|
```
|
|
30
|
-
greedy_search({ query: "What's new in React 19?",
|
|
30
|
+
greedy_search({ query: "What's new in React 19?", depth: "standard" })
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
## Parameters
|
|
@@ -35,19 +35,27 @@ greedy_search({ query: "What's new in React 19?", engine: "all" })
|
|
|
35
35
|
| Parameter | Type | Default | Description |
|
|
36
36
|
|-----------|------|---------|-------------|
|
|
37
37
|
| `query` | string | required | The search question |
|
|
38
|
-
| `engine` | string | `"all"` |
|
|
39
|
-
| `
|
|
38
|
+
| `engine` | string | `"all"` | `all`, `perplexity`, `bing`, `google`, `gemini` |
|
|
39
|
+
| `depth` | string | `"standard"` | `fast` (1 engine), `standard` (3 engines + synthesis), `deep` (3 + fetch + synthesis + confidence) |
|
|
40
40
|
| `fullAnswer` | boolean | `false` | Return complete answer (~3000+ chars) vs truncated preview (~300 chars) |
|
|
41
41
|
|
|
42
|
-
##
|
|
42
|
+
## Depth Levels
|
|
43
43
|
|
|
44
|
-
|
|
|
45
|
-
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
| Depth | Engines | Synthesis | Source Fetch | Time | Best For |
|
|
45
|
+
|-------|---------|-----------|--------------|------|----------|
|
|
46
|
+
| `fast` | 1 | — | — | 15-30s | Quick lookup, single perspective |
|
|
47
|
+
| `standard` | 3 | ✅ | — | 30-90s | Default — balanced speed/quality |
|
|
48
|
+
| `deep` | 3 | ✅ | ✅ (top 5) | 60-180s | Research that matters — architecture decisions |
|
|
49
|
+
|
|
50
|
+
## Engines (for fast mode)
|
|
51
|
+
|
|
52
|
+
| Engine | Alias | Best for |
|
|
53
|
+
|--------|-------|----------|
|
|
54
|
+
| `all` | — | All 3 engines — but for fast single-engine, pick one below |
|
|
55
|
+
| `perplexity` | `p` | Technical Q&A, code explanations, documentation |
|
|
56
|
+
| `bing` | `b` | Recent news, Microsoft ecosystem |
|
|
57
|
+
| `google` | `g` | Broad coverage, multiple perspectives |
|
|
58
|
+
| `gemini` | `gem` | Google's AI with different training data |
|
|
51
59
|
|
|
52
60
|
## Streaming Progress
|
|
53
61
|
|
|
@@ -60,24 +68,21 @@ When using `engine: "all"`, the tool streams progress as each engine completes:
|
|
|
60
68
|
**Searching...** ✅ perplexity done · ✅ bing done · ✅ google done
|
|
61
69
|
```
|
|
62
70
|
|
|
63
|
-
##
|
|
71
|
+
## Deep Research Mode
|
|
64
72
|
|
|
65
|
-
For
|
|
73
|
+
For research that matters — architecture decisions, library comparisons — use `depth: "deep"`:
|
|
66
74
|
|
|
67
75
|
```
|
|
68
|
-
greedy_search({ query: "best auth patterns for SaaS in 2026",
|
|
76
|
+
greedy_search({ query: "best auth patterns for SaaS in 2026", depth: "deep" })
|
|
69
77
|
```
|
|
70
78
|
|
|
71
|
-
|
|
79
|
+
Deep mode: 3 engines + source fetching (top 5) + synthesis + confidence scores. ~60-180s but returns grounded synthesis with fetched evidence.
|
|
72
80
|
|
|
73
|
-
**
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
- Token efficiency matters (one answer vs three)
|
|
81
|
+
**Standard vs Deep:**
|
|
82
|
+
- `standard` (default): 3 engines + synthesis. Good for most research.
|
|
83
|
+
- `deep`: Same + fetches source content for grounded answers. Use when the answer really matters.
|
|
77
84
|
|
|
78
|
-
**
|
|
79
|
-
- You want to see where engines disagree
|
|
80
|
-
- Speed matters
|
|
85
|
+
**Legacy:** `deep_research` tool still works — aliases to `greedy_search` with `depth: "deep"`.
|
|
81
86
|
|
|
82
87
|
## Full vs Short Answers
|
|
83
88
|
|
|
@@ -89,28 +94,28 @@ greedy_search({ query: "explain the React compiler", engine: "perplexity", fullA
|
|
|
89
94
|
|
|
90
95
|
## Examples
|
|
91
96
|
|
|
92
|
-
**Quick
|
|
97
|
+
**Quick lookup (fast):**
|
|
93
98
|
|
|
94
99
|
```
|
|
95
|
-
greedy_search({ query: "How to use async await in Python", engine: "perplexity" })
|
|
100
|
+
greedy_search({ query: "How to use async await in Python", depth: "fast", engine: "perplexity" })
|
|
96
101
|
```
|
|
97
102
|
|
|
98
|
-
**Compare tools (
|
|
103
|
+
**Compare tools (standard):**
|
|
99
104
|
|
|
100
105
|
```
|
|
101
|
-
greedy_search({ query: "Prisma vs Drizzle in 2026",
|
|
106
|
+
greedy_search({ query: "Prisma vs Drizzle in 2026", depth: "standard" })
|
|
102
107
|
```
|
|
103
108
|
|
|
104
|
-
**
|
|
109
|
+
**Deep research (architecture decision):**
|
|
105
110
|
|
|
106
111
|
```
|
|
107
|
-
greedy_search({ query: "Best practices for monorepo structure",
|
|
112
|
+
greedy_search({ query: "Best practices for monorepo structure", depth: "deep" })
|
|
108
113
|
```
|
|
109
114
|
|
|
110
115
|
**Debug an error:**
|
|
111
116
|
|
|
112
117
|
```
|
|
113
|
-
greedy_search({ query: "Error: Cannot find module 'react-dom/client' Next.js 15",
|
|
118
|
+
greedy_search({ query: "Error: Cannot find module 'react-dom/client' Next.js 15", depth: "standard" })
|
|
114
119
|
```
|
|
115
120
|
|
|
116
121
|
## Requirements
|
|
@@ -200,6 +205,17 @@ Sources are now extracted by regex-parsing Markdown links (`[title](url)`) from
|
|
|
200
205
|
|
|
201
206
|
## Changelog
|
|
202
207
|
|
|
208
|
+
### v1.6.1 (2026-03-31)
|
|
209
|
+
- **Single-engine full answers by default** — `engine: "google"` (or any single engine) now returns complete answers instead of truncated previews. Multi-engine (`all`) still truncates to save tokens during synthesis.
|
|
210
|
+
- **Codebase refactored** — extracted 438 lines from `index.ts` into modular formatters (`src/formatters/`) reducing cognitive complexity from 360 to ~60 and maintainability index from 11.2 to ~40+
|
|
211
|
+
- **Removed codebase search confusion** — clarified that `greedy_search` is WEB SEARCH ONLY (not for searching local code)
|
|
212
|
+
|
|
213
|
+
### v1.6.0 (2026-03-29)
|
|
214
|
+
- **Merged deep_research into greedy_search** — new `depth` parameter: `fast` (1 engine), `standard` (3 engines + synthesis), `deep` (3 engines + fetch + synthesis + confidence)
|
|
215
|
+
- **Simpler API** — one tool with clear speed/quality tradeoffs instead of separate tools with overlapping flags
|
|
216
|
+
- **Backward compatible** — `deep_research` still works as alias, `--synthesize` and `--deep-research` flags still function
|
|
217
|
+
- **Updated documentation** — README and skill docs now use `depth` parameter throughout
|
|
218
|
+
|
|
203
219
|
### v1.5.1 (2026-03-29)
|
|
204
220
|
- Fixed npm package — added `.pi-lens/` and test files to `.npmignore`
|
|
205
221
|
|
package/index.ts
CHANGED
|
@@ -15,6 +15,10 @@ import { fileURLToPath } from "node:url";
|
|
|
15
15
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
16
16
|
import { Type } from "@sinclair/typebox";
|
|
17
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
|
+
|
|
18
22
|
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
19
23
|
|
|
20
24
|
const ALL_ENGINES = ["perplexity", "bing", "google"] as const;
|
|
@@ -77,349 +81,6 @@ function runSearch(
|
|
|
77
81
|
});
|
|
78
82
|
}
|
|
79
83
|
|
|
80
|
-
function formatEngineName(engine: string): string {
|
|
81
|
-
if (engine === "bing") return "Bing Copilot";
|
|
82
|
-
if (engine === "google") return "Google AI";
|
|
83
|
-
return engine.charAt(0).toUpperCase() + engine.slice(1);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function humanizeSourceType(sourceType: string): string {
|
|
87
|
-
if (!sourceType) return "";
|
|
88
|
-
if (sourceType === "official-docs") return "official docs";
|
|
89
|
-
return sourceType.replace(/-/g, " ");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function sourceUrl(source: Record<string, unknown>): string {
|
|
93
|
-
return String(source.displayUrl || source.canonicalUrl || source.url || "");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function sourceLabel(source: Record<string, unknown>): string {
|
|
97
|
-
return String(
|
|
98
|
-
source.title || source.domain || sourceUrl(source) || "Untitled source",
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function sourceConsensus(source: Record<string, unknown>): number {
|
|
103
|
-
if (typeof source.engineCount === "number") return source.engineCount;
|
|
104
|
-
const engines = Array.isArray(source.engines)
|
|
105
|
-
? (source.engines as string[])
|
|
106
|
-
: [];
|
|
107
|
-
return engines.length;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function formatAgreementLevel(level: string): string {
|
|
111
|
-
if (!level) return "Mixed";
|
|
112
|
-
return level.charAt(0).toUpperCase() + level.slice(1);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function getSourceMap(
|
|
116
|
-
sources: Array<Record<string, unknown>>,
|
|
117
|
-
): Map<string, Record<string, unknown>> {
|
|
118
|
-
return new Map(
|
|
119
|
-
sources
|
|
120
|
-
.map((source) => [String(source.id || ""), source] as const)
|
|
121
|
-
.filter(([id]) => id),
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function formatSourceLine(source: Record<string, unknown>): string {
|
|
126
|
-
const id = String(source.id || "?");
|
|
127
|
-
const url = sourceUrl(source);
|
|
128
|
-
const title = sourceLabel(source);
|
|
129
|
-
const domain = String(source.domain || "");
|
|
130
|
-
const engines = Array.isArray(source.engines)
|
|
131
|
-
? (source.engines as string[])
|
|
132
|
-
: [];
|
|
133
|
-
const consensus = sourceConsensus(source);
|
|
134
|
-
const typeLabel = humanizeSourceType(String(source.sourceType || ""));
|
|
135
|
-
const fetch = source.fetch as Record<string, unknown> | undefined;
|
|
136
|
-
const fetchStatus = fetch?.ok
|
|
137
|
-
? `fetched ${fetch.status || 200}`
|
|
138
|
-
: fetch?.attempted
|
|
139
|
-
? "fetch failed"
|
|
140
|
-
: "";
|
|
141
|
-
const pieces = [
|
|
142
|
-
`${id} - [${title}](${url})`,
|
|
143
|
-
domain,
|
|
144
|
-
typeLabel,
|
|
145
|
-
engines.length
|
|
146
|
-
? `cited by ${engines.map(formatEngineName).join(", ")} (${consensus}/3)`
|
|
147
|
-
: `${consensus}/3`,
|
|
148
|
-
fetchStatus,
|
|
149
|
-
].filter(Boolean);
|
|
150
|
-
return `- ${pieces.join(" - ")}`;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function renderSourceEvidence(
|
|
154
|
-
lines: string[],
|
|
155
|
-
source: Record<string, unknown>,
|
|
156
|
-
): void {
|
|
157
|
-
const fetch = source.fetch as Record<string, unknown> | undefined;
|
|
158
|
-
if (!fetch?.attempted) return;
|
|
159
|
-
|
|
160
|
-
const snippet = String(fetch.snippet || "").trim();
|
|
161
|
-
const lastModified = String(fetch.lastModified || "").trim();
|
|
162
|
-
if (snippet) lines.push(` Evidence: ${snippet}`);
|
|
163
|
-
if (lastModified) lines.push(` Last-Modified: ${lastModified}`);
|
|
164
|
-
if (fetch.error) lines.push(` Fetch error: ${String(fetch.error)}`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function pickSources(
|
|
168
|
-
sources: Array<Record<string, unknown>>,
|
|
169
|
-
recommendedIds: string[] = [],
|
|
170
|
-
max = 6,
|
|
171
|
-
): Array<Record<string, unknown>> {
|
|
172
|
-
if (!sources.length) return [];
|
|
173
|
-
const sourceMap = getSourceMap(sources);
|
|
174
|
-
const recommended = recommendedIds
|
|
175
|
-
.map((id) => sourceMap.get(id))
|
|
176
|
-
.filter((source): source is Record<string, unknown> => Boolean(source));
|
|
177
|
-
if (recommended.length > 0) return recommended.slice(0, max);
|
|
178
|
-
return sources.slice(0, max);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function renderSynthesis(
|
|
182
|
-
lines: string[],
|
|
183
|
-
synthesis: Record<string, unknown>,
|
|
184
|
-
sources: Array<Record<string, unknown>>,
|
|
185
|
-
maxSources = 6,
|
|
186
|
-
): void {
|
|
187
|
-
if (synthesis.answer) {
|
|
188
|
-
lines.push("## Answer");
|
|
189
|
-
lines.push(String(synthesis.answer));
|
|
190
|
-
lines.push("");
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const agreement = synthesis.agreement as Record<string, unknown> | undefined;
|
|
194
|
-
const agreementSummary = String(agreement?.summary || "").trim();
|
|
195
|
-
const agreementLevel = String(agreement?.level || "").trim();
|
|
196
|
-
if (agreementSummary || agreementLevel) {
|
|
197
|
-
lines.push("## Consensus");
|
|
198
|
-
lines.push(
|
|
199
|
-
`- ${formatAgreementLevel(agreementLevel)}${agreementSummary ? ` - ${agreementSummary}` : ""}`,
|
|
200
|
-
);
|
|
201
|
-
lines.push("");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const differences = Array.isArray(synthesis.differences)
|
|
205
|
-
? (synthesis.differences as string[])
|
|
206
|
-
: [];
|
|
207
|
-
if (differences.length > 0) {
|
|
208
|
-
lines.push("## Where Engines Differ");
|
|
209
|
-
for (const difference of differences) lines.push(`- ${difference}`);
|
|
210
|
-
lines.push("");
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const caveats = Array.isArray(synthesis.caveats)
|
|
214
|
-
? (synthesis.caveats as string[])
|
|
215
|
-
: [];
|
|
216
|
-
if (caveats.length > 0) {
|
|
217
|
-
lines.push("## Caveats");
|
|
218
|
-
for (const caveat of caveats) lines.push(`- ${caveat}`);
|
|
219
|
-
lines.push("");
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const claims = Array.isArray(synthesis.claims)
|
|
223
|
-
? (synthesis.claims as Array<Record<string, unknown>>)
|
|
224
|
-
: [];
|
|
225
|
-
if (claims.length > 0) {
|
|
226
|
-
lines.push("## Key Claims");
|
|
227
|
-
for (const claim of claims) {
|
|
228
|
-
const sourceIds = Array.isArray(claim.sourceIds)
|
|
229
|
-
? (claim.sourceIds as string[])
|
|
230
|
-
: [];
|
|
231
|
-
const support = String(claim.support || "moderate");
|
|
232
|
-
lines.push(
|
|
233
|
-
`- ${String(claim.claim || "")} [${support}${sourceIds.length ? `; ${sourceIds.join(", ")}` : ""}]`,
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
lines.push("");
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const recommendedIds = Array.isArray(synthesis.recommendedSources)
|
|
240
|
-
? (synthesis.recommendedSources as string[])
|
|
241
|
-
: [];
|
|
242
|
-
const topSources = pickSources(sources, recommendedIds, maxSources);
|
|
243
|
-
if (topSources.length > 0) {
|
|
244
|
-
lines.push("## Top Sources");
|
|
245
|
-
for (const source of topSources) lines.push(formatSourceLine(source));
|
|
246
|
-
lines.push("");
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function formatResults(engine: string, data: Record<string, unknown>): string {
|
|
251
|
-
const lines: string[] = [];
|
|
252
|
-
|
|
253
|
-
if (engine === "all") {
|
|
254
|
-
const synthesis = data._synthesis as Record<string, unknown> | undefined;
|
|
255
|
-
const dedupedSources = data._sources as
|
|
256
|
-
| Array<Record<string, unknown>>
|
|
257
|
-
| undefined;
|
|
258
|
-
if (synthesis?.answer) {
|
|
259
|
-
renderSynthesis(lines, synthesis, dedupedSources || [], 6);
|
|
260
|
-
lines.push(
|
|
261
|
-
"*Synthesized from Perplexity, Bing Copilot, and Google AI*\n",
|
|
262
|
-
);
|
|
263
|
-
return lines.join("\n").trim();
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
for (const [eng, result] of Object.entries(data)) {
|
|
267
|
-
if (eng.startsWith("_")) continue;
|
|
268
|
-
lines.push(`\n## ${formatEngineName(eng)}`);
|
|
269
|
-
const r = result as Record<string, unknown>;
|
|
270
|
-
if (r.error) {
|
|
271
|
-
lines.push(`Error: ${r.error}`);
|
|
272
|
-
} else {
|
|
273
|
-
if (r.answer) lines.push(String(r.answer));
|
|
274
|
-
if (Array.isArray(r.sources) && r.sources.length > 0) {
|
|
275
|
-
lines.push("\nSources:");
|
|
276
|
-
for (const s of r.sources.slice(0, 3)) {
|
|
277
|
-
const src = s as Record<string, string>;
|
|
278
|
-
lines.push(`- [${src.title || src.url}](${src.url})`);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
} else {
|
|
284
|
-
if (data.error) {
|
|
285
|
-
lines.push(`Error: ${data.error}`);
|
|
286
|
-
} else {
|
|
287
|
-
if (data.answer) lines.push(String(data.answer));
|
|
288
|
-
if (Array.isArray(data.sources) && data.sources.length > 0) {
|
|
289
|
-
lines.push("\nSources:");
|
|
290
|
-
for (const s of data.sources.slice(0, 5)) {
|
|
291
|
-
const src = s as Record<string, string>;
|
|
292
|
-
lines.push(`- [${src.title || src.url}](${src.url})`);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return lines.join("\n").trim();
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function formatDeepResearch(data: Record<string, unknown>): string {
|
|
302
|
-
const lines: string[] = [];
|
|
303
|
-
const confidence = data._confidence as Record<string, unknown> | undefined;
|
|
304
|
-
const dedupedSources = data._sources as
|
|
305
|
-
| Array<Record<string, unknown>>
|
|
306
|
-
| undefined;
|
|
307
|
-
const synthesis = data._synthesis as Record<string, unknown> | undefined;
|
|
308
|
-
|
|
309
|
-
lines.push("# Deep Research Report\n");
|
|
310
|
-
|
|
311
|
-
if (confidence) {
|
|
312
|
-
const enginesResponded = (confidence.enginesResponded as string[]) || [];
|
|
313
|
-
const enginesFailed = (confidence.enginesFailed as string[]) || [];
|
|
314
|
-
const agreementLevel = String(confidence.agreementLevel || "mixed");
|
|
315
|
-
const firstPartySourceCount = Number(confidence.firstPartySourceCount || 0);
|
|
316
|
-
const sourceTypeBreakdown = confidence.sourceTypeBreakdown as
|
|
317
|
-
| Record<string, number>
|
|
318
|
-
| undefined;
|
|
319
|
-
|
|
320
|
-
lines.push("## Confidence\n");
|
|
321
|
-
lines.push(`- Agreement: ${formatAgreementLevel(agreementLevel)}`);
|
|
322
|
-
lines.push(
|
|
323
|
-
`- Engines responded: ${enginesResponded.map(formatEngineName).join(", ") || "none"}`,
|
|
324
|
-
);
|
|
325
|
-
if (enginesFailed.length > 0) {
|
|
326
|
-
lines.push(
|
|
327
|
-
`- Engines failed: ${enginesFailed.map(formatEngineName).join(", ")}`,
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
lines.push(
|
|
331
|
-
`- Top source consensus: ${confidence.topSourceConsensus || 0}/3 engines`,
|
|
332
|
-
);
|
|
333
|
-
lines.push(`- Total unique sources: ${confidence.sourcesCount || 0}`);
|
|
334
|
-
lines.push(`- Official sources: ${confidence.officialSourceCount || 0}`);
|
|
335
|
-
lines.push(`- First-party sources: ${firstPartySourceCount}`);
|
|
336
|
-
lines.push(
|
|
337
|
-
`- Fetch success rate: ${confidence.fetchedSourceSuccessRate || 0}`,
|
|
338
|
-
);
|
|
339
|
-
if (sourceTypeBreakdown && Object.keys(sourceTypeBreakdown).length > 0) {
|
|
340
|
-
lines.push(
|
|
341
|
-
`- Source mix: ${Object.entries(sourceTypeBreakdown)
|
|
342
|
-
.map(([type, count]) => `${humanizeSourceType(type)} ${count}`)
|
|
343
|
-
.join(", ")}`,
|
|
344
|
-
);
|
|
345
|
-
}
|
|
346
|
-
lines.push("");
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (synthesis?.answer)
|
|
350
|
-
renderSynthesis(lines, synthesis, dedupedSources || [], 8);
|
|
351
|
-
|
|
352
|
-
lines.push("## Engine Perspectives\n");
|
|
353
|
-
for (const engine of ["perplexity", "bing", "google"]) {
|
|
354
|
-
const r = data[engine] as Record<string, unknown> | undefined;
|
|
355
|
-
if (!r) continue;
|
|
356
|
-
lines.push(`### ${formatEngineName(engine)}`);
|
|
357
|
-
if (r.error) {
|
|
358
|
-
lines.push(`⚠️ Error: ${r.error}`);
|
|
359
|
-
} else if (r.answer) {
|
|
360
|
-
lines.push(String(r.answer).slice(0, 2000));
|
|
361
|
-
}
|
|
362
|
-
lines.push("");
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if (dedupedSources && dedupedSources.length > 0) {
|
|
366
|
-
lines.push("## Source Registry\n");
|
|
367
|
-
for (const source of dedupedSources) {
|
|
368
|
-
lines.push(formatSourceLine(source));
|
|
369
|
-
renderSourceEvidence(lines, source);
|
|
370
|
-
}
|
|
371
|
-
lines.push("");
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
return lines.join("\n").trim();
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function formatCodingTask(
|
|
378
|
-
data: Record<string, unknown> | Record<string, Record<string, unknown>>,
|
|
379
|
-
): string {
|
|
380
|
-
const lines: string[] = [];
|
|
381
|
-
|
|
382
|
-
// Check if it's multi-engine result
|
|
383
|
-
const hasMultipleEngines = "gemini" in data || "copilot" in data;
|
|
384
|
-
|
|
385
|
-
if (hasMultipleEngines) {
|
|
386
|
-
// Multi-engine result
|
|
387
|
-
for (const [engineName, result] of Object.entries(data)) {
|
|
388
|
-
const r = result as Record<string, unknown>;
|
|
389
|
-
lines.push(
|
|
390
|
-
`## ${engineName.charAt(0).toUpperCase() + engineName.slice(1)}\n`,
|
|
391
|
-
);
|
|
392
|
-
|
|
393
|
-
if (r.error) {
|
|
394
|
-
lines.push(`⚠️ Error: ${r.error}\n`);
|
|
395
|
-
} else {
|
|
396
|
-
if (r.explanation) lines.push(String(r.explanation));
|
|
397
|
-
if (Array.isArray(r.code) && r.code.length > 0) {
|
|
398
|
-
for (const block of r.code) {
|
|
399
|
-
const b = block as { language: string; code: string };
|
|
400
|
-
lines.push(`\n\`\`\`${b.language}\n${b.code}\n\`\`\`\n`);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
if (r.url) lines.push(`*Source: ${r.url}*`);
|
|
404
|
-
}
|
|
405
|
-
lines.push("");
|
|
406
|
-
}
|
|
407
|
-
} else {
|
|
408
|
-
// Single engine result
|
|
409
|
-
const r = data as Record<string, unknown>;
|
|
410
|
-
if (r.explanation) lines.push(String(r.explanation));
|
|
411
|
-
if (Array.isArray(r.code) && r.code.length > 0) {
|
|
412
|
-
for (const block of r.code) {
|
|
413
|
-
const b = block as { language: string; code: string };
|
|
414
|
-
lines.push(`\n\`\`\`${b.language}\n${b.code}\n\`\`\`\n`);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
if (r.url) lines.push(`*Source: ${r.url}*`);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
return lines.join("\n").trim();
|
|
421
|
-
}
|
|
422
|
-
|
|
423
84
|
export default function greedySearchExtension(pi: ExtensionAPI) {
|
|
424
85
|
pi.on("session_start", async (_event, ctx) => {
|
|
425
86
|
if (!cdpAvailable()) {
|
|
@@ -434,10 +95,10 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
|
|
|
434
95
|
name: "greedy_search",
|
|
435
96
|
label: "Greedy Search",
|
|
436
97
|
description:
|
|
437
|
-
"
|
|
438
|
-
"Optionally
|
|
439
|
-
"
|
|
440
|
-
"
|
|
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.",
|
|
441
102
|
promptSnippet: "Multi-engine AI web search with streaming progress",
|
|
442
103
|
parameters: Type.Object({
|
|
443
104
|
query: Type.String({ description: "The search query" }),
|
|
@@ -456,12 +117,13 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
|
|
|
456
117
|
default: "all",
|
|
457
118
|
},
|
|
458
119
|
),
|
|
459
|
-
|
|
460
|
-
Type.
|
|
120
|
+
depth: Type.Union(
|
|
121
|
+
[Type.Literal("fast"), Type.Literal("standard"), Type.Literal("deep")],
|
|
122
|
+
{
|
|
461
123
|
description:
|
|
462
|
-
|
|
463
|
-
default:
|
|
464
|
-
}
|
|
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
|
+
},
|
|
465
127
|
),
|
|
466
128
|
fullAnswer: Type.Optional(
|
|
467
129
|
Type.Boolean({
|
|
@@ -475,12 +137,12 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
|
|
|
475
137
|
const {
|
|
476
138
|
query,
|
|
477
139
|
engine = "all",
|
|
478
|
-
|
|
479
|
-
fullAnswer
|
|
140
|
+
depth = "standard",
|
|
141
|
+
fullAnswer: fullAnswerParam,
|
|
480
142
|
} = params as {
|
|
481
143
|
query: string;
|
|
482
144
|
engine: string;
|
|
483
|
-
|
|
145
|
+
depth?: "fast" | "standard" | "deep";
|
|
484
146
|
fullAnswer?: boolean;
|
|
485
147
|
};
|
|
486
148
|
|
|
@@ -497,10 +159,13 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
|
|
|
497
159
|
}
|
|
498
160
|
|
|
499
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");
|
|
500
165
|
if (fullAnswer) flags.push("--full");
|
|
501
|
-
if (
|
|
166
|
+
if (depth === "deep") flags.push("--deep");
|
|
167
|
+
else if (depth === "standard" && engine === "all") flags.push("--synthesize");
|
|
502
168
|
|
|
503
|
-
// Track progress for "all" engine mode
|
|
504
169
|
const completed = new Set<string>();
|
|
505
170
|
|
|
506
171
|
const onProgress = (eng: string, _status: "done" | "error") => {
|
|
@@ -510,7 +175,8 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
|
|
|
510
175
|
if (completed.has(e)) parts.push(`✅ ${e} done`);
|
|
511
176
|
else parts.push(`⏳ ${e}`);
|
|
512
177
|
}
|
|
513
|
-
if (
|
|
178
|
+
if (depth !== "fast" && completed.size >= 3)
|
|
179
|
+
parts.push("🔄 synthesizing");
|
|
514
180
|
|
|
515
181
|
onUpdate?.({
|
|
516
182
|
content: [
|
|
@@ -546,15 +212,11 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
|
|
|
546
212
|
// ─── deep_research ─────────────────────────────────────────────────────────
|
|
547
213
|
pi.registerTool({
|
|
548
214
|
name: "deep_research",
|
|
549
|
-
label: "Deep Research",
|
|
215
|
+
label: "Deep Research (legacy)",
|
|
550
216
|
description:
|
|
551
|
-
"
|
|
552
|
-
"
|
|
553
|
-
|
|
554
|
-
"and synthesizes via Gemini. Returns a structured research document with confidence scores. " +
|
|
555
|
-
"Use for architecture decisions, library comparisons, best practices, or any research where the answer matters.",
|
|
556
|
-
promptSnippet:
|
|
557
|
-
"Deep multi-engine research with source deduplication and synthesis",
|
|
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)",
|
|
558
220
|
parameters: Type.Object({
|
|
559
221
|
query: Type.String({ description: "The research question" }),
|
|
560
222
|
}),
|
|
@@ -590,11 +252,10 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
|
|
|
590
252
|
};
|
|
591
253
|
|
|
592
254
|
try {
|
|
593
|
-
// Run deep research (includes full answers, synthesis, and source fetching)
|
|
594
255
|
const data = await runSearch(
|
|
595
256
|
"all",
|
|
596
257
|
query,
|
|
597
|
-
["--deep
|
|
258
|
+
["--deep"],
|
|
598
259
|
signal,
|
|
599
260
|
onProgress,
|
|
600
261
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apmantza/greedysearch-pi",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
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": [
|