@apmantza/greedysearch-pi 1.7.7 → 1.8.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/index.ts CHANGED
@@ -1,421 +1,134 @@
1
- /**
2
- * GreedySearch Pi Extension
3
- *
4
- * Adds a `greedy_search` tool to Pi that fans out queries to Perplexity,
5
- * Bing Copilot, and Google AI in parallel, returning synthesized AI answers.
6
- *
7
- * Reports streaming progress as each engine completes.
8
- * Requires Chrome to be running (or it auto-launches a dedicated instance).
9
- */
10
-
11
- import { spawn } from "node:child_process";
12
- import { existsSync } from "node:fs";
13
- import { dirname, join } from "node:path";
14
- import { fileURLToPath } from "node:url";
15
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
16
- import { Type } from "@sinclair/typebox";
17
-
18
- // Formatters extracted to reduce file complexity
19
- import { formatCodingTask } from "./src/formatters/coding.js";
20
- import { formatResults, formatDeepResearch } from "./src/formatters/results.js";
21
-
22
- const __dir = dirname(fileURLToPath(import.meta.url));
23
-
24
- const ALL_ENGINES = ["perplexity", "bing", "google"] as const;
25
-
26
- function cdpAvailable(): boolean {
27
- return existsSync(join(__dir, "bin", "cdp.mjs"));
28
- }
29
-
30
- function runSearch(
31
- engine: string,
32
- query: string,
33
- flags: string[] = [],
34
- signal?: AbortSignal,
35
- onProgress?: (engine: string, status: "done" | "error") => void,
36
- ): Promise<Record<string, unknown>> {
37
- return new Promise((resolve, reject) => {
38
- const proc = spawn(
39
- "node",
40
- [join(__dir, "bin", "search.mjs"), engine, "--inline", ...flags, query],
41
- {
42
- stdio: ["ignore", "pipe", "pipe"],
43
- },
44
- );
45
- let out = "";
46
- let err = "";
47
-
48
- const onAbort = () => {
49
- proc.kill("SIGTERM");
50
- reject(new Error("Aborted"));
51
- };
52
- signal?.addEventListener("abort", onAbort, { once: true });
53
-
54
- // Watch stderr for progress events (PROGRESS:engine:done|error)
55
- proc.stderr.on("data", (d: Buffer) => {
56
- err += d;
57
- const lines = d.toString().split("\n");
58
- for (const line of lines) {
59
- const match = line.match(/^PROGRESS:(\w+):(done|error)$/);
60
- if (match && onProgress) {
61
- onProgress(match[1], match[2] as "done" | "error");
62
- }
63
- }
64
- });
65
-
66
- proc.stdout.on("data", (d: Buffer) => (out += d));
67
- proc.on("close", (code: number) => {
68
- signal?.removeEventListener("abort", onAbort);
69
- if (code !== 0) {
70
- reject(new Error(err.trim() || `search.mjs exited with code ${code}`));
71
- } else {
72
- try {
73
- resolve(JSON.parse(out.trim()));
74
- } catch {
75
- reject(
76
- new Error(`Invalid JSON from search.mjs: ${out.slice(0, 200)}`),
77
- );
78
- }
79
- }
80
- });
81
- });
82
- }
83
-
84
- export default function greedySearchExtension(pi: ExtensionAPI) {
85
- pi.on("session_start", async (_event, ctx) => {
86
- if (!cdpAvailable()) {
87
- ctx.ui.notify(
88
- "GreedySearch: cdp.mjs missing from package directory — try reinstalling: pi install git:github.com/apmantza/GreedySearch-pi",
89
- "warning",
90
- );
91
- }
92
- });
93
-
94
- pi.registerTool({
95
- name: "greedy_search",
96
- label: "Greedy Search",
97
- description:
98
- "WEB SEARCH ONLY — searches live web via Perplexity, Bing Copilot, and Google AI in parallel. " +
99
- "Optionally synthesizes results with Gemini, deduplicates sources by consensus. " +
100
- "Use for: library docs, recent framework changes, error messages, best practices, current events. " +
101
- "Reports streaming progress as each engine completes.",
102
- promptSnippet: "Multi-engine AI web search with streaming progress",
103
- parameters: Type.Object({
104
- query: Type.String({ description: "The search query" }),
105
- engine: Type.Union(
106
- [
107
- Type.Literal("all"),
108
- Type.Literal("perplexity"),
109
- Type.Literal("bing"),
110
- Type.Literal("google"),
111
- Type.Literal("gemini"),
112
- Type.Literal("gem"),
113
- ],
114
- {
115
- description:
116
- 'Engine to use. "all" fans out to Perplexity, Bing, and Google in parallel (default).',
117
- default: "all",
118
- },
119
- ),
120
- depth: Type.Union(
121
- [Type.Literal("fast"), Type.Literal("standard"), Type.Literal("deep")],
122
- {
123
- description:
124
- "Search depth: fast (single engine, ~15-30s), standard (3 engines + synthesis, ~30-90s), deep (3 engines + source fetching + synthesis + confidence, ~60-180s). Default: standard.",
125
- default: "standard",
126
- },
127
- ),
128
- fullAnswer: Type.Optional(
129
- Type.Boolean({
130
- description:
131
- "When true, returns the complete answer instead of a truncated preview (default: false, answers are shortened to ~300 chars to save tokens).",
132
- default: false,
133
- }),
134
- ),
135
- }),
136
- execute: async (_toolCallId, params, signal, onUpdate) => {
137
- const {
138
- query,
139
- engine = "all",
140
- depth = "standard",
141
- fullAnswer: fullAnswerParam,
142
- } = params as {
143
- query: string;
144
- engine: string;
145
- depth?: "fast" | "standard" | "deep";
146
- fullAnswer?: boolean;
147
- };
148
-
149
- if (!cdpAvailable()) {
150
- return {
151
- content: [
152
- {
153
- type: "text",
154
- text: "cdp.mjs missing — try reinstalling: pi install git:github.com/apmantza/GreedySearch-pi",
155
- },
156
- ],
157
- details: {} as { raw?: Record<string, unknown> },
158
- };
159
- }
160
-
161
- const flags: string[] = [];
162
- // Default to full answer for single-engine queries (unless explicitly set to false)
163
- // For multi-engine, default to truncated to save tokens during synthesis
164
- const fullAnswer = fullAnswerParam ?? (engine !== "all");
165
- if (fullAnswer) flags.push("--full");
166
- if (depth === "deep") flags.push("--depth", "deep");
167
- else if (depth === "standard" && engine === "all") flags.push("--synthesize");
168
-
169
- const completed = new Set<string>();
170
-
171
- const onProgress = (eng: string, _status: "done" | "error") => {
172
- completed.add(eng);
173
- const parts: string[] = [];
174
- for (const e of ALL_ENGINES) {
175
- if (completed.has(e)) parts.push(`✅ ${e} done`);
176
- else parts.push(`⏳ ${e}`);
177
- }
178
- if (depth !== "fast" && completed.size >= 3)
179
- parts.push("🔄 synthesizing");
180
-
181
- onUpdate?.({
182
- content: [
183
- { type: "text", text: `**Searching...** ${parts.join(" · ")}` },
184
- ],
185
- details: { _progress: true },
186
- } as any);
187
- };
188
-
189
- try {
190
- const data = await runSearch(
191
- engine,
192
- query,
193
- flags,
194
- signal,
195
- engine === "all" ? onProgress : undefined,
196
- );
197
- const text = formatResults(engine, data);
198
- return {
199
- content: [{ type: "text", text: text || "No results returned." }],
200
- details: { raw: data },
201
- };
202
- } catch (e) {
203
- const msg = e instanceof Error ? e.message : String(e);
204
- return {
205
- content: [{ type: "text", text: `Search failed: ${msg}` }],
206
- details: {} as { raw?: Record<string, unknown> },
207
- };
208
- }
209
- },
210
- });
211
-
212
- // ─── deep_research ─────────────────────────────────────────────────────────
213
- pi.registerTool({
214
- name: "deep_research",
215
- label: "Deep Research (legacy)",
216
- description:
217
- "DEPRECATED — Use greedy_search with depth: 'deep' instead. " +
218
- "Comprehensive multi-engine research with source fetching and synthesis.",
219
- promptSnippet: "Deep multi-engine research (legacy alias to greedy_search)",
220
- parameters: Type.Object({
221
- query: Type.String({ description: "The research question" }),
222
- }),
223
- execute: async (_toolCallId, params, signal, onUpdate) => {
224
- const { query } = params as { query: string };
225
-
226
- if (!cdpAvailable()) {
227
- return {
228
- content: [
229
- { type: "text", text: "cdp.mjs missing — try reinstalling." },
230
- ],
231
- details: {} as { raw?: Record<string, unknown> },
232
- };
233
- }
234
-
235
- const completed = new Set<string>();
236
-
237
- const onProgress = (eng: string, _status: "done" | "error") => {
238
- completed.add(eng);
239
- const parts: string[] = [];
240
- for (const e of ALL_ENGINES) {
241
- if (completed.has(e)) parts.push(`✅ ${e}`);
242
- else parts.push(`⏳ ${e}`);
243
- }
244
- if (completed.size >= 3) parts.push("🔄 synthesizing");
245
-
246
- onUpdate?.({
247
- content: [
248
- { type: "text", text: `**Researching...** ${parts.join(" · ")}` },
249
- ],
250
- details: { _progress: true },
251
- } as any);
252
- };
253
-
254
- try {
255
- const data = await runSearch(
256
- "all",
257
- query,
258
- ["--deep"],
259
- signal,
260
- onProgress,
261
- );
262
- const text = formatDeepResearch(data);
263
- return {
264
- content: [{ type: "text", text: text || "No results returned." }],
265
- details: { raw: data },
266
- };
267
- } catch (e) {
268
- const msg = e instanceof Error ? e.message : String(e);
269
- return {
270
- content: [{ type: "text", text: `Deep research failed: ${msg}` }],
271
- details: {} as { raw?: Record<string, unknown> },
272
- };
273
- }
274
- },
275
- });
276
-
277
- // ─── coding_task ───────────────────────────────────────────────────────────
278
- pi.registerTool({
279
- name: "coding_task",
280
- label: "Coding Task",
281
- description:
282
- "Delegate a coding task to Gemini and/or Copilot via browser automation. " +
283
- "Returns extracted code blocks and explanations. Supports multiple modes: " +
284
- "'code' (write/modify code), 'review' (senior engineer code review), " +
285
- "'plan' (architect risk assessment), 'test' (edge case testing), " +
286
- "'debug' (fresh-eyes root cause analysis). " +
287
- "Best for getting a 'second opinion' on hard problems, debugging tricky issues, " +
288
- "or risk-assessing major refactors. Use engine 'all' for both perspectives.",
289
- promptSnippet: "Browser-based coding assistant with Gemini and Copilot",
290
- parameters: Type.Object({
291
- task: Type.String({ description: "The coding task or question" }),
292
- engine: Type.Union(
293
- [Type.Literal("all"), Type.Literal("gemini"), Type.Literal("copilot")],
294
- {
295
- description:
296
- 'Engine to use. "all" runs both Gemini and Copilot in parallel.',
297
- default: "gemini",
298
- },
299
- ),
300
- mode: Type.Union(
301
- [
302
- Type.Literal("code"),
303
- Type.Literal("review"),
304
- Type.Literal("plan"),
305
- Type.Literal("test"),
306
- Type.Literal("debug"),
307
- ],
308
- {
309
- description:
310
- "Task mode: code (default), review (code review), plan (architect review), test (write tests), debug (root cause analysis)",
311
- default: "code",
312
- },
313
- ),
314
- context: Type.Optional(
315
- Type.String({
316
- description: "Optional code context/snippet to include with the task",
317
- }),
318
- ),
319
- }),
320
- execute: async (_toolCallId, params, signal, onUpdate) => {
321
- const {
322
- task,
323
- engine = "gemini",
324
- mode = "code",
325
- context,
326
- } = params as {
327
- task: string;
328
- engine: string;
329
- mode: string;
330
- context?: string;
331
- };
332
-
333
- if (!cdpAvailable()) {
334
- return {
335
- content: [
336
- { type: "text", text: "cdp.mjs missing — try reinstalling." },
337
- ],
338
- details: {} as { raw?: Record<string, unknown> },
339
- };
340
- }
341
-
342
- const flags: string[] = ["--engine", engine, "--mode", mode];
343
- if (context) flags.push("--context", context);
344
-
345
- try {
346
- onUpdate?.({
347
- content: [
348
- {
349
- type: "text",
350
- text: `**Coding task...** 🔄 ${engine === "all" ? "Gemini + Copilot" : engine} (${mode} mode)`,
351
- },
352
- ],
353
- details: { _progress: true },
354
- } as any);
355
-
356
- const data = await new Promise<Record<string, unknown>>(
357
- (resolve, reject) => {
358
- const proc = spawn(
359
- "node",
360
- [join(__dir, "bin", "coding-task.mjs"), task, ...flags],
361
- {
362
- stdio: ["ignore", "pipe", "pipe"],
363
- },
364
- );
365
- let out = "";
366
- let err = "";
367
-
368
- const onAbort = () => {
369
- proc.kill("SIGTERM");
370
- reject(new Error("Aborted"));
371
- };
372
- signal?.addEventListener("abort", onAbort, { once: true });
373
-
374
- proc.stdout.on("data", (d: Buffer) => (out += d));
375
- proc.stderr.on("data", (d: Buffer) => {
376
- err += d;
377
- });
378
- proc.on("close", (code: number) => {
379
- signal?.removeEventListener("abort", onAbort);
380
- if (code !== 0) {
381
- reject(
382
- new Error(
383
- err.trim() || `coding-task.mjs exited with code ${code}`,
384
- ),
385
- );
386
- } else {
387
- try {
388
- resolve(JSON.parse(out.trim()));
389
- } catch {
390
- reject(
391
- new Error(
392
- `Invalid JSON from coding-task.mjs: ${out.slice(0, 200)}`,
393
- ),
394
- );
395
- }
396
- }
397
- });
398
-
399
- // Timeout after 3 minutes
400
- setTimeout(() => {
401
- proc.kill("SIGTERM");
402
- reject(new Error("Coding task timed out after 180s"));
403
- }, 180000);
404
- },
405
- );
406
-
407
- const text = formatCodingTask(data);
408
- return {
409
- content: [{ type: "text", text: text || "No response." }],
410
- details: { raw: data },
411
- };
412
- } catch (e) {
413
- const msg = e instanceof Error ? e.message : String(e);
414
- return {
415
- content: [{ type: "text", text: `Coding task failed: ${msg}` }],
416
- details: {} as { raw?: Record<string, unknown> },
417
- };
418
- }
419
- },
420
- });
421
- }
1
+ /**
2
+ * GreedySearch Pi Extension
3
+ *
4
+ * Adds `greedy_search`, `deep_research`, and `coding_task` tools to Pi.
5
+ * Tool handlers are split into separate modules for maintainability.
6
+ *
7
+ * Reports streaming progress as each engine completes.
8
+ * Requires Chrome to be running (or it auto-launches a dedicated instance).
9
+ */
10
+
11
+ import { spawn } from "node:child_process";
12
+ import { dirname, join } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
15
+ import { Type } from "@sinclair/typebox";
16
+
17
+ import { formatCodingTask } from "./src/formatters/coding.js";
18
+ import { registerGreedySearchTool } from "./src/tools/greedy-search-handler.js";
19
+ import { registerDeepResearchTool } from "./src/tools/deep-research-handler.js";
20
+ import { cdpAvailable, type ProgressUpdate } from "./src/tools/shared.js";
21
+ import { DEFAULTS } from "./src/search/defaults.js";
22
+
23
+ const __dir = dirname(fileURLToPath(import.meta.url));
24
+
25
+ export default function greedySearchExtension(pi: ExtensionAPI) {
26
+ pi.on("session_start", async (_event, ctx) => {
27
+ if (!cdpAvailable(__dir)) {
28
+ ctx.ui.notify(
29
+ "GreedySearch: cdp.mjs missing from package directory — try reinstalling: pi install git:github.com/apmantza/GreedySearch-pi",
30
+ "warning",
31
+ );
32
+ }
33
+ });
34
+
35
+ // ─── greedy_search ────────────────────────────────────────────────────────
36
+ registerGreedySearchTool(pi, __dir);
37
+
38
+ // ─── deep_research ────────────────────────────────────────────────────────
39
+ registerDeepResearchTool(pi, __dir);
40
+
41
+ // ─── coding_task ───────────────────────────────────────────────────────────
42
+ pi.registerTool({
43
+ name: "coding_task",
44
+ label: "Coding Task",
45
+ description:
46
+ "Delegate a coding task to Gemini and/or Copilot via browser automation. " +
47
+ "Returns extracted code blocks and explanations. Supports multiple modes: " +
48
+ "'code' (write/modify code), 'review' (senior engineer code review), " +
49
+ "'plan' (architect risk assessment), 'test' (edge case testing), " +
50
+ "'debug' (fresh-eyes root cause analysis). " +
51
+ "Best for getting a 'second opinion' on hard problems, debugging tricky issues, " +
52
+ "or risk-assessing major refactors. Use engine 'all' for both perspectives.",
53
+ promptSnippet: "Browser-based coding assistant with Gemini and Copilot",
54
+ parameters: Type.Object({
55
+ task: Type.String({ description: "The coding task or question" }),
56
+ engine: Type.Union(
57
+ [Type.Literal("all"), Type.Literal("gemini"), Type.Literal("copilot")],
58
+ {
59
+ description:
60
+ 'Engine to use. "all" runs both Gemini and Copilot in parallel.',
61
+ default: "gemini",
62
+ },
63
+ ),
64
+ mode: Type.Union(
65
+ [Type.Literal("code"), Type.Literal("review"), Type.Literal("plan"), Type.Literal("test"), Type.Literal("debug")],
66
+ {
67
+ description: "Task mode: code (default), review (code review), plan (architect review), test (write tests), debug (root cause analysis)",
68
+ default: "code",
69
+ },
70
+ ),
71
+ context: Type.Optional(Type.String({ description: "Optional code context/snippet to include with the task" })),
72
+ }),
73
+ execute: async (_toolCallId, params, signal, onUpdate) => {
74
+ const { task, engine = "gemini", mode = "code", context } = params as {
75
+ task: string; engine: string; mode: string; context?: string;
76
+ };
77
+
78
+ if (!cdpAvailable(__dir)) {
79
+ return {
80
+ content: [{ type: "text", text: "cdp.mjs missing — try reinstalling." }],
81
+ details: {} as { raw?: Record<string, unknown> },
82
+ };
83
+ }
84
+
85
+ const flags: string[] = ["--engine", engine, "--mode", mode];
86
+ if (context) flags.push("--context", context);
87
+
88
+ try {
89
+ onUpdate?.({
90
+ content: [{ type: "text", text: `**Coding task...** 🔄 ${engine === "all" ? "Gemini + Copilot" : engine} (${mode} mode)` }],
91
+ details: { _progress: true },
92
+ } satisfies ProgressUpdate);
93
+
94
+ const data = await new Promise<Record<string, unknown>>((resolve, reject) => {
95
+ const proc = spawn("node", [join(__dir, "bin", "coding-task.mjs"), task, ...flags], {
96
+ stdio: ["ignore", "pipe", "pipe"],
97
+ });
98
+ let out = "";
99
+ let err = "";
100
+
101
+ const onAbort = () => { proc.kill("SIGTERM"); reject(new Error("Aborted")); };
102
+ signal?.addEventListener("abort", onAbort, { once: true });
103
+
104
+ proc.stdout.on("data", (d: Buffer) => (out += d));
105
+ proc.stderr.on("data", (d: Buffer) => (err += d));
106
+ proc.on("close", (code: number) => {
107
+ signal?.removeEventListener("abort", onAbort);
108
+ if (code !== 0) {
109
+ reject(new Error(err.trim() || `coding-task.mjs exited with code ${code}`));
110
+ } else {
111
+ try { resolve(JSON.parse(out.trim())); }
112
+ catch { reject(new Error(`Invalid JSON from coding-task.mjs: ${out.slice(0, 200)}`)); }
113
+ }
114
+ });
115
+
116
+ // Timeout after 3 minutes
117
+ setTimeout(() => { proc.kill("SIGTERM"); reject(new Error(`Coding task timed out after ${DEFAULTS.CODING_TASK_TIMEOUT / 1000}s`)); }, DEFAULTS.CODING_TASK_TIMEOUT);
118
+ });
119
+
120
+ const text = formatCodingTask(data);
121
+ return {
122
+ content: [{ type: "text", text: text || "No response." }],
123
+ details: { raw: data },
124
+ };
125
+ } catch (e) {
126
+ const msg = e instanceof Error ? e.message : String(e);
127
+ return {
128
+ content: [{ type: "text", text: `Coding task failed: ${msg}` }],
129
+ details: {} as { raw?: Record<string, unknown> },
130
+ };
131
+ }
132
+ },
133
+ });
134
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apmantza/greedysearch-pi",
3
- "version": "1.7.7",
3
+ "version": "1.8.0",
4
4
  "description": "Pi extension: multi-engine AI search (Perplexity, Bing Copilot, Google AI) via browser automation -- NO API KEYS needed. Extracts answers with sources, optional Gemini synthesis. Grounded AI answers from real browser interactions.",
5
5
  "type": "module",
6
6
  "keywords": [
package/src/github.mjs CHANGED
@@ -173,7 +173,12 @@ export async function fetchGitHubContent(url) {
173
173
  fetchTree(owner, repo, ref || "HEAD"),
174
174
  ]);
175
175
 
176
- const info = repoInfo.status === "fulfilled" ? repoInfo.value : null;
176
+ // If repo info failed (e.g. 404 repo doesn't exist), bail out
177
+ if (repoInfo.status === "rejected") {
178
+ return { ok: false, error: repoInfo.reason?.message || "Repo not found" };
179
+ }
180
+
181
+ const info = repoInfo.value;
177
182
  const readmeText = readme.status === "fulfilled" ? readme.value : "";
178
183
  const treeItems = tree.status === "fulfilled" ? tree.value : [];
179
184