@cli4ai/chrome 1.1.1 → 1.1.3
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/cli4ai.json +46 -13
- package/dist/run.d.ts +2 -0
- package/dist/run.js +347 -0
- package/package.json +12 -4
- package/run.ts +0 -383
package/cli4ai.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "Chrome browser automation with managed browser instance",
|
|
5
5
|
"author": "cliforai",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
7
|
-
"entry": "run.
|
|
7
|
+
"entry": "dist/run.js",
|
|
8
8
|
"runtime": "node",
|
|
9
9
|
"keywords": [
|
|
10
10
|
"chrome",
|
|
@@ -29,62 +29,95 @@
|
|
|
29
29
|
"navigate": {
|
|
30
30
|
"description": "Open URL in browser",
|
|
31
31
|
"args": [
|
|
32
|
-
{
|
|
32
|
+
{
|
|
33
|
+
"name": "url",
|
|
34
|
+
"required": true
|
|
35
|
+
}
|
|
33
36
|
]
|
|
34
37
|
},
|
|
35
38
|
"click": {
|
|
36
39
|
"description": "Click an element",
|
|
37
40
|
"args": [
|
|
38
|
-
{
|
|
41
|
+
{
|
|
42
|
+
"name": "selector",
|
|
43
|
+
"required": true
|
|
44
|
+
}
|
|
39
45
|
]
|
|
40
46
|
},
|
|
41
47
|
"type": {
|
|
42
48
|
"description": "Type text into an input",
|
|
43
49
|
"args": [
|
|
44
|
-
{
|
|
45
|
-
|
|
50
|
+
{
|
|
51
|
+
"name": "selector",
|
|
52
|
+
"required": true
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "text",
|
|
56
|
+
"required": true
|
|
57
|
+
}
|
|
46
58
|
]
|
|
47
59
|
},
|
|
48
60
|
"eval": {
|
|
49
61
|
"description": "Run JavaScript in page context",
|
|
50
62
|
"args": [
|
|
51
|
-
{
|
|
63
|
+
{
|
|
64
|
+
"name": "script",
|
|
65
|
+
"required": true
|
|
66
|
+
}
|
|
52
67
|
]
|
|
53
68
|
},
|
|
54
69
|
"screenshot": {
|
|
55
70
|
"description": "Capture screenshot",
|
|
56
71
|
"args": [
|
|
57
|
-
{
|
|
72
|
+
{
|
|
73
|
+
"name": "output",
|
|
74
|
+
"required": false
|
|
75
|
+
}
|
|
58
76
|
]
|
|
59
77
|
},
|
|
60
78
|
"pdf": {
|
|
61
79
|
"description": "Save page as PDF",
|
|
62
80
|
"args": [
|
|
63
|
-
{
|
|
81
|
+
{
|
|
82
|
+
"name": "output",
|
|
83
|
+
"required": false
|
|
84
|
+
}
|
|
64
85
|
]
|
|
65
86
|
},
|
|
66
87
|
"cookies": {
|
|
67
88
|
"description": "Get cookies",
|
|
68
89
|
"args": [
|
|
69
|
-
{
|
|
90
|
+
{
|
|
91
|
+
"name": "domain",
|
|
92
|
+
"required": false
|
|
93
|
+
}
|
|
70
94
|
]
|
|
71
95
|
},
|
|
72
96
|
"html": {
|
|
73
97
|
"description": "Get page HTML",
|
|
74
98
|
"args": [
|
|
75
|
-
{
|
|
99
|
+
{
|
|
100
|
+
"name": "selector",
|
|
101
|
+
"required": false
|
|
102
|
+
}
|
|
76
103
|
]
|
|
77
104
|
},
|
|
78
105
|
"text": {
|
|
79
106
|
"description": "Get text content",
|
|
80
107
|
"args": [
|
|
81
|
-
{
|
|
108
|
+
{
|
|
109
|
+
"name": "selector",
|
|
110
|
+
"required": false
|
|
111
|
+
}
|
|
82
112
|
]
|
|
83
113
|
},
|
|
84
114
|
"wait": {
|
|
85
115
|
"description": "Wait for element",
|
|
86
116
|
"args": [
|
|
87
|
-
{
|
|
117
|
+
{
|
|
118
|
+
"name": "selector",
|
|
119
|
+
"required": true
|
|
120
|
+
}
|
|
88
121
|
]
|
|
89
122
|
},
|
|
90
123
|
"tabs": {
|
package/dist/run.d.ts
ADDED
package/dist/run.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import puppeteer from 'puppeteer';
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync } from 'fs';
|
|
4
|
+
import { join, resolve } from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { cli, log, output, outputError, withErrorHandling } from '@cli4ai/lib';
|
|
7
|
+
// Persistent storage paths
|
|
8
|
+
const CLI4AI_DIR = join(homedir(), '.cli4ai');
|
|
9
|
+
const CHROME_DIR = join(CLI4AI_DIR, 'chrome');
|
|
10
|
+
const PROFILE_DIR = join(CHROME_DIR, 'profile');
|
|
11
|
+
const WS_FILE = join(CHROME_DIR, 'ws-endpoint');
|
|
12
|
+
const PID_FILE = join(CHROME_DIR, 'pid');
|
|
13
|
+
// Ensure directories exist
|
|
14
|
+
function ensureDirs() {
|
|
15
|
+
if (!existsSync(CLI4AI_DIR))
|
|
16
|
+
mkdirSync(CLI4AI_DIR, { recursive: true });
|
|
17
|
+
if (!existsSync(CHROME_DIR))
|
|
18
|
+
mkdirSync(CHROME_DIR, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
// Check if browser process is still running
|
|
21
|
+
function isBrowserRunning() {
|
|
22
|
+
if (!existsSync(PID_FILE))
|
|
23
|
+
return false;
|
|
24
|
+
const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10);
|
|
25
|
+
try {
|
|
26
|
+
process.kill(pid, 0); // Check if process exists
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Process not running, clean up stale files
|
|
31
|
+
cleanup();
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Clean up state files
|
|
36
|
+
function cleanup() {
|
|
37
|
+
if (existsSync(WS_FILE))
|
|
38
|
+
unlinkSync(WS_FILE);
|
|
39
|
+
if (existsSync(PID_FILE))
|
|
40
|
+
unlinkSync(PID_FILE);
|
|
41
|
+
}
|
|
42
|
+
// Launch a new browser instance
|
|
43
|
+
async function launchBrowser(headless) {
|
|
44
|
+
ensureDirs();
|
|
45
|
+
const browser = await puppeteer.launch({
|
|
46
|
+
headless: headless ? 'shell' : false,
|
|
47
|
+
userDataDir: PROFILE_DIR,
|
|
48
|
+
args: [
|
|
49
|
+
'--no-first-run',
|
|
50
|
+
'--no-default-browser-check',
|
|
51
|
+
'--disable-infobars',
|
|
52
|
+
'--disable-blink-features=AutomationControlled',
|
|
53
|
+
'--disable-features=IsolateOrigins,site-per-process',
|
|
54
|
+
],
|
|
55
|
+
defaultViewport: null, // Use full window size
|
|
56
|
+
ignoreDefaultArgs: ['--enable-automation'],
|
|
57
|
+
});
|
|
58
|
+
// Remove automation detection signals
|
|
59
|
+
const userAgent = process.platform === 'win32'
|
|
60
|
+
? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36'
|
|
61
|
+
: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36';
|
|
62
|
+
const pages = await browser.pages();
|
|
63
|
+
for (const page of pages) {
|
|
64
|
+
await page.setUserAgent(userAgent);
|
|
65
|
+
await page.evaluateOnNewDocument(() => {
|
|
66
|
+
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// Save connection info
|
|
70
|
+
const wsEndpoint = browser.wsEndpoint();
|
|
71
|
+
writeFileSync(WS_FILE, wsEndpoint);
|
|
72
|
+
// Get browser process PID
|
|
73
|
+
const browserProcess = browser.process();
|
|
74
|
+
if (browserProcess?.pid) {
|
|
75
|
+
writeFileSync(PID_FILE, String(browserProcess.pid));
|
|
76
|
+
}
|
|
77
|
+
return browser;
|
|
78
|
+
}
|
|
79
|
+
// Get or create browser instance
|
|
80
|
+
async function getBrowser(headless) {
|
|
81
|
+
// Try to connect to existing browser
|
|
82
|
+
if (isBrowserRunning() && existsSync(WS_FILE)) {
|
|
83
|
+
try {
|
|
84
|
+
const ws = readFileSync(WS_FILE, 'utf-8').trim();
|
|
85
|
+
return await puppeteer.connect({ browserWSEndpoint: ws });
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Connection failed, clean up and launch new
|
|
89
|
+
cleanup();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Launch new browser
|
|
93
|
+
return launchBrowser(headless);
|
|
94
|
+
}
|
|
95
|
+
// Get a page from the browser
|
|
96
|
+
async function getPage(browser, newTab = false) {
|
|
97
|
+
if (newTab) {
|
|
98
|
+
return browser.newPage();
|
|
99
|
+
}
|
|
100
|
+
const pages = await browser.pages();
|
|
101
|
+
return pages[pages.length - 1] || browser.newPage();
|
|
102
|
+
}
|
|
103
|
+
// Helper to run commands with browser
|
|
104
|
+
async function withBrowser(fn, options = {}) {
|
|
105
|
+
const browser = await getBrowser(options.headless ?? false);
|
|
106
|
+
const page = await getPage(browser, options.newTab);
|
|
107
|
+
try {
|
|
108
|
+
return await fn(browser, page);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
browser.disconnect(); // Disconnect but keep browser running
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const program = cli('chrome', '1.0.9', 'Chrome browser automation');
|
|
115
|
+
// Global headless option
|
|
116
|
+
program.option('--headless', 'Run in headless mode');
|
|
117
|
+
program
|
|
118
|
+
.command('launch')
|
|
119
|
+
.description('Launch managed browser')
|
|
120
|
+
.action(withErrorHandling(async () => {
|
|
121
|
+
const headless = program.opts().headless ?? false;
|
|
122
|
+
if (isBrowserRunning()) {
|
|
123
|
+
log('Browser already running');
|
|
124
|
+
const ws = readFileSync(WS_FILE, 'utf-8').trim();
|
|
125
|
+
output({ running: true, wsEndpoint: ws, profileDir: PROFILE_DIR });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const browser = await launchBrowser(headless);
|
|
129
|
+
log(`Browser launched ${headless ? '(headless)' : ''}`);
|
|
130
|
+
output({
|
|
131
|
+
launched: true,
|
|
132
|
+
headless,
|
|
133
|
+
wsEndpoint: browser.wsEndpoint(),
|
|
134
|
+
profileDir: PROFILE_DIR
|
|
135
|
+
});
|
|
136
|
+
browser.disconnect();
|
|
137
|
+
}));
|
|
138
|
+
program
|
|
139
|
+
.command('close')
|
|
140
|
+
.description('Close managed browser')
|
|
141
|
+
.action(withErrorHandling(async () => {
|
|
142
|
+
if (!isBrowserRunning()) {
|
|
143
|
+
log('No browser running');
|
|
144
|
+
output({ closed: false, reason: 'not_running' });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const ws = readFileSync(WS_FILE, 'utf-8').trim();
|
|
149
|
+
const browser = await puppeteer.connect({ browserWSEndpoint: ws });
|
|
150
|
+
await browser.close();
|
|
151
|
+
cleanup();
|
|
152
|
+
log('Browser closed');
|
|
153
|
+
output({ closed: true });
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
cleanup();
|
|
157
|
+
output({ closed: true, note: 'force_cleaned' });
|
|
158
|
+
}
|
|
159
|
+
}));
|
|
160
|
+
program
|
|
161
|
+
.command('status')
|
|
162
|
+
.description('Check browser status')
|
|
163
|
+
.action(withErrorHandling(async () => {
|
|
164
|
+
const running = isBrowserRunning();
|
|
165
|
+
output({
|
|
166
|
+
running,
|
|
167
|
+
profileDir: PROFILE_DIR,
|
|
168
|
+
wsEndpoint: running && existsSync(WS_FILE) ? readFileSync(WS_FILE, 'utf-8').trim() : null,
|
|
169
|
+
});
|
|
170
|
+
}));
|
|
171
|
+
program
|
|
172
|
+
.command('navigate <url>')
|
|
173
|
+
.description('Open URL in browser')
|
|
174
|
+
.option('--new-tab', 'Open in new tab')
|
|
175
|
+
.option('--wait <event>', 'Wait for: load, domcontentloaded, networkidle0, networkidle2', 'networkidle2')
|
|
176
|
+
.action(withErrorHandling(async (url, options) => {
|
|
177
|
+
const headless = program.opts().headless ?? false;
|
|
178
|
+
const waitUntil = options.wait;
|
|
179
|
+
await withBrowser(async (browser, page) => {
|
|
180
|
+
await page.goto(url, { waitUntil });
|
|
181
|
+
const title = await page.title();
|
|
182
|
+
log(`Navigated to: ${title}`);
|
|
183
|
+
output({ title, url: page.url() });
|
|
184
|
+
}, { headless, newTab: options.newTab });
|
|
185
|
+
}));
|
|
186
|
+
program
|
|
187
|
+
.command('click <selector>')
|
|
188
|
+
.description('Click an element')
|
|
189
|
+
.action(withErrorHandling(async (selector) => {
|
|
190
|
+
const headless = program.opts().headless ?? false;
|
|
191
|
+
await withBrowser(async (browser, page) => {
|
|
192
|
+
await page.waitForSelector(selector, { timeout: 5000 });
|
|
193
|
+
await page.click(selector);
|
|
194
|
+
log(`Clicked: ${selector}`);
|
|
195
|
+
output({ clicked: true, selector });
|
|
196
|
+
}, { headless });
|
|
197
|
+
}));
|
|
198
|
+
program
|
|
199
|
+
.command('type <selector> <text>')
|
|
200
|
+
.description('Type text into an input')
|
|
201
|
+
.option('--clear', 'Clear field first')
|
|
202
|
+
.action(withErrorHandling(async (selector, text, options) => {
|
|
203
|
+
const headless = program.opts().headless ?? false;
|
|
204
|
+
await withBrowser(async (browser, page) => {
|
|
205
|
+
await page.waitForSelector(selector, { timeout: 5000 });
|
|
206
|
+
if (options.clear) {
|
|
207
|
+
await page.click(selector, { clickCount: 3 });
|
|
208
|
+
}
|
|
209
|
+
await page.type(selector, text);
|
|
210
|
+
log(`Typed into: ${selector}`);
|
|
211
|
+
output({ typed: true, selector, length: text.length });
|
|
212
|
+
}, { headless });
|
|
213
|
+
}));
|
|
214
|
+
program
|
|
215
|
+
.command('eval <script>')
|
|
216
|
+
.description('Run JavaScript in page context')
|
|
217
|
+
.action(withErrorHandling(async (script) => {
|
|
218
|
+
const headless = program.opts().headless ?? false;
|
|
219
|
+
log('Executing JavaScript in browser context');
|
|
220
|
+
await withBrowser(async (browser, page) => {
|
|
221
|
+
const result = await page.evaluate((code) => {
|
|
222
|
+
return eval(code);
|
|
223
|
+
}, script);
|
|
224
|
+
if (result !== undefined)
|
|
225
|
+
output(result);
|
|
226
|
+
}, { headless });
|
|
227
|
+
}));
|
|
228
|
+
program
|
|
229
|
+
.command('screenshot [output]')
|
|
230
|
+
.description('Capture screenshot')
|
|
231
|
+
.option('--full-page', 'Capture full page')
|
|
232
|
+
.option('--selector <sel>', 'Screenshot specific element')
|
|
233
|
+
.action(withErrorHandling(async (outputFile = 'screenshot.png', options) => {
|
|
234
|
+
const headless = program.opts().headless ?? false;
|
|
235
|
+
const outputPath = resolve(outputFile);
|
|
236
|
+
await withBrowser(async (browser, page) => {
|
|
237
|
+
if (options.selector) {
|
|
238
|
+
const element = await page.$(options.selector);
|
|
239
|
+
if (!element) {
|
|
240
|
+
outputError('NOT_FOUND', `Element not found: ${options.selector}`);
|
|
241
|
+
}
|
|
242
|
+
await element.screenshot({ path: outputPath });
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
await page.screenshot({ path: outputPath, fullPage: options.fullPage });
|
|
246
|
+
}
|
|
247
|
+
log(`Saved: ${outputPath}`);
|
|
248
|
+
output({ path: outputPath });
|
|
249
|
+
}, { headless });
|
|
250
|
+
}));
|
|
251
|
+
program
|
|
252
|
+
.command('pdf [output]')
|
|
253
|
+
.description('Save page as PDF (headless only)')
|
|
254
|
+
.action(withErrorHandling(async (outputFile = 'page.pdf') => {
|
|
255
|
+
const outputPath = resolve(outputFile);
|
|
256
|
+
// PDF requires headless mode
|
|
257
|
+
await withBrowser(async (browser, page) => {
|
|
258
|
+
await page.pdf({ path: outputPath, format: 'A4' });
|
|
259
|
+
log(`Saved: ${outputPath}`);
|
|
260
|
+
output({ path: outputPath });
|
|
261
|
+
}, { headless: true });
|
|
262
|
+
}));
|
|
263
|
+
program
|
|
264
|
+
.command('cookies [domain]')
|
|
265
|
+
.description('Get cookies')
|
|
266
|
+
.action(withErrorHandling(async (domain) => {
|
|
267
|
+
const headless = program.opts().headless ?? false;
|
|
268
|
+
await withBrowser(async (browser, page) => {
|
|
269
|
+
const client = await page.createCDPSession();
|
|
270
|
+
const { cookies } = await client.send('Network.getAllCookies');
|
|
271
|
+
const filtered = domain
|
|
272
|
+
? cookies.filter((c) => c.domain.includes(domain))
|
|
273
|
+
: cookies;
|
|
274
|
+
output(filtered);
|
|
275
|
+
log(`${filtered.length} cookies`);
|
|
276
|
+
}, { headless });
|
|
277
|
+
}));
|
|
278
|
+
program
|
|
279
|
+
.command('html [selector]')
|
|
280
|
+
.description('Get page HTML or element HTML')
|
|
281
|
+
.action(withErrorHandling(async (selector) => {
|
|
282
|
+
const headless = program.opts().headless ?? false;
|
|
283
|
+
await withBrowser(async (browser, page) => {
|
|
284
|
+
let html;
|
|
285
|
+
if (selector) {
|
|
286
|
+
const element = await page.$(selector);
|
|
287
|
+
if (!element) {
|
|
288
|
+
outputError('NOT_FOUND', `Element not found: ${selector}`);
|
|
289
|
+
}
|
|
290
|
+
html = await page.evaluate(el => el.outerHTML, element);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
html = await page.content();
|
|
294
|
+
}
|
|
295
|
+
output({ html, length: html.length });
|
|
296
|
+
}, { headless });
|
|
297
|
+
}));
|
|
298
|
+
program
|
|
299
|
+
.command('text [selector]')
|
|
300
|
+
.description('Get text content')
|
|
301
|
+
.action(withErrorHandling(async (selector) => {
|
|
302
|
+
const headless = program.opts().headless ?? false;
|
|
303
|
+
await withBrowser(async (browser, page) => {
|
|
304
|
+
let text;
|
|
305
|
+
if (selector) {
|
|
306
|
+
const element = await page.$(selector);
|
|
307
|
+
if (!element) {
|
|
308
|
+
outputError('NOT_FOUND', `Element not found: ${selector}`);
|
|
309
|
+
}
|
|
310
|
+
text = await page.evaluate(el => el.textContent || '', element);
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
text = await page.evaluate(() => document.body.innerText);
|
|
314
|
+
}
|
|
315
|
+
output({ text: text.trim(), length: text.trim().length });
|
|
316
|
+
}, { headless });
|
|
317
|
+
}));
|
|
318
|
+
program
|
|
319
|
+
.command('wait <selector>')
|
|
320
|
+
.description('Wait for element to appear')
|
|
321
|
+
.option('--timeout <ms>', 'Timeout in milliseconds', '30000')
|
|
322
|
+
.action(withErrorHandling(async (selector, options) => {
|
|
323
|
+
const headless = program.opts().headless ?? false;
|
|
324
|
+
const timeout = parseInt(options.timeout, 10);
|
|
325
|
+
await withBrowser(async (browser, page) => {
|
|
326
|
+
await page.waitForSelector(selector, { timeout });
|
|
327
|
+
log(`Found: ${selector}`);
|
|
328
|
+
output({ found: true, selector });
|
|
329
|
+
}, { headless });
|
|
330
|
+
}));
|
|
331
|
+
program
|
|
332
|
+
.command('tabs')
|
|
333
|
+
.description('List open tabs')
|
|
334
|
+
.action(withErrorHandling(async () => {
|
|
335
|
+
const headless = program.opts().headless ?? false;
|
|
336
|
+
await withBrowser(async (browser, page) => {
|
|
337
|
+
const pages = await browser.pages();
|
|
338
|
+
const tabs = await Promise.all(pages.map(async (p, i) => ({
|
|
339
|
+
index: i,
|
|
340
|
+
url: p.url(),
|
|
341
|
+
title: await p.title(),
|
|
342
|
+
})));
|
|
343
|
+
output(tabs);
|
|
344
|
+
log(`${tabs.length} tabs`);
|
|
345
|
+
}, { headless });
|
|
346
|
+
}));
|
|
347
|
+
program.parse();
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cli4ai/chrome",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "Chrome browser automation with managed browser instance",
|
|
5
5
|
"author": "cliforai",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
7
|
-
"main": "run.
|
|
7
|
+
"main": "dist/run.js",
|
|
8
8
|
"bin": {
|
|
9
|
-
"chrome": "./run.
|
|
9
|
+
"chrome": "./dist/run.js"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
12
|
"keywords": [
|
|
@@ -34,12 +34,20 @@
|
|
|
34
34
|
"commander": "^14.0.0"
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
|
-
"
|
|
37
|
+
"dist",
|
|
38
38
|
"cli4ai.json",
|
|
39
39
|
"README.md",
|
|
40
40
|
"LICENSE"
|
|
41
41
|
],
|
|
42
42
|
"publishConfig": {
|
|
43
43
|
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc",
|
|
47
|
+
"prepublishOnly": "npm run build"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"typescript": "^5.0.0",
|
|
51
|
+
"@types/node": "^22.0.0"
|
|
44
52
|
}
|
|
45
53
|
}
|
package/run.ts
DELETED
|
@@ -1,383 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env npx tsx
|
|
2
|
-
import puppeteer, { Browser, Page } from 'puppeteer';
|
|
3
|
-
import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync } from 'fs';
|
|
4
|
-
import { join, resolve } from 'path';
|
|
5
|
-
import { homedir } from 'os';
|
|
6
|
-
import { cli, log, output, outputError, withErrorHandling } from '@cli4ai/lib/cli.ts';
|
|
7
|
-
|
|
8
|
-
// Persistent storage paths
|
|
9
|
-
const CLI4AI_DIR = join(homedir(), '.cli4ai');
|
|
10
|
-
const CHROME_DIR = join(CLI4AI_DIR, 'chrome');
|
|
11
|
-
const PROFILE_DIR = join(CHROME_DIR, 'profile');
|
|
12
|
-
const WS_FILE = join(CHROME_DIR, 'ws-endpoint');
|
|
13
|
-
const PID_FILE = join(CHROME_DIR, 'pid');
|
|
14
|
-
|
|
15
|
-
// Ensure directories exist
|
|
16
|
-
function ensureDirs() {
|
|
17
|
-
if (!existsSync(CLI4AI_DIR)) mkdirSync(CLI4AI_DIR, { recursive: true });
|
|
18
|
-
if (!existsSync(CHROME_DIR)) mkdirSync(CHROME_DIR, { recursive: true });
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Check if browser process is still running
|
|
22
|
-
function isBrowserRunning(): boolean {
|
|
23
|
-
if (!existsSync(PID_FILE)) return false;
|
|
24
|
-
const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10);
|
|
25
|
-
try {
|
|
26
|
-
process.kill(pid, 0); // Check if process exists
|
|
27
|
-
return true;
|
|
28
|
-
} catch {
|
|
29
|
-
// Process not running, clean up stale files
|
|
30
|
-
cleanup();
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Clean up state files
|
|
36
|
-
function cleanup() {
|
|
37
|
-
if (existsSync(WS_FILE)) unlinkSync(WS_FILE);
|
|
38
|
-
if (existsSync(PID_FILE)) unlinkSync(PID_FILE);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Launch a new browser instance
|
|
42
|
-
async function launchBrowser(headless: boolean): Promise<Browser> {
|
|
43
|
-
ensureDirs();
|
|
44
|
-
|
|
45
|
-
const browser = await puppeteer.launch({
|
|
46
|
-
headless: headless ? 'shell' : false,
|
|
47
|
-
userDataDir: PROFILE_DIR,
|
|
48
|
-
args: [
|
|
49
|
-
'--no-first-run',
|
|
50
|
-
'--no-default-browser-check',
|
|
51
|
-
'--disable-infobars',
|
|
52
|
-
'--disable-blink-features=AutomationControlled',
|
|
53
|
-
'--disable-features=IsolateOrigins,site-per-process',
|
|
54
|
-
],
|
|
55
|
-
defaultViewport: null, // Use full window size
|
|
56
|
-
ignoreDefaultArgs: ['--enable-automation'],
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// Remove automation detection signals
|
|
60
|
-
const userAgent = process.platform === 'win32'
|
|
61
|
-
? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36'
|
|
62
|
-
: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36';
|
|
63
|
-
|
|
64
|
-
const pages = await browser.pages();
|
|
65
|
-
for (const page of pages) {
|
|
66
|
-
await page.setUserAgent(userAgent);
|
|
67
|
-
await page.evaluateOnNewDocument(() => {
|
|
68
|
-
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Save connection info
|
|
73
|
-
const wsEndpoint = browser.wsEndpoint();
|
|
74
|
-
writeFileSync(WS_FILE, wsEndpoint);
|
|
75
|
-
|
|
76
|
-
// Get browser process PID
|
|
77
|
-
const browserProcess = browser.process();
|
|
78
|
-
if (browserProcess?.pid) {
|
|
79
|
-
writeFileSync(PID_FILE, String(browserProcess.pid));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return browser;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Get or create browser instance
|
|
86
|
-
async function getBrowser(headless: boolean): Promise<Browser> {
|
|
87
|
-
// Try to connect to existing browser
|
|
88
|
-
if (isBrowserRunning() && existsSync(WS_FILE)) {
|
|
89
|
-
try {
|
|
90
|
-
const ws = readFileSync(WS_FILE, 'utf-8').trim();
|
|
91
|
-
return await puppeteer.connect({ browserWSEndpoint: ws });
|
|
92
|
-
} catch {
|
|
93
|
-
// Connection failed, clean up and launch new
|
|
94
|
-
cleanup();
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Launch new browser
|
|
99
|
-
return launchBrowser(headless);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Get a page from the browser
|
|
103
|
-
async function getPage(browser: Browser, newTab = false): Promise<Page> {
|
|
104
|
-
if (newTab) {
|
|
105
|
-
return browser.newPage();
|
|
106
|
-
}
|
|
107
|
-
const pages = await browser.pages();
|
|
108
|
-
return pages[pages.length - 1] || browser.newPage();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Helper to run commands with browser
|
|
112
|
-
async function withBrowser<T>(
|
|
113
|
-
fn: (browser: Browser, page: Page) => Promise<T>,
|
|
114
|
-
options: { headless?: boolean; newTab?: boolean } = {}
|
|
115
|
-
): Promise<T> {
|
|
116
|
-
const browser = await getBrowser(options.headless ?? false);
|
|
117
|
-
const page = await getPage(browser, options.newTab);
|
|
118
|
-
try {
|
|
119
|
-
return await fn(browser, page);
|
|
120
|
-
} finally {
|
|
121
|
-
browser.disconnect(); // Disconnect but keep browser running
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const program = cli('chrome', '1.0.9', 'Chrome browser automation');
|
|
126
|
-
|
|
127
|
-
// Global headless option
|
|
128
|
-
program.option('--headless', 'Run in headless mode');
|
|
129
|
-
|
|
130
|
-
program
|
|
131
|
-
.command('launch')
|
|
132
|
-
.description('Launch managed browser')
|
|
133
|
-
.action(withErrorHandling(async () => {
|
|
134
|
-
const headless = program.opts().headless ?? false;
|
|
135
|
-
|
|
136
|
-
if (isBrowserRunning()) {
|
|
137
|
-
log('Browser already running');
|
|
138
|
-
const ws = readFileSync(WS_FILE, 'utf-8').trim();
|
|
139
|
-
output({ running: true, wsEndpoint: ws, profileDir: PROFILE_DIR });
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const browser = await launchBrowser(headless);
|
|
144
|
-
log(`Browser launched ${headless ? '(headless)' : ''}`);
|
|
145
|
-
output({
|
|
146
|
-
launched: true,
|
|
147
|
-
headless,
|
|
148
|
-
wsEndpoint: browser.wsEndpoint(),
|
|
149
|
-
profileDir: PROFILE_DIR
|
|
150
|
-
});
|
|
151
|
-
browser.disconnect();
|
|
152
|
-
}));
|
|
153
|
-
|
|
154
|
-
program
|
|
155
|
-
.command('close')
|
|
156
|
-
.description('Close managed browser')
|
|
157
|
-
.action(withErrorHandling(async () => {
|
|
158
|
-
if (!isBrowserRunning()) {
|
|
159
|
-
log('No browser running');
|
|
160
|
-
output({ closed: false, reason: 'not_running' });
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
const ws = readFileSync(WS_FILE, 'utf-8').trim();
|
|
166
|
-
const browser = await puppeteer.connect({ browserWSEndpoint: ws });
|
|
167
|
-
await browser.close();
|
|
168
|
-
cleanup();
|
|
169
|
-
log('Browser closed');
|
|
170
|
-
output({ closed: true });
|
|
171
|
-
} catch (err) {
|
|
172
|
-
cleanup();
|
|
173
|
-
output({ closed: true, note: 'force_cleaned' });
|
|
174
|
-
}
|
|
175
|
-
}));
|
|
176
|
-
|
|
177
|
-
program
|
|
178
|
-
.command('status')
|
|
179
|
-
.description('Check browser status')
|
|
180
|
-
.action(withErrorHandling(async () => {
|
|
181
|
-
const running = isBrowserRunning();
|
|
182
|
-
output({
|
|
183
|
-
running,
|
|
184
|
-
profileDir: PROFILE_DIR,
|
|
185
|
-
wsEndpoint: running && existsSync(WS_FILE) ? readFileSync(WS_FILE, 'utf-8').trim() : null,
|
|
186
|
-
});
|
|
187
|
-
}));
|
|
188
|
-
|
|
189
|
-
program
|
|
190
|
-
.command('navigate <url>')
|
|
191
|
-
.description('Open URL in browser')
|
|
192
|
-
.option('--new-tab', 'Open in new tab')
|
|
193
|
-
.option('--wait <event>', 'Wait for: load, domcontentloaded, networkidle0, networkidle2', 'networkidle2')
|
|
194
|
-
.action(withErrorHandling(async (url: string, options: { newTab?: boolean; wait?: string }) => {
|
|
195
|
-
const headless = program.opts().headless ?? false;
|
|
196
|
-
const waitUntil = options.wait as 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2';
|
|
197
|
-
|
|
198
|
-
await withBrowser(async (browser, page) => {
|
|
199
|
-
await page.goto(url, { waitUntil });
|
|
200
|
-
const title = await page.title();
|
|
201
|
-
log(`Navigated to: ${title}`);
|
|
202
|
-
output({ title, url: page.url() });
|
|
203
|
-
}, { headless, newTab: options.newTab });
|
|
204
|
-
}));
|
|
205
|
-
|
|
206
|
-
program
|
|
207
|
-
.command('click <selector>')
|
|
208
|
-
.description('Click an element')
|
|
209
|
-
.action(withErrorHandling(async (selector: string) => {
|
|
210
|
-
const headless = program.opts().headless ?? false;
|
|
211
|
-
|
|
212
|
-
await withBrowser(async (browser, page) => {
|
|
213
|
-
await page.waitForSelector(selector, { timeout: 5000 });
|
|
214
|
-
await page.click(selector);
|
|
215
|
-
log(`Clicked: ${selector}`);
|
|
216
|
-
output({ clicked: true, selector });
|
|
217
|
-
}, { headless });
|
|
218
|
-
}));
|
|
219
|
-
|
|
220
|
-
program
|
|
221
|
-
.command('type <selector> <text>')
|
|
222
|
-
.description('Type text into an input')
|
|
223
|
-
.option('--clear', 'Clear field first')
|
|
224
|
-
.action(withErrorHandling(async (selector: string, text: string, options: { clear?: boolean }) => {
|
|
225
|
-
const headless = program.opts().headless ?? false;
|
|
226
|
-
|
|
227
|
-
await withBrowser(async (browser, page) => {
|
|
228
|
-
await page.waitForSelector(selector, { timeout: 5000 });
|
|
229
|
-
if (options.clear) {
|
|
230
|
-
await page.click(selector, { clickCount: 3 });
|
|
231
|
-
}
|
|
232
|
-
await page.type(selector, text);
|
|
233
|
-
log(`Typed into: ${selector}`);
|
|
234
|
-
output({ typed: true, selector, length: text.length });
|
|
235
|
-
}, { headless });
|
|
236
|
-
}));
|
|
237
|
-
|
|
238
|
-
program
|
|
239
|
-
.command('eval <script>')
|
|
240
|
-
.description('Run JavaScript in page context')
|
|
241
|
-
.action(withErrorHandling(async (script: string) => {
|
|
242
|
-
const headless = program.opts().headless ?? false;
|
|
243
|
-
log('Executing JavaScript in browser context');
|
|
244
|
-
|
|
245
|
-
await withBrowser(async (browser, page) => {
|
|
246
|
-
const result = await page.evaluate((code: string) => {
|
|
247
|
-
return eval(code);
|
|
248
|
-
}, script);
|
|
249
|
-
if (result !== undefined) output(result);
|
|
250
|
-
}, { headless });
|
|
251
|
-
}));
|
|
252
|
-
|
|
253
|
-
program
|
|
254
|
-
.command('screenshot [output]')
|
|
255
|
-
.description('Capture screenshot')
|
|
256
|
-
.option('--full-page', 'Capture full page')
|
|
257
|
-
.option('--selector <sel>', 'Screenshot specific element')
|
|
258
|
-
.action(withErrorHandling(async (outputFile = 'screenshot.png', options: { fullPage?: boolean; selector?: string }) => {
|
|
259
|
-
const headless = program.opts().headless ?? false;
|
|
260
|
-
const outputPath = resolve(outputFile);
|
|
261
|
-
|
|
262
|
-
await withBrowser(async (browser, page) => {
|
|
263
|
-
if (options.selector) {
|
|
264
|
-
const element = await page.$(options.selector);
|
|
265
|
-
if (!element) {
|
|
266
|
-
outputError('NOT_FOUND', `Element not found: ${options.selector}`);
|
|
267
|
-
}
|
|
268
|
-
await element!.screenshot({ path: outputPath });
|
|
269
|
-
} else {
|
|
270
|
-
await page.screenshot({ path: outputPath, fullPage: options.fullPage });
|
|
271
|
-
}
|
|
272
|
-
log(`Saved: ${outputPath}`);
|
|
273
|
-
output({ path: outputPath });
|
|
274
|
-
}, { headless });
|
|
275
|
-
}));
|
|
276
|
-
|
|
277
|
-
program
|
|
278
|
-
.command('pdf [output]')
|
|
279
|
-
.description('Save page as PDF (headless only)')
|
|
280
|
-
.action(withErrorHandling(async (outputFile = 'page.pdf') => {
|
|
281
|
-
const outputPath = resolve(outputFile);
|
|
282
|
-
|
|
283
|
-
// PDF requires headless mode
|
|
284
|
-
await withBrowser(async (browser, page) => {
|
|
285
|
-
await page.pdf({ path: outputPath, format: 'A4' });
|
|
286
|
-
log(`Saved: ${outputPath}`);
|
|
287
|
-
output({ path: outputPath });
|
|
288
|
-
}, { headless: true });
|
|
289
|
-
}));
|
|
290
|
-
|
|
291
|
-
program
|
|
292
|
-
.command('cookies [domain]')
|
|
293
|
-
.description('Get cookies')
|
|
294
|
-
.action(withErrorHandling(async (domain?: string) => {
|
|
295
|
-
const headless = program.opts().headless ?? false;
|
|
296
|
-
|
|
297
|
-
await withBrowser(async (browser, page) => {
|
|
298
|
-
const client = await page.createCDPSession();
|
|
299
|
-
const { cookies } = await client.send('Network.getAllCookies');
|
|
300
|
-
const filtered = domain
|
|
301
|
-
? cookies.filter((c: { domain: string }) => c.domain.includes(domain))
|
|
302
|
-
: cookies;
|
|
303
|
-
output(filtered);
|
|
304
|
-
log(`${filtered.length} cookies`);
|
|
305
|
-
}, { headless });
|
|
306
|
-
}));
|
|
307
|
-
|
|
308
|
-
program
|
|
309
|
-
.command('html [selector]')
|
|
310
|
-
.description('Get page HTML or element HTML')
|
|
311
|
-
.action(withErrorHandling(async (selector?: string) => {
|
|
312
|
-
const headless = program.opts().headless ?? false;
|
|
313
|
-
|
|
314
|
-
await withBrowser(async (browser, page) => {
|
|
315
|
-
let html: string;
|
|
316
|
-
if (selector) {
|
|
317
|
-
const element = await page.$(selector);
|
|
318
|
-
if (!element) {
|
|
319
|
-
outputError('NOT_FOUND', `Element not found: ${selector}`);
|
|
320
|
-
}
|
|
321
|
-
html = await page.evaluate(el => el!.outerHTML, element);
|
|
322
|
-
} else {
|
|
323
|
-
html = await page.content();
|
|
324
|
-
}
|
|
325
|
-
output({ html, length: html.length });
|
|
326
|
-
}, { headless });
|
|
327
|
-
}));
|
|
328
|
-
|
|
329
|
-
program
|
|
330
|
-
.command('text [selector]')
|
|
331
|
-
.description('Get text content')
|
|
332
|
-
.action(withErrorHandling(async (selector?: string) => {
|
|
333
|
-
const headless = program.opts().headless ?? false;
|
|
334
|
-
|
|
335
|
-
await withBrowser(async (browser, page) => {
|
|
336
|
-
let text: string;
|
|
337
|
-
if (selector) {
|
|
338
|
-
const element = await page.$(selector);
|
|
339
|
-
if (!element) {
|
|
340
|
-
outputError('NOT_FOUND', `Element not found: ${selector}`);
|
|
341
|
-
}
|
|
342
|
-
text = await page.evaluate(el => el!.textContent || '', element);
|
|
343
|
-
} else {
|
|
344
|
-
text = await page.evaluate(() => document.body.innerText);
|
|
345
|
-
}
|
|
346
|
-
output({ text: text.trim(), length: text.trim().length });
|
|
347
|
-
}, { headless });
|
|
348
|
-
}));
|
|
349
|
-
|
|
350
|
-
program
|
|
351
|
-
.command('wait <selector>')
|
|
352
|
-
.description('Wait for element to appear')
|
|
353
|
-
.option('--timeout <ms>', 'Timeout in milliseconds', '30000')
|
|
354
|
-
.action(withErrorHandling(async (selector: string, options: { timeout: string }) => {
|
|
355
|
-
const headless = program.opts().headless ?? false;
|
|
356
|
-
const timeout = parseInt(options.timeout, 10);
|
|
357
|
-
|
|
358
|
-
await withBrowser(async (browser, page) => {
|
|
359
|
-
await page.waitForSelector(selector, { timeout });
|
|
360
|
-
log(`Found: ${selector}`);
|
|
361
|
-
output({ found: true, selector });
|
|
362
|
-
}, { headless });
|
|
363
|
-
}));
|
|
364
|
-
|
|
365
|
-
program
|
|
366
|
-
.command('tabs')
|
|
367
|
-
.description('List open tabs')
|
|
368
|
-
.action(withErrorHandling(async () => {
|
|
369
|
-
const headless = program.opts().headless ?? false;
|
|
370
|
-
|
|
371
|
-
await withBrowser(async (browser, page) => {
|
|
372
|
-
const pages = await browser.pages();
|
|
373
|
-
const tabs = await Promise.all(pages.map(async (p, i) => ({
|
|
374
|
-
index: i,
|
|
375
|
-
url: p.url(),
|
|
376
|
-
title: await p.title(),
|
|
377
|
-
})));
|
|
378
|
-
output(tabs);
|
|
379
|
-
log(`${tabs.length} tabs`);
|
|
380
|
-
}, { headless });
|
|
381
|
-
}));
|
|
382
|
-
|
|
383
|
-
program.parse();
|