@apmantza/greedysearch-pi 1.9.2 → 2.1.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +132 -2
  2. package/README.md +82 -47
  3. package/bin/cdp.mjs +1153 -1108
  4. package/bin/launch.mjs +9 -0
  5. package/bin/search.mjs +318 -81
  6. package/extractors/bing-copilot.mjs +48 -18
  7. package/extractors/chatgpt.mjs +553 -0
  8. package/extractors/common.mjs +213 -22
  9. package/extractors/consensus.mjs +655 -0
  10. package/extractors/consent.mjs +182 -18
  11. package/extractors/gemini.mjs +350 -217
  12. package/extractors/google-ai.mjs +129 -128
  13. package/extractors/logically.mjs +629 -0
  14. package/extractors/perplexity.mjs +547 -217
  15. package/extractors/selectors.mjs +3 -2
  16. package/extractors/semantic-scholar.mjs +219 -0
  17. package/package.json +8 -4
  18. package/skills/greedy-search/skill.md +20 -12
  19. package/src/fetcher.mjs +23 -1
  20. package/src/formatters/results.ts +185 -128
  21. package/src/search/browser-lifecycle.mjs +27 -5
  22. package/src/search/challenge-detect.mjs +205 -0
  23. package/src/search/chrome.mjs +653 -590
  24. package/src/search/constants.mjs +155 -39
  25. package/src/search/engines.mjs +114 -76
  26. package/src/search/fetch-source.mjs +566 -451
  27. package/src/search/pdf.mjs +68 -0
  28. package/src/search/progress.mjs +145 -0
  29. package/src/search/recovery.mjs +73 -45
  30. package/src/search/research.mjs +1419 -62
  31. package/src/search/scale-aware.mjs +93 -0
  32. package/src/search/simple-research.mjs +520 -0
  33. package/src/search/sources.mjs +52 -22
  34. package/src/search/synthesis-runner.mjs +105 -26
  35. package/src/search/synthesis.mjs +286 -246
  36. package/src/tools/greedy-search-handler.ts +129 -59
  37. package/src/tools/shared.ts +312 -186
  38. package/src/types.ts +110 -104
  39. package/test.mjs +537 -18
@@ -16,8 +16,37 @@ import {
16
16
  makeProgressTracker,
17
17
  runSearch,
18
18
  stripQuotes,
19
+ type ProgressUpdate,
20
+ type ToolResult,
19
21
  } from "./shared.js";
20
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
+
21
50
  class Text {
22
51
  constructor(
23
52
  private text: string,
@@ -52,8 +81,10 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
52
81
  name: "greedy_search",
53
82
  label: "Greedy Search",
54
83
  description:
55
- "WEB SEARCH ONLY — searches live web via Perplexity, Bing Copilot, and Google AI in parallel. " +
56
- "Optionally synthesizes results with Gemini, deduplicates sources by consensus. " +
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. Scale-aware: simple queries auto-classify and use a fast single-pass path. " +
57
88
  "Use for: library docs, recent framework changes, error messages, best practices, current events. " +
58
89
  "Reports streaming progress as each engine completes.",
59
90
  promptSnippet: "Multi-engine AI web search with streaming progress",
@@ -61,14 +92,28 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
61
92
  query: Type.String({ description: "The search query" }),
62
93
  engine: Type.String({
63
94
  description:
64
- 'Engine to use: "all" (default), "perplexity", "bing", "google", "gemini", "gem". "all" fans out to Perplexity, Bing, and Google in parallel.',
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.',
65
96
  default: "all",
66
97
  }),
67
- depth: Type.String({
68
- description:
69
- 'Search depth: "fast" (no synthesis/source fetch, ~15-30s), "standard" (synthesis + sources, ~30-90s), "deep" (stronger grounding, ~60-180s), "research" (iterative query/learnings loop; slowest). Default: "standard". Note: single-engine searches default to fast unless depth is "research".',
70
- default: "standard",
71
- }),
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
+ ),
72
117
  breadth: Type.Optional(
73
118
  Type.Number({
74
119
  description:
@@ -89,6 +134,19 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
89
134
  'Only for depth="research": maximum fetched sources for the final report, 3-12.',
90
135
  }),
91
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
+ ),
92
150
  fullAnswer: Type.Optional(
93
151
  Type.Boolean({
94
152
  description:
@@ -118,27 +176,33 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
118
176
  }),
119
177
  ),
120
178
  }),
