@apmantza/greedysearch-pi 1.8.2 → 1.8.4
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 +17 -0
- package/README.md +10 -1
- package/bin/launch.mjs +366 -366
- package/bin/search.mjs +388 -388
- package/extractors/common.mjs +291 -291
- package/extractors/gemini.mjs +146 -146
- package/extractors/google-ai.mjs +125 -125
- package/extractors/perplexity.mjs +147 -145
- package/extractors/selectors.mjs +54 -54
- package/index.ts +256 -278
- package/package.json +1 -1
- package/src/github.mjs +237 -237
- package/src/reddit.mjs +210 -0
- package/src/search/chrome.mjs +222 -222
- package/src/search/constants.mjs +37 -37
- package/src/search/defaults.mjs +14 -14
- package/src/search/engines.mjs +62 -62
- package/src/search/fetch-source.mjs +35 -3
- package/src/search/output.mjs +58 -58
- package/src/search/sources.mjs +445 -445
- package/src/search/synthesis-runner.mjs +63 -63
- package/src/search/synthesis.mjs +223 -223
- package/src/tools/deep-research-handler.ts +36 -36
- package/src/tools/greedy-search-handler.ts +53 -57
- package/src/tools/shared.ts +135 -130
- package/src/types.ts +103 -103
- package/test.mjs +423 -377
package/bin/launch.mjs
CHANGED
|
@@ -1,366 +1,366 @@
|
|
|
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 --kill — stop and restore original DevToolsActivePort
|
|
14
|
-
// node launch.mjs --status — check if running
|
|
15
|
-
//
|
|
16
|
-
// Environment:
|
|
17
|
-
// GREEDY_SEARCH_VISIBLE=1 — Show Chrome window instead of minimizing
|
|
18
|
-
// CHROME_PATH — Path to Chrome executable
|
|
19
|
-
|
|
20
|
-
import { execSync, spawn } from "node:child_process";
|
|
21
|
-
import {
|
|
22
|
-
existsSync,
|
|
23
|
-
mkdirSync,
|
|
24
|
-
readFileSync,
|
|
25
|
-
unlinkSync,
|
|
26
|
-
writeFileSync,
|
|
27
|
-
} from "node:fs";
|
|
28
|
-
import http from "node:http";
|
|
29
|
-
import { platform, tmpdir } from "node:os";
|
|
30
|
-
import { join } from "node:path";
|
|
31
|
-
|
|
32
|
-
const PORT = 9222;
|
|
33
|
-
const PROFILE_DIR = join(tmpdir(), "greedysearch-chrome-profile");
|
|
34
|
-
const ACTIVE_PORT = join(PROFILE_DIR, "DevToolsActivePort");
|
|
35
|
-
const PID_FILE = join(tmpdir(), "greedysearch-chrome.pid");
|
|
36
|
-
|
|
37
|
-
function findChrome() {
|
|
38
|
-
const os = platform();
|
|
39
|
-
const candidates =
|
|
40
|
-
os === "win32"
|
|
41
|
-
? [
|
|
42
|
-
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
43
|
-
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
44
|
-
]
|
|
45
|
-
: os === "darwin"
|
|
46
|
-
? [
|
|
47
|
-
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
48
|
-
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
49
|
-
]
|
|
50
|
-
: [
|
|
51
|
-
"/usr/bin/google-chrome",
|
|
52
|
-
"/usr/bin/google-chrome-stable",
|
|
53
|
-
"/usr/bin/chromium-browser",
|
|
54
|
-
"/usr/bin/chromium",
|
|
55
|
-
"/snap/bin/chromium",
|
|
56
|
-
];
|
|
57
|
-
return candidates.find(existsSync) || null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const CHROME_FLAGS = [
|
|
61
|
-
`--remote-debugging-port=${PORT}`,
|
|
62
|
-
"--disable-features=DevToolsPrivacyUI",
|
|
63
|
-
"--no-first-run",
|
|
64
|
-
"--no-default-browser-check",
|
|
65
|
-
"--disable-default-apps",
|
|
66
|
-
`--user-data-dir=${PROFILE_DIR}`,
|
|
67
|
-
"--profile-directory=Default",
|
|
68
|
-
"about:blank",
|
|
69
|
-
];
|
|
70
|
-
|
|
71
|
-
const isVisible = () => process.env.GREEDY_SEARCH_VISIBLE === "1";
|
|
72
|
-
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
// CDP Window Minimization
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
|
|
77
|
-
async function minimizeViaCDP() {
|
|
78
|
-
if (isVisible()) return;
|
|
79
|
-
console.log("[minimize] Starting...");
|
|
80
|
-
|
|
81
|
-
// Wait for Chrome to be ready
|
|
82
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
// Get browser WebSocket URL
|
|
86
|
-
console.log("[minimize] Getting version info...");
|
|
87
|
-
const version = await new Promise((resolve, reject) => {
|
|
88
|
-
http
|
|
89
|
-
.get(`http://localhost:${PORT}/json/version`, (res) => {
|
|
90
|
-
let body = "";
|
|
91
|
-
res.on("data", (d) => (body += d));
|
|
92
|
-
res.on("end", () => resolve(JSON.parse(body)));
|
|
93
|
-
})
|
|
94
|
-
.on("error", reject);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const wsUrl = version.webSocketDebuggerUrl;
|
|
98
|
-
console.log("[minimize] WebSocket URL:", wsUrl.slice(0, 40) + "...");
|
|
99
|
-
|
|
100
|
-
const WebSocket = globalThis.WebSocket;
|
|
101
|
-
if (!WebSocket) {
|
|
102
|
-
console.log("[minimize] WebSocket not available");
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const ws = new WebSocket(wsUrl);
|
|
107
|
-
let requestId = 0;
|
|
108
|
-
const pending = new Map();
|
|
109
|
-
|
|
110
|
-
ws.onopen = () => {
|
|
111
|
-
console.log("[minimize] Connected, getting targets...");
|
|
112
|
-
// Step 1: Get targets
|
|
113
|
-
const id = ++requestId;
|
|
114
|
-
pending.set(id, {
|
|
115
|
-
resolve: (result) => {
|
|
116
|
-
const targets = result.targetInfos || [];
|
|
117
|
-
console.log(`[minimize] Found ${targets.length} targets`);
|
|
118
|
-
const pageTarget = targets.find((t) => t.type === "page");
|
|
119
|
-
if (!pageTarget) {
|
|
120
|
-
console.log("[minimize] No page target, closing");
|
|
121
|
-
ws.close();
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
console.log(`[minimize] Using target: ${pageTarget.targetId}`);
|
|
126
|
-
// Step 2: Get windowId for target
|
|
127
|
-
const winId = ++requestId;
|
|
128
|
-
pending.set(winId, {
|
|
129
|
-
resolve: (winResult) => {
|
|
130
|
-
const windowId = winResult.windowId;
|
|
131
|
-
console.log(
|
|
132
|
-
`[minimize] Got windowId: ${windowId}, minimizing...`,
|
|
133
|
-
);
|
|
134
|
-
// Step 3: Minimize window
|
|
135
|
-
const minId = ++requestId;
|
|
136
|
-
pending.set(minId, {
|
|
137
|
-
resolve: () =>
|
|
138
|
-
console.log("[minimize] Window minimized successfully"),
|
|
139
|
-
reject: (err) =>
|
|
140
|
-
console.log("[minimize] Minimize failed:", err),
|
|
141
|
-
});
|
|
142
|
-
ws.send(
|
|
143
|
-
JSON.stringify({
|
|
144
|
-
id: minId,
|
|
145
|
-
method: "Browser.setWindowBounds",
|
|
146
|
-
params: { windowId, bounds: { windowState: "minimized" } },
|
|
147
|
-
}),
|
|
148
|
-
);
|
|
149
|
-
setTimeout(() => ws.close(), 500);
|
|
150
|
-
},
|
|
151
|
-
reject: () => ws.close(),
|
|
152
|
-
});
|
|
153
|
-
ws.send(
|
|
154
|
-
JSON.stringify({
|
|
155
|
-
id: winId,
|
|
156
|
-
method: "Browser.getWindowForTarget",
|
|
157
|
-
params: { targetId: pageTarget.targetId },
|
|
158
|
-
}),
|
|
159
|
-
);
|
|
160
|
-
},
|
|
161
|
-
reject: () => ws.close(),
|
|
162
|
-
});
|
|
163
|
-
ws.send(JSON.stringify({ id, method: "Target.getTargets", params: {} }));
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
ws.onmessage = (event) => {
|
|
167
|
-
const msg = JSON.parse(event.data);
|
|
168
|
-
if (msg.id && pending.has(msg.id)) {
|
|
169
|
-
const { resolve, reject } = pending.get(msg.id);
|
|
170
|
-
pending.delete(msg.id);
|
|
171
|
-
if (msg.error) reject?.(msg.error);
|
|
172
|
-
else resolve?.(msg.result);
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
setTimeout(() => ws.close(), 5000);
|
|
177
|
-
} catch {
|
|
178
|
-
// Best-effort
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ---------------------------------------------------------------------------
|
|
183
|
-
// Chrome process management
|
|
184
|
-
// ---------------------------------------------------------------------------
|
|
185
|
-
|
|
186
|
-
function isRunning() {
|
|
187
|
-
if (!existsSync(PID_FILE)) return false;
|
|
188
|
-
const pid = parseInt(readFileSync(PID_FILE, "utf8").trim(), 10);
|
|
189
|
-
if (!pid) return false;
|
|
190
|
-
try {
|
|
191
|
-
process.kill(pid, 0);
|
|
192
|
-
return pid;
|
|
193
|
-
} catch {
|
|
194
|
-
return false;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function getPortPid(port) {
|
|
199
|
-
try {
|
|
200
|
-
const os = platform();
|
|
201
|
-
if (os === "win32") {
|
|
202
|
-
const out = execSync(`netstat -ano -p TCP 2>nul`, { encoding: "utf8" });
|
|
203
|
-
const regex = new RegExp(
|
|
204
|
-
`TCP\\s+[^\\s]*:${port}\\s+[^\\s]*:0\\s+LISTENING\\s+(\\d+)`,
|
|
205
|
-
"i",
|
|
206
|
-
);
|
|
207
|
-
const match = out.match(regex);
|
|
208
|
-
return match ? parseInt(match[1], 10) : null;
|
|
209
|
-
}
|
|
210
|
-
const out = execSync(
|
|
211
|
-
`lsof -i :${port} -t 2>/dev/null || ss -tlnp 2>/dev/null | grep :${port} | grep -oP 'pid=\\K\\d+'`,
|
|
212
|
-
{
|
|
213
|
-
encoding: "utf8",
|
|
214
|
-
},
|
|
215
|
-
).trim();
|
|
216
|
-
return out ? parseInt(out.split("\n")[0], 10) : null;
|
|
217
|
-
} catch {
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function killProcess(pid) {
|
|
223
|
-
try {
|
|
224
|
-
if (platform() === "win32") {
|
|
225
|
-
execSync(`taskkill //F //PID ${pid}`, { stdio: "ignore" });
|
|
226
|
-
} else {
|
|
227
|
-
process.kill(pid, "SIGTERM");
|
|
228
|
-
}
|
|
229
|
-
return true;
|
|
230
|
-
} catch {
|
|
231
|
-
return false;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function cleanupGhostChrome() {
|
|
236
|
-
const portPid = getPortPid(PORT);
|
|
237
|
-
if (!portPid) return;
|
|
238
|
-
|
|
239
|
-
const trackedPid = isRunning();
|
|
240
|
-
if (trackedPid && portPid === trackedPid) return;
|
|
241
|
-
|
|
242
|
-
console.log(`Ghost Chrome on port ${PORT} (pid ${portPid}) — cleaning up...`);
|
|
243
|
-
killProcess(portPid);
|
|
244
|
-
try {
|
|
245
|
-
unlinkSync(PID_FILE);
|
|
246
|
-
} catch {}
|
|
247
|
-
try {
|
|
248
|
-
unlinkSync(ACTIVE_PORT);
|
|
249
|
-
} catch {}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function httpGet(url, timeoutMs = 1000) {
|
|
253
|
-
return new Promise((resolve) => {
|
|
254
|
-
const req = http.get(url, (res) => {
|
|
255
|
-
let body = "";
|
|
256
|
-
res.on("data", (d) => (body += d));
|
|
257
|
-
res.on("end", () => resolve({ ok: res.statusCode === 200, body }));
|
|
258
|
-
});
|
|
259
|
-
req.on("error", () => resolve({ ok: false }));
|
|
260
|
-
req.setTimeout(timeoutMs, () => {
|
|
261
|
-
req.destroy();
|
|
262
|
-
resolve({ ok: false });
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
async function writePortFile(timeoutMs = 15000) {
|
|
268
|
-
const deadline = Date.now() + timeoutMs;
|
|
269
|
-
while (Date.now() < deadline) {
|
|
270
|
-
const { ok, body } = await httpGet(
|
|
271
|
-
`http://localhost:${PORT}/json/version`,
|
|
272
|
-
1500,
|
|
273
|
-
);
|
|
274
|
-
if (ok) {
|
|
275
|
-
try {
|
|
276
|
-
const { webSocketDebuggerUrl } = JSON.parse(body);
|
|
277
|
-
const wsPath = new URL(webSocketDebuggerUrl).pathname;
|
|
278
|
-
writeFileSync(ACTIVE_PORT, `${PORT}\n${wsPath}`, "utf8");
|
|
279
|
-
return true;
|
|
280
|
-
} catch {
|
|
281
|
-
/* ignore */
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
await new Promise((r) => setTimeout(r, 400));
|
|
285
|
-
}
|
|
286
|
-
return false;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// ---------------------------------------------------------------------------
|
|
290
|
-
// Main
|
|
291
|
-
// ---------------------------------------------------------------------------
|
|
292
|
-
|
|
293
|
-
async function main() {
|
|
294
|
-
const arg = process.argv[2];
|
|
295
|
-
|
|
296
|
-
cleanupGhostChrome();
|
|
297
|
-
|
|
298
|
-
if (arg === "--kill") {
|
|
299
|
-
const pid = isRunning();
|
|
300
|
-
if (pid) {
|
|
301
|
-
const ok = killProcess(pid);
|
|
302
|
-
console.log(
|
|
303
|
-
ok ? `Stopped Chrome (pid ${pid}).` : `Failed to stop pid ${pid}.`,
|
|
304
|
-
);
|
|
305
|
-
} else {
|
|
306
|
-
console.log("GreedySearch Chrome is not running.");
|
|
307
|
-
}
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (arg === "--status") {
|
|
312
|
-
const pid = isRunning();
|
|
313
|
-
if (pid) {
|
|
314
|
-
console.log(`Running — pid ${pid}, port ${PORT}`);
|
|
315
|
-
} else {
|
|
316
|
-
console.log("Not running.");
|
|
317
|
-
}
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const existing = isRunning();
|
|
322
|
-
if (existing) {
|
|
323
|
-
const ready = await writePortFile(5000);
|
|
324
|
-
if (ready) {
|
|
325
|
-
console.log(`GreedySearch Chrome already running (pid ${existing}).`);
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
console.log(`Stale PID ${existing} — launching fresh.`);
|
|
329
|
-
try {
|
|
330
|
-
unlinkSync(PID_FILE);
|
|
331
|
-
} catch {}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const CHROME_EXE = process.env.CHROME_PATH || findChrome();
|
|
335
|
-
if (!CHROME_EXE) {
|
|
336
|
-
console.error("Chrome not found. Set CHROME_PATH env var.");
|
|
337
|
-
process.exit(1);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
mkdirSync(PROFILE_DIR, { recursive: true });
|
|
341
|
-
|
|
342
|
-
console.log(`Launching GreedySearch Chrome on port ${PORT}...`);
|
|
343
|
-
if (!isVisible()) {
|
|
344
|
-
console.log("Window will be minimized");
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const proc = spawn(CHROME_EXE, CHROME_FLAGS, {
|
|
348
|
-
detached: true,
|
|
349
|
-
stdio: "ignore",
|
|
350
|
-
});
|
|
351
|
-
proc.unref();
|
|
352
|
-
writeFileSync(PID_FILE, String(proc.pid));
|
|
353
|
-
|
|
354
|
-
const portFileReady = await writePortFile();
|
|
355
|
-
if (!portFileReady) {
|
|
356
|
-
console.error("Chrome did not become ready within 15s.");
|
|
357
|
-
process.exit(1);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Minimize window via CDP
|
|
361
|
-
await minimizeViaCDP();
|
|
362
|
-
|
|
363
|
-
console.log("Ready.");
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
main();
|
|
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 --kill — stop and restore original DevToolsActivePort
|
|
14
|
+
// node launch.mjs --status — check if running
|
|
15
|
+
//
|
|
16
|
+
// Environment:
|
|
17
|
+
// GREEDY_SEARCH_VISIBLE=1 — Show Chrome window instead of minimizing
|
|
18
|
+
// CHROME_PATH — Path to Chrome executable
|
|
19
|
+
|
|
20
|
+
import { execSync, spawn } from "node:child_process";
|
|
21
|
+
import {
|
|
22
|
+
existsSync,
|
|
23
|
+
mkdirSync,
|
|
24
|
+
readFileSync,
|
|
25
|
+
unlinkSync,
|
|
26
|
+
writeFileSync,
|
|
27
|
+
} from "node:fs";
|
|
28
|
+
import http from "node:http";
|
|
29
|
+
import { platform, tmpdir } from "node:os";
|
|
30
|
+
import { join } from "node:path";
|
|
31
|
+
|
|
32
|
+
const PORT = 9222;
|
|
33
|
+
const PROFILE_DIR = join(tmpdir(), "greedysearch-chrome-profile");
|
|
34
|
+
const ACTIVE_PORT = join(PROFILE_DIR, "DevToolsActivePort");
|
|
35
|
+
const PID_FILE = join(tmpdir(), "greedysearch-chrome.pid");
|
|
36
|
+
|
|
37
|
+
function findChrome() {
|
|
38
|
+
const os = platform();
|
|
39
|
+
const candidates =
|
|
40
|
+
os === "win32"
|
|
41
|
+
? [
|
|
42
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
43
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
44
|
+
]
|
|
45
|
+
: os === "darwin"
|
|
46
|
+
? [
|
|
47
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
48
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
49
|
+
]
|
|
50
|
+
: [
|
|
51
|
+
"/usr/bin/google-chrome",
|
|
52
|
+
"/usr/bin/google-chrome-stable",
|
|
53
|
+
"/usr/bin/chromium-browser",
|
|
54
|
+
"/usr/bin/chromium",
|
|
55
|
+
"/snap/bin/chromium",
|
|
56
|
+
];
|
|
57
|
+
return candidates.find(existsSync) || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const CHROME_FLAGS = [
|
|
61
|
+
`--remote-debugging-port=${PORT}`,
|
|
62
|
+
"--disable-features=DevToolsPrivacyUI",
|
|
63
|
+
"--no-first-run",
|
|
64
|
+
"--no-default-browser-check",
|
|
65
|
+
"--disable-default-apps",
|
|
66
|
+
`--user-data-dir=${PROFILE_DIR}`,
|
|
67
|
+
"--profile-directory=Default",
|
|
68
|
+
"about:blank",
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const isVisible = () => process.env.GREEDY_SEARCH_VISIBLE === "1";
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// CDP Window Minimization
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
async function minimizeViaCDP() {
|
|
78
|
+
if (isVisible()) return;
|
|
79
|
+
console.log("[minimize] Starting...");
|
|
80
|
+
|
|
81
|
+
// Wait for Chrome to be ready
|
|
82
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
// Get browser WebSocket URL
|
|
86
|
+
console.log("[minimize] Getting version info...");
|
|
87
|
+
const version = await new Promise((resolve, reject) => {
|
|
88
|
+
http
|
|
89
|
+
.get(`http://localhost:${PORT}/json/version`, (res) => {
|
|
90
|
+
let body = "";
|
|
91
|
+
res.on("data", (d) => (body += d));
|
|
92
|
+
res.on("end", () => resolve(JSON.parse(body)));
|
|
93
|
+
})
|
|
94
|
+
.on("error", reject);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const wsUrl = version.webSocketDebuggerUrl;
|
|
98
|
+
console.log("[minimize] WebSocket URL:", wsUrl.slice(0, 40) + "...");
|
|
99
|
+
|
|
100
|
+
const WebSocket = globalThis.WebSocket;
|
|
101
|
+
if (!WebSocket) {
|
|
102
|
+
console.log("[minimize] WebSocket not available");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const ws = new WebSocket(wsUrl);
|
|
107
|
+
let requestId = 0;
|
|
108
|
+
const pending = new Map();
|
|
109
|
+
|
|
110
|
+
ws.onopen = () => {
|
|
111
|
+
console.log("[minimize] Connected, getting targets...");
|
|
112
|
+
// Step 1: Get targets
|
|
113
|
+
const id = ++requestId;
|
|
114
|
+
pending.set(id, {
|
|
115
|
+
resolve: (result) => {
|
|
116
|
+
const targets = result.targetInfos || [];
|
|
117
|
+
console.log(`[minimize] Found ${targets.length} targets`);
|
|
118
|
+
const pageTarget = targets.find((t) => t.type === "page");
|
|
119
|
+
if (!pageTarget) {
|
|
120
|
+
console.log("[minimize] No page target, closing");
|
|
121
|
+
ws.close();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(`[minimize] Using target: ${pageTarget.targetId}`);
|
|
126
|
+
// Step 2: Get windowId for target
|
|
127
|
+
const winId = ++requestId;
|
|
128
|
+
pending.set(winId, {
|
|
129
|
+
resolve: (winResult) => {
|
|
130
|
+
const windowId = winResult.windowId;
|
|
131
|
+
console.log(
|
|
132
|
+
`[minimize] Got windowId: ${windowId}, minimizing...`,
|
|
133
|
+
);
|
|
134
|
+
// Step 3: Minimize window
|
|
135
|
+
const minId = ++requestId;
|
|
136
|
+
pending.set(minId, {
|
|
137
|
+
resolve: () =>
|
|
138
|
+
console.log("[minimize] Window minimized successfully"),
|
|
139
|
+
reject: (err) =>
|
|
140
|
+
console.log("[minimize] Minimize failed:", err),
|
|
141
|
+
});
|
|
142
|
+
ws.send(
|
|
143
|
+
JSON.stringify({
|
|
144
|
+
id: minId,
|
|
145
|
+
method: "Browser.setWindowBounds",
|
|
146
|
+
params: { windowId, bounds: { windowState: "minimized" } },
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
setTimeout(() => ws.close(), 500);
|
|
150
|
+
},
|
|
151
|
+
reject: () => ws.close(),
|
|
152
|
+
});
|
|
153
|
+
ws.send(
|
|
154
|
+
JSON.stringify({
|
|
155
|
+
id: winId,
|
|
156
|
+
method: "Browser.getWindowForTarget",
|
|
157
|
+
params: { targetId: pageTarget.targetId },
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
},
|
|
161
|
+
reject: () => ws.close(),
|
|
162
|
+
});
|
|
163
|
+
ws.send(JSON.stringify({ id, method: "Target.getTargets", params: {} }));
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
ws.onmessage = (event) => {
|
|
167
|
+
const msg = JSON.parse(event.data);
|
|
168
|
+
if (msg.id && pending.has(msg.id)) {
|
|
169
|
+
const { resolve, reject } = pending.get(msg.id);
|
|
170
|
+
pending.delete(msg.id);
|
|
171
|
+
if (msg.error) reject?.(msg.error);
|
|
172
|
+
else resolve?.(msg.result);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
setTimeout(() => ws.close(), 5000);
|
|
177
|
+
} catch {
|
|
178
|
+
// Best-effort
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Chrome process management
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
function isRunning() {
|
|
187
|
+
if (!existsSync(PID_FILE)) return false;
|
|
188
|
+
const pid = parseInt(readFileSync(PID_FILE, "utf8").trim(), 10);
|
|
189
|
+
if (!pid) return false;
|
|
190
|
+
try {
|
|
191
|
+
process.kill(pid, 0);
|
|
192
|
+
return pid;
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getPortPid(port) {
|
|
199
|
+
try {
|
|
200
|
+
const os = platform();
|
|
201
|
+
if (os === "win32") {
|
|
202
|
+
const out = execSync(`netstat -ano -p TCP 2>nul`, { encoding: "utf8" });
|
|
203
|
+
const regex = new RegExp(
|
|
204
|
+
`TCP\\s+[^\\s]*:${port}\\s+[^\\s]*:0\\s+LISTENING\\s+(\\d+)`,
|
|
205
|
+
"i",
|
|
206
|
+
);
|
|
207
|
+
const match = out.match(regex);
|
|
208
|
+
return match ? parseInt(match[1], 10) : null;
|
|
209
|
+
}
|
|
210
|
+
const out = execSync(
|
|
211
|
+
`lsof -i :${port} -t 2>/dev/null || ss -tlnp 2>/dev/null | grep :${port} | grep -oP 'pid=\\K\\d+'`,
|
|
212
|
+
{
|
|
213
|
+
encoding: "utf8",
|
|
214
|
+
},
|
|
215
|
+
).trim();
|
|
216
|
+
return out ? parseInt(out.split("\n")[0], 10) : null;
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function killProcess(pid) {
|
|
223
|
+
try {
|
|
224
|
+
if (platform() === "win32") {
|
|
225
|
+
execSync(`taskkill //F //PID ${pid}`, { stdio: "ignore" });
|
|
226
|
+
} else {
|
|
227
|
+
process.kill(pid, "SIGTERM");
|
|
228
|
+
}
|
|
229
|
+
return true;
|
|
230
|
+
} catch {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function cleanupGhostChrome() {
|
|
236
|
+
const portPid = getPortPid(PORT);
|
|
237
|
+
if (!portPid) return;
|
|
238
|
+
|
|
239
|
+
const trackedPid = isRunning();
|
|
240
|
+
if (trackedPid && portPid === trackedPid) return;
|
|
241
|
+
|
|
242
|
+
console.log(`Ghost Chrome on port ${PORT} (pid ${portPid}) — cleaning up...`);
|
|
243
|
+
killProcess(portPid);
|
|
244
|
+
try {
|
|
245
|
+
unlinkSync(PID_FILE);
|
|
246
|
+
} catch {}
|
|
247
|
+
try {
|
|
248
|
+
unlinkSync(ACTIVE_PORT);
|
|
249
|
+
} catch {}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function httpGet(url, timeoutMs = 1000) {
|
|
253
|
+
return new Promise((resolve) => {
|
|
254
|
+
const req = http.get(url, (res) => {
|
|
255
|
+
let body = "";
|
|
256
|
+
res.on("data", (d) => (body += d));
|
|
257
|
+
res.on("end", () => resolve({ ok: res.statusCode === 200, body }));
|
|
258
|
+
});
|
|
259
|
+
req.on("error", () => resolve({ ok: false }));
|
|
260
|
+
req.setTimeout(timeoutMs, () => {
|
|
261
|
+
req.destroy();
|
|
262
|
+
resolve({ ok: false });
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function writePortFile(timeoutMs = 15000) {
|
|
268
|
+
const deadline = Date.now() + timeoutMs;
|
|
269
|
+
while (Date.now() < deadline) {
|
|
270
|
+
const { ok, body } = await httpGet(
|
|
271
|
+
`http://localhost:${PORT}/json/version`,
|
|
272
|
+
1500,
|
|
273
|
+
);
|
|
274
|
+
if (ok) {
|
|
275
|
+
try {
|
|
276
|
+
const { webSocketDebuggerUrl } = JSON.parse(body);
|
|
277
|
+
const wsPath = new URL(webSocketDebuggerUrl).pathname;
|
|
278
|
+
writeFileSync(ACTIVE_PORT, `${PORT}\n${wsPath}`, "utf8");
|
|
279
|
+
return true;
|
|
280
|
+
} catch {
|
|
281
|
+
/* ignore */
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// Main
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
async function main() {
|
|
294
|
+
const arg = process.argv[2];
|
|
295
|
+
|
|
296
|
+
cleanupGhostChrome();
|
|
297
|
+
|
|
298
|
+
if (arg === "--kill") {
|
|
299
|
+
const pid = isRunning();
|
|
300
|
+
if (pid) {
|
|
301
|
+
const ok = killProcess(pid);
|
|
302
|
+
console.log(
|
|
303
|
+
ok ? `Stopped Chrome (pid ${pid}).` : `Failed to stop pid ${pid}.`,
|
|
304
|
+
);
|
|
305
|
+
} else {
|
|
306
|
+
console.log("GreedySearch Chrome is not running.");
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (arg === "--status") {
|
|
312
|
+
const pid = isRunning();
|
|
313
|
+
if (pid) {
|
|
314
|
+
console.log(`Running — pid ${pid}, port ${PORT}`);
|
|
315
|
+
} else {
|
|
316
|
+
console.log("Not running.");
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const existing = isRunning();
|
|
322
|
+
if (existing) {
|
|
323
|
+
const ready = await writePortFile(5000);
|
|
324
|
+
if (ready) {
|
|
325
|
+
console.log(`GreedySearch Chrome already running (pid ${existing}).`);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
console.log(`Stale PID ${existing} — launching fresh.`);
|
|
329
|
+
try {
|
|
330
|
+
unlinkSync(PID_FILE);
|
|
331
|
+
} catch {}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const CHROME_EXE = process.env.CHROME_PATH || findChrome();
|
|
335
|
+
if (!CHROME_EXE) {
|
|
336
|
+
console.error("Chrome not found. Set CHROME_PATH env var.");
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
mkdirSync(PROFILE_DIR, { recursive: true });
|
|
341
|
+
|
|
342
|
+
console.log(`Launching GreedySearch Chrome on port ${PORT}...`);
|
|
343
|
+
if (!isVisible()) {
|
|
344
|
+
console.log("Window will be minimized");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const proc = spawn(CHROME_EXE, CHROME_FLAGS, {
|
|
348
|
+
detached: true,
|
|
349
|
+
stdio: "ignore",
|
|
350
|
+
});
|
|
351
|
+
proc.unref();
|
|
352
|
+
writeFileSync(PID_FILE, String(proc.pid));
|
|
353
|
+
|
|
354
|
+
const portFileReady = await writePortFile();
|
|
355
|
+
if (!portFileReady) {
|
|
356
|
+
console.error("Chrome did not become ready within 15s.");
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Minimize window via CDP
|
|
361
|
+
await minimizeViaCDP();
|
|
362
|
+
|
|
363
|
+
console.log("Ready.");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
main();
|