@apmantza/greedysearch-pi 1.0.10 → 1.0.12

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.
Files changed (2) hide show
  1. package/coding-task.mjs +349 -324
  2. package/package.json +1 -1
package/coding-task.mjs CHANGED
@@ -1,324 +1,349 @@
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();
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
+ // Mode system prompts — prepended to the user's task
24
+ const MODE_PROMPTS = {
25
+ code: null, // no preamble default behaviour
26
+ review: `You are a senior software engineer doing a thorough code review. Analyse the code below for: correctness and edge cases, security issues, performance problems, readability and naming, missing error handling, and anything that would not survive a production incident. Be specific — cite line-level issues where relevant. Suggest concrete fixes, not vague advice.`,
27
+ plan: `You are a senior software architect. The user will describe something they want to build and their current plan. Your job is to: (1) identify risks, gaps, and hidden assumptions in the plan, (2) flag anything that will cause pain later (scaling, ops, security, maintainability), (3) suggest better alternatives where the plan is suboptimal, (4) call out what's missing entirely. Be direct and opinionated — the goal is to find problems before they're built.`,
28
+ test: `You are a senior engineer writing tests for code written by someone else. Your goal is to find what they missed. Write a comprehensive test suite that covers: edge cases the author likely didn't think of, boundary conditions (empty input, nulls, max values, type coercion), error paths and exception handling, concurrency or ordering issues if relevant, and any behaviour that differs from what the function name implies. Use the same language and testing framework as the code if apparent, otherwise default to the most common one for that language. Output runnable test code — not a list of what to test.`,
29
+ };
30
+
31
+ const STREAM_POLL_INTERVAL = 800;
32
+ const STREAM_STABLE_ROUNDS = 4;
33
+ const STREAM_TIMEOUT = 120000; // coding tasks take longer
34
+ const MIN_RESPONSE_LENGTH = 50;
35
+
36
+ // ---------------------------------------------------------------------------
37
+
38
+ function cdp(args, timeoutMs = 30000) {
39
+ return new Promise((resolve, reject) => {
40
+ const proc = spawn('node', [CDP, ...args], { stdio: ['ignore', 'pipe', 'pipe'] });
41
+ let out = '', err = '';
42
+ proc.stdout.on('data', d => out += d);
43
+ proc.stderr.on('data', d => err += d);
44
+ const timer = setTimeout(() => { proc.kill(); reject(new Error(`cdp timeout: ${args[0]}`)); }, timeoutMs);
45
+ proc.on('close', code => {
46
+ clearTimeout(timer);
47
+ if (code !== 0) reject(new Error(err.trim() || `cdp exit ${code}`));
48
+ else resolve(out.trim());
49
+ });
50
+ });
51
+ }
52
+
53
+ async function getAnyTab() {
54
+ const list = await cdp(['list']);
55
+ return list.split('\n')[0].slice(0, 8);
56
+ }
57
+
58
+ async function openNewTab() {
59
+ const anchor = await getAnyTab();
60
+ const raw = await cdp(['evalraw', anchor, 'Target.createTarget', '{"url":"about:blank"}']);
61
+ return JSON.parse(raw).targetId;
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Engine implementations
66
+
67
+ const ENGINES = {
68
+ gemini: {
69
+ url: 'https://gemini.google.com/app',
70
+ domain: 'gemini.google.com',
71
+
72
+ async type(tab, text) {
73
+ await cdp(['eval', tab, `
74
+ (function(t) {
75
+ var el = document.querySelector('rich-textarea .ql-editor');
76
+ el.focus();
77
+ document.execCommand('insertText', false, t);
78
+ })(${JSON.stringify(text)})
79
+ `]);
80
+ },
81
+
82
+ async send(tab) {
83
+ await cdp(['eval', tab, `document.querySelector('button[aria-label*="Send"]')?.click()`]);
84
+ },
85
+
86
+ async waitReady(tab) {
87
+ const deadline = Date.now() + 12000;
88
+ while (Date.now() < deadline) {
89
+ const ok = await cdp(['eval', tab, `!!document.querySelector('rich-textarea .ql-editor')`]).catch(() => 'false');
90
+ if (ok === 'true') return;
91
+ await new Promise(r => setTimeout(r, 400));
92
+ }
93
+ throw new Error('Gemini input never appeared');
94
+ },
95
+
96
+ async waitStream(tab) {
97
+ const deadline = Date.now() + STREAM_TIMEOUT;
98
+ let started = false, stableCount = 0, lastLen = -1;
99
+
100
+ while (Date.now() < deadline) {
101
+ await new Promise(r => setTimeout(r, STREAM_POLL_INTERVAL));
102
+ const stopVisible = await cdp(['eval', tab, `!!document.querySelector('button[aria-label*="Stop"]')`]).catch(() => 'false');
103
+ if (stopVisible === 'true') { started = true; continue; }
104
+ if (!started) continue;
105
+
106
+ const lenStr = await cdp(['eval', tab,
107
+ `(function(){var els=document.querySelectorAll('model-response .markdown');var l=els[els.length-1];return(l?.innerText?.length||0)+''})()`,
108
+ ]).catch(() => '0');
109
+ const len = parseInt(lenStr) || 0;
110
+ if (len >= MIN_RESPONSE_LENGTH && len === lastLen) {
111
+ if (++stableCount >= STREAM_STABLE_ROUNDS) return;
112
+ } else { stableCount = 0; lastLen = len; }
113
+ }
114
+ if (lastLen >= MIN_RESPONSE_LENGTH) return;
115
+ throw new Error('Gemini response did not stabilise');
116
+ },
117
+
118
+ async extract(tab) {
119
+ return cdp(['eval', tab, `
120
+ (function(){
121
+ var els = document.querySelectorAll('model-response .markdown');
122
+ return els[els.length-1]?.innerText?.trim() || '';
123
+ })()
124
+ `]);
125
+ },
126
+ },
127
+
128
+ copilot: {
129
+ url: 'https://copilot.microsoft.com/',
130
+ domain: 'copilot.microsoft.com',
131
+
132
+ async type(tab, text) {
133
+ await cdp(['click', tab, '#userInput']);
134
+ await new Promise(r => setTimeout(r, 300));
135
+ await cdp(['type', tab, text]);
136
+ },
137
+
138
+ async send(tab) {
139
+ await cdp(['eval', tab,
140
+ `document.querySelector('#userInput')?.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true,keyCode:13})), 'ok'`
141
+ ]);
142
+ },
143
+
144
+ async waitReady(tab) {
145
+ const deadline = Date.now() + 10000;
146
+ while (Date.now() < deadline) {
147
+ const ok = await cdp(['eval', tab, `!!document.querySelector('#userInput')`]).catch(() => 'false');
148
+ if (ok === 'true') return;
149
+ await new Promise(r => setTimeout(r, 400));
150
+ }
151
+ throw new Error('Copilot input never appeared');
152
+ },
153
+
154
+ async waitStream(tab) {
155
+ const deadline = Date.now() + STREAM_TIMEOUT;
156
+ let stableCount = 0, lastLen = -1;
157
+ while (Date.now() < deadline) {
158
+ await new Promise(r => setTimeout(r, STREAM_POLL_INTERVAL));
159
+ const lenStr = await cdp(['eval', tab, `
160
+ (function(){
161
+ var items = Array.from(document.querySelectorAll('[class*="ai-message-item"]'));
162
+ var filled = items.filter(el => (el.innerText?.length||0) > 0);
163
+ var last = filled[filled.length-1];
164
+ return (last?.innerText?.length||0)+'';
165
+ })()`
166
+ ]).catch(() => '0');
167
+ const len = parseInt(lenStr) || 0;
168
+ if (len >= MIN_RESPONSE_LENGTH && len === lastLen) {
169
+ if (++stableCount >= STREAM_STABLE_ROUNDS) return;
170
+ } else { stableCount = 0; lastLen = len; }
171
+ }
172
+ if (lastLen >= MIN_RESPONSE_LENGTH) return;
173
+ throw new Error('Copilot response did not stabilise');
174
+ },
175
+
176
+ async extract(tab) {
177
+ return cdp(['eval', tab, `
178
+ (function(){
179
+ var items = Array.from(document.querySelectorAll('[class*="ai-message-item"]'));
180
+ var last = items.filter(e=>(e.innerText?.length||0)>0).pop();
181
+ return last?.innerText?.trim()||'';
182
+ })()
183
+ `]);
184
+ },
185
+ },
186
+ };
187
+
188
+ // ---------------------------------------------------------------------------
189
+
190
+ function extractCodeBlocks(text) {
191
+ const blocks = [];
192
+ const regex = /```(\w+)?\n([\s\S]*?)```/g;
193
+ let match;
194
+ while ((match = regex.exec(text)) !== null) {
195
+ blocks.push({ language: match[1] || 'text', code: match[2].trim() });
196
+ }
197
+ // If no fenced blocks, look for indented blocks as fallback
198
+ if (blocks.length === 0) {
199
+ const lines = text.split('\n');
200
+ const indented = lines.filter(l => l.startsWith(' ')).map(l => l.slice(4));
201
+ if (indented.length > 3) blocks.push({ language: 'text', code: indented.join('\n') });
202
+ }
203
+ return blocks;
204
+ }
205
+
206
+ function extractExplanation(text, codeBlocks) {
207
+ // Remove code blocks from text to get the explanation
208
+ let explanation = text.replace(/```[\s\S]*?```/g, '').trim();
209
+ explanation = explanation.replace(/\n{3,}/g, '\n\n').trim();
210
+ return explanation.slice(0, 1000); // cap explanation at 1000 chars
211
+ }
212
+
213
+ async function runEngine(engineName, task, context, mode, tabPrefix) {
214
+ const engine = ENGINES[engineName];
215
+ if (!engine) throw new Error(`Unknown engine: ${engineName}`);
216
+
217
+ // Find or open a tab
218
+ let tab = tabPrefix;
219
+ if (!tab) {
220
+ if (existsSync(PAGES_CACHE)) {
221
+ const pages = JSON.parse(readFileSync(PAGES_CACHE, 'utf8'));
222
+ const existing = pages.find(p => p.url.includes(engine.domain));
223
+ if (existing) tab = existing.targetId.slice(0, 8);
224
+ }
225
+ if (!tab) tab = await openNewTab();
226
+ }
227
+
228
+ // Navigate to fresh conversation — fall back to new tab if cached tab is stale
229
+ try {
230
+ await cdp(['nav', tab, engine.url], 35000);
231
+ } catch (e) {
232
+ if (e.message.includes('No target matching')) {
233
+ tab = await openNewTab();
234
+ await cdp(['nav', tab, engine.url], 35000);
235
+ } else throw e;
236
+ }
237
+ await new Promise(r => setTimeout(r, 2000));
238
+ await dismissConsent(tab, cdp);
239
+ await handleVerification(tab, cdp, 60000);
240
+ await engine.waitReady(tab);
241
+ await new Promise(r => setTimeout(r, 300));
242
+
243
+ // Build the prompt
244
+ const preamble = MODE_PROMPTS[mode] || null;
245
+ const body = context
246
+ ? `${task}\n\nHere is the relevant code/context:\n\`\`\`\n${context}\n\`\`\``
247
+ : task;
248
+ const prompt = preamble ? `${preamble}\n\n---\n\n${body}` : body;
249
+
250
+ await engine.type(tab, prompt);
251
+ await new Promise(r => setTimeout(r, 400));
252
+ await engine.send(tab);
253
+ await engine.waitStream(tab);
254
+
255
+ const raw = await engine.extract(tab);
256
+ if (!raw) throw new Error(`No response from ${engineName}`);
257
+
258
+ const code = extractCodeBlocks(raw);
259
+ const explanation = extractExplanation(raw, code);
260
+ const url = await cdp(['eval', tab, 'document.location.href']).catch(() => engine.url);
261
+
262
+ return { engine: engineName, task, code, explanation, raw, url };
263
+ }
264
+
265
+ // ---------------------------------------------------------------------------
266
+
267
+ async function main() {
268
+ const args = process.argv.slice(2);
269
+ if (!args.length || args[0] === '--help') {
270
+ process.stderr.write([
271
+ 'Usage: node coding-task.mjs "<task>" --engine gemini|copilot|all [--mode code|review|plan]',
272
+ ' node coding-task.mjs "<task>" --engine gemini --context "<code>"',
273
+ '',
274
+ 'Modes:',
275
+ ' code (default) write or modify code',
276
+ ' review senior engineer code review: correctness, security, performance',
277
+ ' plan — architect review: risks, gaps, alternatives for a build plan',
278
+ ' test — write tests an author would miss: edge cases, error paths, boundary conditions',
279
+ '',
280
+ 'Examples:',
281
+ ' node coding-task.mjs "write a debounce function in JS" --engine gemini',
282
+ ' node coding-task.mjs "review this module" --mode review --engine all --context "$(cat myfile.mjs)"',
283
+ ' node coding-task.mjs "I want to build X, here is my plan: ..." --mode plan --engine all',
284
+ ].join('\n') + '\n');
285
+ process.exit(1);
286
+ }
287
+
288
+ const engineFlagIdx = args.indexOf('--engine');
289
+ const engineArg = engineFlagIdx !== -1 ? args[engineFlagIdx + 1] : 'gemini';
290
+ const contextFlagIdx = args.indexOf('--context');
291
+ const context = contextFlagIdx !== -1 ? args[contextFlagIdx + 1] : null;
292
+ const outIdx = args.indexOf('--out');
293
+ const outFile = outIdx !== -1 ? args[outIdx + 1] : null;
294
+ const tabFlagIdx = args.indexOf('--tab');
295
+ const tabPrefix = tabFlagIdx !== -1 ? args[tabFlagIdx + 1] : null;
296
+ const modeFlagIdx = args.indexOf('--mode');
297
+ const mode = modeFlagIdx !== -1 ? args[modeFlagIdx + 1] : 'code';
298
+
299
+ if (!MODE_PROMPTS.hasOwnProperty(mode)) {
300
+ process.stderr.write(`Error: unknown mode "${mode}". Use: code, review, plan\n`);
301
+ process.exit(1);
302
+ }
303
+
304
+ const skipFlags = new Set([
305
+ ...(engineFlagIdx >= 0 ? [engineFlagIdx, engineFlagIdx + 1] : []),
306
+ ...(contextFlagIdx >= 0 ? [contextFlagIdx, contextFlagIdx + 1] : []),
307
+ ...(outIdx >= 0 ? [outIdx, outIdx + 1] : []),
308
+ ...(tabFlagIdx >= 0 ? [tabFlagIdx, tabFlagIdx + 1] : []),
309
+ ...(modeFlagIdx >= 0 ? [modeFlagIdx, modeFlagIdx + 1] : []),
310
+ ]);
311
+ const task = args.filter((_, i) => !skipFlags.has(i)).join(' ');
312
+
313
+ if (!task) {
314
+ process.stderr.write('Error: no task provided\n');
315
+ process.exit(1);
316
+ }
317
+
318
+ await cdp(['list']); // ensure Chrome is reachable
319
+
320
+ let result;
321
+
322
+ if (engineArg === 'all') {
323
+ const results = await Promise.allSettled(
324
+ Object.keys(ENGINES).map(e => runEngine(e, task, context, mode, null))
325
+ );
326
+ result = {};
327
+ for (const [i, r] of results.entries()) {
328
+ const name = Object.keys(ENGINES)[i];
329
+ result[name] = r.status === 'fulfilled' ? r.value : { engine: name, error: r.reason?.message };
330
+ }
331
+ } else {
332
+ try {
333
+ result = await runEngine(engineArg, task, context, mode, tabPrefix);
334
+ } catch (e) {
335
+ process.stderr.write(`Error: ${e.message}\n`);
336
+ process.exit(1);
337
+ }
338
+ }
339
+
340
+ const json = JSON.stringify(result, null, 2) + '\n';
341
+ if (outFile) {
342
+ writeFileSync(outFile, json, 'utf8');
343
+ process.stderr.write(`Results written to ${outFile}\n`);
344
+ } else {
345
+ process.stdout.write(json);
346
+ }
347
+ }
348
+
349
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apmantza/greedysearch-pi",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
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": [