@apmantza/greedysearch-pi 1.0.9 → 1.0.10

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.
@@ -0,0 +1,324 @@
1
+ #!/usr/bin/env node
2
+ // coding-task.mjs — delegate a coding task to Gemini or Copilot via browser CDP
3
+ //
4
+ // Usage:
5
+ // node coding-task.mjs "<task>" --engine gemini|copilot [--tab <prefix>]
6
+ // node coding-task.mjs "<task>" --engine gemini --context "<code snippet>"
7
+ // node coding-task.mjs all "<task>" — run both engines in parallel
8
+ //
9
+ // Output (stdout): JSON { engine, task, code: [{language, code}], explanation, raw }
10
+ // Errors go to stderr only.
11
+
12
+ import { spawn } from 'child_process';
13
+ import { tmpdir } from 'os';
14
+ import { join, dirname } from 'path';
15
+ import { fileURLToPath } from 'url';
16
+ import { readFileSync, existsSync, writeFileSync } from 'fs';
17
+ import { dismissConsent, handleVerification } from './extractors/consent.mjs';
18
+
19
+ const __dir = dirname(fileURLToPath(import.meta.url));
20
+ const CDP = join(__dir, 'cdp.mjs');
21
+ const PAGES_CACHE = `${tmpdir().replace(/\\/g, '/')}/cdp-pages.json`;
22
+
23
+ const STREAM_POLL_INTERVAL = 800;
24
+ const STREAM_STABLE_ROUNDS = 4;
25
+ const STREAM_TIMEOUT = 120000; // coding tasks take longer
26
+ const MIN_RESPONSE_LENGTH = 50;
27
+
28
+ // ---------------------------------------------------------------------------
29
+
30
+ function cdp(args, timeoutMs = 30000) {
31
+ return new Promise((resolve, reject) => {
32
+ const proc = spawn('node', [CDP, ...args], { stdio: ['ignore', 'pipe', 'pipe'] });
33
+ let out = '', err = '';
34
+ proc.stdout.on('data', d => out += d);
35
+ proc.stderr.on('data', d => err += d);
36
+ const timer = setTimeout(() => { proc.kill(); reject(new Error(`cdp timeout: ${args[0]}`)); }, timeoutMs);
37
+ proc.on('close', code => {
38
+ clearTimeout(timer);
39
+ if (code !== 0) reject(new Error(err.trim() || `cdp exit ${code}`));
40
+ else resolve(out.trim());
41
+ });
42
+ });
43
+ }
44
+
45
+ async function getAnyTab() {
46
+ const list = await cdp(['list']);
47
+ return list.split('\n')[0].slice(0, 8);
48
+ }
49
+
50
+ async function openNewTab() {
51
+ const anchor = await getAnyTab();
52
+ const raw = await cdp(['evalraw', anchor, 'Target.createTarget', '{"url":"about:blank"}']);
53
+ return JSON.parse(raw).targetId;
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Engine implementations
58
+
59
+ const ENGINES = {
60
+ gemini: {
61
+ url: 'https://gemini.google.com/app',
62
+ domain: 'gemini.google.com',
63
+
64
+ async type(tab, text) {
65
+ await cdp(['eval', tab, `
66
+ (function(t) {
67
+ var el = document.querySelector('rich-textarea .ql-editor');
68
+ el.focus();
69
+ document.execCommand('insertText', false, t);
70
+ })(${JSON.stringify(text)})
71
+ `]);
72
+ },
73
+
74
+ async send(tab) {
75
+ await cdp(['eval', tab, `document.querySelector('button[aria-label*="Send"]')?.click()`]);
76
+ },
77
+
78
+ async waitReady(tab) {
79
+ const deadline = Date.now() + 12000;
80
+ while (Date.now() < deadline) {
81
+ const ok = await cdp(['eval', tab, `!!document.querySelector('rich-textarea .ql-editor')`]).catch(() => 'false');
82
+ if (ok === 'true') return;
83
+ await new Promise(r => setTimeout(r, 400));
84
+ }
85
+ throw new Error('Gemini input never appeared');
86
+ },
87
+
88
+ async waitStream(tab) {
89
+ const deadline = Date.now() + STREAM_TIMEOUT;
90
+ let started = false, stableCount = 0, lastLen = -1;
91
+
92
+ while (Date.now() < deadline) {
93
+ await new Promise(r => setTimeout(r, STREAM_POLL_INTERVAL));
94
+ const stopVisible = await cdp(['eval', tab, `!!document.querySelector('button[aria-label*="Stop"]')`]).catch(() => 'false');
95
+ if (stopVisible === 'true') { started = true; continue; }
96
+ if (!started) continue;
97
+
98
+ const lenStr = await cdp(['eval', tab,
99
+ `(function(){var els=document.querySelectorAll('model-response .markdown');var l=els[els.length-1];return(l?.innerText?.length||0)+''})()`,
100
+ ]).catch(() => '0');
101
+ const len = parseInt(lenStr) || 0;
102
+ if (len >= MIN_RESPONSE_LENGTH && len === lastLen) {
103
+ if (++stableCount >= STREAM_STABLE_ROUNDS) return;
104
+ } else { stableCount = 0; lastLen = len; }
105
+ }
106
+ if (lastLen >= MIN_RESPONSE_LENGTH) return;
107
+ throw new Error('Gemini response did not stabilise');
108
+ },
109
+
110
+ async extract(tab) {
111
+ return cdp(['eval', tab, `
112
+ (function(){
113
+ var els = document.querySelectorAll('model-response .markdown');
114
+ return els[els.length-1]?.innerText?.trim() || '';
115
+ })()
116
+ `]);
117
+ },
118
+ },
119
+
120
+ copilot: {
121
+ url: 'https://copilot.microsoft.com/',
122
+ domain: 'copilot.microsoft.com',
123
+
124
+ async type(tab, text) {
125
+ await cdp(['click', tab, '#userInput']);
126
+ await new Promise(r => setTimeout(r, 300));
127
+ await cdp(['type', tab, text]);
128
+ },
129
+
130
+ async send(tab) {
131
+ await cdp(['eval', tab,
132
+ `document.querySelector('#userInput')?.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true,keyCode:13})), 'ok'`
133
+ ]);
134
+ },
135
+
136
+ async waitReady(tab) {
137
+ const deadline = Date.now() + 10000;
138
+ while (Date.now() < deadline) {
139
+ const ok = await cdp(['eval', tab, `!!document.querySelector('#userInput')`]).catch(() => 'false');
140
+ if (ok === 'true') return;
141
+ await new Promise(r => setTimeout(r, 400));
142
+ }
143
+ throw new Error('Copilot input never appeared');
144
+ },
145
+
146
+ async waitStream(tab) {
147
+ const deadline = Date.now() + STREAM_TIMEOUT;
148
+ let stableCount = 0, lastLen = -1;
149
+ while (Date.now() < deadline) {
150
+ await new Promise(r => setTimeout(r, STREAM_POLL_INTERVAL));
151
+ const lenStr = await cdp(['eval', tab, `
152
+ (function(){
153
+ var items = Array.from(document.querySelectorAll('[class*="ai-message-item"]'));
154
+ var filled = items.filter(el => (el.innerText?.length||0) > 0);
155
+ var last = filled[filled.length-1];
156
+ return (last?.innerText?.length||0)+'';
157
+ })()`
158
+ ]).catch(() => '0');
159
+ const len = parseInt(lenStr) || 0;
160
+ if (len >= MIN_RESPONSE_LENGTH && len === lastLen) {
161
+ if (++stableCount >= STREAM_STABLE_ROUNDS) return;
162
+ } else { stableCount = 0; lastLen = len; }
163
+ }
164
+ if (lastLen >= MIN_RESPONSE_LENGTH) return;
165
+ throw new Error('Copilot response did not stabilise');
166
+ },
167
+
168
+ async extract(tab) {
169
+ return cdp(['eval', tab, `
170
+ (function(){
171
+ var items = Array.from(document.querySelectorAll('[class*="ai-message-item"]'));
172
+ var last = items.filter(e=>(e.innerText?.length||0)>0).pop();
173
+ return last?.innerText?.trim()||'';
174
+ })()
175
+ `]);
176
+ },
177
+ },
178
+ };
179
+
180
+ // ---------------------------------------------------------------------------
181
+
182
+ function extractCodeBlocks(text) {
183
+ const blocks = [];
184
+ const regex = /```(\w+)?\n([\s\S]*?)```/g;
185
+ let match;
186
+ while ((match = regex.exec(text)) !== null) {
187
+ blocks.push({ language: match[1] || 'text', code: match[2].trim() });
188
+ }
189
+ // If no fenced blocks, look for indented blocks as fallback
190
+ if (blocks.length === 0) {
191
+ const lines = text.split('\n');
192
+ const indented = lines.filter(l => l.startsWith(' ')).map(l => l.slice(4));
193
+ if (indented.length > 3) blocks.push({ language: 'text', code: indented.join('\n') });
194
+ }
195
+ return blocks;
196
+ }
197
+
198
+ function extractExplanation(text, codeBlocks) {
199
+ // Remove code blocks from text to get the explanation
200
+ let explanation = text.replace(/```[\s\S]*?```/g, '').trim();
201
+ explanation = explanation.replace(/\n{3,}/g, '\n\n').trim();
202
+ return explanation.slice(0, 1000); // cap explanation at 1000 chars
203
+ }
204
+
205
+ async function runEngine(engineName, task, context, tabPrefix) {
206
+ const engine = ENGINES[engineName];
207
+ if (!engine) throw new Error(`Unknown engine: ${engineName}`);
208
+
209
+ // Find or open a tab
210
+ let tab = tabPrefix;
211
+ if (!tab) {
212
+ if (existsSync(PAGES_CACHE)) {
213
+ const pages = JSON.parse(readFileSync(PAGES_CACHE, 'utf8'));
214
+ const existing = pages.find(p => p.url.includes(engine.domain));
215
+ if (existing) tab = existing.targetId.slice(0, 8);
216
+ }
217
+ if (!tab) tab = await openNewTab();
218
+ }
219
+
220
+ // Navigate to fresh conversation — fall back to new tab if cached tab is stale
221
+ try {
222
+ await cdp(['nav', tab, engine.url], 35000);
223
+ } catch (e) {
224
+ if (e.message.includes('No target matching')) {
225
+ tab = await openNewTab();
226
+ await cdp(['nav', tab, engine.url], 35000);
227
+ } else throw e;
228
+ }
229
+ await new Promise(r => setTimeout(r, 2000));
230
+ await dismissConsent(tab, cdp);
231
+ await handleVerification(tab, cdp, 60000);
232
+ await engine.waitReady(tab);
233
+ await new Promise(r => setTimeout(r, 300));
234
+
235
+ // Build the prompt
236
+ const prompt = context
237
+ ? `${task}\n\nHere is the relevant code/context:\n\`\`\`\n${context}\n\`\`\``
238
+ : task;
239
+
240
+ await engine.type(tab, prompt);
241
+ await new Promise(r => setTimeout(r, 400));
242
+ await engine.send(tab);
243
+ await engine.waitStream(tab);
244
+
245
+ const raw = await engine.extract(tab);
246
+ if (!raw) throw new Error(`No response from ${engineName}`);
247
+
248
+ const code = extractCodeBlocks(raw);
249
+ const explanation = extractExplanation(raw, code);
250
+ const url = await cdp(['eval', tab, 'document.location.href']).catch(() => engine.url);
251
+
252
+ return { engine: engineName, task, code, explanation, raw, url };
253
+ }
254
+
255
+ // ---------------------------------------------------------------------------
256
+
257
+ async function main() {
258
+ const args = process.argv.slice(2);
259
+ if (!args.length || args[0] === '--help') {
260
+ process.stderr.write([
261
+ 'Usage: node coding-task.mjs "<task>" --engine gemini|copilot|all',
262
+ ' node coding-task.mjs "<task>" --engine gemini --context "<code>"',
263
+ '',
264
+ 'Examples:',
265
+ ' node coding-task.mjs "write a debounce function in JS" --engine gemini',
266
+ ' node coding-task.mjs "refactor this to use async/await" --engine all --context "cb code here"',
267
+ ].join('\n') + '\n');
268
+ process.exit(1);
269
+ }
270
+
271
+ const engineFlagIdx = args.indexOf('--engine');
272
+ const engineArg = engineFlagIdx !== -1 ? args[engineFlagIdx + 1] : 'gemini';
273
+ const contextFlagIdx = args.indexOf('--context');
274
+ const context = contextFlagIdx !== -1 ? args[contextFlagIdx + 1] : null;
275
+ const outIdx = args.indexOf('--out');
276
+ const outFile = outIdx !== -1 ? args[outIdx + 1] : null;
277
+ const tabFlagIdx = args.indexOf('--tab');
278
+ const tabPrefix = tabFlagIdx !== -1 ? args[tabFlagIdx + 1] : null;
279
+
280
+ const skipFlags = new Set([
281
+ ...(engineFlagIdx >= 0 ? [engineFlagIdx, engineFlagIdx + 1] : []),
282
+ ...(contextFlagIdx >= 0 ? [contextFlagIdx, contextFlagIdx + 1] : []),
283
+ ...(outIdx >= 0 ? [outIdx, outIdx + 1] : []),
284
+ ...(tabFlagIdx >= 0 ? [tabFlagIdx, tabFlagIdx + 1] : []),
285
+ ]);
286
+ const task = args.filter((_, i) => !skipFlags.has(i)).join(' ');
287
+
288
+ if (!task) {
289
+ process.stderr.write('Error: no task provided\n');
290
+ process.exit(1);
291
+ }
292
+
293
+ await cdp(['list']); // ensure Chrome is reachable
294
+
295
+ let result;
296
+
297
+ if (engineArg === 'all') {
298
+ const results = await Promise.allSettled(
299
+ Object.keys(ENGINES).map(e => runEngine(e, task, context, null))
300
+ );
301
+ result = {};
302
+ for (const [i, r] of results.entries()) {
303
+ const name = Object.keys(ENGINES)[i];
304
+ result[name] = r.status === 'fulfilled' ? r.value : { engine: name, error: r.reason?.message };
305
+ }
306
+ } else {
307
+ try {
308
+ result = await runEngine(engineArg, task, context, tabPrefix);
309
+ } catch (e) {
310
+ process.stderr.write(`Error: ${e.message}\n`);
311
+ process.exit(1);
312
+ }
313
+ }
314
+
315
+ const json = JSON.stringify(result, null, 2) + '\n';
316
+ if (outFile) {
317
+ writeFileSync(outFile, json, 'utf8');
318
+ process.stderr.write(`Results written to ${outFile}\n`);
319
+ } else {
320
+ process.stdout.write(json);
321
+ }
322
+ }
323
+
324
+ main();
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+ // extractors/gemini.mjs
3
+ // Navigate gemini.google.com/app, submit query, wait for answer, return clean answer + sources.
4
+ //
5
+ // Usage:
6
+ // node extractors/gemini.mjs "<query>" [--tab <prefix>]
7
+ //
8
+ // Output (stdout): JSON { answer, sources, query, url }
9
+ // Errors go to stderr only — stdout is always clean JSON for piping.
10
+
11
+ import { readFileSync, existsSync } from 'fs';
12
+ import { spawn } from 'child_process';
13
+ import { tmpdir } from 'os';
14
+ import { join, dirname } from 'path';
15
+ import { fileURLToPath } from 'url';
16
+ import { dismissConsent, handleVerification } from './consent.mjs';
17
+
18
+ const __dir = dirname(fileURLToPath(import.meta.url));
19
+ const CDP = join(__dir, '..', 'cdp.mjs');
20
+ const PAGES_CACHE = `${tmpdir().replace(/\\/g, '/')}/cdp-pages.json`;
21
+
22
+ const STREAM_POLL_INTERVAL = 600;
23
+ const STREAM_STABLE_ROUNDS = 3;
24
+ const STREAM_TIMEOUT = 60000;
25
+ const MIN_ANSWER_LENGTH = 20;
26
+
27
+ // ---------------------------------------------------------------------------
28
+
29
+ function cdp(args, timeoutMs = 30000) {
30
+ return new Promise((resolve, reject) => {
31
+ const proc = spawn('node', [CDP, ...args], { stdio: ['ignore', 'pipe', 'pipe'] });
32
+ let out = '', err = '';
33
+ proc.stdout.on('data', d => out += d);
34
+ proc.stderr.on('data', d => err += d);
35
+ const timer = setTimeout(() => { proc.kill(); reject(new Error(`cdp timeout: ${args[0]}`)); }, timeoutMs);
36
+ proc.on('close', code => {
37
+ clearTimeout(timer);
38
+ if (code !== 0) reject(new Error(err.trim() || `cdp exit ${code}`));
39
+ else resolve(out.trim());
40
+ });
41
+ });
42
+ }
43
+
44
+ async function getOrOpenTab(tabPrefix) {
45
+ if (tabPrefix) return tabPrefix;
46
+ if (existsSync(PAGES_CACHE)) {
47
+ const pages = JSON.parse(readFileSync(PAGES_CACHE, 'utf8'));
48
+ const existing = pages.find(p => p.url.includes('gemini.google.com'));
49
+ if (existing) return existing.targetId.slice(0, 8);
50
+ }
51
+ const list = await cdp(['list']);
52
+ return list.split('\n')[0].slice(0, 8);
53
+ }
54
+
55
+ async function typeIntoGemini(tab, text) {
56
+ await cdp(['eval', tab, `
57
+ (function(t) {
58
+ var el = document.querySelector('rich-textarea .ql-editor');
59
+ if (!el) return false;
60
+ el.focus();
61
+ document.execCommand('insertText', false, t);
62
+ return true;
63
+ })(${JSON.stringify(text)})
64
+ `]);
65
+ }
66
+
67
+ async function waitForStreamComplete(tab) {
68
+ // Wait for Stop button to appear (streaming started), then disappear (streaming done)
69
+ const deadline = Date.now() + STREAM_TIMEOUT;
70
+ let streamingStarted = false;
71
+ let stableCount = 0;
72
+ let lastLen = -1;
73
+
74
+ while (Date.now() < deadline) {
75
+ await new Promise(r => setTimeout(r, STREAM_POLL_INTERVAL));
76
+
77
+ const stopVisible = await cdp(['eval', tab,
78
+ `!!document.querySelector('button[aria-label*="Stop"]')`
79
+ ]).catch(() => 'false');
80
+
81
+ if (stopVisible === 'true') {
82
+ streamingStarted = true;
83
+ } else if (streamingStarted) {
84
+ // Stop button gone — streaming finished. Confirm with stable text length.
85
+ const lenStr = await cdp(['eval', tab,
86
+ `(function(){var els=document.querySelectorAll('model-response .markdown');var last=els[els.length-1];return (last?.innerText?.length||0)+''})()`
87
+ ]).catch(() => '0');
88
+ const len = parseInt(lenStr) || 0;
89
+ if (len >= MIN_ANSWER_LENGTH && len === lastLen) {
90
+ stableCount++;
91
+ if (stableCount >= STREAM_STABLE_ROUNDS) return len;
92
+ } else {
93
+ stableCount = 0;
94
+ lastLen = len;
95
+ }
96
+ }
97
+ }
98
+
99
+ if (lastLen >= MIN_ANSWER_LENGTH) return lastLen;
100
+ throw new Error(`Gemini answer did not stabilise within ${STREAM_TIMEOUT}ms`);
101
+ }
102
+
103
+ async function extractAnswer(tab) {
104
+ const raw = await cdp(['eval', tab, `
105
+ (function() {
106
+ var els = document.querySelectorAll('model-response .markdown');
107
+ var last = els[els.length - 1];
108
+ if (!last) return JSON.stringify({ answer: '', sources: [] });
109
+ var answer = last.innerText.trim();
110
+ var sources = Array.from(document.querySelectorAll('model-response a[href^="http"]'))
111
+ .map(a => ({ url: a.href.split('#')[0], title: a.innerText?.trim().split('\\n')[0] || '' }))
112
+ .filter(s => s.url && !s.url.includes('gemini.google') && !s.url.includes('gstatic'))
113
+ .filter((v, i, arr) => arr.findIndex(x => x.url === v.url) === i)
114
+ .slice(0, 8);
115
+ return JSON.stringify({ answer, sources });
116
+ })()
117
+ `]);
118
+ return JSON.parse(raw);
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+
123
+ async function main() {
124
+ const args = process.argv.slice(2);
125
+ if (!args.length || args[0] === '--help') {
126
+ process.stderr.write('Usage: node extractors/gemini.mjs "<query>" [--tab <prefix>]\n');
127
+ process.exit(1);
128
+ }
129
+
130
+ const short = args.includes('--short');
131
+ const rest = args.filter(a => a !== '--short');
132
+ const tabFlagIdx = rest.indexOf('--tab');
133
+ const tabPrefix = tabFlagIdx !== -1 ? rest[tabFlagIdx + 1] : null;
134
+ const query = tabFlagIdx !== -1
135
+ ? rest.filter((_, i) => i !== tabFlagIdx && i !== tabFlagIdx + 1).join(' ')
136
+ : rest.join(' ');
137
+
138
+ try {
139
+ await cdp(['list']);
140
+ const tab = await getOrOpenTab(tabPrefix);
141
+
142
+ // Each search = fresh conversation
143
+ await cdp(['nav', tab, 'https://gemini.google.com/app'], 35000);
144
+ await new Promise(r => setTimeout(r, 2000));
145
+ await dismissConsent(tab, cdp);
146
+ await handleVerification(tab, cdp, 60000);
147
+
148
+ // Wait for input to be ready
149
+ const deadline = Date.now() + 10000;
150
+ while (Date.now() < deadline) {
151
+ const ready = await cdp(['eval', tab, `!!document.querySelector('rich-textarea .ql-editor')`]).catch(() => 'false');
152
+ if (ready === 'true') break;
153
+ await new Promise(r => setTimeout(r, 400));
154
+ }
155
+ await new Promise(r => setTimeout(r, 300));
156
+
157
+ await typeIntoGemini(tab, query);
158
+ await new Promise(r => setTimeout(r, 400));
159
+
160
+ await cdp(['eval', tab, `document.querySelector('button[aria-label*="Send"]')?.click()`]);
161
+
162
+ await waitForStreamComplete(tab);
163
+
164
+ const { answer, sources } = await extractAnswer(tab);
165
+ if (!answer) throw new Error('No answer extracted from Gemini');
166
+ const out = short ? answer.slice(0, 300).replace(/\s+\S*$/, '') + '…' : answer;
167
+
168
+ const finalUrl = await cdp(['eval', tab, 'document.location.href']).catch(() => 'https://gemini.google.com/app');
169
+ process.stdout.write(JSON.stringify({ query, url: finalUrl, answer: out, sources }, null, 2) + '\n');
170
+ } catch (e) {
171
+ process.stderr.write(`Error: ${e.message}\n`);
172
+ process.exit(1);
173
+ }
174
+ }
175
+
176
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apmantza/greedysearch-pi",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Pi extension: search Perplexity, Bing Copilot, and Google AI simultaneously — synthesized AI answers, not just links",
5
5
  "type": "module",
6
6
  "keywords": [