@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.
@@ -1,124 +1,226 @@
1
- /**
2
- * greedy_search tool handler — multi-engine AI web search
3
- */
4
-
5
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
- import { Type } from "@sinclair/typebox";
7
- import { formatResults } from "../formatters/results.js";
8
- import {
9
- ALL_ENGINES,
10
- cdpAvailable,
11
- cdpMissingResult,
12
- errorResult,
13
- makeProgressTracker,
14
- runSearch,
15
- stripQuotes,
16
- } from "./shared.js";
17
-
18
- export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
19
- pi.registerTool({
20
- name: "greedy_search",
21
- label: "Greedy Search",
22
- description:
23
- "WEB SEARCH ONLY — searches live web via Perplexity, Bing Copilot, and Google AI in parallel. " +
24
- "Optionally synthesizes results with Gemini, deduplicates sources by consensus. " +
25
- "Use for: library docs, recent framework changes, error messages, best practices, current events. " +
26
- "Reports streaming progress as each engine completes.",
27
- promptSnippet: "Multi-engine AI web search with streaming progress",
28
- parameters: Type.Object({
29
- query: Type.String({ description: "The search query" }),
30
- engine: Type.String({
31
- description:
32
- 'Engine to use: "all" (default), "perplexity", "bing", "google", "gemini", "gem". "all" fans out to Perplexity, Bing, and Google in parallel.',
33
- default: "all",
34
- }),
35
- depth: Type.String({
36
- description:
37
- 'Search depth: "fast" (single engine, ~15-30s), "standard" (3 engines + synthesis, ~30-90s), "deep" (3 engines + source fetching + synthesis + confidence, ~60-180s). Default: "standard".',
38
- default: "standard",
39
- }),
40
- fullAnswer: Type.Optional(
41
- Type.Boolean({
42
- description:
43
- "When true, returns the complete answer instead of a truncated preview (default: false, answers are shortened to ~300 chars to save tokens).",
44
- default: false,
45
- }),
46
- ),
47
- headless: Type.Optional(
48
- Type.Boolean({
49
- description:
50
- "Set to false to show Chrome window (headless is the default). Set GREEDY_SEARCH_VISIBLE=1 to disable headless globally.",
51
- default: true,
52
- }),
53
- ),
54
- visible: Type.Optional(
55
- Type.Boolean({
56
- description:
57
- "Set to true to always use visible Chrome for this search. Alias for headless: false.",
58
- default: false,
59
- }),
60
- ),
61
- alwaysVisible: Type.Optional(
62
- Type.Boolean({
63
- description:
64
- "Set to true to keep GreedySearch in visible Chrome mode for this search. Alias for visible: true.",
65
- default: false,
66
- }),
67
- ),
68
- }),
69
- execute: async (_toolCallId, params, signal, onUpdate) => {
70
- const { query, fullAnswer: fullAnswerParam } = params as {
71
- query: string;
72
- engine: string;
73
- depth?: "fast" | "standard" | "deep";
74
- fullAnswer?: boolean;
75
- headless?: boolean;
76
- visible?: boolean;
77
- alwaysVisible?: boolean;
78
- };
79
- const engine = stripQuotes((params as any).engine ?? "all") || "all";
80
- const depth = (stripQuotes((params as any).depth ?? "standard") ||
81
- "standard") as "fast" | "standard" | "deep";
82
- const visible =
83
- (params as any).visible === true ||
84
- (params as any).alwaysVisible === true ||
85
- (params as any).headless === false ||
86
- process.env.GREEDY_SEARCH_VISIBLE === "1" ||
87
- process.env.GREEDY_SEARCH_ALWAYS_VISIBLE === "1";
88
- const headless = !visible;
89
-
90
- if (!cdpAvailable(baseDir)) return cdpMissingResult();
91
-
92
- const flags: string[] = [];
93
- const fullAnswer = fullAnswerParam ?? engine !== "all";
94
- if (fullAnswer) flags.push("--full");
95
- if (depth === "deep") flags.push("--depth", "deep");
96
- else if (depth === "standard" && engine === "all")
97
- flags.push("--synthesize");
98
-
99
- const onProgress =
100
- engine === "all"
101
- ? makeProgressTracker(ALL_ENGINES, onUpdate, "Searching", depth)
102
- : undefined;
103
-
104
- try {
105
- const data = await runSearch(
106
- engine,
107
- query,
108
- flags,
109
- `${baseDir}/bin/search.mjs`,
110
- signal,
111
- onProgress,
112
- headless,
113
- );
114
- const text = formatResults(engine, data);
115
- return {
116
- content: [{ type: "text", text: text || "No results returned." }],
117
- details: { raw: data },
118
- };
119
- } catch (e) {
120
- return errorResult("Search failed", e);
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
+ }