@apmantza/greedysearch-pi 1.8.2 → 1.8.4

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,64 +1,64 @@
1
- // src/search/synthesis-runner.mjs — Run Gemini synthesis via CDP
2
- //
3
- // Extracted from search.mjs.
4
-
5
- import { spawn } from "node:child_process";
6
- import { join } from "node:path";
7
- import { GREEDY_PROFILE_DIR } from "./constants.mjs";
8
- import { parseStructuredJson, normalizeSynthesisPayload, buildSynthesisPrompt } from "./synthesis.mjs";
9
- import { cdp, openNewTab, closeTab, activateTab } from "./chrome.mjs";
10
- import { trimText } from "./sources.mjs";
11
-
12
- const __dir = import.meta.dirname || new URL(".", import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1");
13
-
14
- export async function synthesizeWithGemini(
15
- query,
16
- results,
17
- { grounded = false, tabPrefix = null } = {},
18
- ) {
19
- const sources = Array.isArray(results._sources)
20
- ? results._sources
21
- : buildSourceRegistry(results);
22
- const prompt = buildSynthesisPrompt(query, results, sources, { grounded });
23
-
24
- return new Promise((resolve, reject) => {
25
- const extraArgs = tabPrefix ? ["--tab", String(tabPrefix)] : [];
26
- const proc = spawn(
27
- "node",
28
- [join(__dir, "..", "..", "extractors", "gemini.mjs"), prompt, ...extraArgs],
29
- {
30
- stdio: ["ignore", "pipe", "pipe"],
31
- env: { ...process.env, CDP_PROFILE_DIR: GREEDY_PROFILE_DIR },
32
- },
33
- );
34
- let out = "";
35
- let err = "";
36
- proc.stdout.on("data", (d) => (out += d));
37
- proc.stderr.on("data", (d) => (err += d));
38
- const t = setTimeout(() => {
39
- proc.kill();
40
- reject(new Error("Gemini synthesis timed out after 180s"));
41
- }, 180000);
42
- proc.on("close", (code) => {
43
- clearTimeout(t);
44
- if (code !== 0)
45
- reject(new Error(err.trim() || "gemini extractor failed"));
46
- else {
47
- try {
48
- const raw = JSON.parse(out.trim());
49
- const structured = parseStructuredJson(raw.answer || "");
50
- resolve({
51
- ...normalizeSynthesisPayload(structured, sources, raw.answer || ""),
52
- rawAnswer: raw.answer || "",
53
- geminiSources: raw.sources || [],
54
- });
55
- } catch {
56
- reject(new Error(`bad JSON from gemini: ${out.slice(0, 100)}`));
57
- }
58
- }
59
- });
60
- });
61
- }
62
-
63
- // Need to import buildSourceRegistry for fallback
1
+ // src/search/synthesis-runner.mjs — Run Gemini synthesis via CDP
2
+ //
3
+ // Extracted from search.mjs.
4
+
5
+ import { spawn } from "node:child_process";
6
+ import { join } from "node:path";
7
+ import { GREEDY_PROFILE_DIR } from "./constants.mjs";
8
+ import { parseStructuredJson, normalizeSynthesisPayload, buildSynthesisPrompt } from "./synthesis.mjs";
9
+ import { cdp, openNewTab, closeTab, activateTab } from "./chrome.mjs";
10
+ import { trimText } from "./sources.mjs";
11
+
12
+ const __dir = import.meta.dirname || new URL(".", import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1");
13
+
14
+ export async function synthesizeWithGemini(
15
+ query,
16
+ results,
17
+ { grounded = false, tabPrefix = null } = {},
18
+ ) {
19
+ const sources = Array.isArray(results._sources)
20
+ ? results._sources
21
+ : buildSourceRegistry(results);
22
+ const prompt = buildSynthesisPrompt(query, results, sources, { grounded });
23
+
24
+ return new Promise((resolve, reject) => {
25
+ const extraArgs = tabPrefix ? ["--tab", String(tabPrefix)] : [];
26
+ const proc = spawn(
27
+ "node",
28
+ [join(__dir, "..", "..", "extractors", "gemini.mjs"), prompt, ...extraArgs],
29
+ {
30
+ stdio: ["ignore", "pipe", "pipe"],
31
+ env: { ...process.env, CDP_PROFILE_DIR: GREEDY_PROFILE_DIR },
32
+ },
33
+ );
34
+ let out = "";
35
+ let err = "";
36
+ proc.stdout.on("data", (d) => (out += d));
37
+ proc.stderr.on("data", (d) => (err += d));
38
+ const t = setTimeout(() => {
39
+ proc.kill();
40
+ reject(new Error("Gemini synthesis timed out after 180s"));
41
+ }, 180000);
42
+ proc.on("close", (code) => {
43
+ clearTimeout(t);
44
+ if (code !== 0)
45
+ reject(new Error(err.trim() || "gemini extractor failed"));
46
+ else {
47
+ try {
48
+ const raw = JSON.parse(out.trim());
49
+ const structured = parseStructuredJson(raw.answer || "");
50
+ resolve({
51
+ ...normalizeSynthesisPayload(structured, sources, raw.answer || ""),
52
+ rawAnswer: raw.answer || "",
53
+ geminiSources: raw.sources || [],
54
+ });
55
+ } catch {
56
+ reject(new Error(`bad JSON from gemini: ${out.slice(0, 100)}`));
57
+ }
58
+ }
59
+ });
60
+ });
61
+ }
62
+
63
+ // Need to import buildSourceRegistry for fallback
64
64
  import { buildSourceRegistry } from "./sources.mjs";
@@ -1,223 +1,223 @@
1
- // src/search/synthesis.mjs — Synthesis prompt building, structured JSON parsing,
2
- // confidence metrics, and payload normalization
3
- //
4
- // Extracted from search.mjs to reduce file complexity.
5
-
6
- import { ALL_ENGINES } from "./constants.mjs";
7
- import { trimText } from "./sources.mjs";
8
-
9
- export function parseStructuredJson(text) {
10
- if (!text) return null;
11
- let trimmed = String(text).trim();
12
-
13
- // Look for BEGIN_JSON/END_JSON markers first
14
- const beginIdx = trimmed.indexOf("BEGIN_JSON");
15
- const endIdx = trimmed.indexOf("END_JSON");
16
- if (beginIdx !== -1 && endIdx !== -1 && beginIdx < endIdx) {
17
- trimmed = trimmed.slice(beginIdx + "BEGIN_JSON".length, endIdx).trim();
18
- } else {
19
- // Strip out common LLM preamble text before the actual JSON
20
- const jsonStart = trimmed.indexOf("{");
21
- if (jsonStart > 0) {
22
- trimmed = trimmed.slice(jsonStart);
23
- }
24
- }
25
-
26
- const candidates = [
27
- trimmed,
28
- trimmed
29
- .replace(/^```json\s*/i, "")
30
- .replace(/^```\s*/i, "")
31
- .replace(/```$/i, "")
32
- .trim(),
33
- ];
34
-
35
- const objectMatch = trimmed.match(/\{[\s\S]*\}$/);
36
- if (objectMatch) candidates.push(objectMatch[0]);
37
-
38
- for (const candidate of candidates) {
39
- try {
40
- return JSON.parse(candidate);
41
- } catch {
42
- // try next candidate
43
- }
44
- }
45
- return null;
46
- }
47
-
48
- export function normalizeSynthesisPayload(
49
- payload,
50
- sources,
51
- fallbackAnswer = "",
52
- ) {
53
- const sourceIds = new Set(sources.map((source) => source.id));
54
- const agreementLevel = [
55
- "high",
56
- "medium",
57
- "low",
58
- "mixed",
59
- "conflicting",
60
- ].includes(payload?.agreement?.level)
61
- ? payload.agreement.level
62
- : "mixed";
63
- const claims = Array.isArray(payload?.claims)
64
- ? payload.claims
65
- .map((claim) => ({
66
- claim: trimText(claim?.claim || "", 260),
67
- support: ["strong", "moderate", "weak", "conflicting"].includes(
68
- claim?.support,
69
- )
70
- ? claim.support
71
- : "moderate",
72
- sourceIds: Array.isArray(claim?.sourceIds)
73
- ? claim.sourceIds.filter((id) => sourceIds.has(id))
74
- : [],
75
- }))
76
- .filter((claim) => claim.claim)
77
- : [];
78
- const recommendedSources = Array.isArray(payload?.recommendedSources)
79
- ? payload.recommendedSources.filter((id) => sourceIds.has(id)).slice(0, 6)
80
- : [];
81
-
82
- // Clean up fallback answer if it contains preamble text
83
- const cleanFallback = fallbackAnswer
84
- ? fallbackAnswer.replace(/^[\s\S]*?\{/m, "{").replace(/}\s*[\s\S]*$/m, "}")
85
- : "";
86
-
87
- return {
88
- answer: trimText(payload?.answer || cleanFallback || fallbackAnswer, 4000),
89
- agreement: {
90
- level: agreementLevel,
91
- summary: trimText(payload?.agreement?.summary || "", 280),
92
- },
93
- differences: Array.isArray(payload?.differences)
94
- ? payload.differences
95
- .map((item) => trimText(item, 220))
96
- .filter(Boolean)
97
- .slice(0, 5)
98
- : [],
99
- caveats: Array.isArray(payload?.caveats)
100
- ? payload.caveats
101
- .map((item) => trimText(item, 220))
102
- .filter(Boolean)
103
- .slice(0, 5)
104
- : [],
105
- claims,
106
- recommendedSources,
107
- };
108
- }
109
-
110
- export function buildSynthesisPrompt(
111
- query,
112
- results,
113
- sources,
114
- { grounded = false } = {},
115
- ) {
116
- const engineSummaries = {};
117
- for (const engine of ["perplexity", "bing", "google"]) {
118
- const result = results[engine];
119
- if (!result) continue;
120
- if (result.error) {
121
- engineSummaries[engine] = {
122
- status: "error",
123
- error: String(result.error),
124
- };
125
- continue;
126
- }
127
-
128
- engineSummaries[engine] = {
129
- status: "ok",
130
- answer: trimText(result.answer || "", grounded ? 4500 : 2200),
131
- sourceIds: sources
132
- .filter((source) => source.engines.includes(engine))
133
- .sort(
134
- (a, b) =>
135
- (a.perEngine[engine]?.rank || 99) -
136
- (b.perEngine[engine]?.rank || 99),
137
- )
138
- .map((source) => source.id)
139
- .slice(0, 6),
140
- };
141
- }
142
-
143
- const sourceRegistry = sources.slice(0, grounded ? 10 : 8).map((source) => ({
144
- id: source.id,
145
- title: source.title,
146
- domain: source.domain,
147
- canonicalUrl: source.canonicalUrl,
148
- sourceType: source.sourceType,
149
- isOfficial: source.isOfficial,
150
- engines: source.engines,
151
- engineCount: source.engineCount,
152
- perEngine: source.perEngine,
153
- fetch: source.fetch?.attempted
154
- ? {
155
- ok: source.fetch.ok,
156
- status: source.fetch.status,
157
- publishedTime: source.fetch.publishedTime || "",
158
- lastModified: source.fetch.lastModified || "",
159
- byline: source.fetch.byline || "",
160
- siteName: source.fetch.siteName || "",
161
- ...(grounded
162
- ? { snippet: trimText(source.fetch.snippet || "", 700) }
163
- : {}),
164
- }
165
- : undefined,
166
- }));
167
-
168
- return [
169
- "Synthesize the following search results into a concise answer.",
170
- "Compare the three engine responses (Perplexity, Bing, Google) and identify:",
171
- "1. The main answer to the query",
172
- "2. Where the engines agree",
173
- "3. Where they disagree (if anywhere)",
174
- "4. Any caveats or limitations",
175
- "Use source IDs like S1, S2 when citing sources.",
176
- "Format: Start with a brief answer, then list key points.",
177
- "",
178
- `Query: ${query}`,
179
- "",
180
- `Engine results:\n${JSON.stringify(engineSummaries, null, 2)}`,
181
- "",
182
- `Source registry:\n${JSON.stringify(sourceRegistry, null, 2)}`,
183
- ].join("\n");
184
- }
185
-
186
- export function buildConfidence(out) {
187
- const sources = Array.isArray(out._sources) ? out._sources : [];
188
- const topConsensus = sources.length > 0 ? sources[0]?.engineCount || 0 : 0;
189
- const officialSourceCount = sources.filter(
190
- (source) => source.isOfficial,
191
- ).length;
192
- const firstPartySourceCount = sources.filter(
193
- (source) => source.isOfficial || source.sourceType === "maintainer-blog",
194
- ).length;
195
- const fetchedAttempted = sources.filter(
196
- (source) => source.fetch?.attempted,
197
- ).length;
198
- const fetchedSucceeded = sources.filter((source) => source.fetch?.ok).length;
199
- const sourceTypeBreakdown = sources.reduce((acc, source) => {
200
- acc[source.sourceType] = (acc[source.sourceType] || 0) + 1;
201
- return acc;
202
- }, {});
203
- const synthesisLevel = out._synthesis?.agreement?.level;
204
-
205
- return {
206
- sourcesCount: sources.length,
207
- topSourceConsensus: topConsensus,
208
- agreementLevel:
209
- synthesisLevel ||
210
- (topConsensus >= 3 ? "high" : topConsensus >= 2 ? "medium" : "low"),
211
- enginesResponded: ALL_ENGINES.filter(
212
- (engine) => out[engine]?.answer && !out[engine]?.error,
213
- ),
214
- enginesFailed: ALL_ENGINES.filter((engine) => out[engine]?.error),
215
- officialSourceCount,
216
- firstPartySourceCount,
217
- fetchedSourceSuccessRate:
218
- fetchedAttempted > 0
219
- ? Number((fetchedSucceeded / fetchedAttempted).toFixed(2))
220
- : 0,
221
- sourceTypeBreakdown,
222
- };
223
- }
1
+ // src/search/synthesis.mjs — Synthesis prompt building, structured JSON parsing,
2
+ // confidence metrics, and payload normalization
3
+ //
4
+ // Extracted from search.mjs to reduce file complexity.
5
+
6
+ import { ALL_ENGINES } from "./constants.mjs";
7
+ import { trimText } from "./sources.mjs";
8
+
9
+ export function parseStructuredJson(text) {
10
+ if (!text) return null;
11
+ let trimmed = String(text).trim();
12
+
13
+ // Look for BEGIN_JSON/END_JSON markers first
14
+ const beginIdx = trimmed.indexOf("BEGIN_JSON");
15
+ const endIdx = trimmed.indexOf("END_JSON");
16
+ if (beginIdx !== -1 && endIdx !== -1 && beginIdx < endIdx) {
17
+ trimmed = trimmed.slice(beginIdx + "BEGIN_JSON".length, endIdx).trim();
18
+ } else {
19
+ // Strip out common LLM preamble text before the actual JSON
20
+ const jsonStart = trimmed.indexOf("{");
21
+ if (jsonStart > 0) {
22
+ trimmed = trimmed.slice(jsonStart);
23
+ }
24
+ }
25
+
26
+ const candidates = [
27
+ trimmed,
28
+ trimmed
29
+ .replace(/^```json\s*/i, "")
30
+ .replace(/^```\s*/i, "")
31
+ .replace(/```$/i, "")
32
+ .trim(),
33
+ ];
34
+
35
+ const objectMatch = trimmed.match(/\{[\s\S]*\}$/);
36
+ if (objectMatch) candidates.push(objectMatch[0]);
37
+
38
+ for (const candidate of candidates) {
39
+ try {
40
+ return JSON.parse(candidate);
41
+ } catch {
42
+ // try next candidate
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+
48
+ export function normalizeSynthesisPayload(
49
+ payload,
50
+ sources,
51
+ fallbackAnswer = "",
52
+ ) {
53
+ const sourceIds = new Set(sources.map((source) => source.id));
54
+ const agreementLevel = [
55
+ "high",
56
+ "medium",
57
+ "low",
58
+ "mixed",
59
+ "conflicting",
60
+ ].includes(payload?.agreement?.level)
61
+ ? payload.agreement.level
62
+ : "mixed";
63
+ const claims = Array.isArray(payload?.claims)
64
+ ? payload.claims
65
+ .map((claim) => ({
66
+ claim: trimText(claim?.claim || "", 260),
67
+ support: ["strong", "moderate", "weak", "conflicting"].includes(
68
+ claim?.support,
69
+ )
70
+ ? claim.support
71
+ : "moderate",
72
+ sourceIds: Array.isArray(claim?.sourceIds)
73
+ ? claim.sourceIds.filter((id) => sourceIds.has(id))
74
+ : [],
75
+ }))
76
+ .filter((claim) => claim.claim)
77
+ : [];
78
+ const recommendedSources = Array.isArray(payload?.recommendedSources)
79
+ ? payload.recommendedSources.filter((id) => sourceIds.has(id)).slice(0, 6)
80
+ : [];
81
+
82
+ // Clean up fallback answer if it contains preamble text
83
+ const cleanFallback = fallbackAnswer
84
+ ? fallbackAnswer.replace(/^[\s\S]*?\{/m, "{").replace(/}\s*[\s\S]*$/m, "}")
85
+ : "";
86
+
87
+ return {
88
+ answer: trimText(payload?.answer || cleanFallback || fallbackAnswer, 4000),
89
+ agreement: {
90
+ level: agreementLevel,
91
+ summary: trimText(payload?.agreement?.summary || "", 280),
92
+ },
93
+ differences: Array.isArray(payload?.differences)
94
+ ? payload.differences
95
+ .map((item) => trimText(item, 220))
96
+ .filter(Boolean)
97
+ .slice(0, 5)
98
+ : [],
99
+ caveats: Array.isArray(payload?.caveats)
100
+ ? payload.caveats
101
+ .map((item) => trimText(item, 220))
102
+ .filter(Boolean)
103
+ .slice(0, 5)
104
+ : [],
105
+ claims,
106
+ recommendedSources,
107
+ };
108
+ }
109
+
110
+ export function buildSynthesisPrompt(
111
+ query,
112
+ results,
113
+ sources,
114
+ { grounded = false } = {},
115
+ ) {
116
+ const engineSummaries = {};
117
+ for (const engine of ["perplexity", "bing", "google"]) {
118
+ const result = results[engine];
119
+ if (!result) continue;
120
+ if (result.error) {
121
+ engineSummaries[engine] = {
122
+ status: "error",
123
+ error: String(result.error),
124
+ };
125
+ continue;
126
+ }
127
+
128
+ engineSummaries[engine] = {
129
+ status: "ok",
130
+ answer: trimText(result.answer || "", grounded ? 4500 : 2200),
131
+ sourceIds: sources
132
+ .filter((source) => source.engines.includes(engine))
133
+ .sort(
134
+ (a, b) =>
135
+ (a.perEngine[engine]?.rank || 99) -
136
+ (b.perEngine[engine]?.rank || 99),
137
+ )
138
+ .map((source) => source.id)
139
+ .slice(0, 6),
140
+ };
141
+ }
142
+
143
+ const sourceRegistry = sources.slice(0, grounded ? 10 : 8).map((source) => ({
144
+ id: source.id,
145
+ title: source.title,
146
+ domain: source.domain,
147
+ canonicalUrl: source.canonicalUrl,
148
+ sourceType: source.sourceType,
149
+ isOfficial: source.isOfficial,
150
+ engines: source.engines,
151
+ engineCount: source.engineCount,
152
+ perEngine: source.perEngine,
153
+ fetch: source.fetch?.attempted
154
+ ? {
155
+ ok: source.fetch.ok,
156
+ status: source.fetch.status,
157
+ publishedTime: source.fetch.publishedTime || "",
158
+ lastModified: source.fetch.lastModified || "",
159
+ byline: source.fetch.byline || "",
160
+ siteName: source.fetch.siteName || "",
161
+ ...(grounded
162
+ ? { snippet: trimText(source.fetch.snippet || "", 700) }
163
+ : {}),
164
+ }
165
+ : undefined,
166
+ }));
167
+
168
+ return [
169
+ "Synthesize the following search results into a concise answer.",
170
+ "Compare the three engine responses (Perplexity, Bing, Google) and identify:",
171
+ "1. The main answer to the query",
172
+ "2. Where the engines agree",
173
+ "3. Where they disagree (if anywhere)",
174
+ "4. Any caveats or limitations",
175
+ "Use source IDs like S1, S2 when citing sources.",
176
+ "Format: Start with a brief answer, then list key points.",
177
+ "",
178
+ `Query: ${query}`,
179
+ "",
180
+ `Engine results:\n${JSON.stringify(engineSummaries, null, 2)}`,
181
+ "",
182
+ `Source registry:\n${JSON.stringify(sourceRegistry, null, 2)}`,
183
+ ].join("\n");
184
+ }
185
+
186
+ export function buildConfidence(out) {
187
+ const sources = Array.isArray(out._sources) ? out._sources : [];
188
+ const topConsensus = sources.length > 0 ? sources[0]?.engineCount || 0 : 0;
189
+ const officialSourceCount = sources.filter(
190
+ (source) => source.isOfficial,
191
+ ).length;
192
+ const firstPartySourceCount = sources.filter(
193
+ (source) => source.isOfficial || source.sourceType === "maintainer-blog",
194
+ ).length;
195
+ const fetchedAttempted = sources.filter(
196
+ (source) => source.fetch?.attempted,
197
+ ).length;
198
+ const fetchedSucceeded = sources.filter((source) => source.fetch?.ok).length;
199
+ const sourceTypeBreakdown = sources.reduce((acc, source) => {
200
+ acc[source.sourceType] = (acc[source.sourceType] || 0) + 1;
201
+ return acc;
202
+ }, {});
203
+ const synthesisLevel = out._synthesis?.agreement?.level;
204
+
205
+ return {
206
+ sourcesCount: sources.length,
207
+ topSourceConsensus: topConsensus,
208
+ agreementLevel:
209
+ synthesisLevel ||
210
+ (topConsensus >= 3 ? "high" : topConsensus >= 2 ? "medium" : "low"),
211
+ enginesResponded: ALL_ENGINES.filter(
212
+ (engine) => out[engine]?.answer && !out[engine]?.error,
213
+ ),
214
+ enginesFailed: ALL_ENGINES.filter((engine) => out[engine]?.error),
215
+ officialSourceCount,
216
+ firstPartySourceCount,
217
+ fetchedSourceSuccessRate:
218
+ fetchedAttempted > 0
219
+ ? Number((fetchedSucceeded / fetchedAttempted).toFixed(2))
220
+ : 0,
221
+ sourceTypeBreakdown,
222
+ };
223
+ }
@@ -1,37 +1,37 @@
1
- /**
2
- * deep_research tool handler — legacy alias to greedy_search with depth: deep
3
- */
4
-
5
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
- import { Type } from "@sinclair/typebox";
7
- import { formatDeepResearch } from "../formatters/results.js";
8
- import { ALL_ENGINES, cdpAvailable, cdpMissingResult, errorResult, makeProgressTracker, runSearch } from "./shared.js";
9
-
10
- export function registerDeepResearchTool(pi: ExtensionAPI, baseDir: string) {
11
- pi.registerTool({
12
- name: "deep_research",
13
- label: "Deep Research (legacy)",
14
- description:
15
- "DEPRECATED — Use greedy_search with depth: 'deep' instead. " +
16
- "Comprehensive multi-engine research with source fetching and synthesis.",
17
- promptSnippet: "Deep multi-engine research (legacy alias to greedy_search)",
18
- parameters: Type.Object({
19
- query: Type.String({ description: "The research question" }),
20
- }),
21
- execute: async (_toolCallId, params, signal, onUpdate) => {
22
- const { query } = params as { query: string };
23
-
24
- if (!cdpAvailable(baseDir)) return cdpMissingResult();
25
-
26
- const onProgress = makeProgressTracker(ALL_ENGINES, onUpdate, "Researching", "standard");
27
-
28
- try {
29
- const data = await runSearch("all", query, ["--deep"], `${baseDir}/bin/search.mjs`, signal, onProgress);
30
- const text = formatDeepResearch(data);
31
- return { content: [{ type: "text", text: text || "No results returned." }], details: { raw: data } };
32
- } catch (e) {
33
- return errorResult("Deep research failed", e);
34
- }
35
- },
36
- });
1
+ /**
2
+ * deep_research tool handler — legacy alias to greedy_search with depth: deep
3
+ */
4
+
5
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
+ import { Type } from "@sinclair/typebox";
7
+ import { formatDeepResearch } from "../formatters/results.js";
8
+ import { ALL_ENGINES, cdpAvailable, cdpMissingResult, errorResult, makeProgressTracker, runSearch } from "./shared.js";
9
+
10
+ export function registerDeepResearchTool(pi: ExtensionAPI, baseDir: string) {
11
+ pi.registerTool({
12
+ name: "deep_research",
13
+ label: "Deep Research (legacy)",
14
+ description:
15
+ "DEPRECATED — Use greedy_search with depth: 'deep' instead. " +
16
+ "Comprehensive multi-engine research with source fetching and synthesis.",
17
+ promptSnippet: "Deep multi-engine research (legacy alias to greedy_search)",
18
+ parameters: Type.Object({
19
+ query: Type.String({ description: "The research question" }),
20
+ }),
21
+ execute: async (_toolCallId, params, signal, onUpdate) => {
22
+ const { query } = params as { query: string };
23
+
24
+ if (!cdpAvailable(baseDir)) return cdpMissingResult();
25
+
26
+ const onProgress = makeProgressTracker(ALL_ENGINES, onUpdate, "Researching", "standard");
27
+
28
+ try {
29
+ const data = await runSearch("all", query, ["--deep"], `${baseDir}/bin/search.mjs`, signal, onProgress);
30
+ const text = formatDeepResearch(data);
31
+ return { content: [{ type: "text", text: text || "No results returned." }], details: { raw: data } };
32
+ } catch (e) {
33
+ return errorResult("Deep research failed", e);
34
+ }
35
+ },
36
+ });
37
37
  }