@apmantza/greedysearch-pi 1.6.0 → 1.6.2
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 +23 -0
- package/README.md +5 -0
- package/extractors/gemini.mjs +21 -0
- package/extractors/perplexity.mjs +2 -0
- package/index.ts +13 -356
- package/package.json +1 -1
- package/skills/greedy-search/SKILL.md +36 -2
- 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,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.6.2 (2026-04-01)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
- **Anti-bot detection evasion** — Gemini synthesis now performs gentle scroll every ~6 seconds while waiting for the copy button. This prevents the button from hanging due to anti-bot "human activity" checks.
|
|
7
|
+
|
|
8
|
+
## v1.6.1 (2026-03-31)
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
- **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.
|
|
12
|
+
|
|
13
|
+
### Code Quality
|
|
14
|
+
- **Major refactoring** — extracted 438 lines from `index.ts` (856 → 418 lines) into modular formatters:
|
|
15
|
+
- `src/formatters/coding.ts` — coding task formatting
|
|
16
|
+
- `src/formatters/results.ts` — search and deep research formatting
|
|
17
|
+
- `src/formatters/sources.ts` — source utilities (URL, label, consensus, formatting)
|
|
18
|
+
- `src/formatters/synthesis.ts` — synthesis rendering
|
|
19
|
+
- `src/utils/helpers.ts` — shared formatting utilities
|
|
20
|
+
- **Complexity reduced** — cognitive complexity dropped from 360 to ~60, maintainability index improved from 11.2 to ~40+
|
|
21
|
+
- **Eliminated code duplication** — removed 6 duplicate blocks, consolidated 4+ single-use helper functions
|
|
22
|
+
|
|
23
|
+
### Documentation
|
|
24
|
+
- Clarified `greedy_search` is WEB SEARCH ONLY — removed "NOT for codebase search" from tool description (still in skill documentation)
|
|
25
|
+
|
|
3
26
|
## v1.6.0 (2026-03-29)
|
|
4
27
|
|
|
5
28
|
### Breaking Changes (Backward Compatible)
|
package/README.md
CHANGED
|
@@ -205,6 +205,11 @@ Sources are now extracted by regex-parsing Markdown links (`[title](url)`) from
|
|
|
205
205
|
|
|
206
206
|
## Changelog
|
|
207
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
|
+
|
|
208
213
|
### v1.6.0 (2026-03-29)
|
|
209
214
|
- **Merged deep_research into greedy_search** — new `depth` parameter: `fast` (1 engine), `standard` (3 engines + synthesis), `deep` (3 engines + fetch + synthesis + confidence)
|
|
210
215
|
- **Simpler API** — one tool with clear speed/quality tradeoffs instead of separate tools with overlapping flags
|
package/extractors/gemini.mjs
CHANGED
|
@@ -48,8 +48,29 @@ async function typeIntoGemini(tab, text) {
|
|
|
48
48
|
|
|
49
49
|
async function waitForCopyButton(tab, timeout = 120000) {
|
|
50
50
|
const deadline = Date.now() + timeout;
|
|
51
|
+
let scrollCount = 0;
|
|
51
52
|
while (Date.now() < deadline) {
|
|
52
53
|
await new Promise((r) => setTimeout(r, 600));
|
|
54
|
+
|
|
55
|
+
// Gentle scroll every ~6 seconds to keep page "active" (anti-bot evasion)
|
|
56
|
+
if (++scrollCount % 10 === 0) {
|
|
57
|
+
await cdp([
|
|
58
|
+
"eval",
|
|
59
|
+
tab,
|
|
60
|
+
`
|
|
61
|
+
(function() {
|
|
62
|
+
const chat = document.querySelector('chat-window, [role="main"], main') || document.body;
|
|
63
|
+
const currentScroll = chat.scrollTop || window.scrollY || 0;
|
|
64
|
+
const scrollHeight = chat.scrollHeight || document.body.scrollHeight || 0;
|
|
65
|
+
// Small random scroll movement to mimic human reading
|
|
66
|
+
const jitter = Math.floor(Math.random() * 50) - 25;
|
|
67
|
+
const targetScroll = Math.min(scrollHeight, Math.max(0, currentScroll + jitter));
|
|
68
|
+
chat.scrollTo ? chat.scrollTo({ top: targetScroll, behavior: 'smooth' }) : window.scrollTo(0, targetScroll);
|
|
69
|
+
})()
|
|
70
|
+
`,
|
|
71
|
+
]).catch(() => null);
|
|
72
|
+
}
|
|
73
|
+
|
|
53
74
|
const found = await cdp([
|
|
54
75
|
"eval",
|
|
55
76
|
tab,
|
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" }),
|
|
@@ -477,7 +138,7 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
|
|
|
477
138
|
query,
|
|
478
139
|
engine = "all",
|
|
479
140
|
depth = "standard",
|
|
480
|
-
fullAnswer
|
|
141
|
+
fullAnswer: fullAnswerParam,
|
|
481
142
|
} = params as {
|
|
482
143
|
query: string;
|
|
483
144
|
engine: string;
|
|
@@ -498,14 +159,13 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
|
|
|
498
159
|
}
|
|
499
160
|
|
|
500
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");
|
|
501
165
|
if (fullAnswer) flags.push("--full");
|
|
502
|
-
// Map depth to CLI flags
|
|
503
166
|
if (depth === "deep") flags.push("--deep");
|
|
504
|
-
else if (depth === "standard" && engine === "all")
|
|
505
|
-
flags.push("--synthesize");
|
|
506
|
-
// For "fast" depth with "all" engine, we run 3 engines but no synthesis (just pick first result)
|
|
167
|
+
else if (depth === "standard" && engine === "all") flags.push("--synthesize");
|
|
507
168
|
|
|
508
|
-
// Track progress for "all" engine mode
|
|
509
169
|
const completed = new Set<string>();
|
|
510
170
|
|
|
511
171
|
const onProgress = (eng: string, _status: "done" | "error") => {
|
|
@@ -550,8 +210,6 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
|
|
|
550
210
|
});
|
|
551
211
|
|
|
552
212
|
// ─── deep_research ─────────────────────────────────────────────────────────
|
|
553
|
-
// DEPRECATED: Use greedy_search with depth: "deep" instead.
|
|
554
|
-
// Kept for backward compatibility — aliases to greedy_search.
|
|
555
213
|
pi.registerTool({
|
|
556
214
|
name: "deep_research",
|
|
557
215
|
label: "Deep Research (legacy)",
|
|
@@ -594,7 +252,6 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
|
|
|
594
252
|
};
|
|
595
253
|
|
|
596
254
|
try {
|
|
597
|
-
// Delegate to greedy_search with depth: "deep"
|
|
598
255
|
const data = await runSearch(
|
|
599
256
|
"all",
|
|
600
257
|
query,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apmantza/greedysearch-pi",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.2",
|
|
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": [
|
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: greedy-search
|
|
3
|
-
description: Multi-engine AI
|
|
3
|
+
description: Multi-engine AI **WEB SEARCH** tool — NOT for codebase search. Use greedy_search for high-quality web research where training data may be stale or single-engine results are insufficient. Searches Perplexity, Bing, Google via browser automation. NO API KEYS needed.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
# ⚠️ WEB SEARCH ONLY — NOT CODEBASE SEARCH
|
|
7
|
+
|
|
8
|
+
**`greedy_search` searches the live web**, not your local codebase.
|
|
9
|
+
|
|
10
|
+
| Tool | Searches |
|
|
11
|
+
|------|----------|
|
|
12
|
+
| `greedy_search` | **Live web** (Perplexity, Bing, Google) |
|
|
13
|
+
| `ast_grep_search` | **Local codebase** — use this for code patterns |
|
|
14
|
+
| `bash` with `grep/rg` | **Local codebase** — use this for text search |
|
|
15
|
+
|
|
16
|
+
**DO NOT use `greedy_search` for:**
|
|
17
|
+
- Finding functions in your codebase
|
|
18
|
+
- Searching local files
|
|
19
|
+
- Code review of your project
|
|
20
|
+
- Understanding project structure
|
|
21
|
+
|
|
22
|
+
**DO use `greedy_search` for:**
|
|
23
|
+
- Library documentation
|
|
24
|
+
- Recent framework changes
|
|
25
|
+
- Error message explanations
|
|
26
|
+
- Best practices research
|
|
27
|
+
- Current events/news
|
|
28
|
+
|
|
6
29
|
# GreedySearch Tools
|
|
7
30
|
|
|
8
31
|
| Tool | Speed | Use For |
|
|
@@ -43,12 +66,23 @@ Multi-engine AI search (Perplexity, Bing, Google) with three depth levels.
|
|
|
43
66
|
- `google`: Broad coverage
|
|
44
67
|
- `gemini`: Different training data
|
|
45
68
|
|
|
46
|
-
### Examples
|
|
69
|
+
### Examples — Web Research Only
|
|
47
70
|
|
|
71
|
+
**✅ GOOD — Web research:**
|
|
48
72
|
```greedy_search({ query: "what changed in React 19", depth: "fast" })```
|
|
49
73
|
```greedy_search({ query: "best auth patterns for SaaS", depth: "deep" })```
|
|
50
74
|
```greedy_search({ query: "Prisma vs Drizzle 2026", depth: "standard", fullAnswer: true })```
|
|
51
75
|
|
|
76
|
+
**❌ WRONG — Don't use for codebase search:**
|
|
77
|
+
```javascript
|
|
78
|
+
// DON'T: Searching your own codebase
|
|
79
|
+
// greedy_search({ query: "find UserService class" }) // ❌ Won't find it!
|
|
80
|
+
|
|
81
|
+
// DO: Use these instead for codebase search:
|
|
82
|
+
// ast_grep_search({ pattern: "class UserService", lang: "typescript" })
|
|
83
|
+
// bash({ command: "rg 'class UserService' --type ts" })
|
|
84
|
+
```
|
|
85
|
+
|
|
52
86
|
### Legacy
|
|
53
87
|
|
|
54
88
|
`deep_research` tool still works — aliases to `greedy_search` with `depth: "deep"`.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coding task result formatters
|
|
3
|
+
* Extracted from index.ts to reduce complexity
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { formatEngineName } from "../utils/helpers.js";
|
|
7
|
+
|
|
8
|
+
interface CodeBlock {
|
|
9
|
+
language: string;
|
|
10
|
+
code: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface CodingResult {
|
|
14
|
+
explanation?: string;
|
|
15
|
+
code?: CodeBlock[];
|
|
16
|
+
url?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Format a single coding result (explanation + code blocks + source)
|
|
22
|
+
* Extracted to avoid duplication in multi-engine and single-engine paths
|
|
23
|
+
*/
|
|
24
|
+
function formatCodingResult(result: CodingResult, lines: string[]): void {
|
|
25
|
+
if (result.error) {
|
|
26
|
+
lines.push(`⚠️ Error: ${result.error}\n`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (result.explanation) {
|
|
31
|
+
lines.push(String(result.explanation));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (Array.isArray(result.code) && result.code.length > 0) {
|
|
35
|
+
for (const block of result.code) {
|
|
36
|
+
lines.push(`\n\`\`\`${block.language}\n${block.code}\n\`\`\`\n`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (result.url) {
|
|
41
|
+
lines.push(`*Source: ${result.url}*\n`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Format coding task results - supports both single and multi-engine results
|
|
47
|
+
*/
|
|
48
|
+
export function formatCodingTask(
|
|
49
|
+
data: Record<string, unknown> | Record<string, Record<string, unknown>>,
|
|
50
|
+
): string {
|
|
51
|
+
const lines: string[] = [];
|
|
52
|
+
|
|
53
|
+
// Check if it's multi-engine result
|
|
54
|
+
const hasMultipleEngines = "gemini" in data || "copilot" in data;
|
|
55
|
+
|
|
56
|
+
if (hasMultipleEngines) {
|
|
57
|
+
// Multi-engine result
|
|
58
|
+
for (const [engineName, result] of Object.entries(data)) {
|
|
59
|
+
lines.push(`## ${formatEngineName(engineName)}\n`);
|
|
60
|
+
formatCodingResult(result as CodingResult, lines);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
// Single engine result
|
|
64
|
+
formatCodingResult(data as CodingResult, lines);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return lines.join("\n").trim();
|
|
68
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source formatting utilities
|
|
3
|
+
* Extracted from index.ts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { formatEngineName, humanizeSourceType } from "../utils/helpers.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get source URL from various possible fields
|
|
10
|
+
*/
|
|
11
|
+
export function sourceUrl(source: Record<string, unknown>): string {
|
|
12
|
+
return String(source.displayUrl || source.canonicalUrl || source.url || "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get source label/title from various possible fields
|
|
17
|
+
*/
|
|
18
|
+
export function sourceLabel(source: Record<string, unknown>): string {
|
|
19
|
+
return String(
|
|
20
|
+
source.title || source.domain || sourceUrl(source) || "Untitled source",
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Calculate consensus score (engine count)
|
|
26
|
+
*/
|
|
27
|
+
export function sourceConsensus(source: Record<string, unknown>): number {
|
|
28
|
+
if (typeof source.engineCount === "number") return source.engineCount;
|
|
29
|
+
const engines = Array.isArray(source.engines)
|
|
30
|
+
? (source.engines as string[])
|
|
31
|
+
: [];
|
|
32
|
+
return engines.length;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build a map of sources by ID for quick lookup
|
|
37
|
+
*/
|
|
38
|
+
export function getSourceMap(
|
|
39
|
+
sources: Array<Record<string, unknown>>,
|
|
40
|
+
): Map<string, Record<string, unknown>> {
|
|
41
|
+
return new Map(
|
|
42
|
+
sources
|
|
43
|
+
.map((source) => [String(source.id || ""), source] as const)
|
|
44
|
+
.filter(([id]) => id),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Format a single source line for display
|
|
50
|
+
*/
|
|
51
|
+
export function formatSourceLine(source: Record<string, unknown>): string {
|
|
52
|
+
const id = String(source.id || "?");
|
|
53
|
+
const url = sourceUrl(source);
|
|
54
|
+
const title = sourceLabel(source);
|
|
55
|
+
const domain = String(source.domain || "");
|
|
56
|
+
const engines = Array.isArray(source.engines)
|
|
57
|
+
? (source.engines as string[])
|
|
58
|
+
: [];
|
|
59
|
+
const consensus = sourceConsensus(source);
|
|
60
|
+
const typeLabel = humanizeSourceType(String(source.sourceType || ""));
|
|
61
|
+
const fetch = source.fetch as Record<string, unknown> | undefined;
|
|
62
|
+
const fetchStatus = fetch?.ok
|
|
63
|
+
? `fetched ${fetch.status || 200}`
|
|
64
|
+
: fetch?.attempted
|
|
65
|
+
? "fetch failed"
|
|
66
|
+
: "";
|
|
67
|
+
|
|
68
|
+
const pieces = [
|
|
69
|
+
`${id} - [${title}](${url})`,
|
|
70
|
+
domain,
|
|
71
|
+
typeLabel,
|
|
72
|
+
engines.length
|
|
73
|
+
? `cited by ${engines.map(formatEngineName).join(", ")} (${consensus}/3)`
|
|
74
|
+
: `${consensus}/3`,
|
|
75
|
+
fetchStatus,
|
|
76
|
+
].filter(Boolean);
|
|
77
|
+
|
|
78
|
+
return `- ${pieces.join(" - ")}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Render source evidence (snippet, last modified, errors)
|
|
83
|
+
*/
|
|
84
|
+
export function renderSourceEvidence(
|
|
85
|
+
lines: string[],
|
|
86
|
+
source: Record<string, unknown>,
|
|
87
|
+
): void {
|
|
88
|
+
const fetch = source.fetch as Record<string, unknown> | undefined;
|
|
89
|
+
if (!fetch?.attempted) return;
|
|
90
|
+
|
|
91
|
+
const snippet = String(fetch.snippet || "").trim();
|
|
92
|
+
const lastModified = String(fetch.lastModified || "").trim();
|
|
93
|
+
|
|
94
|
+
if (snippet) lines.push(` Evidence: ${snippet}`);
|
|
95
|
+
if (lastModified) lines.push(` Last-Modified: ${lastModified}`);
|
|
96
|
+
if (fetch.error) lines.push(` Fetch error: ${String(fetch.error)}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Pick top sources, preferring recommended ones
|
|
101
|
+
*/
|
|
102
|
+
export function pickSources(
|
|
103
|
+
sources: Array<Record<string, unknown>>,
|
|
104
|
+
recommendedIds: string[] = [],
|
|
105
|
+
max = 6,
|
|
106
|
+
): Array<Record<string, unknown>> {
|
|
107
|
+
if (!sources.length) return [];
|
|
108
|
+
|
|
109
|
+
const sourceMap = getSourceMap(sources);
|
|
110
|
+
const recommended = recommendedIds
|
|
111
|
+
.map((id) => sourceMap.get(id))
|
|
112
|
+
.filter((source): source is Record<string, unknown> => Boolean(source));
|
|
113
|
+
|
|
114
|
+
if (recommended.length > 0) return recommended.slice(0, max);
|
|
115
|
+
return sources.slice(0, max);
|
|
116
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synthesis and research result formatters
|
|
3
|
+
* Extracted from index.ts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { formatAgreementLevel } from "../utils/helpers.js";
|
|
7
|
+
import {
|
|
8
|
+
formatSourceLine,
|
|
9
|
+
pickSources,
|
|
10
|
+
renderSourceEvidence,
|
|
11
|
+
} from "./sources.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Render synthesis data (answer, consensus, differences, caveats, claims, sources)
|
|
15
|
+
*/
|
|
16
|
+
export function renderSynthesis(
|
|
17
|
+
lines: string[],
|
|
18
|
+
synthesis: Record<string, unknown>,
|
|
19
|
+
sources: Array<Record<string, unknown>>,
|
|
20
|
+
maxSources = 6,
|
|
21
|
+
): void {
|
|
22
|
+
// Answer section
|
|
23
|
+
if (synthesis.answer) {
|
|
24
|
+
lines.push("## Answer");
|
|
25
|
+
lines.push(String(synthesis.answer));
|
|
26
|
+
lines.push("");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Consensus section
|
|
30
|
+
const agreement = synthesis.agreement as Record<string, unknown> | undefined;
|
|
31
|
+
const agreementSummary = String(agreement?.summary || "").trim();
|
|
32
|
+
const agreementLevel = String(agreement?.level || "").trim();
|
|
33
|
+
|
|
34
|
+
if (agreementSummary || agreementLevel) {
|
|
35
|
+
lines.push("## Consensus");
|
|
36
|
+
lines.push(
|
|
37
|
+
`- ${formatAgreementLevel(agreementLevel)}${agreementSummary ? ` - ${agreementSummary}` : ""}`,
|
|
38
|
+
);
|
|
39
|
+
lines.push("");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Differences section
|
|
43
|
+
const differences = Array.isArray(synthesis.differences)
|
|
44
|
+
? (synthesis.differences as string[])
|
|
45
|
+
: [];
|
|
46
|
+
if (differences.length > 0) {
|
|
47
|
+
lines.push("## Where Engines Differ");
|
|
48
|
+
for (const difference of differences) lines.push(`- ${difference}`);
|
|
49
|
+
lines.push("");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Caveats section
|
|
53
|
+
const caveats = Array.isArray(synthesis.caveats)
|
|
54
|
+
? (synthesis.caveats as string[])
|
|
55
|
+
: [];
|
|
56
|
+
if (caveats.length > 0) {
|
|
57
|
+
lines.push("## Caveats");
|
|
58
|
+
for (const caveat of caveats) lines.push(`- ${caveat}`);
|
|
59
|
+
lines.push("");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Claims section
|
|
63
|
+
const claims = Array.isArray(synthesis.claims)
|
|
64
|
+
? (synthesis.claims as Array<Record<string, unknown>>)
|
|
65
|
+
: [];
|
|
66
|
+
if (claims.length > 0) {
|
|
67
|
+
lines.push("## Key Claims");
|
|
68
|
+
for (const claim of claims) {
|
|
69
|
+
const sourceIds = Array.isArray(claim.sourceIds)
|
|
70
|
+
? (claim.sourceIds as string[])
|
|
71
|
+
: [];
|
|
72
|
+
const support = String(claim.support || "moderate");
|
|
73
|
+
lines.push(
|
|
74
|
+
`- ${String(claim.claim || "")} [${support}${sourceIds.length ? `; ${sourceIds.join(", ")}` : ""}]`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
lines.push("");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Top sources section
|
|
81
|
+
const recommendedIds = Array.isArray(synthesis.recommendedSources)
|
|
82
|
+
? (synthesis.recommendedSources as string[])
|
|
83
|
+
: [];
|
|
84
|
+
const topSources = pickSources(sources, recommendedIds, maxSources);
|
|
85
|
+
|
|
86
|
+
if (topSources.length > 0) {
|
|
87
|
+
lines.push("## Top Sources");
|
|
88
|
+
for (const source of topSources) lines.push(formatSourceLine(source));
|
|
89
|
+
lines.push("");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Format agreement level display
|
|
95
|
+
* Note: Also in helpers.ts for shared use
|
|
96
|
+
*/
|
|
97
|
+
function formatAgreementLevelLocal(level: string): string {
|
|
98
|
+
if (!level) return "Mixed";
|
|
99
|
+
return level.charAt(0).toUpperCase() + level.slice(1);
|
|
100
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility helpers for formatting
|
|
3
|
+
* Consolidated from single-use functions in index.ts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format engine name for display
|
|
8
|
+
* Replaces 'bing' with 'Bing Copilot', etc.
|
|
9
|
+
*/
|
|
10
|
+
export function formatEngineName(engine: string): string {
|
|
11
|
+
const displayNames: Record<string, string> = {
|
|
12
|
+
bing: "Bing Copilot",
|
|
13
|
+
google: "Google AI",
|
|
14
|
+
gemini: "Gemini",
|
|
15
|
+
copilot: "Copilot",
|
|
16
|
+
perplexity: "Perplexity",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
displayNames[engine] ??
|
|
21
|
+
engine.charAt(0).toUpperCase() + engine.slice(1)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Humanize source type labels
|
|
27
|
+
*/
|
|
28
|
+
export function humanizeSourceType(sourceType: string): string {
|
|
29
|
+
if (!sourceType) return "";
|
|
30
|
+
if (sourceType === "official-docs") return "official docs";
|
|
31
|
+
return sourceType.replace(/-/g, " ");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Format agreement level with proper capitalization
|
|
36
|
+
*/
|
|
37
|
+
export function formatAgreementLevel(level: string): string {
|
|
38
|
+
if (!level) return "Mixed";
|
|
39
|
+
return level.charAt(0).toUpperCase() + level.slice(1);
|
|
40
|
+
}
|