@apmantza/greedysearch-pi 1.9.0 → 1.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +11 -1
- package/bin/launch-visible.mjs +65 -0
- package/bin/launch.mjs +442 -417
- package/bin/search.mjs +757 -679
- package/extractors/bing-copilot.mjs +490 -374
- package/extractors/common.mjs +703 -596
- package/extractors/consent.mjs +421 -388
- package/extractors/selectors.mjs +55 -54
- package/index.ts +176 -177
- package/package.json +8 -3
- package/skills/greedy-search/skill.md +5 -19
- package/src/fetcher.mjs +666 -652
- package/src/formatters/synthesis.ts +1 -5
- package/src/search/output.mjs +23 -1
- package/src/search/research.mjs +1581 -0
- package/src/search/sources.mjs +488 -466
- package/src/search/synthesis-runner.mjs +52 -46
- package/src/tools/greedy-search-handler.ts +298 -124
- package/test.mjs +971 -534
package/bin/launch.mjs
CHANGED
|
@@ -1,417 +1,442 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// launch.mjs — start a dedicated Chrome instance for GreedySearch
|
|
3
|
-
//
|
|
4
|
-
// This Chrome instance uses --disable-features=DevToolsPrivacyUI which suppresses
|
|
5
|
-
// the "Allow remote debugging?" dialog entirely. It runs on port 9222 so it doesn't
|
|
6
|
-
// conflict with your main Chrome session (which may use port 9223).
|
|
7
|
-
//
|
|
8
|
-
// search.mjs passes CDP_PROFILE_DIR so cdp.mjs targets this dedicated Chrome
|
|
9
|
-
// without ever touching the user's main Chrome DevToolsActivePort file.
|
|
10
|
-
//
|
|
11
|
-
// Usage:
|
|
12
|
-
// node launch.mjs — launch (or report if already running)
|
|
13
|
-
// node launch.mjs --headless — launch in headless mode (no GUI window)
|
|
14
|
-
// node launch.mjs --kill — stop and restore original DevToolsActivePort
|
|
15
|
-
// node launch.mjs --status — check if running
|
|
16
|
-
//
|
|
17
|
-
// Environment:
|
|
18
|
-
// GREEDY_SEARCH_VISIBLE=1 — Show Chrome window (disables headless mode)
|
|
19
|
-
// CHROME_PATH — Path to Chrome executable
|
|
20
|
-
|
|
21
|
-
import { execSync, spawn } from "node:child_process";
|
|
22
|
-
import {
|
|
23
|
-
existsSync,
|
|
24
|
-
mkdirSync,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
import
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"C:/Program Files
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"/Applications/
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"/usr/bin/google-chrome
|
|
55
|
-
"/usr/bin/
|
|
56
|
-
"/usr/bin/chromium",
|
|
57
|
-
"/
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"--
|
|
68
|
-
"--no-
|
|
69
|
-
"--
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"--
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
if (
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// launch.mjs — start a dedicated Chrome instance for GreedySearch
|
|
3
|
+
//
|
|
4
|
+
// This Chrome instance uses --disable-features=DevToolsPrivacyUI which suppresses
|
|
5
|
+
// the "Allow remote debugging?" dialog entirely. It runs on port 9222 so it doesn't
|
|
6
|
+
// conflict with your main Chrome session (which may use port 9223).
|
|
7
|
+
//
|
|
8
|
+
// search.mjs passes CDP_PROFILE_DIR so cdp.mjs targets this dedicated Chrome
|
|
9
|
+
// without ever touching the user's main Chrome DevToolsActivePort file.
|
|
10
|
+
//
|
|
11
|
+
// Usage:
|
|
12
|
+
// node launch.mjs — launch (or report if already running)
|
|
13
|
+
// node launch.mjs --headless — launch in headless mode (no GUI window)
|
|
14
|
+
// node launch.mjs --kill — stop and restore original DevToolsActivePort
|
|
15
|
+
// node launch.mjs --status — check if running
|
|
16
|
+
//
|
|
17
|
+
// Environment:
|
|
18
|
+
// GREEDY_SEARCH_VISIBLE=1 — Show Chrome window (disables headless mode)
|
|
19
|
+
// CHROME_PATH — Path to Chrome executable
|
|
20
|
+
|
|
21
|
+
import { execSync, spawn } from "node:child_process";
|
|
22
|
+
import {
|
|
23
|
+
existsSync,
|
|
24
|
+
mkdirSync,
|
|
25
|
+
readdirSync,
|
|
26
|
+
readFileSync,
|
|
27
|
+
unlinkSync,
|
|
28
|
+
writeFileSync,
|
|
29
|
+
} from "node:fs";
|
|
30
|
+
import http from "node:http";
|
|
31
|
+
import { platform, tmpdir } from "node:os";
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
|
|
34
|
+
const PORT = 9222;
|
|
35
|
+
const PROFILE_DIR = join(tmpdir(), "greedysearch-chrome-profile");
|
|
36
|
+
const ACTIVE_PORT = join(PROFILE_DIR, "DevToolsActivePort");
|
|
37
|
+
const PID_FILE = join(tmpdir(), "greedysearch-chrome.pid");
|
|
38
|
+
const MODE_FILE = join(tmpdir(), "greedysearch-chrome-mode");
|
|
39
|
+
|
|
40
|
+
function findChrome() {
|
|
41
|
+
const os = platform();
|
|
42
|
+
const candidates =
|
|
43
|
+
os === "win32"
|
|
44
|
+
? [
|
|
45
|
+
"C:/Program Files/Google/Chrome/Application/chrome.exe",
|
|
46
|
+
"C:/Program Files (x86)/Google/Chrome/Application/chrome.exe",
|
|
47
|
+
]
|
|
48
|
+
: os === "darwin"
|
|
49
|
+
? [
|
|
50
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
51
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
52
|
+
]
|
|
53
|
+
: [
|
|
54
|
+
"/usr/bin/google-chrome",
|
|
55
|
+
"/usr/bin/google-chrome-stable",
|
|
56
|
+
"/usr/bin/chromium-browser",
|
|
57
|
+
"/usr/bin/chromium",
|
|
58
|
+
"/snap/bin/chromium",
|
|
59
|
+
];
|
|
60
|
+
return candidates.find(existsSync) || null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const isHeadless = () => process.env.GREEDY_SEARCH_VISIBLE !== "1";
|
|
64
|
+
|
|
65
|
+
const BASE_CHROME_FLAGS = [
|
|
66
|
+
`--remote-debugging-port=${PORT}`,
|
|
67
|
+
"--disable-features=DevToolsPrivacyUI",
|
|
68
|
+
"--no-first-run",
|
|
69
|
+
"--no-default-browser-check",
|
|
70
|
+
"--disable-default-apps",
|
|
71
|
+
// Anti-detection: suppress the AutomationControlled flag that exposes CDP usage.
|
|
72
|
+
// Must be set for BOTH headless and visible — Cloudflare / DataDome detect it.
|
|
73
|
+
"--disable-blink-features=AutomationControlled",
|
|
74
|
+
`--user-data-dir=${PROFILE_DIR}`,
|
|
75
|
+
"--profile-directory=Default",
|
|
76
|
+
"--window-size=1920,1080",
|
|
77
|
+
"--lang=en-US",
|
|
78
|
+
"--force-color-profile=srgb",
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
function getChromeVersion(chromePath) {
|
|
82
|
+
// Primary: versioned sub-directory inside the Chrome Application folder.
|
|
83
|
+
// Chrome always creates one (e.g. "148.0.7778.168") — works on all platforms,
|
|
84
|
+
// avoids launching the GUI process just to read a version string.
|
|
85
|
+
try {
|
|
86
|
+
const appDir = join(chromePath, "..");
|
|
87
|
+
const entries = readdirSync(appDir);
|
|
88
|
+
const ver = entries.find((e) =>
|
|
89
|
+
/^\d{1,10}\.\d{1,10}\.\d{1,10}\.\d{1,10}$/.test(e),
|
|
90
|
+
);
|
|
91
|
+
if (ver) return ver.split(".")[0];
|
|
92
|
+
} catch {}
|
|
93
|
+
|
|
94
|
+
// Fallback: `chrome --version` — works on macOS/Linux where Chrome is a CLI process.
|
|
95
|
+
try {
|
|
96
|
+
const out = execSync(`"${chromePath}" --version`, {
|
|
97
|
+
encoding: "utf8",
|
|
98
|
+
timeout: 5000,
|
|
99
|
+
}).trim();
|
|
100
|
+
const m = out.match(/(\d{1,10})\.\d{1,10}\.\d{1,10}/);
|
|
101
|
+
if (m) return m[1];
|
|
102
|
+
} catch {}
|
|
103
|
+
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function buildChromeFlags(chromePath) {
|
|
108
|
+
const flags = [...BASE_CHROME_FLAGS];
|
|
109
|
+
if (isHeadless()) {
|
|
110
|
+
flags.push("--headless=new");
|
|
111
|
+
const major = getChromeVersion(chromePath) || "136";
|
|
112
|
+
flags.push(
|
|
113
|
+
`--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${major}.0.0.0 Safari/537.36`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
flags.push("about:blank");
|
|
117
|
+
return flags;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const isVisible = () => process.env.GREEDY_SEARCH_VISIBLE === "1";
|
|
121
|
+
|
|
122
|
+
/** Check if the running Chrome was launched headless from the mode marker file */
|
|
123
|
+
function isModeFileHeadless() {
|
|
124
|
+
try {
|
|
125
|
+
if (!existsSync(MODE_FILE)) return true; // default: assume headless
|
|
126
|
+
return readFileSync(MODE_FILE, "utf8").trim() === "headless";
|
|
127
|
+
} catch {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// CDP Window Minimization
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
async function minimizeViaCDP() {
|
|
137
|
+
if (isHeadless()) return;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
// Get browser WebSocket URL
|
|
141
|
+
const version = await new Promise((resolve, reject) => {
|
|
142
|
+
http
|
|
143
|
+
.get(`http://localhost:${PORT}/json/version`, (res) => {
|
|
144
|
+
let body = "";
|
|
145
|
+
res.on("data", (d) => (body += d));
|
|
146
|
+
res.on("end", () => resolve(JSON.parse(body)));
|
|
147
|
+
})
|
|
148
|
+
.on("error", reject);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const wsPath = new URL(version.webSocketDebuggerUrl).pathname;
|
|
152
|
+
|
|
153
|
+
const WebSocket = globalThis.WebSocket;
|
|
154
|
+
if (!WebSocket) return;
|
|
155
|
+
|
|
156
|
+
const ws = new WebSocket(`ws://localhost:${PORT}${wsPath}`);
|
|
157
|
+
let requestId = 0;
|
|
158
|
+
const pending = new Map();
|
|
159
|
+
|
|
160
|
+
ws.onopen = () => {
|
|
161
|
+
// Step 1: Get targets
|
|
162
|
+
const id = ++requestId;
|
|
163
|
+
pending.set(id, {
|
|
164
|
+
resolve: (result) => {
|
|
165
|
+
const targets = result.targetInfos || [];
|
|
166
|
+
const pageTarget = targets.find((t) => t.type === "page");
|
|
167
|
+
if (!pageTarget) {
|
|
168
|
+
ws.close();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Step 2: Get windowId for target
|
|
173
|
+
const winId = ++requestId;
|
|
174
|
+
pending.set(winId, {
|
|
175
|
+
resolve: (winResult) => {
|
|
176
|
+
const windowId = winResult.windowId;
|
|
177
|
+
// Step 3: Minimize window
|
|
178
|
+
const minId = ++requestId;
|
|
179
|
+
pending.set(minId, { resolve: () => {}, reject: () => {} });
|
|
180
|
+
ws.send(
|
|
181
|
+
JSON.stringify({
|
|
182
|
+
id: minId,
|
|
183
|
+
method: "Browser.setWindowBounds",
|
|
184
|
+
params: { windowId, bounds: { windowState: "minimized" } },
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
setTimeout(() => ws.close(), 500);
|
|
188
|
+
},
|
|
189
|
+
reject: () => ws.close(),
|
|
190
|
+
});
|
|
191
|
+
ws.send(
|
|
192
|
+
JSON.stringify({
|
|
193
|
+
id: winId,
|
|
194
|
+
method: "Browser.getWindowForTarget",
|
|
195
|
+
params: { targetId: pageTarget.targetId },
|
|
196
|
+
}),
|
|
197
|
+
);
|
|
198
|
+
},
|
|
199
|
+
reject: () => ws.close(),
|
|
200
|
+
});
|
|
201
|
+
ws.send(JSON.stringify({ id, method: "Target.getTargets", params: {} }));
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
ws.onmessage = (event) => {
|
|
205
|
+
const msg = JSON.parse(event.data);
|
|
206
|
+
if (msg.id && pending.has(msg.id)) {
|
|
207
|
+
const { resolve, reject } = pending.get(msg.id);
|
|
208
|
+
pending.delete(msg.id);
|
|
209
|
+
if (msg.error) reject?.(msg.error);
|
|
210
|
+
else resolve?.(msg.result);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
setTimeout(() => ws.close(), 5000);
|
|
215
|
+
} catch {
|
|
216
|
+
// Best-effort
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Chrome process management
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
function isRunning() {
|
|
225
|
+
if (!existsSync(PID_FILE)) return false;
|
|
226
|
+
const pid = Number.parseInt(readFileSync(PID_FILE, "utf8").trim(), 10);
|
|
227
|
+
if (!pid) return false;
|
|
228
|
+
try {
|
|
229
|
+
process.kill(pid, 0);
|
|
230
|
+
return pid;
|
|
231
|
+
} catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function getPortPid(port) {
|
|
237
|
+
try {
|
|
238
|
+
const os = platform();
|
|
239
|
+
if (os === "win32") {
|
|
240
|
+
const out = execSync(`netstat -ano -p TCP 2>nul`, { encoding: "utf8" });
|
|
241
|
+
const regex = new RegExp(
|
|
242
|
+
String.raw`TCP\s+[^\s]*:${port}\s+[^\s]*:0\s+LISTENING\s+(\d+)`,
|
|
243
|
+
"i",
|
|
244
|
+
);
|
|
245
|
+
const match = out.match(regex);
|
|
246
|
+
return match ? Number.parseInt(match[1], 10) : null;
|
|
247
|
+
}
|
|
248
|
+
const out = execSync(
|
|
249
|
+
String.raw`lsof -i :${port} -t 2>/dev/null || ss -tlnp 2>/dev/null | grep :${port} | grep -oP 'pid=\K\d+'`,
|
|
250
|
+
{
|
|
251
|
+
encoding: "utf8",
|
|
252
|
+
},
|
|
253
|
+
).trim();
|
|
254
|
+
return out ? Number.parseInt(out.split("\n")[0], 10) : null;
|
|
255
|
+
} catch {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function killProcess(pid) {
|
|
261
|
+
try {
|
|
262
|
+
if (platform() === "win32") {
|
|
263
|
+
execSync(`taskkill /F /T /PID ${pid}`, { stdio: "ignore" });
|
|
264
|
+
} else {
|
|
265
|
+
process.kill(pid, "SIGTERM");
|
|
266
|
+
}
|
|
267
|
+
return true;
|
|
268
|
+
} catch {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function cleanupGhostChrome() {
|
|
274
|
+
const portPid = getPortPid(PORT);
|
|
275
|
+
if (!portPid) return;
|
|
276
|
+
|
|
277
|
+
const trackedPid = isRunning();
|
|
278
|
+
if (trackedPid && portPid === trackedPid) return;
|
|
279
|
+
|
|
280
|
+
console.log(`Ghost Chrome on port ${PORT} (pid ${portPid}) — cleaning up...`);
|
|
281
|
+
killProcess(portPid);
|
|
282
|
+
try {
|
|
283
|
+
unlinkSync(PID_FILE);
|
|
284
|
+
} catch {}
|
|
285
|
+
try {
|
|
286
|
+
unlinkSync(ACTIVE_PORT);
|
|
287
|
+
} catch {}
|
|
288
|
+
try {
|
|
289
|
+
unlinkSync(MODE_FILE);
|
|
290
|
+
} catch {}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function httpGet(url, timeoutMs = 1000) {
|
|
294
|
+
return new Promise((resolve) => {
|
|
295
|
+
const req = http.get(url, (res) => {
|
|
296
|
+
let body = "";
|
|
297
|
+
res.on("data", (d) => (body += d));
|
|
298
|
+
res.on("end", () => resolve({ ok: res.statusCode === 200, body }));
|
|
299
|
+
});
|
|
300
|
+
req.on("error", () => resolve({ ok: false }));
|
|
301
|
+
req.setTimeout(timeoutMs, () => {
|
|
302
|
+
req.destroy();
|
|
303
|
+
resolve({ ok: false });
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function writePortFile(timeoutMs = 15000) {
|
|
309
|
+
const deadline = Date.now() + timeoutMs;
|
|
310
|
+
while (Date.now() < deadline) {
|
|
311
|
+
const { ok, body } = await httpGet(
|
|
312
|
+
`http://localhost:${PORT}/json/version`,
|
|
313
|
+
1500,
|
|
314
|
+
);
|
|
315
|
+
if (ok) {
|
|
316
|
+
try {
|
|
317
|
+
const { webSocketDebuggerUrl } = JSON.parse(body);
|
|
318
|
+
const wsPath = new URL(webSocketDebuggerUrl).pathname;
|
|
319
|
+
writeFileSync(ACTIVE_PORT, `${PORT}\n${wsPath}`, "utf8");
|
|
320
|
+
return true;
|
|
321
|
+
} catch {
|
|
322
|
+
/* ignore */
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
326
|
+
}
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// Main
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
async function main() {
|
|
335
|
+
const arg = process.argv[2];
|
|
336
|
+
|
|
337
|
+
cleanupGhostChrome();
|
|
338
|
+
|
|
339
|
+
if (arg === "--kill") {
|
|
340
|
+
const pid = isRunning() || getPortPid(PORT);
|
|
341
|
+
if (pid) {
|
|
342
|
+
const ok = killProcess(pid);
|
|
343
|
+
console.log(
|
|
344
|
+
ok ? `Stopped Chrome (pid ${pid}).` : `Failed to stop pid ${pid}.`,
|
|
345
|
+
);
|
|
346
|
+
} else {
|
|
347
|
+
console.log("GreedySearch Chrome is not running.");
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
unlinkSync(PID_FILE);
|
|
351
|
+
} catch {}
|
|
352
|
+
try {
|
|
353
|
+
unlinkSync(ACTIVE_PORT);
|
|
354
|
+
} catch {}
|
|
355
|
+
try {
|
|
356
|
+
unlinkSync(MODE_FILE);
|
|
357
|
+
} catch {}
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (arg === "--status") {
|
|
362
|
+
const pid = isRunning();
|
|
363
|
+
if (pid) {
|
|
364
|
+
console.log(`Running — pid ${pid}, port ${PORT}`);
|
|
365
|
+
} else {
|
|
366
|
+
console.log("Not running.");
|
|
367
|
+
}
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const existing = isRunning();
|
|
372
|
+
if (existing) {
|
|
373
|
+
// Mode check: if caller wants visible but Chrome is headless, kill and relaunch
|
|
374
|
+
const isWantingVisible =
|
|
375
|
+
process.env.GREEDY_SEARCH_VISIBLE === "1" &&
|
|
376
|
+
!process.argv.includes("--headless");
|
|
377
|
+
if (isWantingVisible && isModeFileHeadless()) {
|
|
378
|
+
console.log(
|
|
379
|
+
`Headless Chrome running (pid ${existing}) but visible requested — killing...`,
|
|
380
|
+
);
|
|
381
|
+
killProcess(existing);
|
|
382
|
+
try {
|
|
383
|
+
unlinkSync(PID_FILE);
|
|
384
|
+
} catch {}
|
|
385
|
+
try {
|
|
386
|
+
unlinkSync(MODE_FILE);
|
|
387
|
+
} catch {}
|
|
388
|
+
// Fall through to fresh launch below
|
|
389
|
+
} else {
|
|
390
|
+
const ready = await writePortFile(5000);
|
|
391
|
+
if (ready) {
|
|
392
|
+
console.log(`GreedySearch Chrome already running (pid ${existing}).`);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
console.log(`Stale PID ${existing} — launching fresh.`);
|
|
396
|
+
try {
|
|
397
|
+
unlinkSync(PID_FILE);
|
|
398
|
+
} catch {}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const CHROME_EXE = process.env.CHROME_PATH || findChrome();
|
|
403
|
+
if (!CHROME_EXE) {
|
|
404
|
+
console.error("Chrome not found. Set CHROME_PATH env var.");
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
mkdirSync(PROFILE_DIR, { recursive: true });
|
|
409
|
+
|
|
410
|
+
console.log(`Launching GreedySearch Chrome on port ${PORT}...`);
|
|
411
|
+
if (isHeadless()) {
|
|
412
|
+
console.log("Headless mode — no window will be shown");
|
|
413
|
+
} else if (!isVisible()) {
|
|
414
|
+
console.log("Window will be minimized");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const proc = spawn(CHROME_EXE, buildChromeFlags(CHROME_EXE), {
|
|
418
|
+
detached: true,
|
|
419
|
+
stdio: "ignore",
|
|
420
|
+
});
|
|
421
|
+
proc.unref();
|
|
422
|
+
writeFileSync(PID_FILE, String(proc.pid));
|
|
423
|
+
// Write mode marker so ensureChrome() can detect headless vs visible
|
|
424
|
+
writeFileSync(MODE_FILE, isHeadless() ? "headless" : "visible", "utf8");
|
|
425
|
+
|
|
426
|
+
const portFileReady = await writePortFile();
|
|
427
|
+
if (!portFileReady) {
|
|
428
|
+
console.error("Chrome did not become ready within 15s.");
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (isHeadless()) {
|
|
433
|
+
// No window to minimize in headless mode
|
|
434
|
+
console.log("Ready (headless).");
|
|
435
|
+
} else {
|
|
436
|
+
// Minimize window via CDP
|
|
437
|
+
await minimizeViaCDP();
|
|
438
|
+
console.log("Ready.");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
main();
|