@apmantza/greedysearch-pi 1.9.2 → 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 +80 -1
- package/README.md +82 -47
- package/bin/cdp.mjs +1153 -1108
- package/bin/launch.mjs +9 -0
- package/bin/search.mjs +197 -68
- package/extractors/bing-copilot.mjs +42 -4
- package/extractors/chatgpt.mjs +436 -0
- package/extractors/common.mjs +155 -21
- package/extractors/consensus.mjs +655 -0
- 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/package.json +7 -3
- package/skills/greedy-search/skill.md +9 -3
- 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 +1059 -61
- package/src/search/sources.mjs +52 -22
- package/src/search/synthesis-runner.mjs +105 -26
- package/src/search/synthesis.mjs +286 -246
- package/src/tools/greedy-search-handler.ts +124 -52
- package/src/tools/shared.ts +187 -186
- package/src/types.ts +110 -104
- package/test.mjs +377 -6
|
@@ -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,
|
|
56
|
-
"
|
|
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. " +
|
|
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", "
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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 (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
const
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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,26 +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 (
|
|
215
|
+
if (researchMode) {
|
|
152
216
|
flags.push("--depth", "research");
|
|
153
|
-
if (typeof
|
|
154
|
-
flags.push("--breadth", String(
|
|
155
|
-
if (typeof
|
|
156
|
-
flags.push("--iterations", String(
|
|
157
|
-
if (typeof
|
|
158
|
-
flags.push("--max-sources", String(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
234
|
const onProgress =
|
|
165
235
|
effectiveEngine === "all"
|
|
166
236
|
? makeProgressTracker(
|
|
167
237
|
ALL_ENGINES,
|
|
168
238
|
onUpdate,
|
|
169
|
-
|
|
170
|
-
|
|
239
|
+
researchMode ? "Researching" : "Searching",
|
|
240
|
+
synthesize,
|
|
171
241
|
)
|
|
172
242
|
: undefined;
|
|
173
243
|
|
|
@@ -179,7 +249,7 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
|
|
|
179
249
|
`${baseDir}/bin/search.mjs`,
|
|
180
250
|
signal,
|
|
181
251
|
onProgress,
|
|
182
|
-
headless,
|
|
252
|
+
{ headless },
|
|
183
253
|
);
|
|
184
254
|
const text = formatResults(effectiveEngine, data);
|
|
185
255
|
return {
|
|
@@ -191,7 +261,7 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
|
|
|
191
261
|
}
|
|
192
262
|
},
|
|
193
263
|
|
|
194
|
-
renderCall(args
|
|
264
|
+
renderCall(args: Partial<GreedySearchParams>, theme: ToolTheme) {
|
|
195
265
|
const q = (args.query || "").slice(0, 60);
|
|
196
266
|
const qDisplay = q.length < (args.query || "").length ? `${q}...` : q;
|
|
197
267
|
const engineDisplay =
|
|
@@ -205,11 +275,15 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
|
|
|
205
275
|
);
|
|
206
276
|
},
|
|
207
277
|
|
|
208
|
-
renderResult(
|
|
278
|
+
renderResult(
|
|
279
|
+
result: ToolResult,
|
|
280
|
+
{ expanded, isPartial }: RenderState,
|
|
281
|
+
theme: ToolTheme,
|
|
282
|
+
) {
|
|
209
283
|
if (isPartial) {
|
|
210
|
-
const progressText = (
|
|
211
|
-
|
|
212
|
-
)?.text
|
|
284
|
+
const progressText = result.content.find(
|
|
285
|
+
(c) => c.type === "text",
|
|
286
|
+
)?.text;
|
|
213
287
|
const display = progressText
|
|
214
288
|
? progressText.replace(/\*\*/g, "")
|
|
215
289
|
: "Searching...";
|
|
@@ -217,9 +291,7 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
|
|
|
217
291
|
}
|
|
218
292
|
|
|
219
293
|
const textContent = result.content.find((c) => c.type === "text");
|
|
220
|
-
const raw =
|
|
221
|
-
| Record<string, unknown>
|
|
222
|
-
| undefined;
|
|
294
|
+
const raw = result.details?.raw as Record<string, unknown> | undefined;
|
|
223
295
|
|
|
224
296
|
// Collapsed: one-line summary only
|
|
225
297
|
if (!expanded) {
|
|
@@ -256,7 +328,7 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
|
|
|
256
328
|
);
|
|
257
329
|
let totalSources = 0;
|
|
258
330
|
for (const key of engineKeys) {
|
|
259
|
-
const eng =
|
|
331
|
+
const eng = raw?.[key] as Record<string, unknown> | undefined;
|
|
260
332
|
const s = eng?.sources as Array<unknown> | undefined;
|
|
261
333
|
if (Array.isArray(s)) totalSources += s.length;
|
|
262
334
|
}
|
|
@@ -272,7 +344,7 @@ export function registerGreedySearchTool(pi: ExtensionAPI, baseDir: string) {
|
|
|
272
344
|
}
|
|
273
345
|
|
|
274
346
|
// No structured data — show content text as error/fallback
|
|
275
|
-
const snippet =
|
|
347
|
+
const snippet = textContent?.text;
|
|
276
348
|
if (snippet) {
|
|
277
349
|
return new Text(
|
|
278
350
|
theme.fg("warning", ` → ${snippet.slice(0, 80)}`),
|
package/src/tools/shared.ts
CHANGED
|
@@ -1,186 +1,187 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared types, utilities, and runSearch for Pi tool handlers
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
import { existsSync } from "node:fs";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
import type { ProgressUpdate, ToolResult } from "../types.js";
|
|
9
|
-
|
|
10
|
-
export type { ProgressUpdate, ToolResult } from "../types.js";
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export { ALL_ENGINES };
|
|
16
|
-
|
|
17
|
-
/** Strip surrounding double-quotes that some framework versions inject into string params */
|
|
18
|
-
export function stripQuotes(val: string): string {
|
|
19
|
-
return val.replace(/^"|"$/g, "");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Check if the CDP module is available in the package directory
|
|
24
|
-
*/
|
|
25
|
-
export function cdpAvailable(baseDir: string): boolean {
|
|
26
|
-
return existsSync(join(baseDir, "bin", "cdp.mjs"));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Create a "cdp missing" error result
|
|
31
|
-
*/
|
|
32
|
-
export function cdpMissingResult(): ToolResult {
|
|
33
|
-
return {
|
|
34
|
-
content: [
|
|
35
|
-
{
|
|
36
|
-
type: "text",
|
|
37
|
-
text: "cdp.mjs missing — try reinstalling: pi install git:github.com/apmantza/GreedySearch-pi",
|
|
38
|
-
},
|
|
39
|
-
],
|
|
40
|
-
details: {} as Record<string, unknown>,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Create an error result with a message
|
|
46
|
-
*/
|
|
47
|
-
export function errorResult(prefix: string, e: unknown): ToolResult {
|
|
48
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
49
|
-
return {
|
|
50
|
-
content: [{ type: "text", text: `${prefix}: ${msg}` }],
|
|
51
|
-
details: {} as Record<string, unknown>,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Spawn search.mjs and collect JSON results, with progress streaming via stderr.
|
|
57
|
-
* Shared by GreedySearch tool handlers.
|
|
58
|
-
*/
|
|
59
|
-
export function runSearch(
|
|
60
|
-
engine: string,
|
|
61
|
-
query: string,
|
|
62
|
-
flags: string[],
|
|
63
|
-
searchBin: string,
|
|
64
|
-
signal?: AbortSignal,
|
|
65
|
-
onProgress?: (
|
|
66
|
-
engine: string,
|
|
67
|
-
status: "done" | "error" | "needs-human",
|
|
68
|
-
) => void,
|
|
69
|
-
headless?: boolean
|
|
70
|
-
): Promise<Record<string, unknown>> {
|
|
71
|
-
return new Promise((resolve, reject) => {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
if
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
procEnv.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
proc.stdin.
|
|
92
|
-
|
|
93
|
-
let
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
else
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
else if (synStatus === "
|
|
176
|
-
else parts.push("
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Shared types, utilities, and runSearch for Pi tool handlers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import type { ProgressUpdate, ToolResult } from "../types.js";
|
|
9
|
+
|
|
10
|
+
export type { ProgressUpdate, ToolResult } from "../types.js";
|
|
11
|
+
|
|
12
|
+
// Import and re-export ALL_ENGINES from constants.mjs so it's always in sync.
|
|
13
|
+
// constants.mjs reads ~/.pi/greedyconfig for user overrides.
|
|
14
|
+
import { ALL_ENGINES } from "../search/constants.mjs";
|
|
15
|
+
export { ALL_ENGINES };
|
|
16
|
+
|
|
17
|
+
/** Strip surrounding double-quotes that some framework versions inject into string params */
|
|
18
|
+
export function stripQuotes(val: string): string {
|
|
19
|
+
return val.replace(/^"|"$/g, "");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if the CDP module is available in the package directory
|
|
24
|
+
*/
|
|
25
|
+
export function cdpAvailable(baseDir: string): boolean {
|
|
26
|
+
return existsSync(join(baseDir, "bin", "cdp.mjs"));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a "cdp missing" error result
|
|
31
|
+
*/
|
|
32
|
+
export function cdpMissingResult(): ToolResult {
|
|
33
|
+
return {
|
|
34
|
+
content: [
|
|
35
|
+
{
|
|
36
|
+
type: "text",
|
|
37
|
+
text: "cdp.mjs missing — try reinstalling: pi install git:github.com/apmantza/GreedySearch-pi",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
details: {} as Record<string, unknown>,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create an error result with a message
|
|
46
|
+
*/
|
|
47
|
+
export function errorResult(prefix: string, e: unknown): ToolResult {
|
|
48
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
49
|
+
return {
|
|
50
|
+
content: [{ type: "text", text: `${prefix}: ${msg}` }],
|
|
51
|
+
details: {} as Record<string, unknown>,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Spawn search.mjs and collect JSON results, with progress streaming via stderr.
|
|
57
|
+
* Shared by GreedySearch tool handlers.
|
|
58
|
+
*/
|
|
59
|
+
export function runSearch(
|
|
60
|
+
engine: string,
|
|
61
|
+
query: string,
|
|
62
|
+
flags: string[],
|
|
63
|
+
searchBin: string,
|
|
64
|
+
signal?: AbortSignal,
|
|
65
|
+
onProgress?: (
|
|
66
|
+
engine: string,
|
|
67
|
+
status: "done" | "error" | "needs-human",
|
|
68
|
+
) => void,
|
|
69
|
+
options: { headless?: boolean } = {},
|
|
70
|
+
): Promise<Record<string, unknown>> {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const { headless = true } = options;
|
|
73
|
+
const allFlags = [...flags];
|
|
74
|
+
// Headless is default — only skip if explicitly false or GREEDY_SEARCH_VISIBLE=1
|
|
75
|
+
if (headless !== false && process.env.GREEDY_SEARCH_VISIBLE !== "1")
|
|
76
|
+
allFlags.push("--headless");
|
|
77
|
+
if (headless === false) allFlags.push("--always-visible");
|
|
78
|
+
// Propagate visibility preference via env (--headless flag is informational;
|
|
79
|
+
// the actual headless control in search.mjs / launch.mjs reads the env var).
|
|
80
|
+
const procEnv = { ...process.env };
|
|
81
|
+
if (headless === false) {
|
|
82
|
+
procEnv.GREEDY_SEARCH_VISIBLE = "1";
|
|
83
|
+
procEnv.GREEDY_SEARCH_ALWAYS_VISIBLE = "1";
|
|
84
|
+
}
|
|
85
|
+
const proc = spawn(
|
|
86
|
+
process.execPath,
|
|
87
|
+
[searchBin, engine, "--inline", "--stdin", ...allFlags],
|
|
88
|
+
{ stdio: ["pipe", "pipe", "pipe"], env: procEnv },
|
|
89
|
+
);
|
|
90
|
+
// Pipe query via stdin to avoid leaking it in process table command-line
|
|
91
|
+
proc.stdin.write(query);
|
|
92
|
+
proc.stdin.end();
|
|
93
|
+
let out = "";
|
|
94
|
+
let err = "";
|
|
95
|
+
|
|
96
|
+
const onAbort = () => {
|
|
97
|
+
proc.kill("SIGTERM");
|
|
98
|
+
reject(new Error("Aborted"));
|
|
99
|
+
};
|
|
100
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
101
|
+
|
|
102
|
+
proc.stderr.on("data", (d: Buffer) => {
|
|
103
|
+
err += d;
|
|
104
|
+
// Match PROGRESS lines for any known engine.
|
|
105
|
+
const ENGINE_PROGRESS_RE =
|
|
106
|
+
/^PROGRESS:(perplexity|google|chatgpt|bing|gemini|semantic-scholar|semanticscholar|s2|logically):(done|error|needs-human)$/;
|
|
107
|
+
for (const line of d.toString().split("\n")) {
|
|
108
|
+
// Engine progress: any known engine
|
|
109
|
+
const engineMatch = line.match(ENGINE_PROGRESS_RE);
|
|
110
|
+
if (engineMatch && onProgress) {
|
|
111
|
+
onProgress(
|
|
112
|
+
engineMatch[1],
|
|
113
|
+
engineMatch[2] as "done" | "error" | "needs-human",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
// Synthesis progress: skipped (manual verification) or done/error
|
|
117
|
+
const synthMatch = line.match(
|
|
118
|
+
/^PROGRESS:synthesis:(done|error|skipped)$/,
|
|
119
|
+
);
|
|
120
|
+
if (synthMatch && onProgress) {
|
|
121
|
+
onProgress(
|
|
122
|
+
"synthesis",
|
|
123
|
+
synthMatch[1] as "done" | "error" | "needs-human",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
proc.stdout.on("data", (d: Buffer) => (out += d));
|
|
130
|
+
proc.on("close", (code: number) => {
|
|
131
|
+
signal?.removeEventListener("abort", onAbort);
|
|
132
|
+
if (code !== 0) {
|
|
133
|
+
reject(new Error(err.trim() || `search.mjs exited with code ${code}`));
|
|
134
|
+
} else {
|
|
135
|
+
try {
|
|
136
|
+
resolve(JSON.parse(out.trim()));
|
|
137
|
+
} catch {
|
|
138
|
+
reject(
|
|
139
|
+
new Error(`Invalid JSON from search.mjs: ${out.slice(0, 200)}`),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Build a progress callback that tracks completed engines.
|
|
149
|
+
* Returns an onProgress function suitable for runSearch.
|
|
150
|
+
*/
|
|
151
|
+
export function makeProgressTracker(
|
|
152
|
+
engines: readonly string[],
|
|
153
|
+
onUpdate: ((update: ProgressUpdate) => void) | undefined,
|
|
154
|
+
suffix: "Searching" | "Researching",
|
|
155
|
+
showSynthesis: boolean,
|
|
156
|
+
) {
|
|
157
|
+
const completed = new Map<string, "done" | "error" | "needs-human">();
|
|
158
|
+
|
|
159
|
+
return (eng: string, status: "done" | "error" | "needs-human") => {
|
|
160
|
+
completed.set(eng, status);
|
|
161
|
+
const parts: string[] = [];
|
|
162
|
+
for (const e of engines) {
|
|
163
|
+
const s = completed.get(e);
|
|
164
|
+
if (s === "done") parts.push(`✅ ${e} done`);
|
|
165
|
+
else if (s === "error") parts.push(`❌ ${e} failed`);
|
|
166
|
+
else if (s === "needs-human")
|
|
167
|
+
parts.push(`🔓 ${e} needs manual verification`);
|
|
168
|
+
else parts.push(`⏳ ${e}`);
|
|
169
|
+
}
|
|
170
|
+
// Synthesis status is shown only when the caller explicitly requested
|
|
171
|
+
// Gemini synthesis for a multi-engine search.
|
|
172
|
+
if (showSynthesis && completed.size >= engines.length) {
|
|
173
|
+
const synStatus = completed.get("synthesis");
|
|
174
|
+
if (synStatus === "done") parts.push("✅ synthesized");
|
|
175
|
+
else if (synStatus === "error") parts.push("❌ synthesis failed");
|
|
176
|
+
else if (synStatus === "needs-human") parts.push("⏭️ synthesis skipped");
|
|
177
|
+
else parts.push("🔄 synthesizing");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
onUpdate?.({
|
|
181
|
+
content: [
|
|
182
|
+
{ type: "text", text: `**${suffix}...** ${parts.join(" · ")}` },
|
|
183
|
+
],
|
|
184
|
+
details: { _progress: true },
|
|
185
|
+
} satisfies ProgressUpdate);
|
|
186
|
+
};
|
|
187
|
+
}
|