@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.
package/index.ts CHANGED
@@ -1,278 +1,256 @@
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 { DEFAULTS } from "./src/search/defaults.mjs";
19
- import { registerDeepResearchTool } from "./src/tools/deep-research-handler.js";
20
- import { registerGreedySearchTool } from "./src/tools/greedy-search-handler.js";
21
- import { cdpAvailable, type ProgressUpdate } from "./src/tools/shared.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
- [
66
- Type.Literal("code"),
67
- Type.Literal("review"),
68
- Type.Literal("plan"),
69
- Type.Literal("test"),
70
- Type.Literal("debug"),
71
- ],
72
- {
73
- description:
74
- "Task mode: code (default), review (code review), plan (architect review), test (write tests), debug (root cause analysis)",
75
- default: "code",
76
- },
77
- ),
78
- context: Type.Optional(
79
- Type.String({
80
- description: "Optional code context/snippet to include with the task",
81
- }),
82
- ),
83
- }),
84
- execute: async (_toolCallId, params, signal, onUpdate) => {
85
- const {
86
- task,
87
- engine = "gemini",
88
- mode = "code",
89
- context,
90
- } = params as {
91
- task: string;
92
- engine: string;
93
- mode: string;
94
- context?: string;
95
- };
96
-
97
- if (!cdpAvailable(__dir)) {
98
- return {
99
- content: [
100
- { type: "text", text: "cdp.mjs missing — try reinstalling." },
101
- ],
102
- details: {} as { raw?: Record<string, unknown> },
103
- };
104
- }
105
-
106
- const flags: string[] = ["--engine", engine, "--mode", mode];
107
- if (context) flags.push("--context", context);
108
-
109
- try {
110
- onUpdate?.({
111
- content: [
112
- {
113
- type: "text",
114
- text: `**Coding task...** 🔄 ${engine === "all" ? "Gemini + Copilot" : engine} (${mode} mode)`,
115
- },
116
- ],
117
- details: { _progress: true },
118
- } satisfies ProgressUpdate);
119
-
120
- const data = await new Promise<Record<string, unknown>>(
121
- (resolve, reject) => {
122
- const proc = spawn(
123
- "node",
124
- [join(__dir, "bin", "coding-task.mjs"), task, ...flags],
125
- {
126
- stdio: ["ignore", "pipe", "pipe"],
127
- },
128
- );
129
- let out = "";
130
- let err = "";
131
-
132
- const onAbort = () => {
133
- proc.kill("SIGTERM");
134
- reject(new Error("Aborted"));
135
- };
136
- signal?.addEventListener("abort", onAbort, { once: true });
137
-
138
- proc.stdout.on("data", (d: Buffer) => (out += d));
139
- proc.stderr.on("data", (d: Buffer) => (err += d));
140
- proc.on("close", (code: number) => {
141
- signal?.removeEventListener("abort", onAbort);
142
- if (code !== 0) {
143
- reject(
144
- new Error(
145
- err.trim() || `coding-task.mjs exited with code ${code}`,
146
- ),
147
- );
148
- } else {
149
- try {
150
- resolve(JSON.parse(out.trim()));
151
- } catch {
152
- reject(
153
- new Error(
154
- `Invalid JSON from coding-task.mjs: ${out.slice(0, 200)}`,
155
- ),
156
- );
157
- }
158
- }
159
- });
160
-
161
- // Timeout after 3 minutes
162
- setTimeout(() => {
163
- proc.kill("SIGTERM");
164
- reject(
165
- new Error(
166
- `Coding task timed out after ${DEFAULTS.CODING_TASK_TIMEOUT / 1000}s`,
167
- ),
168
- );
169
- }, DEFAULTS.CODING_TASK_TIMEOUT);
170
- },
171
- );
172
-
173
- const text = formatCodingTask(data);
174
- return {
175
- content: [{ type: "text", text: text || "No response." }],
176
- details: { raw: data },
177
- };
178
- } catch (e) {
179
- const msg = e instanceof Error ? e.message : String(e);
180
- return {
181
- content: [{ type: "text", text: `Coding task failed: ${msg}` }],
182
- details: {} as { raw?: Record<string, unknown> },
183
- };
184
- }
185
- },
186
- });
187
-
188
- // ─── /set-greedy-locale command ───────────────────────────────────────────
189
- pi.registerCommand("set-greedy-locale", {
190
- description:
191
- "Set default locale for GreedySearch results (e.g., /set-greedy-locale de, /set-greedy-locale --clear, /set-greedy-locale --show)",
192
- handler: async (args, ctx) => {
193
- const arg = args.trim() || "--show";
194
-
195
- if (arg === "--show") {
196
- const config = loadUserConfig();
197
- if (config.locale) {
198
- ctx.ui.notify(`Default locale: ${config.locale}`, "info");
199
- } else {
200
- ctx.ui.notify("No default locale (uses: en)", "info");
201
- }
202
- return;
203
- }
204
-
205
- if (arg === "--clear") {
206
- const config = loadUserConfig();
207
- delete config.locale;
208
- saveUserConfig(config);
209
- ctx.ui.notify("Default locale cleared (now uses: en).", "info");
210
- return;
211
- }
212
-
213
- // Set locale
214
- const locale = arg.toLowerCase();
215
- const VALID_LOCALES = [
216
- "en",
217
- "de",
218
- "fr",
219
- "es",
220
- "it",
221
- "pt",
222
- "nl",
223
- "pl",
224
- "ru",
225
- "ja",
226
- "ko",
227
- "zh",
228
- "ar",
229
- "hi",
230
- "tr",
231
- "sv",
232
- "da",
233
- "no",
234
- "fi",
235
- "cs",
236
- "hu",
237
- "ro",
238
- "el",
239
- ];
240
-
241
- if (!VALID_LOCALES.includes(locale)) {
242
- ctx.ui.notify(
243
- `Invalid locale "${locale}". Valid: ${VALID_LOCALES.join(", ")}`,
244
- "error",
245
- );
246
- return;
247
- }
248
-
249
- const config = loadUserConfig();
250
- config.locale = locale;
251
- saveUserConfig(config);
252
- ctx.ui.notify(`Default locale set to: ${locale}`, "info");
253
- },
254
- });
255
- }
256
-
257
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
258
- // Config helpers for /set-greedy-locale command
259
- import { homedir } from "node:os";
260
-
261
- const USER_CONFIG_DIR = join(homedir(), ".config", "greedysearch");
262
- const USER_CONFIG_FILE = join(USER_CONFIG_DIR, "config.json");
263
-
264
- function loadUserConfig(): Record<string, string> {
265
- try {
266
- if (existsSync(USER_CONFIG_FILE)) {
267
- return JSON.parse(readFileSync(USER_CONFIG_FILE, "utf8"));
268
- }
269
- } catch {
270
- // Ignore parse errors
271
- }
272
- return {};
273
- }
274
-
275
- function saveUserConfig(config: Record<string, string>): void {
276
- mkdirSync(USER_CONFIG_DIR, { recursive: true });
277
- writeFileSync(USER_CONFIG_FILE, JSON.stringify(config, null, 2), "utf8");
278
- }
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 { DEFAULTS } from "./src/search/defaults.mjs";
19
+ import { registerDeepResearchTool } from "./src/tools/deep-research-handler.js";
20
+ import { registerGreedySearchTool } from "./src/tools/greedy-search-handler.js";
21
+ import { cdpAvailable, stripQuotes, type ProgressUpdate } from "./src/tools/shared.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.String({
57
+ description: 'Engine to use: "all" (runs both Gemini and Copilot in parallel), "gemini" (default), "copilot".',
58
+ default: "gemini",
59
+ }),
60
+ mode: Type.String({
61
+ description: 'Task mode: "code" (default), "review" (code review), "plan" (architect review), "test" (write tests), "debug" (root cause analysis).',
62
+ default: "code",
63
+ }),
64
+ context: Type.Optional(
65
+ Type.String({
66
+ description: "Optional code context/snippet to include with the task",
67
+ }),
68
+ ),
69
+ }),
70
+ execute: async (_toolCallId, params, signal, onUpdate) => {
71
+ const { task, context } = params as { task: string; engine: string; mode: string; context?: string };
72
+ const engine = stripQuotes((params as any).engine ?? "gemini") || "gemini";
73
+ const mode = stripQuotes((params as any).mode ?? "code") || "code";
74
+
75
+ if (!cdpAvailable(__dir)) {
76
+ return {
77
+ content: [
78
+ { type: "text", text: "cdp.mjs missing — try reinstalling." },
79
+ ],
80
+ details: {} as { raw?: Record<string, unknown> },
81
+ };
82
+ }
83
+
84
+ const flags: string[] = ["--engine", engine, "--mode", mode];
85
+ if (context) flags.push("--context", context);
86
+
87
+ try {
88
+ onUpdate?.({
89
+ content: [
90
+ {
91
+ type: "text",
92
+ text: `**Coding task...** 🔄 ${engine === "all" ? "Gemini + Copilot" : engine} (${mode} mode)`,
93
+ },
94
+ ],
95
+ details: { _progress: true },
96
+ } satisfies ProgressUpdate);
97
+
98
+ const data = await new Promise<Record<string, unknown>>(
99
+ (resolve, reject) => {
100
+ const proc = spawn(
101
+ "node",
102
+ [join(__dir, "bin", "coding-task.mjs"), task, ...flags],
103
+ {
104
+ stdio: ["ignore", "pipe", "pipe"],
105
+ },
106
+ );
107
+ let out = "";
108
+ let err = "";
109
+
110
+ const onAbort = () => {
111
+ proc.kill("SIGTERM");
112
+ reject(new Error("Aborted"));
113
+ };
114
+ signal?.addEventListener("abort", onAbort, { once: true });
115
+
116
+ proc.stdout.on("data", (d: Buffer) => (out += d));
117
+ proc.stderr.on("data", (d: Buffer) => (err += d));
118
+ proc.on("close", (code: number) => {
119
+ signal?.removeEventListener("abort", onAbort);
120
+ if (code !== 0) {
121
+ reject(
122
+ new Error(
123
+ err.trim() || `coding-task.mjs exited with code ${code}`,
124
+ ),
125
+ );
126
+ } else {
127
+ try {
128
+ resolve(JSON.parse(out.trim()));
129
+ } catch {
130
+ reject(
131
+ new Error(
132
+ `Invalid JSON from coding-task.mjs: ${out.slice(0, 200)}`,
133
+ ),
134
+ );
135
+ }
136
+ }
137
+ });
138
+
139
+ // Timeout after 3 minutes
140
+ setTimeout(() => {
141
+ proc.kill("SIGTERM");
142
+ reject(
143
+ new Error(
144
+ `Coding task timed out after ${DEFAULTS.CODING_TASK_TIMEOUT / 1000}s`,
145
+ ),
146
+ );
147
+ }, DEFAULTS.CODING_TASK_TIMEOUT);
148
+ },
149
+ );
150
+
151
+ const text = formatCodingTask(data);
152
+ return {
153
+ content: [{ type: "text", text: text || "No response." }],
154
+ details: { raw: data },
155
+ };
156
+ } catch (e) {
157
+ const msg = e instanceof Error ? e.message : String(e);
158
+ return {
159
+ content: [{ type: "text", text: `Coding task failed: ${msg}` }],
160
+ details: {} as { raw?: Record<string, unknown> },
161
+ };
162
+ }
163
+ },
164
+ });
165
+
166
+ // ─── /set-greedy-locale command ───────────────────────────────────────────
167
+ pi.registerCommand("set-greedy-locale", {
168
+ description:
169
+ "Set default locale for GreedySearch results (e.g., /set-greedy-locale de, /set-greedy-locale --clear, /set-greedy-locale --show)",
170
+ handler: async (args, ctx) => {
171
+ const arg = args.trim() || "--show";
172
+
173
+ if (arg === "--show") {
174
+ const config = loadUserConfig();
175
+ if (config.locale) {
176
+ ctx.ui.notify(`Default locale: ${config.locale}`, "info");
177
+ } else {
178
+ ctx.ui.notify("No default locale (uses: en)", "info");
179
+ }
180
+ return;
181
+ }
182
+
183
+ if (arg === "--clear") {
184
+ const config = loadUserConfig();
185
+ delete config.locale;
186
+ saveUserConfig(config);
187
+ ctx.ui.notify("Default locale cleared (now uses: en).", "info");
188
+ return;
189
+ }
190
+
191
+ // Set locale
192
+ const locale = arg.toLowerCase();
193
+ const VALID_LOCALES = [
194
+ "en",
195
+ "de",
196
+ "fr",
197
+ "es",
198
+ "it",
199
+ "pt",
200
+ "nl",
201
+ "pl",
202
+ "ru",
203
+ "ja",
204
+ "ko",
205
+ "zh",
206
+ "ar",
207
+ "hi",
208
+ "tr",
209
+ "sv",
210
+ "da",
211
+ "no",
212
+ "fi",
213
+ "cs",
214
+ "hu",
215
+ "ro",
216
+ "el",
217
+ ];
218
+
219
+ if (!VALID_LOCALES.includes(locale)) {
220
+ ctx.ui.notify(
221
+ `Invalid locale "${locale}". Valid: ${VALID_LOCALES.join(", ")}`,
222
+ "error",
223
+ );
224
+ return;
225
+ }
226
+
227
+ const config = loadUserConfig();
228
+ config.locale = locale;
229
+ saveUserConfig(config);
230
+ ctx.ui.notify(`Default locale set to: ${locale}`, "info");
231
+ },
232
+ });
233
+ }
234
+
235
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
236
+ // Config helpers for /set-greedy-locale command
237
+ import { homedir } from "node:os";
238
+
239
+ const USER_CONFIG_DIR = join(homedir(), ".config", "greedysearch");
240
+ const USER_CONFIG_FILE = join(USER_CONFIG_DIR, "config.json");
241
+
242
+ function loadUserConfig(): Record<string, string> {
243
+ try {
244
+ if (existsSync(USER_CONFIG_FILE)) {
245
+ return JSON.parse(readFileSync(USER_CONFIG_FILE, "utf8"));
246
+ }
247
+ } catch {
248
+ // Ignore parse errors
249
+ }
250
+ return {};
251
+ }
252
+
253
+ function saveUserConfig(config: Record<string, string>): void {
254
+ mkdirSync(USER_CONFIG_DIR, { recursive: true });
255
+ writeFileSync(USER_CONFIG_FILE, JSON.stringify(config, null, 2), "utf8");
256
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apmantza/greedysearch-pi",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
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": [