@apmantza/greedysearch-pi 1.4.2 → 1.5.0
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/.pi-lens/cache/jscpd.json +112 -0
- package/.pi-lens/cache/jscpd.meta.json +3 -0
- package/.pi-lens/cache/knip.json +111 -0
- package/.pi-lens/cache/knip.meta.json +4 -0
- package/.pi-lens/fix-plan.md +13 -0
- package/.pi-lens/fix-session.json +11 -0
- package/.pi-lens/metrics-history.json +182 -0
- package/.pi-lens/reports/fix-plan.tsv +38 -0
- package/.pi-lens/turn-state.json +6 -0
- package/CHANGELOG.md +30 -0
- package/README.md +233 -219
- package/cdp.mjs +1002 -797
- package/coding-task.mjs +392 -369
- package/extractors/bing-copilot.mjs +167 -195
- package/extractors/common.mjs +237 -0
- package/extractors/consent.mjs +273 -255
- package/extractors/gemini.mjs +142 -180
- package/extractors/google-ai.mjs +156 -162
- package/extractors/perplexity.mjs +126 -181
- package/extractors/selectors.mjs +43 -43
- package/index.ts +230 -93
- package/launch.mjs +283 -161
- package/package.json +26 -26
- package/search.mjs +1219 -997
- package/skills/greedy-search/SKILL.md +38 -109
- package/test.mjs +308 -0
- package/test.sh +298 -298
- package/newfeaturesideas.md +0 -105
package/coding-task.mjs
CHANGED
|
@@ -1,369 +1,392 @@
|
|
|
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 {
|
|
13
|
-
import { tmpdir } from
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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 { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { cdp, injectClipboardInterceptor } from "./extractors/common.mjs";
|
|
16
|
+
import { dismissConsent, handleVerification } from "./extractors/consent.mjs";
|
|
17
|
+
|
|
18
|
+
const __dir = fileURLToPath(new URL(".", import.meta.url));
|
|
19
|
+
const PAGES_CACHE = `${tmpdir().replace(/\\/g, "/")}/cdp-pages.json`;
|
|
20
|
+
|
|
21
|
+
// Target the dedicated GreedySearch Chrome instance (port 9222)
|
|
22
|
+
const GREEDY_PROFILE_DIR = `${tmpdir().replace(/\\/g, "/")}/greedysearch-chrome-profile`;
|
|
23
|
+
process.env.CDP_PROFILE_DIR = GREEDY_PROFILE_DIR;
|
|
24
|
+
|
|
25
|
+
// Mode system prompts — prepended to the user's task
|
|
26
|
+
const MODE_PROMPTS = {
|
|
27
|
+
code: null, // no preamble — default behaviour
|
|
28
|
+
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.`,
|
|
29
|
+
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.`,
|
|
30
|
+
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.`,
|
|
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
|
+
};
|
|
33
|
+
|
|
34
|
+
const STREAM_POLL_INTERVAL = 800;
|
|
35
|
+
const STREAM_STABLE_ROUNDS = 4;
|
|
36
|
+
const STREAM_TIMEOUT = 120000; // coding tasks take longer
|
|
37
|
+
const MIN_RESPONSE_LENGTH = 50;
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Tab management
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
async function getAnyTab() {
|
|
44
|
+
const list = await cdp(["list"]);
|
|
45
|
+
return list.split("\n")[0].slice(0, 8);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function openNewTab() {
|
|
49
|
+
const anchor = await getAnyTab();
|
|
50
|
+
const raw = await cdp([
|
|
51
|
+
"evalraw",
|
|
52
|
+
anchor,
|
|
53
|
+
"Target.createTarget",
|
|
54
|
+
'{"url":"about:blank"}',
|
|
55
|
+
]);
|
|
56
|
+
const { targetId } = JSON.parse(raw);
|
|
57
|
+
await cdp(["list"]); // refresh cache so cdp nav can find the new tab
|
|
58
|
+
return targetId.slice(0, 8);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Engine implementations
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
const ENGINES = {
|
|
66
|
+
gemini: {
|
|
67
|
+
url: "https://gemini.google.com/app",
|
|
68
|
+
domain: "gemini.google.com",
|
|
69
|
+
|
|
70
|
+
async type(tab, text) {
|
|
71
|
+
await cdp([
|
|
72
|
+
"eval",
|
|
73
|
+
tab,
|
|
74
|
+
`
|
|
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
|
+
|
|
84
|
+
async send(tab) {
|
|
85
|
+
await cdp([
|
|
86
|
+
"eval",
|
|
87
|
+
tab,
|
|
88
|
+
`document.querySelector('button[aria-label*="Send"]')?.click()`,
|
|
89
|
+
]);
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
async waitReady(tab) {
|
|
93
|
+
const deadline = Date.now() + 12000;
|
|
94
|
+
while (Date.now() < deadline) {
|
|
95
|
+
const ok = await cdp([
|
|
96
|
+
"eval",
|
|
97
|
+
tab,
|
|
98
|
+
`!!document.querySelector('rich-textarea .ql-editor')`,
|
|
99
|
+
]).catch(() => "false");
|
|
100
|
+
if (ok === "true") return;
|
|
101
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
102
|
+
}
|
|
103
|
+
throw new Error("Gemini input never appeared");
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
async waitForCopyButton(tab) {
|
|
107
|
+
const deadline = Date.now() + STREAM_TIMEOUT;
|
|
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");
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
async extract(tab) {
|
|
121
|
+
// Click copy button → clipboard interceptor captures the markdown
|
|
122
|
+
await cdp([
|
|
123
|
+
"eval",
|
|
124
|
+
tab,
|
|
125
|
+
`document.querySelector('button[aria-label="Copy"]')?.click()`,
|
|
126
|
+
]);
|
|
127
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
128
|
+
return cdp(["eval", tab, "window.__codingTaskClipboard || ''"]);
|
|
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([
|
|
144
|
+
"eval",
|
|
145
|
+
tab,
|
|
146
|
+
`document.querySelector('#userInput')?.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true,keyCode:13})), 'ok'`,
|
|
147
|
+
]);
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
async waitReady(tab) {
|
|
151
|
+
const deadline = Date.now() + 10000;
|
|
152
|
+
while (Date.now() < deadline) {
|
|
153
|
+
const ok = await cdp([
|
|
154
|
+
"eval",
|
|
155
|
+
tab,
|
|
156
|
+
`!!document.querySelector('#userInput')`,
|
|
157
|
+
]).catch(() => "false");
|
|
158
|
+
if (ok === "true") return;
|
|
159
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
160
|
+
}
|
|
161
|
+
throw new Error("Copilot input never appeared");
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
async waitForCopyButton(tab) {
|
|
165
|
+
const deadline = Date.now() + STREAM_TIMEOUT;
|
|
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");
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
async extract(tab) {
|
|
179
|
+
// Click copy button → clipboard interceptor captures the markdown
|
|
180
|
+
await cdp([
|
|
181
|
+
"eval",
|
|
182
|
+
tab,
|
|
183
|
+
`document.querySelector('button[data-testid="copy-ai-message-button"]')?.click()`,
|
|
184
|
+
]);
|
|
185
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
186
|
+
return cdp(["eval", tab, "window.__codingTaskClipboard || ''"]);
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Code extraction
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
function extractCodeBlocks(text) {
|
|
196
|
+
const blocks = [];
|
|
197
|
+
const regex = /```(\w+)?\n([\s\S]*?)```/g;
|
|
198
|
+
let match = regex.exec(text);
|
|
199
|
+
while (match !== null) {
|
|
200
|
+
blocks.push({ language: match[1] || "text", code: match[2].trim() });
|
|
201
|
+
match = regex.exec(text);
|
|
202
|
+
}
|
|
203
|
+
// If no fenced blocks, look for indented blocks as fallback
|
|
204
|
+
if (blocks.length === 0) {
|
|
205
|
+
const lines = text.split("\n");
|
|
206
|
+
const indented = lines
|
|
207
|
+
.filter((l) => l.startsWith(" "))
|
|
208
|
+
.map((l) => l.slice(4));
|
|
209
|
+
if (indented.length > 3)
|
|
210
|
+
blocks.push({ language: "text", code: indented.join("\n") });
|
|
211
|
+
}
|
|
212
|
+
return blocks;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function extractExplanation(text, _codeBlocks) {
|
|
216
|
+
// Remove code blocks from text to get the explanation
|
|
217
|
+
let explanation = text.replace(/```[\s\S]*?```/g, "").trim();
|
|
218
|
+
explanation = explanation.replace(/\n{3,}/g, "\n\n").trim();
|
|
219
|
+
return explanation.slice(0, 1000); // cap explanation at 1000 chars
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function runEngine(engineName, task, context, mode, tabPrefix) {
|
|
223
|
+
const engine = ENGINES[engineName];
|
|
224
|
+
if (!engine) throw new Error(`Unknown engine: ${engineName}`);
|
|
225
|
+
|
|
226
|
+
// Find or open a tab
|
|
227
|
+
let tab = tabPrefix;
|
|
228
|
+
if (!tab) {
|
|
229
|
+
if (existsSync(PAGES_CACHE)) {
|
|
230
|
+
const pages = JSON.parse(readFileSync(PAGES_CACHE, "utf8"));
|
|
231
|
+
const existing = pages.find((p) => p.url.includes(engine.domain));
|
|
232
|
+
if (existing) tab = existing.targetId.slice(0, 8);
|
|
233
|
+
}
|
|
234
|
+
if (!tab) tab = await openNewTab();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Navigate to fresh conversation — fall back to new tab if cached tab is stale
|
|
238
|
+
try {
|
|
239
|
+
await cdp(["nav", tab, engine.url], 35000);
|
|
240
|
+
} catch (e) {
|
|
241
|
+
if (e.message.includes("No target matching")) {
|
|
242
|
+
tab = await openNewTab();
|
|
243
|
+
await cdp(["nav", tab, engine.url], 35000);
|
|
244
|
+
} else throw e;
|
|
245
|
+
}
|
|
246
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
247
|
+
await dismissConsent(tab, cdp);
|
|
248
|
+
await handleVerification(tab, cdp, 60000);
|
|
249
|
+
await engine.waitReady(tab);
|
|
250
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
251
|
+
|
|
252
|
+
// Inject clipboard interceptor to capture markdown when copy button clicked
|
|
253
|
+
await injectClipboardInterceptor(tab, "__codingTaskClipboard");
|
|
254
|
+
|
|
255
|
+
// Build the prompt
|
|
256
|
+
const preamble = MODE_PROMPTS[mode] || null;
|
|
257
|
+
const body = context
|
|
258
|
+
? `${task}\n\nHere is the relevant code/context:\n\`\`\`\n${context}\n\`\`\``
|
|
259
|
+
: task;
|
|
260
|
+
const prompt = preamble ? `${preamble}\n\n---\n\n${body}` : body;
|
|
261
|
+
|
|
262
|
+
await engine.type(tab, prompt);
|
|
263
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
264
|
+
await engine.send(tab);
|
|
265
|
+
await engine.waitForCopyButton(tab);
|
|
266
|
+
|
|
267
|
+
const raw = await engine.extract(tab);
|
|
268
|
+
if (!raw) throw new Error(`No response from ${engineName}`);
|
|
269
|
+
|
|
270
|
+
const code = extractCodeBlocks(raw);
|
|
271
|
+
const explanation = extractExplanation(raw, code);
|
|
272
|
+
const url = await cdp(["eval", tab, "document.location.href"]).catch(
|
|
273
|
+
() => engine.url,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
return { engine: engineName, task, code, explanation, raw, url };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Main
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
async function main() {
|
|
284
|
+
const args = process.argv.slice(2);
|
|
285
|
+
if (!args.length || args[0] === "--help") {
|
|
286
|
+
process.stderr.write(
|
|
287
|
+
`${[
|
|
288
|
+
'Usage: node coding-task.mjs "<task>" --engine gemini|copilot|all [--mode code|review|plan]',
|
|
289
|
+
' node coding-task.mjs "<task>" --engine gemini --context "<code>"',
|
|
290
|
+
"",
|
|
291
|
+
"Modes:",
|
|
292
|
+
" code (default) — write or modify code",
|
|
293
|
+
" review — senior engineer code review: correctness, security, performance",
|
|
294
|
+
" plan — architect review: risks, gaps, alternatives for a build plan",
|
|
295
|
+
" test — write tests an author would miss: edge cases, error paths, boundary conditions",
|
|
296
|
+
" debug — fresh-eyes root cause analysis: exact line, why it manifests, minimal fix",
|
|
297
|
+
"",
|
|
298
|
+
"Examples:",
|
|
299
|
+
' node coding-task.mjs "write a debounce function in JS" --engine gemini',
|
|
300
|
+
' node coding-task.mjs "review this module" --mode review --engine all --file src/myfile.mjs',
|
|
301
|
+
' node coding-task.mjs "debug this" --mode debug --engine all --file a.mjs --file b.mjs',
|
|
302
|
+
' node coding-task.mjs "I want to build X, here is my plan: ..." --mode plan --engine all',
|
|
303
|
+
].join("\n")}\n`,
|
|
304
|
+
);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const engineFlagIdx = args.indexOf("--engine");
|
|
309
|
+
const engineArg = engineFlagIdx !== -1 ? args[engineFlagIdx + 1] : "gemini";
|
|
310
|
+
const contextFlagIdx = args.indexOf("--context");
|
|
311
|
+
const outIdx = args.indexOf("--out");
|
|
312
|
+
const outFile = outIdx !== -1 ? args[outIdx + 1] : null;
|
|
313
|
+
const tabFlagIdx = args.indexOf("--tab");
|
|
314
|
+
const tabPrefix = tabFlagIdx !== -1 ? args[tabFlagIdx + 1] : null;
|
|
315
|
+
const modeFlagIdx = args.indexOf("--mode");
|
|
316
|
+
const mode = modeFlagIdx !== -1 ? args[modeFlagIdx + 1] : "code";
|
|
317
|
+
|
|
318
|
+
if (!Object.hasOwn(MODE_PROMPTS, mode)) {
|
|
319
|
+
process.stderr.write(
|
|
320
|
+
`Error: unknown mode "${mode}". Use: code, review, plan, test, debug\n`,
|
|
321
|
+
);
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// --file can be repeated: --file a.mjs --file b.mjs
|
|
326
|
+
const fileIndices = [];
|
|
327
|
+
const filePaths = [];
|
|
328
|
+
for (let i = 0; i < args.length; i++) {
|
|
329
|
+
if (args[i] === "--file" && args[i + 1]) {
|
|
330
|
+
fileIndices.push(i, i + 1);
|
|
331
|
+
filePaths.push(args[i + 1]);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const fileContext =
|
|
335
|
+
filePaths.length > 0
|
|
336
|
+
? filePaths
|
|
337
|
+
.map((p) => `// FILE: ${p}\n${readFileSync(p, "utf8")}`)
|
|
338
|
+
.join("\n\n")
|
|
339
|
+
: null;
|
|
340
|
+
const context =
|
|
341
|
+
fileContext || (contextFlagIdx !== -1 ? args[contextFlagIdx + 1] : null);
|
|
342
|
+
|
|
343
|
+
const skipFlags = new Set([
|
|
344
|
+
...(engineFlagIdx >= 0 ? [engineFlagIdx, engineFlagIdx + 1] : []),
|
|
345
|
+
...(contextFlagIdx >= 0 ? [contextFlagIdx, contextFlagIdx + 1] : []),
|
|
346
|
+
...(outIdx >= 0 ? [outIdx, outIdx + 1] : []),
|
|
347
|
+
...(tabFlagIdx >= 0 ? [tabFlagIdx, tabFlagIdx + 1] : []),
|
|
348
|
+
...(modeFlagIdx >= 0 ? [modeFlagIdx, modeFlagIdx + 1] : []),
|
|
349
|
+
...fileIndices,
|
|
350
|
+
]);
|
|
351
|
+
const task = args.filter((_, i) => !skipFlags.has(i)).join(" ");
|
|
352
|
+
|
|
353
|
+
if (!task) {
|
|
354
|
+
process.stderr.write("Error: no task provided\n");
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
await cdp(["list"]); // ensure Chrome is reachable
|
|
359
|
+
|
|
360
|
+
let result;
|
|
361
|
+
|
|
362
|
+
if (engineArg === "all") {
|
|
363
|
+
const results = await Promise.allSettled(
|
|
364
|
+
Object.keys(ENGINES).map((e) => runEngine(e, task, context, mode, null)),
|
|
365
|
+
);
|
|
366
|
+
result = {};
|
|
367
|
+
for (const [i, r] of results.entries()) {
|
|
368
|
+
const name = Object.keys(ENGINES)[i];
|
|
369
|
+
result[name] =
|
|
370
|
+
r.status === "fulfilled"
|
|
371
|
+
? r.value
|
|
372
|
+
: { engine: name, error: r.reason?.message };
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
try {
|
|
376
|
+
result = await runEngine(engineArg, task, context, mode, tabPrefix);
|
|
377
|
+
} catch (e) {
|
|
378
|
+
process.stderr.write(`Error: ${e.message}\n`);
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const json = `${JSON.stringify(result, null, 2)}\n`;
|
|
384
|
+
if (outFile) {
|
|
385
|
+
writeFileSync(outFile, json, "utf8");
|
|
386
|
+
process.stderr.write(`Results written to ${outFile}\n`);
|
|
387
|
+
} else {
|
|
388
|
+
process.stdout.write(json);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
main();
|