@apmantza/greedysearch-pi 1.9.1 → 2.0.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/CHANGELOG.md +110 -14
- package/README.md +86 -41
- package/bin/cdp.mjs +1153 -1108
- package/bin/launch.mjs +11 -0
- package/bin/search.mjs +886 -674
- package/extractors/bing-copilot.mjs +528 -374
- package/extractors/chatgpt.mjs +436 -0
- package/extractors/common.mjs +837 -645
- package/extractors/consensus.mjs +655 -0
- package/extractors/consent.mjs +421 -388
- package/extractors/gemini.mjs +335 -217
- package/extractors/logically.mjs +567 -0
- package/extractors/selectors.mjs +3 -2
- package/extractors/semantic-scholar.mjs +219 -0
- package/index.ts +2 -1
- package/package.json +14 -6
- package/skills/greedy-search/skill.md +9 -12
- package/src/fetcher.mjs +8 -1
- package/src/formatters/results.ts +163 -128
- package/src/search/browser-lifecycle.mjs +27 -5
- package/src/search/chrome.mjs +653 -590
- package/src/search/constants.mjs +150 -39
- package/src/search/engines.mjs +114 -76
- package/src/search/fetch-source.mjs +566 -451
- package/src/search/pdf.mjs +68 -0
- package/src/search/recovery.mjs +51 -45
- package/src/search/research.mjs +2579 -0
- package/src/search/sources.mjs +77 -25
- package/src/search/synthesis-runner.mjs +142 -57
- package/src/search/synthesis.mjs +286 -246
- package/src/tools/greedy-search-handler.ts +189 -45
- package/src/tools/shared.ts +187 -186
- package/src/types.ts +110 -104
- package/test.mjs +1342 -534
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* greedy_search tool handler — multi-engine AI web search
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
7
5
|
import { Type } from "@sinclair/typebox";
|
|
6
|
+
|
|
7
|
+
type ExtensionAPI = {
|
|
8
|
+
registerTool(tool: Record<string, unknown>): void;
|
|
9
|
+
};
|
|
8
10
|
import { formatResults } from "../formatters/results.js";
|
|
9
11
|
import {
|
|
10
12
|
ALL_ENGINES,
|
|
@@ -14,15 +16,75 @@ import {
|
|
|
14
16
|
makeProgressTracker,
|
|
15
17
|
runSearch,
|
|
16
18
|
stripQuotes,
|
|
19
|
+
type ProgressUpdate,
|
|
20
|
+
type ToolResult,
|
|
17
21
|
} from "./shared.js";
|
|
18
22
|
|
|
23
|
+
type GreedySearchParams = {
|
|
24
|
+
query: string;
|
|
25
|
+
engine?: string;
|
|
26
|
+
synthesize?: boolean;
|
|
27
|
+
synthesizer?: string;
|
|
28
|
+
depth?: "fast" | "standard" | "deep" | "research" | string;
|
|
29
|
+
breadth?: number;
|
|
30
|
+
iterations?: number;
|
|
31
|
+
maxSources?: number;
|
|
32
|
+
researchOutDir?: string;
|
|
33
|
+
writeResearchBundle?: boolean;
|
|
34
|
+
fullAnswer?: boolean;
|
|
35
|
+
headless?: boolean;
|
|
36
|
+
visible?: boolean;
|
|
37
|
+
alwaysVisible?: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type ToolTheme = {
|
|
41
|
+
fg(style: string, text: string): string;
|
|
42
|
+
bold(text: string): string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type RenderState = {
|
|
46
|
+
expanded: boolean;
|
|
47
|
+
isPartial?: boolean;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
class Text {
|
|
51
|
+
constructor(
|
|
52
|
+
private text: string,
|
|
53
|
+
private paddingX = 0,
|
|
54
|
+
private paddingY = 0,
|
|
55
|
+
) {}
|
|
56
|
+
|
|
57
|
+
render(width: number): string[] {
|
|
58
|
+
const horizontal = " ".repeat(this.paddingX);
|
|
59
|
+
const blank = "";
|
|
60
|
+
const contentWidth = Math.max(1, width - this.paddingX * 2);
|
|
61
|
+
const lines = this.text.split("\n").flatMap((line) => {
|
|
62
|
+
if (line.length <= contentWidth) return [`${horizontal}${line}`];
|
|
63
|
+
const wrapped: string[] = [];
|
|
64
|
+
for (let i = 0; i < line.length; i += contentWidth) {
|
|
65
|
+
wrapped.push(`${horizontal}${line.slice(i, i + contentWidth)}`);
|
|
66
|
+
}
|
|
67
|
+
return wrapped;
|
|
68
|
+
});
|
|
69
|
+
return [
|
|
70
|
+
...Array.from({ length: this.paddingY }, () => blank),
|
|
71
|
+
...lines,
|
|
72
|
+
...Array.from({ length: this.paddingY }, () => blank),
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
invalidate() {}
|
|
77
|
+
}
|
|
78
|
+
|
|
19
79
|
export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
|
|
20
80
|
pi.registerTool({
|
|
21
81
|
name: "greedy_search",
|
|
22
82
|
label: "Greedy Search",
|
|
23
83
|
description:
|
|
24
|
-
"WEB SEARCH ONLY — searches live web via Perplexity,
|
|
25
|
-
"
|
|
84
|
+
"WEB/RESEARCH SEARCH ONLY — searches live web via Perplexity, Google AI, ChatGPT, and Gemini, plus opt-in research through Semantic Scholar and Logically. " +
|
|
85
|
+
"Research mode reuses the configured ~/.pi/greedyconfig engines for child searches and Gemini for planning/final synthesis. " +
|
|
86
|
+
"Research mode is the centerpiece: it plans follow-up actions, fetches sources, audits citations, " +
|
|
87
|
+
"and writes a structured research bundle on disk. " +
|
|
26
88
|
"Use for: library docs, recent framework changes, error messages, best practices, current events. " +
|
|
27
89
|
"Reports streaming progress as each engine completes.",
|
|
28
90
|
promptSnippet: "Multi-engine AI web search with streaming progress",
|
|
@@ -30,14 +92,61 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
|
|
|
30
92
|
query: Type.String({ description: "The search query" }),
|
|
31
93
|
engine: Type.String({
|
|
32
94
|
description:
|
|
33
|
-
'Engine to use: "all" (default), "perplexity", "
|
|
95
|
+
'Engine to use: "all" (default), "perplexity", "google", "chatgpt", "gemini", "gem". Research engines: "semantic-scholar" (alias "s2") and "logically". "all" fans out to the configured engines and fetches top sources. Customize via ~/.pi/greedyconfig. Bing Copilot is still available as "bing" for signed-in users.',
|
|
34
96
|
default: "all",
|
|
35
97
|
}),
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
98
|
+
synthesize: Type.Optional(
|
|
99
|
+
Type.Boolean({
|
|
100
|
+
description:
|
|
101
|
+
'Only for engine="all": synthesize the multi-engine results and fetched sources. Default: false.',
|
|
102
|
+
default: false,
|
|
103
|
+
}),
|
|
104
|
+
),
|
|
105
|
+
synthesizer: Type.Optional(
|
|
106
|
+
Type.String({
|
|
107
|
+
description:
|
|
108
|
+
'Synthesis engine for synthesize=true. Defaults to ~/.pi/greedyconfig synthesizer (currently "gemini" by default). Supported: "gemini", "chatgpt".',
|
|
109
|
+
}),
|
|
110
|
+
),
|
|
111
|
+
depth: Type.Optional(
|
|
112
|
+
Type.String({
|
|
113
|
+
description:
|
|
114
|
+
'Deprecated except "research". Use depth="research" for the iterative research workflow. Research child searches use ~/.pi/greedyconfig engines; Gemini handles research planning/final synthesis. Legacy values: "fast" skips source fetching; "standard"/"deep" alias synthesize=true.',
|
|
115
|
+
}),
|
|
116
|
+
),
|
|
117
|
+
breadth: Type.Optional(
|
|
118
|
+
Type.Number({
|
|
119
|
+
description:
|
|
120
|
+
'Only for depth="research": number of parallel research directions per round, 1-5 (default: 3).',
|
|
121
|
+
default: 3,
|
|
122
|
+
}),
|
|
123
|
+
),
|
|
124
|
+
iterations: Type.Optional(
|
|
125
|
+
Type.Number({
|
|
126
|
+
description:
|
|
127
|
+
'Only for depth="research": number of iterative research rounds, 1-3 (default: 2).',
|
|
128
|
+
default: 2,
|
|
129
|
+
}),
|
|
130
|
+
),
|
|
131
|
+
maxSources: Type.Optional(
|
|
132
|
+
Type.Number({
|
|
133
|
+
description:
|
|
134
|
+
'Only for depth="research": maximum fetched sources for the final report, 3-12.',
|
|
135
|
+
}),
|
|
136
|
+
),
|
|
137
|
+
researchOutDir: Type.Optional(
|
|
138
|
+
Type.String({
|
|
139
|
+
description:
|
|
140
|
+
'Only for depth="research": optional directory for the structured research bundle. Defaults to .pi/greedysearch-research/<timestamp>_<query>.',
|
|
141
|
+
}),
|
|
142
|
+
),
|
|
143
|
+
writeResearchBundle: Type.Optional(
|
|
144
|
+
Type.Boolean({
|
|
145
|
+
description:
|
|
146
|
+
'Only for depth="research": write the structured research bundle to disk (default true).',
|
|
147
|
+
default: true,
|
|
148
|
+
}),
|
|
149
|
+
),
|
|
41
150
|
fullAnswer: Type.Optional(
|
|
42
151
|
Type.Boolean({
|
|
43
152
|
description:
|
|
@@ -67,23 +176,33 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
|
|
|
67
176
|
}),
|
|
68
177
|
),
|
|
69
178
|
}),
|
|
70
|
-
execute: async (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
179
|
+
execute: async (
|
|
180
|
+
_toolCallId: string,
|
|
181
|
+
params: GreedySearchParams,
|
|
182
|
+
signal?: AbortSignal,
|
|
183
|
+
onUpdate?: (update: ProgressUpdate) => void,
|
|
184
|
+
) => {
|
|
185
|
+
const { query, fullAnswer: fullAnswerParam } = params;
|
|
186
|
+
const engine = stripQuotes(params.engine ?? "all") || "all";
|
|
187
|
+
const depthRaw = stripQuotes(params.depth ?? "") as
|
|
188
|
+
| "fast"
|
|
189
|
+
| "standard"
|
|
190
|
+
| "deep"
|
|
191
|
+
| "research"
|
|
192
|
+
| "";
|
|
193
|
+
const researchMode = depthRaw === "research";
|
|
194
|
+
const legacyFast = depthRaw === "fast";
|
|
195
|
+
const legacySynthesisDepth =
|
|
196
|
+
depthRaw === "standard" || depthRaw === "deep";
|
|
197
|
+
const synthesize =
|
|
198
|
+
engine === "all" &&
|
|
199
|
+
!legacyFast &&
|
|
200
|
+
(params.synthesize === true || legacySynthesisDepth);
|
|
201
|
+
const effectiveEngine = researchMode ? "all" : engine;
|
|
83
202
|
const visible =
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
203
|
+
params.visible === true ||
|
|
204
|
+
params.alwaysVisible === true ||
|
|
205
|
+
params.headless === false ||
|
|
87
206
|
process.env.GREEDY_SEARCH_VISIBLE === "1" ||
|
|
88
207
|
process.env.GREEDY_SEARCH_ALWAYS_VISIBLE === "1";
|
|
89
208
|
const headless = !visible;
|
|
@@ -91,29 +210,48 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
|
|
|
91
210
|
if (!cdpAvailable(baseDir)) return cdpMissingResult();
|
|
92
211
|
|
|
93
212
|
const flags: string[] = [];
|
|
94
|
-
const fullAnswer = fullAnswerParam ??
|
|
213
|
+
const fullAnswer = fullAnswerParam ?? effectiveEngine !== "all";
|
|
95
214
|
if (fullAnswer) flags.push("--full");
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
215
|
+
if (researchMode) {
|
|
216
|
+
flags.push("--depth", "research");
|
|
217
|
+
if (typeof params.breadth === "number")
|
|
218
|
+
flags.push("--breadth", String(params.breadth));
|
|
219
|
+
if (typeof params.iterations === "number")
|
|
220
|
+
flags.push("--iterations", String(params.iterations));
|
|
221
|
+
if (typeof params.maxSources === "number")
|
|
222
|
+
flags.push("--max-sources", String(params.maxSources));
|
|
223
|
+
if (typeof params.researchOutDir === "string")
|
|
224
|
+
flags.push("--research-out-dir", params.researchOutDir);
|
|
225
|
+
if (params.writeResearchBundle === false)
|
|
226
|
+
flags.push("--no-research-bundle");
|
|
227
|
+
} else if (legacyFast) flags.push("--fast");
|
|
228
|
+
else if (depthRaw === "deep") flags.push("--depth", "deep");
|
|
229
|
+
else if (synthesize) flags.push("--synthesize");
|
|
230
|
+
if (synthesize && typeof params.synthesizer === "string") {
|
|
231
|
+
flags.push("--synthesizer", params.synthesizer);
|
|
232
|
+
}
|
|
100
233
|
|
|
101
234
|
const onProgress =
|
|
102
|
-
|
|
103
|
-
? makeProgressTracker(
|
|
235
|
+
effectiveEngine === "all"
|
|
236
|
+
? makeProgressTracker(
|
|
237
|
+
ALL_ENGINES,
|
|
238
|
+
onUpdate,
|
|
239
|
+
researchMode ? "Researching" : "Searching",
|
|
240
|
+
synthesize,
|
|
241
|
+
)
|
|
104
242
|
: undefined;
|
|
105
243
|
|
|
106
244
|
try {
|
|
107
245
|
const data = await runSearch(
|
|
108
|
-
|
|
246
|
+
effectiveEngine,
|
|
109
247
|
query,
|
|
110
248
|
flags,
|
|
111
249
|
`${baseDir}/bin/search.mjs`,
|
|
112
250
|
signal,
|
|
113
251
|
onProgress,
|
|
114
|
-
headless,
|
|
252
|
+
{ headless },
|
|
115
253
|
);
|
|
116
|
-
const text = formatResults(
|
|
254
|
+
const text = formatResults(effectiveEngine, data);
|
|
117
255
|
return {
|
|
118
256
|
content: [{ type: "text", text: text || "No results returned." }],
|
|
119
257
|
details: { raw: data },
|
|
@@ -123,7 +261,7 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
|
|
|
123
261
|
}
|
|
124
262
|
},
|
|
125
263
|
|
|
126
|
-
renderCall(args
|
|
264
|
+
renderCall(args: Partial<GreedySearchParams>, theme: ToolTheme) {
|
|
127
265
|
const q = (args.query || "").slice(0, 60);
|
|
128
266
|
const qDisplay = q.length < (args.query || "").length ? `${q}...` : q;
|
|
129
267
|
const engineDisplay =
|
|
@@ -137,9 +275,15 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
|
|
|
137
275
|
);
|
|
138
276
|
},
|
|
139
277
|
|
|
140
|
-
renderResult(
|
|
278
|
+
renderResult(
|
|
279
|
+
result: ToolResult,
|
|
280
|
+
{ expanded, isPartial }: RenderState,
|
|
281
|
+
theme: ToolTheme,
|
|
282
|
+
) {
|
|
141
283
|
if (isPartial) {
|
|
142
|
-
const progressText =
|
|
284
|
+
const progressText = result.content.find(
|
|
285
|
+
(c) => c.type === "text",
|
|
286
|
+
)?.text;
|
|
143
287
|
const display = progressText
|
|
144
288
|
? progressText.replace(/\*\*/g, "")
|
|
145
289
|
: "Searching...";
|
|
@@ -147,9 +291,7 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
|
|
|
147
291
|
}
|
|
148
292
|
|
|
149
293
|
const textContent = result.content.find((c) => c.type === "text");
|
|
150
|
-
const raw =
|
|
151
|
-
| Record<string, unknown>
|
|
152
|
-
| undefined;
|
|
294
|
+
const raw = result.details?.raw as Record<string, unknown> | undefined;
|
|
153
295
|
|
|
154
296
|
// Collapsed: one-line summary only
|
|
155
297
|
if (!expanded) {
|
|
@@ -170,7 +312,9 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
|
|
|
170
312
|
const sources = raw?._sources as Array<unknown> | undefined;
|
|
171
313
|
if (synthesis) {
|
|
172
314
|
const sourceCount = Array.isArray(sources) ? sources.length : 0;
|
|
173
|
-
const agreement = (
|
|
315
|
+
const agreement = (
|
|
316
|
+
synthesis.agreement as Record<string, unknown> | undefined
|
|
317
|
+
)?.level as string | undefined;
|
|
174
318
|
let summary = " → Synthesized";
|
|
175
319
|
if (sourceCount > 0)
|
|
176
320
|
summary += ` · ${sourceCount} source${sourceCount > 1 ? "s" : ""}`;
|
|
@@ -184,7 +328,7 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
|
|
|
184
328
|
);
|
|
185
329
|
let totalSources = 0;
|
|
186
330
|
for (const key of engineKeys) {
|
|
187
|
-
const eng =
|
|
331
|
+
const eng = raw?.[key] as Record<string, unknown> | undefined;
|
|
188
332
|
const s = eng?.sources as Array<unknown> | undefined;
|
|
189
333
|
if (Array.isArray(s)) totalSources += s.length;
|
|
190
334
|
}
|
|
@@ -200,7 +344,7 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
|
|
|
200
344
|
}
|
|
201
345
|
|
|
202
346
|
// No structured data — show content text as error/fallback
|
|
203
|
-
const snippet =
|
|
347
|
+
const snippet = textContent?.text;
|
|
204
348
|
if (snippet) {
|
|
205
349
|
return new Text(
|
|
206
350
|
theme.fg("warning", ` → ${snippet.slice(0, 80)}`),
|