@apmantza/greedysearch-pi 1.9.0 → 1.9.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 +29 -0
- package/bin/launch-visible.mjs +65 -0
- package/bin/launch.mjs +440 -417
- package/bin/search.mjs +7 -12
- package/extractors/common.mjs +49 -0
- package/extractors/selectors.mjs +55 -54
- package/index.ts +175 -177
- package/package.json +3 -2
- package/skills/greedy-search/skill.md +2 -7
- package/src/fetcher.mjs +666 -652
- package/src/formatters/synthesis.ts +1 -5
- package/src/search/output.mjs +23 -1
- package/src/search/sources.mjs +466 -466
- package/src/tools/greedy-search-handler.ts +226 -124
|
@@ -1,124 +1,226 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* greedy_search tool handler — multi-engine AI web search
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { ExtensionAPI } from "@
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
(params as any).
|
|
85
|
-
(params as any).
|
|
86
|
-
|
|
87
|
-
process.env.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* greedy_search tool handler — multi-engine AI web search
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
7
|
+
import { Type } from "@sinclair/typebox";
|
|
8
|
+
import { formatResults } from "../formatters/results.js";
|
|
9
|
+
import {
|
|
10
|
+
ALL_ENGINES,
|
|
11
|
+
cdpAvailable,
|
|
12
|
+
cdpMissingResult,
|
|
13
|
+
errorResult,
|
|
14
|
+
makeProgressTracker,
|
|
15
|
+
runSearch,
|
|
16
|
+
stripQuotes,
|
|
17
|
+
} from "./shared.js";
|
|
18
|
+
|
|
19
|
+
export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
|
|
20
|
+
pi.registerTool({
|
|
21
|
+
name: "greedy_search",
|
|
22
|
+
label: "Greedy Search",
|
|
23
|
+
description:
|
|
24
|
+
"WEB SEARCH ONLY — searches live web via Perplexity, Bing Copilot, and Google AI in parallel. " +
|
|
25
|
+
"Optionally synthesizes results with Gemini, deduplicates sources by consensus. " +
|
|
26
|
+
"Use for: library docs, recent framework changes, error messages, best practices, current events. " +
|
|
27
|
+
"Reports streaming progress as each engine completes.",
|
|
28
|
+
promptSnippet: "Multi-engine AI web search with streaming progress",
|
|
29
|
+
parameters: Type.Object({
|
|
30
|
+
query: Type.String({ description: "The search query" }),
|
|
31
|
+
engine: Type.String({
|
|
32
|
+
description:
|
|
33
|
+
'Engine to use: "all" (default), "perplexity", "bing", "google", "gemini", "gem". "all" fans out to Perplexity, Bing, and Google in parallel.',
|
|
34
|
+
default: "all",
|
|
35
|
+
}),
|
|
36
|
+
depth: Type.String({
|
|
37
|
+
description:
|
|
38
|
+
'Search depth: "fast" (no synthesis/source fetch, ~15-30s), "standard" (synthesis + sources, ~30-90s), "deep" (synthesis + source fetching + confidence, ~60-180s). Default: "standard". Note: single-engine searches always run in fast mode regardless of this setting — synthesis requires multiple engines.',
|
|
39
|
+
default: "standard",
|
|
40
|
+
}),
|
|
41
|
+
fullAnswer: Type.Optional(
|
|
42
|
+
Type.Boolean({
|
|
43
|
+
description:
|
|
44
|
+
"When true, returns the complete answer instead of a truncated preview (default: false, answers are shortened to ~300 chars to save tokens).",
|
|
45
|
+
default: false,
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
48
|
+
headless: Type.Optional(
|
|
49
|
+
Type.Boolean({
|
|
50
|
+
description:
|
|
51
|
+
"Set to false to show Chrome window (headless is the default). Set GREEDY_SEARCH_VISIBLE=1 to disable headless globally.",
|
|
52
|
+
default: true,
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
55
|
+
visible: Type.Optional(
|
|
56
|
+
Type.Boolean({
|
|
57
|
+
description:
|
|
58
|
+
"Set to true to always use visible Chrome for this search. Alias for headless: false.",
|
|
59
|
+
default: false,
|
|
60
|
+
}),
|
|
61
|
+
),
|
|
62
|
+
alwaysVisible: Type.Optional(
|
|
63
|
+
Type.Boolean({
|
|
64
|
+
description:
|
|
65
|
+
"Set to true to keep GreedySearch in visible Chrome mode for this search. Alias for visible: true.",
|
|
66
|
+
default: false,
|
|
67
|
+
}),
|
|
68
|
+
),
|
|
69
|
+
}),
|
|
70
|
+
execute: async (_toolCallId, params, signal, onUpdate) => {
|
|
71
|
+
const { query, fullAnswer: fullAnswerParam } = params as {
|
|
72
|
+
query: string;
|
|
73
|
+
engine: string;
|
|
74
|
+
depth?: "fast" | "standard" | "deep";
|
|
75
|
+
fullAnswer?: boolean;
|
|
76
|
+
headless?: boolean;
|
|
77
|
+
visible?: boolean;
|
|
78
|
+
alwaysVisible?: boolean;
|
|
79
|
+
};
|
|
80
|
+
const engine = stripQuotes((params as any).engine ?? "all") || "all";
|
|
81
|
+
const depth = (stripQuotes((params as any).depth ?? "standard") ||
|
|
82
|
+
"standard") as "fast" | "standard" | "deep";
|
|
83
|
+
const visible =
|
|
84
|
+
(params as any).visible === true ||
|
|
85
|
+
(params as any).alwaysVisible === true ||
|
|
86
|
+
(params as any).headless === false ||
|
|
87
|
+
process.env.GREEDY_SEARCH_VISIBLE === "1" ||
|
|
88
|
+
process.env.GREEDY_SEARCH_ALWAYS_VISIBLE === "1";
|
|
89
|
+
const headless = !visible;
|
|
90
|
+
|
|
91
|
+
if (!cdpAvailable(baseDir)) return cdpMissingResult();
|
|
92
|
+
|
|
93
|
+
const flags: string[] = [];
|
|
94
|
+
const fullAnswer = fullAnswerParam ?? engine !== "all";
|
|
95
|
+
if (fullAnswer) flags.push("--full");
|
|
96
|
+
if (depth === "deep") flags.push("--depth", "deep");
|
|
97
|
+
else if (depth === "fast") flags.push("--fast");
|
|
98
|
+
else if (depth === "standard" && engine === "all")
|
|
99
|
+
flags.push("--synthesize");
|
|
100
|
+
|
|
101
|
+
const onProgress =
|
|
102
|
+
engine === "all"
|
|
103
|
+
? makeProgressTracker(ALL_ENGINES, onUpdate, "Searching", depth)
|
|
104
|
+
: undefined;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const data = await runSearch(
|
|
108
|
+
engine,
|
|
109
|
+
query,
|
|
110
|
+
flags,
|
|
111
|
+
`${baseDir}/bin/search.mjs`,
|
|
112
|
+
signal,
|
|
113
|
+
onProgress,
|
|
114
|
+
headless,
|
|
115
|
+
);
|
|
116
|
+
const text = formatResults(engine, data);
|
|
117
|
+
return {
|
|
118
|
+
content: [{ type: "text", text: text || "No results returned." }],
|
|
119
|
+
details: { raw: data },
|
|
120
|
+
};
|
|
121
|
+
} catch (e) {
|
|
122
|
+
return errorResult("Search failed", e);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
renderCall(args, theme) {
|
|
127
|
+
const q = (args.query || "").slice(0, 60);
|
|
128
|
+
const qDisplay = q.length < (args.query || "").length ? `${q}...` : q;
|
|
129
|
+
const engineDisplay =
|
|
130
|
+
args.engine && args.engine !== "all"
|
|
131
|
+
? theme.fg("dim", ` (${args.engine})`)
|
|
132
|
+
: "";
|
|
133
|
+
return new Text(
|
|
134
|
+
`${theme.fg("toolTitle", theme.bold("greedy_search"))} "${theme.fg("accent", qDisplay)}"${engineDisplay}`,
|
|
135
|
+
0,
|
|
136
|
+
0,
|
|
137
|
+
);
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
141
|
+
if (isPartial) {
|
|
142
|
+
const progressText = (result.content.find((c) => c.type === "text") as any)?.text as string | undefined;
|
|
143
|
+
const display = progressText
|
|
144
|
+
? progressText.replace(/\*\*/g, "")
|
|
145
|
+
: "Searching...";
|
|
146
|
+
return new Text(theme.fg("warning", display), 0, 0);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const textContent = result.content.find((c) => c.type === "text");
|
|
150
|
+
const raw = (result.details as any)?.raw as
|
|
151
|
+
| Record<string, unknown>
|
|
152
|
+
| undefined;
|
|
153
|
+
|
|
154
|
+
// Collapsed: one-line summary only
|
|
155
|
+
if (!expanded) {
|
|
156
|
+
const needsHuman = raw?._needsHumanVerification as
|
|
157
|
+
| Record<string, unknown>
|
|
158
|
+
| undefined;
|
|
159
|
+
if (needsHuman) {
|
|
160
|
+
return new Text(
|
|
161
|
+
theme.fg("warning", " → Manual verification required"),
|
|
162
|
+
0,
|
|
163
|
+
0,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const synthesis = raw?._synthesis as
|
|
168
|
+
| Record<string, unknown>
|
|
169
|
+
| undefined;
|
|
170
|
+
const sources = raw?._sources as Array<unknown> | undefined;
|
|
171
|
+
if (synthesis) {
|
|
172
|
+
const sourceCount = Array.isArray(sources) ? sources.length : 0;
|
|
173
|
+
const agreement = (synthesis.agreement as Record<string, unknown> | undefined)?.level as string | undefined;
|
|
174
|
+
let summary = " → Synthesized";
|
|
175
|
+
if (sourceCount > 0)
|
|
176
|
+
summary += ` · ${sourceCount} source${sourceCount > 1 ? "s" : ""}`;
|
|
177
|
+
if (agreement) summary += ` · ${agreement}`;
|
|
178
|
+
return new Text(theme.fg("muted", summary), 0, 0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Single engine: count its sources
|
|
182
|
+
const engineKeys = Object.keys(raw || {}).filter(
|
|
183
|
+
(k) => !k.startsWith("_"),
|
|
184
|
+
);
|
|
185
|
+
let totalSources = 0;
|
|
186
|
+
for (const key of engineKeys) {
|
|
187
|
+
const eng = (raw as any)[key] as Record<string, unknown> | undefined;
|
|
188
|
+
const s = eng?.sources as Array<unknown> | undefined;
|
|
189
|
+
if (Array.isArray(s)) totalSources += s.length;
|
|
190
|
+
}
|
|
191
|
+
if (totalSources > 0) {
|
|
192
|
+
return new Text(
|
|
193
|
+
theme.fg(
|
|
194
|
+
"muted",
|
|
195
|
+
` → ${totalSources} source${totalSources > 1 ? "s" : ""}`,
|
|
196
|
+
),
|
|
197
|
+
0,
|
|
198
|
+
0,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// No structured data — show content text as error/fallback
|
|
203
|
+
const snippet = (textContent as any)?.text as string | undefined;
|
|
204
|
+
if (snippet) {
|
|
205
|
+
return new Text(
|
|
206
|
+
theme.fg("warning", ` → ${snippet.slice(0, 80)}`),
|
|
207
|
+
0,
|
|
208
|
+
0,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return new Text(theme.fg("muted", " → Done"), 0, 0);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Expanded: full output
|
|
215
|
+
if (!textContent || textContent.type !== "text") {
|
|
216
|
+
return new Text("", 0, 0);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const lines = textContent.text
|
|
220
|
+
.split("\n")
|
|
221
|
+
.map((line) => theme.fg("toolOutput", line))
|
|
222
|
+
.join("\n");
|
|
223
|
+
return new Text(`\n${lines}`, 0, 0);
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
}
|