@apmantza/greedysearch-pi 1.7.3 → 1.7.5
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 +18 -0
- package/bin/coding-task.mjs +4 -26
- package/bin/search.mjs +1 -27
- package/extractors/bing-copilot.mjs +8 -20
- package/extractors/common.mjs +43 -1
- package/extractors/gemini.mjs +20 -35
- package/extractors/google-ai.mjs +10 -42
- package/package.json +1 -1
- package/skills/greedy-search/skill.md +44 -0
- package/skills/greedy-search/SKILL.md +0 -117
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.7.5 (2026-04-10)
|
|
4
|
+
|
|
5
|
+
### Plugin
|
|
6
|
+
- **Claude Code plugin** — added `.claude-plugin/plugin.json` and `marketplace.json` so GreedySearch can be installed directly as a Claude Code plugin via `claude plugin install`.
|
|
7
|
+
- **Auto-mirror GH Action** — every push to `GreedySearch-pi/master` automatically syncs to `GreedySearch-claude/main`, keeping the Claude plugin up to date.
|
|
8
|
+
- **Tightened `skill.md`** — removed verbose guidance sections; kept parameters, depth table, and coding_task reference. -72 lines.
|
|
9
|
+
|
|
10
|
+
## v1.7.4 (2026-04-10)
|
|
11
|
+
|
|
12
|
+
### Refactor
|
|
13
|
+
- **Shared `waitForCopyButton()`** — consolidated duplicate copy-button polling loops from `bing-copilot`, `gemini`, and `coding-task` into a single `waitForCopyButton(tab, selector, { timeout, onPoll })` in `common.mjs`. Gemini's scroll-to-bottom logic passed as `onPoll` callback.
|
|
14
|
+
- **Shared `TIMING` constants** — replaced 30+ scattered `setTimeout` magic numbers with named constants (`postNav`, `postNavSlow`, `postClick`, `postType`, `inputPoll`, `copyPoll`, `afterVerify`) in `common.mjs`.
|
|
15
|
+
- **`waitForStreamComplete` improvements** — added `minLength` option and graceful last-value fallback; `google-ai` now uses the shared implementation instead of its own copy.
|
|
16
|
+
- **Removed dead code** — deleted unused `_getOrReuseBlankTab` and `_getOrOpenEngineTab` from `bin/search.mjs`; removed unused `STREAM_POLL_INTERVAL` and `STREAM_STABLE_ROUNDS` from `coding-task`.
|
|
17
|
+
|
|
18
|
+
### Fixes
|
|
19
|
+
- **Synthesis tab regression** — `getOrOpenEngineTab("gemini")` call during synthesis was broken by the dead-code removal; replaced with `openNewTab()`.
|
|
20
|
+
|
|
3
21
|
## v1.7.3 (2026-04-10)
|
|
4
22
|
|
|
5
23
|
### Fixes
|
package/bin/coding-task.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { tmpdir } from "node:os";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
|
-
import { cdp, injectClipboardInterceptor } from "../extractors/common.mjs";
|
|
15
|
+
import { cdp, injectClipboardInterceptor, waitForCopyButton } from "../extractors/common.mjs";
|
|
16
16
|
import { dismissConsent, handleVerification } from "../extractors/consent.mjs";
|
|
17
17
|
|
|
18
18
|
const __dir = fileURLToPath(new URL(".", import.meta.url));
|
|
@@ -31,9 +31,7 @@ const MODE_PROMPTS = {
|
|
|
31
31
|
debug: `You are a senior engineer debugging someone else's code. You have fresh eyes — no prior assumptions about what should work. Given the bug description and relevant code: (1) identify the most likely root cause, being specific about the exact line or condition, (2) explain why it manifests the way it does, (3) suggest the minimal fix, (4) flag any other latent bugs you notice while reading. Do not guess vaguely — reason from the code. If you need information that isn't provided, say exactly what you'd add to narrow it down.`,
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
const
|
|
35
|
-
const STREAM_STABLE_ROUNDS = 4;
|
|
36
|
-
const STREAM_TIMEOUT = 120000; // coding tasks take longer
|
|
34
|
+
const STREAM_TIMEOUT = 120000; // coding tasks take longer — passed to waitForCopyButton
|
|
37
35
|
const MIN_RESPONSE_LENGTH = 50;
|
|
38
36
|
|
|
39
37
|
// ---------------------------------------------------------------------------
|
|
@@ -104,17 +102,7 @@ const ENGINES = {
|
|
|
104
102
|
},
|
|
105
103
|
|
|
106
104
|
async waitForCopyButton(tab) {
|
|
107
|
-
|
|
108
|
-
while (Date.now() < deadline) {
|
|
109
|
-
await new Promise((r) => setTimeout(r, STREAM_POLL_INTERVAL));
|
|
110
|
-
const found = await cdp([
|
|
111
|
-
"eval",
|
|
112
|
-
tab,
|
|
113
|
-
`!!document.querySelector('button[aria-label="Copy"]')`,
|
|
114
|
-
]).catch(() => "false");
|
|
115
|
-
if (found === "true") return;
|
|
116
|
-
}
|
|
117
|
-
throw new Error("Gemini copy button did not appear within timeout");
|
|
105
|
+
await waitForCopyButton(tab, 'button[aria-label="Copy"]', { timeout: STREAM_TIMEOUT });
|
|
118
106
|
},
|
|
119
107
|
|
|
120
108
|
async extract(tab) {
|
|
@@ -162,17 +150,7 @@ const ENGINES = {
|
|
|
162
150
|
},
|
|
163
151
|
|
|
164
152
|
async waitForCopyButton(tab) {
|
|
165
|
-
|
|
166
|
-
while (Date.now() < deadline) {
|
|
167
|
-
await new Promise((r) => setTimeout(r, STREAM_POLL_INTERVAL));
|
|
168
|
-
const found = await cdp([
|
|
169
|
-
"eval",
|
|
170
|
-
tab,
|
|
171
|
-
`!!document.querySelector('button[data-testid="copy-ai-message-button"]')`,
|
|
172
|
-
]).catch(() => "false");
|
|
173
|
-
if (found === "true") return;
|
|
174
|
-
}
|
|
175
|
-
throw new Error("Copilot copy button did not appear within timeout");
|
|
153
|
+
await waitForCopyButton(tab, 'button[data-testid="copy-ai-message-button"]', { timeout: STREAM_TIMEOUT });
|
|
176
154
|
},
|
|
177
155
|
|
|
178
156
|
async extract(tab) {
|
package/bin/search.mjs
CHANGED
|
@@ -752,27 +752,6 @@ async function getAnyTab() {
|
|
|
752
752
|
return first.slice(0, 8);
|
|
753
753
|
}
|
|
754
754
|
|
|
755
|
-
async function _getOrReuseBlankTab() {
|
|
756
|
-
// Reuse an existing about:blank tab rather than always creating a new one
|
|
757
|
-
const listOut = await cdp(["list"]);
|
|
758
|
-
const lines = listOut.split("\n").filter(Boolean);
|
|
759
|
-
for (const line of lines) {
|
|
760
|
-
if (line.includes("about:blank")) {
|
|
761
|
-
return line.slice(0, 8); // prefix of the blank tab's targetId
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
// No blank tab — open a new one
|
|
765
|
-
const anchor = await getAnyTab();
|
|
766
|
-
const raw = await cdp([
|
|
767
|
-
"evalraw",
|
|
768
|
-
anchor,
|
|
769
|
-
"Target.createTarget",
|
|
770
|
-
'{"url":"about:blank"}',
|
|
771
|
-
]);
|
|
772
|
-
const { targetId } = JSON.parse(raw);
|
|
773
|
-
return targetId;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
755
|
async function openNewTab() {
|
|
777
756
|
const anchor = await getAnyTab();
|
|
778
757
|
const raw = await cdp([
|
|
@@ -785,11 +764,6 @@ async function openNewTab() {
|
|
|
785
764
|
return targetId;
|
|
786
765
|
}
|
|
787
766
|
|
|
788
|
-
async function _getOrOpenEngineTab(engine) {
|
|
789
|
-
await cdp(["list"]);
|
|
790
|
-
return getFullTabFromCache(engine) || openNewTab();
|
|
791
|
-
}
|
|
792
|
-
|
|
793
767
|
async function activateTab(targetId) {
|
|
794
768
|
try {
|
|
795
769
|
const anchor = await getAnyTab();
|
|
@@ -1487,7 +1461,7 @@ async function main() {
|
|
|
1487
1461
|
"[greedysearch] Synthesizing results with Gemini...\n",
|
|
1488
1462
|
);
|
|
1489
1463
|
try {
|
|
1490
|
-
const geminiTab = await
|
|
1464
|
+
const geminiTab = await openNewTab();
|
|
1491
1465
|
await activateTab(geminiTab);
|
|
1492
1466
|
const synthesis = await synthesizeWithGemini(query, out, {
|
|
1493
1467
|
grounded: depth === "deep",
|
|
@@ -18,7 +18,9 @@ import {
|
|
|
18
18
|
outputJson,
|
|
19
19
|
parseArgs,
|
|
20
20
|
parseSourcesFromMarkdown,
|
|
21
|
+
TIMING,
|
|
21
22
|
validateQuery,
|
|
23
|
+
waitForCopyButton,
|
|
22
24
|
} from "./common.mjs";
|
|
23
25
|
import { dismissConsent, handleVerification } from "./consent.mjs";
|
|
24
26
|
import { SELECTORS } from "./selectors.mjs";
|
|
@@ -30,20 +32,6 @@ const GLOBAL_VAR = "__bingClipboard";
|
|
|
30
32
|
// Bing Copilot-specific helpers
|
|
31
33
|
// ============================================================================
|
|
32
34
|
|
|
33
|
-
async function waitForCopyButton(tab, timeout = 60000) {
|
|
34
|
-
const deadline = Date.now() + timeout;
|
|
35
|
-
while (Date.now() < deadline) {
|
|
36
|
-
await new Promise((r) => setTimeout(r, 700));
|
|
37
|
-
const found = await cdp([
|
|
38
|
-
"eval",
|
|
39
|
-
tab,
|
|
40
|
-
`!!document.querySelector('${S.copyButton}')`,
|
|
41
|
-
]).catch(() => "false");
|
|
42
|
-
if (found === "true") return;
|
|
43
|
-
}
|
|
44
|
-
throw new Error(`Copilot copy button did not appear within ${timeout}ms`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
35
|
async function extractAnswer(tab) {
|
|
48
36
|
await cdp([
|
|
49
37
|
"eval",
|
|
@@ -78,7 +66,7 @@ async function main() {
|
|
|
78
66
|
|
|
79
67
|
// Navigate to Copilot homepage and use the chat input
|
|
80
68
|
await cdp(["nav", tab, "https://copilot.microsoft.com/"], 35000);
|
|
81
|
-
await new Promise((r) => setTimeout(r,
|
|
69
|
+
await new Promise((r) => setTimeout(r, TIMING.postNavSlow));
|
|
82
70
|
await dismissConsent(tab, cdp);
|
|
83
71
|
|
|
84
72
|
// Handle verification challenges (Cloudflare Turnstile, Microsoft auth, etc.)
|
|
@@ -91,7 +79,7 @@ async function main() {
|
|
|
91
79
|
|
|
92
80
|
// After verification, page may have redirected or reloaded — wait for it to settle
|
|
93
81
|
if (verifyResult === "clicked") {
|
|
94
|
-
await new Promise((r) => setTimeout(r,
|
|
82
|
+
await new Promise((r) => setTimeout(r, TIMING.afterVerify));
|
|
95
83
|
|
|
96
84
|
// Re-navigate if we got redirected
|
|
97
85
|
const currentUrl = await cdp([
|
|
@@ -101,7 +89,7 @@ async function main() {
|
|
|
101
89
|
]).catch(() => "");
|
|
102
90
|
if (!currentUrl.includes("copilot.microsoft.com")) {
|
|
103
91
|
await cdp(["nav", tab, "https://copilot.microsoft.com/"], 35000);
|
|
104
|
-
await new Promise((r) => setTimeout(r,
|
|
92
|
+
await new Promise((r) => setTimeout(r, TIMING.postNavSlow));
|
|
105
93
|
await dismissConsent(tab, cdp);
|
|
106
94
|
}
|
|
107
95
|
}
|
|
@@ -133,9 +121,9 @@ async function main() {
|
|
|
133
121
|
|
|
134
122
|
await injectClipboardInterceptor(tab, GLOBAL_VAR);
|
|
135
123
|
await cdp(["click", tab, S.input]);
|
|
136
|
-
await new Promise((r) => setTimeout(r,
|
|
124
|
+
await new Promise((r) => setTimeout(r, TIMING.postClick));
|
|
137
125
|
await cdp(["type", tab, query]);
|
|
138
|
-
await new Promise((r) => setTimeout(r,
|
|
126
|
+
await new Promise((r) => setTimeout(r, TIMING.postType));
|
|
139
127
|
|
|
140
128
|
// Submit with Enter (most reliable across locales and Chrome instances)
|
|
141
129
|
await cdp([
|
|
@@ -144,7 +132,7 @@ async function main() {
|
|
|
144
132
|
`document.querySelector('${S.input}')?.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true,keyCode:13})), 'ok'`,
|
|
145
133
|
]);
|
|
146
134
|
|
|
147
|
-
await waitForCopyButton(tab);
|
|
135
|
+
await waitForCopyButton(tab, S.copyButton, { timeout: 60000 });
|
|
148
136
|
|
|
149
137
|
const { answer, sources } = await extractAnswer(tab);
|
|
150
138
|
if (!answer)
|
package/extractors/common.mjs
CHANGED
|
@@ -119,6 +119,46 @@ export function parseSourcesFromMarkdown(text) {
|
|
|
119
119
|
.slice(0, 10);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// ============================================================================
|
|
123
|
+
// Timing constants
|
|
124
|
+
// ============================================================================
|
|
125
|
+
|
|
126
|
+
export const TIMING = {
|
|
127
|
+
postNav: 1500, // settle after navigation
|
|
128
|
+
postNavSlow: 2000, // settle after slower navigations (Bing, Gemini)
|
|
129
|
+
postClick: 400, // settle after a UI click
|
|
130
|
+
postType: 400, // settle after typing
|
|
131
|
+
inputPoll: 400, // polling interval when waiting for input to appear
|
|
132
|
+
copyPoll: 600, // polling interval when waiting for copy button
|
|
133
|
+
afterVerify: 3000, // settle after a verification challenge completes
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// Copy button polling
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Wait for a copy button to appear in the DOM.
|
|
142
|
+
* @param {string} tab - Tab identifier
|
|
143
|
+
* @param {string} selector - CSS selector for the copy button
|
|
144
|
+
* @param {object} [options]
|
|
145
|
+
* @param {number} [options.timeout=60000] - Max wait in ms
|
|
146
|
+
* @param {Function} [options.onPoll] - Optional async callback on each poll tick (e.g. scroll)
|
|
147
|
+
* @returns {Promise<void>}
|
|
148
|
+
*/
|
|
149
|
+
export async function waitForCopyButton(tab, selector, options = {}) {
|
|
150
|
+
const { timeout = 60000, onPoll } = options;
|
|
151
|
+
const deadline = Date.now() + timeout;
|
|
152
|
+
let tick = 0;
|
|
153
|
+
while (Date.now() < deadline) {
|
|
154
|
+
await new Promise((r) => setTimeout(r, TIMING.copyPoll));
|
|
155
|
+
if (onPoll) await onPoll(++tick).catch(() => null);
|
|
156
|
+
const found = await cdp(["eval", tab, `!!document.querySelector('${selector}')`]).catch(() => "false");
|
|
157
|
+
if (found === "true") return;
|
|
158
|
+
}
|
|
159
|
+
throw new Error(`Copy button ('${selector}') did not appear within ${timeout}ms`);
|
|
160
|
+
}
|
|
161
|
+
|
|
122
162
|
// ============================================================================
|
|
123
163
|
// Stream completion detection
|
|
124
164
|
// ============================================================================
|
|
@@ -139,6 +179,7 @@ export async function waitForStreamComplete(tab, options = {}) {
|
|
|
139
179
|
interval = 600,
|
|
140
180
|
stableRounds = 3,
|
|
141
181
|
selector = "document.body",
|
|
182
|
+
minLength = 0,
|
|
142
183
|
} = options;
|
|
143
184
|
|
|
144
185
|
const deadline = Date.now() + timeout;
|
|
@@ -154,7 +195,7 @@ export async function waitForStreamComplete(tab, options = {}) {
|
|
|
154
195
|
]).catch(() => "0");
|
|
155
196
|
const currentLen = parseInt(lenStr, 10) || 0;
|
|
156
197
|
|
|
157
|
-
if (currentLen
|
|
198
|
+
if (currentLen >= minLength) {
|
|
158
199
|
if (currentLen === lastLen) {
|
|
159
200
|
stableCount++;
|
|
160
201
|
if (stableCount >= stableRounds) return currentLen;
|
|
@@ -165,6 +206,7 @@ export async function waitForStreamComplete(tab, options = {}) {
|
|
|
165
206
|
}
|
|
166
207
|
}
|
|
167
208
|
|
|
209
|
+
if (lastLen >= minLength) return lastLen;
|
|
168
210
|
throw new Error(`Generation did not stabilise within ${timeout}ms`);
|
|
169
211
|
}
|
|
170
212
|
|
package/extractors/gemini.mjs
CHANGED
|
@@ -18,7 +18,9 @@ import {
|
|
|
18
18
|
outputJson,
|
|
19
19
|
parseArgs,
|
|
20
20
|
parseSourcesFromMarkdown,
|
|
21
|
+
TIMING,
|
|
21
22
|
validateQuery,
|
|
23
|
+
waitForCopyButton,
|
|
22
24
|
} from "./common.mjs";
|
|
23
25
|
import { dismissConsent, handleVerification } from "./consent.mjs";
|
|
24
26
|
import { SELECTORS } from "./selectors.mjs";
|
|
@@ -46,36 +48,15 @@ async function typeIntoGemini(tab, text) {
|
|
|
46
48
|
]);
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
async function
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"eval",
|
|
59
|
-
tab,
|
|
60
|
-
`
|
|
61
|
-
(function() {
|
|
62
|
-
const chat = document.querySelector('chat-window, [role="main"], main') || document.body;
|
|
63
|
-
const scrollHeight = chat.scrollHeight || document.body.scrollHeight || 0;
|
|
64
|
-
// Scroll to bottom to ensure all content is loaded
|
|
65
|
-
chat.scrollTo ? chat.scrollTo({ top: scrollHeight, behavior: 'smooth' }) : window.scrollTo(0, scrollHeight);
|
|
66
|
-
})()
|
|
67
|
-
`,
|
|
68
|
-
]).catch(() => null);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const found = await cdp([
|
|
72
|
-
"eval",
|
|
73
|
-
tab,
|
|
74
|
-
`!!document.querySelector('${S.copyButton}')`,
|
|
75
|
-
]).catch(() => "false");
|
|
76
|
-
if (found === "true") return;
|
|
77
|
-
}
|
|
78
|
-
throw new Error(`Gemini copy button did not appear within ${timeout}ms`);
|
|
51
|
+
async function scrollToBottom(tab) {
|
|
52
|
+
await cdp([
|
|
53
|
+
"eval",
|
|
54
|
+
tab,
|
|
55
|
+
`(function() {
|
|
56
|
+
const chat = document.querySelector('chat-window, [role="main"], main') || document.body;
|
|
57
|
+
chat.scrollTo ? chat.scrollTo({ top: chat.scrollHeight, behavior: 'smooth' }) : window.scrollTo(0, document.body.scrollHeight);
|
|
58
|
+
})()`,
|
|
59
|
+
]);
|
|
79
60
|
}
|
|
80
61
|
|
|
81
62
|
async function extractAnswer(tab) {
|
|
@@ -111,7 +92,7 @@ async function main() {
|
|
|
111
92
|
|
|
112
93
|
// Each search = fresh conversation
|
|
113
94
|
await cdp(["nav", tab, "https://gemini.google.com/app"], 35000);
|
|
114
|
-
await new Promise((r) => setTimeout(r,
|
|
95
|
+
await new Promise((r) => setTimeout(r, TIMING.postNavSlow));
|
|
115
96
|
await dismissConsent(tab, cdp);
|
|
116
97
|
await handleVerification(tab, cdp, 60000);
|
|
117
98
|
|
|
@@ -124,13 +105,13 @@ async function main() {
|
|
|
124
105
|
`!!document.querySelector('${S.input}')`,
|
|
125
106
|
]).catch(() => "false");
|
|
126
107
|
if (ready === "true") break;
|
|
127
|
-
await new Promise((r) => setTimeout(r,
|
|
108
|
+
await new Promise((r) => setTimeout(r, TIMING.inputPoll));
|
|
128
109
|
}
|
|
129
|
-
await new Promise((r) => setTimeout(r,
|
|
110
|
+
await new Promise((r) => setTimeout(r, TIMING.postClick));
|
|
130
111
|
|
|
131
112
|
await injectClipboardInterceptor(tab, GLOBAL_VAR);
|
|
132
113
|
await typeIntoGemini(tab, query);
|
|
133
|
-
await new Promise((r) => setTimeout(r,
|
|
114
|
+
await new Promise((r) => setTimeout(r, TIMING.postType));
|
|
134
115
|
|
|
135
116
|
await cdp([
|
|
136
117
|
"eval",
|
|
@@ -138,7 +119,11 @@ async function main() {
|
|
|
138
119
|
`document.querySelector('${S.sendButton}')?.click()`,
|
|
139
120
|
]);
|
|
140
121
|
|
|
141
|
-
|
|
122
|
+
// Scroll to bottom every ~6s while waiting to trigger lazy-loaded content
|
|
123
|
+
await waitForCopyButton(tab, S.copyButton, {
|
|
124
|
+
timeout: 120000,
|
|
125
|
+
onPoll: (tick) => tick % 10 === 0 ? scrollToBottom(tab) : Promise.resolve(),
|
|
126
|
+
});
|
|
142
127
|
|
|
143
128
|
const { answer, sources } = await extractAnswer(tab);
|
|
144
129
|
if (!answer) throw new Error("No answer captured from Gemini clipboard");
|
package/extractors/google-ai.mjs
CHANGED
|
@@ -16,53 +16,17 @@ import {
|
|
|
16
16
|
handleError,
|
|
17
17
|
outputJson,
|
|
18
18
|
parseArgs,
|
|
19
|
+
TIMING,
|
|
19
20
|
validateQuery,
|
|
21
|
+
waitForStreamComplete,
|
|
20
22
|
} from "./common.mjs";
|
|
21
23
|
import { dismissConsent, handleVerification } from "./consent.mjs";
|
|
22
24
|
import { SELECTORS } from "./selectors.mjs";
|
|
23
25
|
|
|
24
26
|
const S = SELECTORS.google;
|
|
25
27
|
|
|
26
|
-
const STREAM_POLL_INTERVAL = 600;
|
|
27
|
-
const STREAM_STABLE_ROUNDS = 3;
|
|
28
|
-
const STREAM_TIMEOUT = 45000;
|
|
29
28
|
const MIN_ANSWER_LENGTH = 50;
|
|
30
29
|
|
|
31
|
-
// ============================================================================
|
|
32
|
-
// Google AI-specific helpers
|
|
33
|
-
// ============================================================================
|
|
34
|
-
|
|
35
|
-
async function waitForGoogleStreamComplete(tab) {
|
|
36
|
-
const deadline = Date.now() + STREAM_TIMEOUT;
|
|
37
|
-
let stableCount = 0;
|
|
38
|
-
let lastLen = -1;
|
|
39
|
-
|
|
40
|
-
while (Date.now() < deadline) {
|
|
41
|
-
await new Promise((r) => setTimeout(r, STREAM_POLL_INTERVAL));
|
|
42
|
-
|
|
43
|
-
const lenStr = await cdp([
|
|
44
|
-
"eval",
|
|
45
|
-
tab,
|
|
46
|
-
`(document.querySelector('${S.answerContainer}')?.innerText?.length || 0) + ''`,
|
|
47
|
-
]).catch(() => "0");
|
|
48
|
-
|
|
49
|
-
const len = parseInt(lenStr, 10) || 0;
|
|
50
|
-
|
|
51
|
-
if (len >= MIN_ANSWER_LENGTH && len === lastLen) {
|
|
52
|
-
stableCount++;
|
|
53
|
-
if (stableCount >= STREAM_STABLE_ROUNDS) return len;
|
|
54
|
-
} else {
|
|
55
|
-
stableCount = 0;
|
|
56
|
-
lastLen = len;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (lastLen >= MIN_ANSWER_LENGTH) return lastLen;
|
|
61
|
-
throw new Error(
|
|
62
|
-
`Google AI answer did not stabilise within ${STREAM_TIMEOUT}ms`,
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
30
|
async function extractAnswer(tab) {
|
|
67
31
|
const excludeFilter = S.sourceExclude
|
|
68
32
|
.map((e) => `!a.href.includes('${e}')`)
|
|
@@ -107,7 +71,7 @@ async function main() {
|
|
|
107
71
|
|
|
108
72
|
const url = `https://www.google.com/search?q=${encodeURIComponent(query)}&udm=50&hl=en`;
|
|
109
73
|
await cdp(["nav", tab, url], 35000);
|
|
110
|
-
await new Promise((r) => setTimeout(r,
|
|
74
|
+
await new Promise((r) => setTimeout(r, TIMING.postNav));
|
|
111
75
|
await dismissConsent(tab, cdp);
|
|
112
76
|
|
|
113
77
|
// If consent redirected us away, navigate back
|
|
@@ -116,7 +80,7 @@ async function main() {
|
|
|
116
80
|
);
|
|
117
81
|
if (!currentUrl.includes("google.com/search")) {
|
|
118
82
|
await cdp(["nav", tab, url], 35000);
|
|
119
|
-
await new Promise((r) => setTimeout(r,
|
|
83
|
+
await new Promise((r) => setTimeout(r, TIMING.postNav));
|
|
120
84
|
}
|
|
121
85
|
|
|
122
86
|
// Handle "verify you're human" — auto-click simple buttons, wait for user on hard CAPTCHA
|
|
@@ -128,10 +92,14 @@ async function main() {
|
|
|
128
92
|
if (verifyResult === "clicked" || verifyResult === "cleared-by-user") {
|
|
129
93
|
// Re-navigate to the search URL after verification
|
|
130
94
|
await cdp(["nav", tab, url], 35000);
|
|
131
|
-
await new Promise((r) => setTimeout(r,
|
|
95
|
+
await new Promise((r) => setTimeout(r, TIMING.postNav));
|
|
132
96
|
}
|
|
133
97
|
|
|
134
|
-
await
|
|
98
|
+
await waitForStreamComplete(tab, {
|
|
99
|
+
timeout: 45000,
|
|
100
|
+
selector: `document.querySelector('${S.answerContainer}')`,
|
|
101
|
+
minLength: MIN_ANSWER_LENGTH,
|
|
102
|
+
});
|
|
135
103
|
|
|
136
104
|
const { answer, sources } = await extractAnswer(tab);
|
|
137
105
|
if (!answer)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apmantza/greedysearch-pi",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.5",
|
|
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": [
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: greedy-search
|
|
3
|
+
description: Live web search via Perplexity, Bing, and Google AI in parallel. Use for library docs, recent framework changes, error messages, dependency selection, or anything where training data may be stale. NOT for codebase search.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# GreedySearch — Live Web Search
|
|
7
|
+
|
|
8
|
+
Runs Perplexity, Bing Copilot, and Google AI in parallel. Gemini synthesizes results.
|
|
9
|
+
|
|
10
|
+
## greedy_search
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
greedy_search({ query: "React 19 changes", depth: "standard" })
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
| Parameter | Type | Default | Description |
|
|
17
|
+
|-----------|------|---------|-------------|
|
|
18
|
+
| `query` | string | required | Search question |
|
|
19
|
+
| `engine` | string | `"all"` | `all`, `perplexity`, `bing`, `google`, `gemini` |
|
|
20
|
+
| `depth` | string | `"standard"` | `fast`, `standard`, `deep` |
|
|
21
|
+
| `fullAnswer` | boolean | `false` | Full answer vs ~300 char summary |
|
|
22
|
+
|
|
23
|
+
| Depth | Engines | Synthesis | Source Fetch | Time |
|
|
24
|
+
|-------|---------|-----------|--------------|------|
|
|
25
|
+
| `fast` | 1 | — | — | 15-30s |
|
|
26
|
+
| `standard` | 3 | Gemini | — | 30-90s |
|
|
27
|
+
| `deep` | 3 | Gemini | top 5 | 60-180s |
|
|
28
|
+
|
|
29
|
+
**When engines agree** → high confidence. **When they diverge** → note both perspectives.
|
|
30
|
+
|
|
31
|
+
## coding_task
|
|
32
|
+
|
|
33
|
+
Second opinion from Gemini/Copilot on hard problems.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
coding_task({ task: "debug race condition", mode: "debug", engine: "gemini" })
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
| Parameter | Type | Default | Options |
|
|
40
|
+
|-----------|------|---------|---------|
|
|
41
|
+
| `task` | string | required | — |
|
|
42
|
+
| `engine` | string | `"gemini"` | `gemini`, `copilot`, `all` |
|
|
43
|
+
| `mode` | string | `"code"` | `debug`, `plan`, `review`, `test`, `code` |
|
|
44
|
+
| `context` | string | — | Code snippet |
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: greedy-search
|
|
3
|
-
description: Multi-engine AI **WEB SEARCH** tool — NOT for codebase search. Use greedy_search for high-quality web research where training data may be stale or single-engine results are insufficient. Searches Perplexity, Bing, Google via browser automation. NO API KEYS needed.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# ⚠️ WEB SEARCH ONLY — NOT CODEBASE SEARCH
|
|
7
|
-
|
|
8
|
-
**`greedy_search` searches the live web**, not your local codebase.
|
|
9
|
-
|
|
10
|
-
| Tool | Searches |
|
|
11
|
-
|------|----------|
|
|
12
|
-
| `greedy_search` | **Live web** (Perplexity, Bing, Google) |
|
|
13
|
-
| `ast_grep_search` | **Local codebase** — use this for code patterns |
|
|
14
|
-
| `bash` with `grep/rg` | **Local codebase** — use this for text search |
|
|
15
|
-
|
|
16
|
-
**DO NOT use `greedy_search` for:**
|
|
17
|
-
- Finding functions in your codebase
|
|
18
|
-
- Searching local files
|
|
19
|
-
- Code review of your project
|
|
20
|
-
- Understanding project structure
|
|
21
|
-
|
|
22
|
-
**DO use `greedy_search` for:**
|
|
23
|
-
- Library documentation
|
|
24
|
-
- Recent framework changes
|
|
25
|
-
- Error message explanations
|
|
26
|
-
- Best practices research
|
|
27
|
-
- Current events/news
|
|
28
|
-
|
|
29
|
-
# GreedySearch Tools
|
|
30
|
-
|
|
31
|
-
| Tool | Speed | Use For |
|
|
32
|
-
|------|-------|---------|
|
|
33
|
-
| `greedy_search` | 15-180s | Multi-engine search with depth levels |
|
|
34
|
-
| `coding_task` | 60-180s | Debug, review, plan modes for hard problems |
|
|
35
|
-
|
|
36
|
-
## greedy_search
|
|
37
|
-
|
|
38
|
-
Multi-engine AI search (Perplexity, Bing, Google) with three depth levels.
|
|
39
|
-
|
|
40
|
-
```greedy_search({ query: "React 19 changes", depth: "standard" })```
|
|
41
|
-
|
|
42
|
-
| Parameter | Type | Default | Description |
|
|
43
|
-
|-----------|------|---------|-------------|
|
|
44
|
-
| `query` | string | required | Search question |
|
|
45
|
-
| `engine` | string | `"all"` | `all`, `perplexity`, `bing`, `google`, `gemini` |
|
|
46
|
-
| `depth` | string | `"standard"` | `fast`, `standard`, `deep` — see below |
|
|
47
|
-
| `fullAnswer` | boolean | `false` | Complete vs ~300 char summary |
|
|
48
|
-
|
|
49
|
-
### Depth Levels
|
|
50
|
-
|
|
51
|
-
| Depth | Engines | Synthesis | Source Fetch | Time | Use When |
|
|
52
|
-
|-------|---------|-----------|--------------|------|----------|
|
|
53
|
-
| `fast` | 1 | ❌ | ❌ | 15-30s | Quick lookup, single perspective |
|
|
54
|
-
| `standard` | 3 | ✅ | ❌ | 30-90s | Default — balanced speed/quality |
|
|
55
|
-
| `deep` | 3 | ✅ | ✅ (top 5) | 60-180s | Research that matters — architecture decisions |
|
|
56
|
-
|
|
57
|
-
**Standard** (default): Runs 3 engines, deduplicates sources, synthesizes via Gemini.
|
|
58
|
-
**Deep**: Same + fetches content from top sources for grounded synthesis + confidence scores.
|
|
59
|
-
|
|
60
|
-
### Engine Selection (for fast mode)
|
|
61
|
-
|
|
62
|
-
```greedy_search({ query: "...", engine: "perplexity", depth: "fast" })```
|
|
63
|
-
|
|
64
|
-
- `perplexity`: Technical Q&A, citations
|
|
65
|
-
- `bing`: Recent news, Microsoft ecosystem
|
|
66
|
-
- `google`: Broad coverage
|
|
67
|
-
- `gemini`: Different training data
|
|
68
|
-
|
|
69
|
-
### Examples — Web Research Only
|
|
70
|
-
|
|
71
|
-
**✅ GOOD — Web research:**
|
|
72
|
-
```greedy_search({ query: "what changed in React 19", depth: "fast" })```
|
|
73
|
-
```greedy_search({ query: "best auth patterns for SaaS", depth: "deep" })```
|
|
74
|
-
```greedy_search({ query: "Prisma vs Drizzle 2026", depth: "standard", fullAnswer: true })```
|
|
75
|
-
|
|
76
|
-
**❌ WRONG — Don't use for codebase search:**
|
|
77
|
-
```javascript
|
|
78
|
-
// DON'T: Searching your own codebase
|
|
79
|
-
// greedy_search({ query: "find UserService class" }) // ❌ Won't find it!
|
|
80
|
-
|
|
81
|
-
// DO: Use these instead for codebase search:
|
|
82
|
-
// ast_grep_search({ pattern: "class UserService", lang: "typescript" })
|
|
83
|
-
// bash({ command: "rg 'class UserService' --type ts" })
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### Legacy
|
|
87
|
-
|
|
88
|
-
`deep_research` tool still works — aliases to `greedy_search` with `depth: "deep"`.
|
|
89
|
-
|
|
90
|
-
## coding_task
|
|
91
|
-
|
|
92
|
-
Browser-based coding assistant via Gemini/Copilot.
|
|
93
|
-
|
|
94
|
-
```coding_task({ task: "debug race condition", mode: "debug", engine: "gemini" })```
|
|
95
|
-
|
|
96
|
-
| Parameter | Type | Default | Description |
|
|
97
|
-
|-----------|------|---------|-------------|
|
|
98
|
-
| `task` | string | required | Coding task/question |
|
|
99
|
-
| `engine` | string | `"gemini"` | `gemini`, `copilot`, `all` |
|
|
100
|
-
| `mode` | string | `"code"` | `debug`, `plan`, `review`, `test`, `code` |
|
|
101
|
-
| `context` | string | — | Code snippet to include |
|
|
102
|
-
|
|
103
|
-
**Modes:**
|
|
104
|
-
- `debug`: Tricky bugs — fresh eyes catch different failure modes
|
|
105
|
-
- `plan`: Big refactor — plays devil's advocate on risks
|
|
106
|
-
- `review`: High-stakes code review before merge
|
|
107
|
-
- `test`: Edge cases the author missed
|
|
108
|
-
- `code`: Simple generation (but you're probably faster)
|
|
109
|
-
|
|
110
|
-
**When to use:** Second opinions on hard problems. Skip for simple code.
|
|
111
|
-
|
|
112
|
-
## Result Interpretation
|
|
113
|
-
|
|
114
|
-
- **All 3 engines agree** → High confidence, present as fact
|
|
115
|
-
- **2 agree, 1 differs** → Likely correct, note dissent
|
|
116
|
-
- **Sources [3/3] or [2/3]** → Multiple engines cite, higher confidence
|
|
117
|
-
- **Deep research confidence scores** → Structured confidence metadata
|