121
- execute: async (_toolCallId, params, signal, onUpdate) => {
122
- const { query, fullAnswer: fullAnswerParam } = params as {
123
- query: string;
124
- engine: string;
125
- depth?: "fast" | "standard" | "deep" | "research";
126
- breadth?: number;
127
- iterations?: number;
128
- maxSources?: number;
129
- fullAnswer?: boolean;
130
- headless?: boolean;
131
- visible?: boolean;
132
- alwaysVisible?: boolean;
133
- };
134
- const engine = stripQuotes((params as any).engine ?? "all") || "all";
135
- const depth = (stripQuotes((params as any).depth ?? "standard") ||
136
- "standard") as "fast" | "standard" | "deep" | "research";
137
- const effectiveEngine = depth === "research" ? "all" : engine;
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;
138
202
  const visible =
139
- (params as any).visible === true ||
140
- (params as any).alwaysVisible === true ||
141
- (params as any).headless === false ||
203
+ params.visible === true ||
204
+ params.alwaysVisible === true ||
205
+ params.headless === false ||
142
206
  process.env.GREEDY_SEARCH_VISIBLE === "1" ||
143
207
  process.env.GREEDY_SEARCH_ALWAYS_VISIBLE === "1";
144
208
  const headless = !visible;
@@ -148,28 +212,32 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
148
212
  const flags: string[] = [];
149
213
  const fullAnswer = fullAnswerParam ?? effectiveEngine !== "all";
150
214
  if (fullAnswer) flags.push("--full");
151
- if (depth === "research") {
215
+ if (researchMode) {
152
216
  flags.push("--depth", "research");
153
- if (typeof (params as any).breadth === "number")
154
- flags.push("--breadth", String((params as any).breadth));
155
- if (typeof (params as any).iterations === "number")
156
- flags.push("--iterations", String((params as any).iterations));
157
- if (typeof (params as any).maxSources === "number")
158
- flags.push("--max-sources", String((params as any).maxSources));
159
- } else if (depth === "deep") flags.push("--depth", "deep");
160
- else if (depth === "fast") flags.push("--fast");
161
- else if (depth === "standard" && engine === "all")
162
- flags.push("--synthesize");
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
+ }
163
233
 
164
- const onProgress =
165
- effectiveEngine === "all"
166
- ? makeProgressTracker(
167
- ALL_ENGINES,
168
- onUpdate,
169
- depth === "research" ? "Researching" : "Searching",
170
- depth,
171
- )
172
- : undefined;
234
+ const onProgress = makeProgressTracker(
235
+ effectiveEngine === "all" ? ALL_ENGINES : [effectiveEngine],
236
+ onUpdate,
237
+ researchMode ? "Researching" : "Searching",
238
+ synthesize && effectiveEngine === "all",
239
+ query,
240
+ );
173
241
 
174
242
  try {
175
243
  const data = await runSearch(
@@ -179,7 +247,7 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
179
247
  `${baseDir}/bin/search.mjs`,
180
248
  signal,
181
249
  onProgress,
182
- headless,
250
+ { headless },
183
251
  );
184
252
  const text = formatResults(effectiveEngine, data);
185
253
  return {
@@ -191,7 +259,7 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
191
259
  }
192
260
  },
193
261
 
194
- renderCall(args, theme) {
262
+ renderCall(args: Partial<GreedySearchParams>, theme: ToolTheme) {
195
263
  const q = (args.query || "").slice(0, 60);
196
264
  const qDisplay = q.length < (args.query || "").length ? `${q}...` : q;
197
265
  const engineDisplay =
@@ -205,11 +273,15 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
205
273
  );
206
274
  },
207
275
 
208
- renderResult(result, { expanded, isPartial }, theme) {
276
+ renderResult(
277
+ result: ToolResult,
278
+ { expanded, isPartial }: RenderState,
279
+ theme: ToolTheme,
280
+ ) {
209
281
  if (isPartial) {
210
- const progressText = (
211
- result.content.find((c) => c.type === "text") as any
212
- )?.text as string | undefined;
282
+ const progressText = result.content.find(
283
+ (c) => c.type === "text",
284
+ )?.text;
213
285
  const display = progressText
214
286
  ? progressText.replace(/\*\*/g, "")
215
287
  : "Searching...";
@@ -217,9 +289,7 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
217
289
  }
218
290
 
219
291
  const textContent = result.content.find((c) => c.type === "text");
220
- const raw = (result.details as any)?.raw as
221
- | Record<string, unknown>
222
- | undefined;
292
+ const raw = result.details?.raw as Record<string, unknown> | undefined;
223
293
 
224
294
  // Collapsed: one-line summary only
225
295
  if (!expanded) {
@@ -256,7 +326,7 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
256
326
  );
257
327
  let totalSources = 0;
258
328
  for (const key of engineKeys) {
259
- const eng = (raw as any)[key] as Record<string, unknown> | undefined;
329
+ const eng = raw?.[key] as Record<string, unknown> | undefined;
260
330
  const s = eng?.sources as Array<unknown> | undefined;
261
331
  if (Array.isArray(s)) totalSources += s.length;
262
332
  }
@@ -272,7 +342,7 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
272
342
  }
273
343
 
274
344
  // No structured data — show content text as error/fallback
275
- const snippet = (textContent as any)?.text as string | undefined;
345
+ const snippet = textContent?.text;
276
346
  if (snippet) {
277
347
  return new Text(
278
348
  theme.fg("warning", ` → ${snippet.slice(0, 80)}`),