@apmantza/greedysearch-pi 1.0.18 → 1.0.19
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/coding-task.mjs +369 -366
- package/extractors/bing-copilot.mjs +176 -176
- package/extractors/consent.mjs +76 -76
- package/extractors/google-ai.mjs +161 -161
- package/extractors/mistral.mjs +171 -171
- package/extractors/perplexity.mjs +179 -179
- package/extractors/stackoverflow-ai.mjs +169 -169
- package/launch.mjs +210 -199
- package/package.json +26 -26
- package/search.mjs +340 -340
package/coding-task.mjs
CHANGED
|
@@ -1,366 +1,369 @@
|
|
|
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
|
-
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.`,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const STREAM_POLL_INTERVAL = 800;
|
|
33
|
-
const STREAM_STABLE_ROUNDS = 4;
|
|
34
|
-
const STREAM_TIMEOUT = 120000; // coding tasks take longer
|
|
35
|
-
const MIN_RESPONSE_LENGTH = 50;
|
|
36
|
-
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
|
|
39
|
-
function cdp(args, timeoutMs = 30000) {
|
|
40
|
-
return new Promise((resolve, reject) => {
|
|
41
|
-
const proc = spawn('node', [CDP, ...args], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
42
|
-
let out = '', err = '';
|
|
43
|
-
proc.stdout.on('data', d => out += d);
|
|
44
|
-
proc.stderr.on('data', d => err += d);
|
|
45
|
-
const timer = setTimeout(() => { proc.kill(); reject(new Error(`cdp timeout: ${args[0]}`)); }, timeoutMs);
|
|
46
|
-
proc.on('close', code => {
|
|
47
|
-
clearTimeout(timer);
|
|
48
|
-
if (code !== 0) reject(new Error(err.trim() || `cdp exit ${code}`));
|
|
49
|
-
else resolve(out.trim());
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function getAnyTab() {
|
|
55
|
-
const list = await cdp(['list']);
|
|
56
|
-
return list.split('\n')[0].slice(0, 8);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function openNewTab() {
|
|
60
|
-
const anchor = await getAnyTab();
|
|
61
|
-
const raw = await cdp(['evalraw', anchor, 'Target.createTarget', '{"url":"about:blank"}']);
|
|
62
|
-
return JSON.parse(raw).targetId;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
// Engine implementations
|
|
67
|
-
|
|
68
|
-
const ENGINES = {
|
|
69
|
-
gemini: {
|
|
70
|
-
url: 'https://gemini.google.com/app',
|
|
71
|
-
domain: 'gemini.google.com',
|
|
72
|
-
|
|
73
|
-
async type(tab, text) {
|
|
74
|
-
await cdp(['eval', tab, `
|
|
75
|
-
(function(t) {
|
|
76
|
-
var el = document.querySelector('rich-textarea .ql-editor');
|
|
77
|
-
el.focus();
|
|
78
|
-
document.execCommand('insertText', false, t);
|
|
79
|
-
})(${JSON.stringify(text)})
|
|
80
|
-
`]);
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
async send(tab) {
|
|
84
|
-
await cdp(['eval', tab, `document.querySelector('button[aria-label*="Send"]')?.click()`]);
|
|
85
|
-
},
|
|
86
|
-
|
|
87
|
-
async waitReady(tab) {
|
|
88
|
-
const deadline = Date.now() + 12000;
|
|
89
|
-
while (Date.now() < deadline) {
|
|
90
|
-
const ok = await cdp(['eval', tab, `!!document.querySelector('rich-textarea .ql-editor')`]).catch(() => 'false');
|
|
91
|
-
if (ok === 'true') return;
|
|
92
|
-
await new Promise(r => setTimeout(r, 400));
|
|
93
|
-
}
|
|
94
|
-
throw new Error('Gemini input never appeared');
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
async waitStream(tab) {
|
|
98
|
-
const deadline = Date.now() + STREAM_TIMEOUT;
|
|
99
|
-
let started = false, stableCount = 0, lastLen = -1;
|
|
100
|
-
|
|
101
|
-
while (Date.now() < deadline) {
|
|
102
|
-
await new Promise(r => setTimeout(r, STREAM_POLL_INTERVAL));
|
|
103
|
-
const stopVisible = await cdp(['eval', tab, `!!document.querySelector('button[aria-label*="Stop"]')`]).catch(() => 'false');
|
|
104
|
-
if (stopVisible === 'true') { started = true;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const lenStr = await cdp(['eval', tab,
|
|
108
|
-
`(function(){var els=document.querySelectorAll('model-response
|
|
109
|
-
]).catch(() => '0');
|
|
110
|
-
const len = parseInt(lenStr) || 0;
|
|
111
|
-
if (len >=
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
await
|
|
242
|
-
await
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
await engine.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
'
|
|
276
|
-
'
|
|
277
|
-
'
|
|
278
|
-
'
|
|
279
|
-
'
|
|
280
|
-
'
|
|
281
|
-
'',
|
|
282
|
-
'
|
|
283
|
-
'
|
|
284
|
-
'
|
|
285
|
-
'
|
|
286
|
-
' node coding-task.mjs "
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
const
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
...(
|
|
325
|
-
...(
|
|
326
|
-
...
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
|
|
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
|
+
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.`,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const STREAM_POLL_INTERVAL = 800;
|
|
33
|
+
const STREAM_STABLE_ROUNDS = 4;
|
|
34
|
+
const STREAM_TIMEOUT = 120000; // coding tasks take longer
|
|
35
|
+
const MIN_RESPONSE_LENGTH = 50;
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
function cdp(args, timeoutMs = 30000) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const proc = spawn('node', [CDP, ...args], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
42
|
+
let out = '', err = '';
|
|
43
|
+
proc.stdout.on('data', d => out += d);
|
|
44
|
+
proc.stderr.on('data', d => err += d);
|
|
45
|
+
const timer = setTimeout(() => { proc.kill(); reject(new Error(`cdp timeout: ${args[0]}`)); }, timeoutMs);
|
|
46
|
+
proc.on('close', code => {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
if (code !== 0) reject(new Error(err.trim() || `cdp exit ${code}`));
|
|
49
|
+
else resolve(out.trim());
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function getAnyTab() {
|
|
55
|
+
const list = await cdp(['list']);
|
|
56
|
+
return list.split('\n')[0].slice(0, 8);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function openNewTab() {
|
|
60
|
+
const anchor = await getAnyTab();
|
|
61
|
+
const raw = await cdp(['evalraw', anchor, 'Target.createTarget', '{"url":"about:blank"}']);
|
|
62
|
+
return JSON.parse(raw).targetId;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Engine implementations
|
|
67
|
+
|
|
68
|
+
const ENGINES = {
|
|
69
|
+
gemini: {
|
|
70
|
+
url: 'https://gemini.google.com/app',
|
|
71
|
+
domain: 'gemini.google.com',
|
|
72
|
+
|
|
73
|
+
async type(tab, text) {
|
|
74
|
+
await cdp(['eval', tab, `
|
|
75
|
+
(function(t) {
|
|
76
|
+
var el = document.querySelector('rich-textarea .ql-editor');
|
|
77
|
+
el.focus();
|
|
78
|
+
document.execCommand('insertText', false, t);
|
|
79
|
+
})(${JSON.stringify(text)})
|
|
80
|
+
`]);
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
async send(tab) {
|
|
84
|
+
await cdp(['eval', tab, `document.querySelector('button[aria-label*="Send"]')?.click()`]);
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
async waitReady(tab) {
|
|
88
|
+
const deadline = Date.now() + 12000;
|
|
89
|
+
while (Date.now() < deadline) {
|
|
90
|
+
const ok = await cdp(['eval', tab, `!!document.querySelector('rich-textarea .ql-editor')`]).catch(() => 'false');
|
|
91
|
+
if (ok === 'true') return;
|
|
92
|
+
await new Promise(r => setTimeout(r, 400));
|
|
93
|
+
}
|
|
94
|
+
throw new Error('Gemini input never appeared');
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async waitStream(tab) {
|
|
98
|
+
const deadline = Date.now() + STREAM_TIMEOUT;
|
|
99
|
+
let started = false, stableCount = 0, lastLen = -1;
|
|
100
|
+
|
|
101
|
+
while (Date.now() < deadline) {
|
|
102
|
+
await new Promise(r => setTimeout(r, STREAM_POLL_INTERVAL));
|
|
103
|
+
const stopVisible = await cdp(['eval', tab, `!!document.querySelector('button[aria-label*="Stop"]')`]).catch(() => 'false');
|
|
104
|
+
if (stopVisible === 'true') { started = true; }
|
|
105
|
+
|
|
106
|
+
// Use p/li/h* — reliable even when message-content has aria-busy="true"
|
|
107
|
+
const lenStr = await cdp(['eval', tab,
|
|
108
|
+
`(function(){var els=document.querySelectorAll('model-response p,model-response li,model-response h1,model-response h2,model-response h3');return Array.from(els).reduce(function(a,e){return a+(e.innerText?.length||0)},0)+''})()`,
|
|
109
|
+
]).catch(() => '0');
|
|
110
|
+
const len = parseInt(lenStr) || 0;
|
|
111
|
+
if (len >= 10) started = true;
|
|
112
|
+
if (!started) continue;
|
|
113
|
+
if (len >= 10 && len === lastLen && stopVisible === 'false') {
|
|
114
|
+
if (++stableCount >= STREAM_STABLE_ROUNDS) return;
|
|
115
|
+
} else { stableCount = 0; lastLen = len; }
|
|
116
|
+
}
|
|
117
|
+
if (lastLen >= 10) return;
|
|
118
|
+
throw new Error('Gemini response did not stabilise');
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
async extract(tab) {
|
|
122
|
+
return cdp(['eval', tab, `
|
|
123
|
+
(function(){
|
|
124
|
+
var UI_LABELS = /^(Gemini said|Query successful|Show code|Analysis|Hide code|Run code|View code)$/i;
|
|
125
|
+
var els = document.querySelectorAll('model-response p,model-response li,model-response h1,model-response h2,model-response h3');
|
|
126
|
+
return Array.from(els).map(function(e){return e.innerText?.trim()}).filter(function(t){return t && !UI_LABELS.test(t)}).join('\\n') || '';
|
|
127
|
+
})()
|
|
128
|
+
`]);
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
copilot: {
|
|
133
|
+
url: 'https://copilot.microsoft.com/',
|
|
134
|
+
domain: 'copilot.microsoft.com',
|
|
135
|
+
|
|
136
|
+
async type(tab, text) {
|
|
137
|
+
await cdp(['click', tab, '#userInput']);
|
|
138
|
+
await new Promise(r => setTimeout(r, 300));
|
|
139
|
+
await cdp(['type', tab, text]);
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
async send(tab) {
|
|
143
|
+
await cdp(['eval', tab,
|
|
144
|
+
`document.querySelector('#userInput')?.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true,keyCode:13})), 'ok'`
|
|
145
|
+
]);
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
async waitReady(tab) {
|
|
149
|
+
const deadline = Date.now() + 10000;
|
|
150
|
+
while (Date.now() < deadline) {
|
|
151
|
+
const ok = await cdp(['eval', tab, `!!document.querySelector('#userInput')`]).catch(() => 'false');
|
|
152
|
+
if (ok === 'true') return;
|
|
153
|
+
await new Promise(r => setTimeout(r, 400));
|
|
154
|
+
}
|
|
155
|
+
throw new Error('Copilot input never appeared');
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
async waitStream(tab) {
|
|
159
|
+
const deadline = Date.now() + STREAM_TIMEOUT;
|
|
160
|
+
let stableCount = 0, lastLen = -1;
|
|
161
|
+
while (Date.now() < deadline) {
|
|
162
|
+
await new Promise(r => setTimeout(r, STREAM_POLL_INTERVAL));
|
|
163
|
+
const lenStr = await cdp(['eval', tab, `
|
|
164
|
+
(function(){
|
|
165
|
+
var items = Array.from(document.querySelectorAll('[class*="ai-message-item"]'));
|
|
166
|
+
var filled = items.filter(el => (el.innerText?.length||0) > 0);
|
|
167
|
+
var last = filled[filled.length-1];
|
|
168
|
+
return (last?.innerText?.length||0)+'';
|
|
169
|
+
})()`
|
|
170
|
+
]).catch(() => '0');
|
|
171
|
+
const len = parseInt(lenStr) || 0;
|
|
172
|
+
if (len >= MIN_RESPONSE_LENGTH && len === lastLen) {
|
|
173
|
+
if (++stableCount >= STREAM_STABLE_ROUNDS) return;
|
|
174
|
+
} else { stableCount = 0; lastLen = len; }
|
|
175
|
+
}
|
|
176
|
+
if (lastLen >= MIN_RESPONSE_LENGTH) return;
|
|
177
|
+
throw new Error('Copilot response did not stabilise');
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
async extract(tab) {
|
|
181
|
+
return cdp(['eval', tab, `
|
|
182
|
+
(function(){
|
|
183
|
+
var items = Array.from(document.querySelectorAll('[class*="ai-message-item"]'));
|
|
184
|
+
var last = items.filter(e=>(e.innerText?.length||0)>0).pop();
|
|
185
|
+
return last?.innerText?.trim()||'';
|
|
186
|
+
})()
|
|
187
|
+
`]);
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
function extractCodeBlocks(text) {
|
|
195
|
+
const blocks = [];
|
|
196
|
+
const regex = /```(\w+)?\n([\s\S]*?)```/g;
|
|
197
|
+
let match;
|
|
198
|
+
while ((match = regex.exec(text)) !== null) {
|
|
199
|
+
blocks.push({ language: match[1] || 'text', code: match[2].trim() });
|
|
200
|
+
}
|
|
201
|
+
// If no fenced blocks, look for indented blocks as fallback
|
|
202
|
+
if (blocks.length === 0) {
|
|
203
|
+
const lines = text.split('\n');
|
|
204
|
+
const indented = lines.filter(l => l.startsWith(' ')).map(l => l.slice(4));
|
|
205
|
+
if (indented.length > 3) blocks.push({ language: 'text', code: indented.join('\n') });
|
|
206
|
+
}
|
|
207
|
+
return blocks;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function extractExplanation(text, codeBlocks) {
|
|
211
|
+
// Remove code blocks from text to get the explanation
|
|
212
|
+
let explanation = text.replace(/```[\s\S]*?```/g, '').trim();
|
|
213
|
+
explanation = explanation.replace(/\n{3,}/g, '\n\n').trim();
|
|
214
|
+
return explanation.slice(0, 1000); // cap explanation at 1000 chars
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function runEngine(engineName, task, context, mode, tabPrefix) {
|
|
218
|
+
const engine = ENGINES[engineName];
|
|
219
|
+
if (!engine) throw new Error(`Unknown engine: ${engineName}`);
|
|
220
|
+
|
|
221
|
+
// Find or open a tab
|
|
222
|
+
let tab = tabPrefix;
|
|
223
|
+
if (!tab) {
|
|
224
|
+
if (existsSync(PAGES_CACHE)) {
|
|
225
|
+
const pages = JSON.parse(readFileSync(PAGES_CACHE, 'utf8'));
|
|
226
|
+
const existing = pages.find(p => p.url.includes(engine.domain));
|
|
227
|
+
if (existing) tab = existing.targetId.slice(0, 8);
|
|
228
|
+
}
|
|
229
|
+
if (!tab) tab = await openNewTab();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Navigate to fresh conversation — fall back to new tab if cached tab is stale
|
|
233
|
+
try {
|
|
234
|
+
await cdp(['nav', tab, engine.url], 35000);
|
|
235
|
+
} catch (e) {
|
|
236
|
+
if (e.message.includes('No target matching')) {
|
|
237
|
+
tab = await openNewTab();
|
|
238
|
+
await cdp(['nav', tab, engine.url], 35000);
|
|
239
|
+
} else throw e;
|
|
240
|
+
}
|
|
241
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
242
|
+
await dismissConsent(tab, cdp);
|
|
243
|
+
await handleVerification(tab, cdp, 60000);
|
|
244
|
+
await engine.waitReady(tab);
|
|
245
|
+
await new Promise(r => setTimeout(r, 300));
|
|
246
|
+
|
|
247
|
+
// Build the prompt
|
|
248
|
+
const preamble = MODE_PROMPTS[mode] || null;
|
|
249
|
+
const body = context
|
|
250
|
+
? `${task}\n\nHere is the relevant code/context:\n\`\`\`\n${context}\n\`\`\``
|
|
251
|
+
: task;
|
|
252
|
+
const prompt = preamble ? `${preamble}\n\n---\n\n${body}` : body;
|
|
253
|
+
|
|
254
|
+
await engine.type(tab, prompt);
|
|
255
|
+
await new Promise(r => setTimeout(r, 400));
|
|
256
|
+
await engine.send(tab);
|
|
257
|
+
await engine.waitStream(tab);
|
|
258
|
+
|
|
259
|
+
const raw = await engine.extract(tab);
|
|
260
|
+
if (!raw) throw new Error(`No response from ${engineName}`);
|
|
261
|
+
|
|
262
|
+
const code = extractCodeBlocks(raw);
|
|
263
|
+
const explanation = extractExplanation(raw, code);
|
|
264
|
+
const url = await cdp(['eval', tab, 'document.location.href']).catch(() => engine.url);
|
|
265
|
+
|
|
266
|
+
return { engine: engineName, task, code, explanation, raw, url };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
async function main() {
|
|
272
|
+
const args = process.argv.slice(2);
|
|
273
|
+
if (!args.length || args[0] === '--help') {
|
|
274
|
+
process.stderr.write([
|
|
275
|
+
'Usage: node coding-task.mjs "<task>" --engine gemini|copilot|all [--mode code|review|plan]',
|
|
276
|
+
' node coding-task.mjs "<task>" --engine gemini --context "<code>"',
|
|
277
|
+
'',
|
|
278
|
+
'Modes:',
|
|
279
|
+
' code (default) — write or modify code',
|
|
280
|
+
' review — senior engineer code review: correctness, security, performance',
|
|
281
|
+
' plan — architect review: risks, gaps, alternatives for a build plan',
|
|
282
|
+
' test — write tests an author would miss: edge cases, error paths, boundary conditions',
|
|
283
|
+
' debug — fresh-eyes root cause analysis: exact line, why it manifests, minimal fix',
|
|
284
|
+
'',
|
|
285
|
+
'Examples:',
|
|
286
|
+
' node coding-task.mjs "write a debounce function in JS" --engine gemini',
|
|
287
|
+
' node coding-task.mjs "review this module" --mode review --engine all --file src/myfile.mjs',
|
|
288
|
+
' node coding-task.mjs "debug this" --mode debug --engine all --file a.mjs --file b.mjs',
|
|
289
|
+
' node coding-task.mjs "I want to build X, here is my plan: ..." --mode plan --engine all',
|
|
290
|
+
].join('\n') + '\n');
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const engineFlagIdx = args.indexOf('--engine');
|
|
295
|
+
const engineArg = engineFlagIdx !== -1 ? args[engineFlagIdx + 1] : 'gemini';
|
|
296
|
+
const contextFlagIdx = args.indexOf('--context');
|
|
297
|
+
const outIdx = args.indexOf('--out');
|
|
298
|
+
const outFile = outIdx !== -1 ? args[outIdx + 1] : null;
|
|
299
|
+
const tabFlagIdx = args.indexOf('--tab');
|
|
300
|
+
const tabPrefix = tabFlagIdx !== -1 ? args[tabFlagIdx + 1] : null;
|
|
301
|
+
const modeFlagIdx = args.indexOf('--mode');
|
|
302
|
+
const mode = modeFlagIdx !== -1 ? args[modeFlagIdx + 1] : 'code';
|
|
303
|
+
|
|
304
|
+
if (!MODE_PROMPTS.hasOwnProperty(mode)) {
|
|
305
|
+
process.stderr.write(`Error: unknown mode "${mode}". Use: code, review, plan, test, debug\n`);
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// --file can be repeated: --file a.mjs --file b.mjs
|
|
310
|
+
const fileIndices = [];
|
|
311
|
+
const filePaths = [];
|
|
312
|
+
for (let i = 0; i < args.length; i++) {
|
|
313
|
+
if (args[i] === '--file' && args[i + 1]) {
|
|
314
|
+
fileIndices.push(i, i + 1);
|
|
315
|
+
filePaths.push(args[i + 1]);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const fileContext = filePaths.length > 0
|
|
319
|
+
? filePaths.map(p => `// FILE: ${p}\n${readFileSync(p, 'utf8')}`).join('\n\n')
|
|
320
|
+
: null;
|
|
321
|
+
const context = fileContext || (contextFlagIdx !== -1 ? args[contextFlagIdx + 1] : null);
|
|
322
|
+
|
|
323
|
+
const skipFlags = new Set([
|
|
324
|
+
...(engineFlagIdx >= 0 ? [engineFlagIdx, engineFlagIdx + 1] : []),
|
|
325
|
+
...(contextFlagIdx >= 0 ? [contextFlagIdx, contextFlagIdx + 1] : []),
|
|
326
|
+
...(outIdx >= 0 ? [outIdx, outIdx + 1] : []),
|
|
327
|
+
...(tabFlagIdx >= 0 ? [tabFlagIdx, tabFlagIdx + 1] : []),
|
|
328
|
+
...(modeFlagIdx >= 0 ? [modeFlagIdx, modeFlagIdx + 1] : []),
|
|
329
|
+
...fileIndices,
|
|
330
|
+
]);
|
|
331
|
+
const task = args.filter((_, i) => !skipFlags.has(i)).join(' ');
|
|
332
|
+
|
|
333
|
+
if (!task) {
|
|
334
|
+
process.stderr.write('Error: no task provided\n');
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
await cdp(['list']); // ensure Chrome is reachable
|
|
339
|
+
|
|
340
|
+
let result;
|
|
341
|
+
|
|
342
|
+
if (engineArg === 'all') {
|
|
343
|
+
const results = await Promise.allSettled(
|
|
344
|
+
Object.keys(ENGINES).map(e => runEngine(e, task, context, mode, null))
|
|
345
|
+
);
|
|
346
|
+
result = {};
|
|
347
|
+
for (const [i, r] of results.entries()) {
|
|
348
|
+
const name = Object.keys(ENGINES)[i];
|
|
349
|
+
result[name] = r.status === 'fulfilled' ? r.value : { engine: name, error: r.reason?.message };
|
|
350
|
+
}
|
|
351
|
+
} else {
|
|
352
|
+
try {
|
|
353
|
+
result = await runEngine(engineArg, task, context, mode, tabPrefix);
|
|
354
|
+
} catch (e) {
|
|
355
|
+
process.stderr.write(`Error: ${e.message}\n`);
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const json = JSON.stringify(result, null, 2) + '\n';
|
|
361
|
+
if (outFile) {
|
|
362
|
+
writeFileSync(outFile, json, 'utf8');
|
|
363
|
+
process.stderr.write(`Results written to ${outFile}\n`);
|
|
364
|
+
} else {
|
|
365
|
+
process.stdout.write(json);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
main();
|