agentscreenshots 0.1.0 → 0.2.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/AGENT-INSTRUCTIONS.md +87 -79
- package/README.md +65 -3
- package/dist/browser.js +225 -0
- package/dist/capture.js +98 -10
- package/dist/doctor.js +16 -4
- package/dist/index.js +151 -9
- package/dist/postinstall.js +10 -0
- package/package.json +2 -10
package/AGENT-INSTRUCTIONS.md
CHANGED
|
@@ -1,102 +1,110 @@
|
|
|
1
|
-
# AgentScreenshots
|
|
1
|
+
# AgentScreenshots CLI agent instructions
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use `agentshot` for rendered webpage screenshots and visual UI checks.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Assume `agentshot` is already installed and configured. Do not install it, authenticate it, or change its configuration unless the user explicitly asks. If `agentshot` is not available, report that clearly and continue with the best available fallback.
|
|
5
|
+
What this tool is good for:
|
|
6
|
+
- Inspecting a page, component, section, or responsive viewport after frontend edits.
|
|
7
|
+
- Verifying layout, spacing, overflow, wrapping, missing assets, and visual regressions.
|
|
8
|
+
- Capturing localhost, preview, staging, or production pages.
|
|
9
|
+
- Giving an AI coding agent a PNG artifact it can inspect before judging the UI.
|
|
11
10
|
|
|
12
11
|
Default workflow:
|
|
12
|
+
1. Identify the exact URL to inspect. For local dev, use the active localhost URL such as `http://127.0.0.1:5173`.
|
|
13
|
+
2. Save screenshots into a predictable project folder, preferably `.agents/screenshots/`.
|
|
14
|
+
3. Prefer selector, viewport, or vertical-slice captures over huge full-page screenshots when they answer the question.
|
|
15
|
+
4. Inspect the saved PNG with image-reading/viewing capability before making visual claims.
|
|
16
|
+
5. If the UI is wrong, make a focused fix, capture again, and inspect again.
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
- For local dev, use the active localhost URL, for example `http://127.0.0.1:5173`.
|
|
16
|
-
- For deployed work, use the preview/staging/production URL the user provided.
|
|
17
|
-
|
|
18
|
-
2. Save screenshots into a predictable project folder:
|
|
19
|
-
- Prefer `.agents/screenshots/`.
|
|
20
|
-
- Use short descriptive filenames, for example `home.png`, `pricing-section.png`, `mobile-nav.png`.
|
|
21
|
-
|
|
22
|
-
3. Capture the relevant view with `agentshot`.
|
|
23
|
-
|
|
24
|
-
4. Inspect the saved PNG with your image-reading/viewing capability before judging the UI.
|
|
25
|
-
|
|
26
|
-
5. If the UI is wrong, make a focused fix, capture again, and re-inspect.
|
|
27
|
-
|
|
28
|
-
If `agentshot` itself fails, run:
|
|
29
|
-
|
|
30
|
-
agentshot doctor
|
|
31
|
-
|
|
32
|
-
Use the doctor output to identify whether the issue is missing auth, browser setup, API reachability, output permissions, or the target page itself.
|
|
33
|
-
|
|
34
|
-
Use full-page capture when you need overall page context:
|
|
18
|
+
Core commands:
|
|
35
19
|
|
|
20
|
+
```bash
|
|
36
21
|
agentshot "<url>" ".agents/screenshots/page.png" --scroll --wait 1000
|
|
37
|
-
|
|
38
|
-
Use a fixed top slice when the issue is near the top of the page:
|
|
39
|
-
|
|
40
22
|
agentshot "<url>" ".agents/screenshots/top.png" --height 1200 --wait 500
|
|
41
|
-
|
|
42
|
-
Use a vertical slice when the issue is in a known scroll range:
|
|
43
|
-
|
|
44
23
|
agentshot "<url>" ".agents/screenshots/slice.png" --from 1600 --to 2400 --wait 500
|
|
45
|
-
|
|
46
|
-
Use selector capture for a specific section or component:
|
|
47
|
-
|
|
48
|
-
agentshot "<url>" ".agents/screenshots/pricing.png" --selector "section:has-text('Pricing')" --padding 24 --wait 500
|
|
49
|
-
|
|
50
|
-
Use `--nth` when multiple elements match:
|
|
51
|
-
|
|
24
|
+
agentshot "<url>" ".agents/screenshots/section.png" --selector "section:has-text('Pricing')" --padding 24 --wait 500
|
|
52
25
|
agentshot "<url>" ".agents/screenshots/card-2.png" --selector ".pricing-card" --nth 1 --padding 16 --wait 500
|
|
53
|
-
|
|
54
|
-
Use mobile viewport checks for responsive layout:
|
|
55
|
-
|
|
56
26
|
agentshot "<url>" ".agents/screenshots/mobile.png" --viewport 390x844 --scroll --wait 1000
|
|
57
|
-
|
|
58
|
-
Use desktop viewport checks when layout width matters:
|
|
59
|
-
|
|
60
27
|
agentshot "<url>" ".agents/screenshots/desktop.png" --viewport 1440x1000 --scroll --wait 1000
|
|
28
|
+
agentshot "https://example.com" ".agents/screenshots/live-page.png" --scroll --wait 2500 --wait-until load
|
|
29
|
+
agentshot "<url>" "pricing-section.png" --temp --selector "section:has-text('Pricing')" --padding 24 --wait 500
|
|
30
|
+
agentshot "https://example.com" ".agents/screenshots/no-cookie-banner.png" --click-if-present "button:has-text('Reject all')" --wait 500
|
|
31
|
+
agentshot "<url>" ".agents/screenshots/hover-menu.png" --hover ".nav-item" --selector ".nav-region" --padding 24 --wait 500
|
|
32
|
+
agentshot "<url>" ".agents/screenshots/small.png" --viewport 1440x1000 --device-scale-factor 1 --wait 500
|
|
33
|
+
```
|
|
61
34
|
|
|
62
|
-
|
|
35
|
+
Capture options:
|
|
36
|
+
- `--selector SELECTOR`: capture the first matching Playwright/CSS selector.
|
|
37
|
+
- `--section SELECTOR`: alias for `--selector`.
|
|
38
|
+
- `--nth INDEX`: choose another selector match, zero-based.
|
|
39
|
+
- `--padding PX`: add padding around selector captures.
|
|
40
|
+
- `--height PX`: capture from page top or `--from` to a fixed height.
|
|
41
|
+
- `--from PX --to PX`: capture a vertical page slice.
|
|
42
|
+
- `--viewport WIDTHxHEIGHT`: set viewport size.
|
|
43
|
+
- `--device-scale-factor N`: capture higher-density pixels without changing CSS layout. Defaults to `2`; use `1` when smaller files matter more than visual fidelity.
|
|
44
|
+
- `--scroll`: scroll first to trigger lazy-loaded or scroll-triggered content.
|
|
45
|
+
- `--wait MS`: wait after navigation/scroll before capture.
|
|
46
|
+
- `--wait-for CSS`: wait until a specific element exists before capture.
|
|
47
|
+
- `--click SELECTOR`: click a required Playwright/CSS selector before capture. It searches the main page and child frames. Repeat the flag for multiple clicks.
|
|
48
|
+
- `--click-if-present SELECTOR`: try to click a Playwright/CSS selector before capture, but continue if it is missing. It searches the main page and child frames. Repeat the flag for multiple possible overlays.
|
|
49
|
+
- `--hover SELECTOR`: hover a required Playwright/CSS selector before capture. It searches the main page and child frames. Repeat the flag for multiple hovers.
|
|
50
|
+
- `--hover-if-present SELECTOR`: try to hover a Playwright/CSS selector before capture, but continue if it is missing. It searches the main page and child frames. Repeat the flag for multiple optional hover targets.
|
|
51
|
+
- `--wait-until load|domcontentloaded|networkidle`: choose load-state wait.
|
|
52
|
+
- `--temp`: save in the OS temp directory. With a filename hint, for example `pricing.png --temp`, the output is named like `pricing-temp-df2d.png`; without a filename hint, the name is derived from the URL. It does not add timed deletion or cleanup tracking.
|
|
53
|
+
- `--json`: print machine-readable capture metadata.
|
|
54
|
+
- `--no-report`: skip usage reporting for tests/smoke checks.
|
|
55
|
+
|
|
56
|
+
Output path best practices:
|
|
57
|
+
- For project captures, pass an explicit output path so the capture lands exactly where the user or agent expects.
|
|
58
|
+
- For project work, save screenshots inside the active repo, preferably under `.agents/screenshots/`.
|
|
59
|
+
- Use task-specific subfolders for larger runs, such as `.agents/screenshots/qc/`, `.agents/screenshots/mobile/`, or `.agents/screenshots/2026-05-17-pricing/`.
|
|
60
|
+
- Use descriptive filenames that include the page, viewport, section, or state, for example `pricing-mobile.png`, `hero-hover-menu.png`, or `checkout-modal-closed.png`.
|
|
61
|
+
- Add `.agents/screenshots/` to `.gitignore` unless the screenshots are intentionally part of the repo.
|
|
62
|
+
- For fast development captures that do not need versioned screenshot history, use `--temp` when available.
|
|
63
|
+
- If the user provides a specific output path or saving instructions, use that path and those instructions exactly.
|
|
63
64
|
|
|
65
|
+
Selector notes:
|
|
64
66
|
- Plain CSS selectors work: `.hero`, `#pricing`, `[data-testid='nav']`.
|
|
65
67
|
- Playwright text selectors work: `text=Pricing`.
|
|
66
68
|
- Playwright text filters work: `section:has-text('Pricing')`.
|
|
67
69
|
- XPath works when needed: `xpath=//section[.//h2[contains(., 'Pricing')]]`.
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
-
|
|
72
|
-
- Use
|
|
73
|
-
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
- Use
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
71
|
+
Live-site notes:
|
|
72
|
+
- For production websites, prefer `--wait-until load --wait 1500` to `--wait-until networkidle` unless you know the page settles cleanly.
|
|
73
|
+
- `networkidle` can time out on live sites with analytics, chat widgets, streaming requests, or long-running background fetches.
|
|
74
|
+
- Use `--scroll --wait 1500` or longer when external images, lazy sections, or scroll-triggered animations need time to render.
|
|
75
|
+
- Use `--click-if-present "button:has-text('Reject all')"`, `--click-if-present "button[aria-label='Close']"`, or a site-specific selector to dismiss cookie banners and fixed overlays before capture. Prefer reject/close actions when available.
|
|
76
|
+
- Use `--click` or `--click-if-present` to open or close click-driven UI such as modals, drawers, accordions, details sections, tabs, dropdown menus, and collapsed content.
|
|
77
|
+
- Use `--hover` or `--hover-if-present` for hover-driven UI such as tooltips, hover cards, menu flyouts, hover-revealed controls, and CSS `:hover` states.
|
|
78
|
+
- Many live sites do not use semantic `<section>` tags. If selector capture is unreliable, use stable IDs, text selectors, or measured vertical slices with `--from` and `--to`.
|
|
79
|
+
|
|
80
|
+
When to use full-page capture:
|
|
81
|
+
- Use it for broad page reviews and final sanity checks.
|
|
82
|
+
- Pair it with `--scroll --wait 1000` when images, animations, or lazy content may not render until scrolled.
|
|
83
|
+
- Avoid repeated full-page screenshots when a section or slice would answer the visual question faster.
|
|
84
|
+
|
|
85
|
+
When to use selector capture:
|
|
86
|
+
- Use it after editing a specific component or section.
|
|
87
|
+
- Use `--padding` so borders, shadows, focus rings, and nearby spacing are visible.
|
|
88
|
+
- Use `--nth` when multiple elements match the selector.
|
|
89
|
+
|
|
90
|
+
When to use mobile/desktop viewports:
|
|
91
|
+
- Use mobile checks for nav, wrapping, overflow, CTA layout, form layout, and horizontal scrolling.
|
|
92
|
+
- Use desktop checks when max-widths, columns, hero composition, or large-screen spacing matter.
|
|
93
|
+
- Screenshots default to `--device-scale-factor 2` for sharper image understanding. Use `--device-scale-factor 1` for large batch or matrix runs, such as dozens or hundreds of screenshots, when reducing disk/CI artifact size or upload time matters more than visual fidelity.
|
|
94
|
+
- Use `--viewport` to test different responsive widths. Browser/page zoom is not a first-class `agentshot` option; viewport changes are the supported way to inspect narrower or wider layouts.
|
|
95
|
+
|
|
96
|
+
Troubleshooting:
|
|
97
|
+
- If capture fails, run `agentshot doctor`.
|
|
98
|
+
- If browser setup is missing, run `agentshot install-browser` only when setup changes are acceptable.
|
|
99
|
+
- If a selector is missing, verify the URL and selector, then try `--wait-for "<selector>"`.
|
|
100
|
+
- If content is blank or incomplete, try `--scroll`, `--wait 1500`, or `--wait-for "<selector>"`. Use `--wait-until networkidle` only when the target page is expected to become idle.
|
|
86
101
|
|
|
87
102
|
Do not:
|
|
88
|
-
|
|
89
|
-
- Do not paste base64 screenshots into chat.
|
|
90
103
|
- Do not rely only on command success; inspect the PNG.
|
|
91
|
-
- Do not
|
|
92
|
-
- Do not
|
|
93
|
-
- Do not
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
## Short Version
|
|
97
|
-
|
|
98
|
-
Use this when the agent instruction surface is small:
|
|
104
|
+
- Do not paste base64 screenshots into chat.
|
|
105
|
+
- Do not use full-page screenshots as the default for every small UI question.
|
|
106
|
+
- Do not keep re-capturing without a concrete hypothesis or a code change.
|
|
107
|
+
- Do not install, authenticate, or change `agentshot` configuration unless the user explicitly asks.
|
|
99
108
|
|
|
100
|
-
|
|
101
|
-
Use `
|
|
102
|
-
```
|
|
109
|
+
Fallback:
|
|
110
|
+
- Use `agent-browser` instead when the task requires complex clicking, filling forms, login/session state, console/network inspection, annotated element refs, or multi-step browser exploration. Use `agentshot --click-if-present` for simple one-click overlay dismissal before a screenshot.
|
package/README.md
CHANGED
|
@@ -22,12 +22,20 @@ Install globally:
|
|
|
22
22
|
npm install -g agentscreenshots
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
The package installs the `agentshot` binary and
|
|
25
|
+
The package installs the `agentshot` binary and checks for an existing local Chrome/Chromium browser during `postinstall`.
|
|
26
|
+
|
|
27
|
+
- If a local browser is found, AgentScreenshots uses it and skips the Playwright Chromium download.
|
|
28
|
+
- If no local browser is found, AgentScreenshots downloads Playwright Chromium automatically.
|
|
29
|
+
|
|
30
|
+
If browser installation is blocked in your environment, install the package with the automatic browser step disabled:
|
|
26
31
|
|
|
27
32
|
```bash
|
|
28
|
-
|
|
33
|
+
AGENTSHOT_SKIP_BROWSER_INSTALL=1 npm install -g agentscreenshots
|
|
34
|
+
agentshot install-browser
|
|
29
35
|
```
|
|
30
36
|
|
|
37
|
+
Run `agentshot install-browser --force` if you want to download Playwright Chromium even when a system browser is already available.
|
|
38
|
+
|
|
31
39
|
## Authenticate
|
|
32
40
|
|
|
33
41
|
Create a free or paid license in the AgentScreenshots dashboard, then save it locally:
|
|
@@ -59,6 +67,12 @@ Basic full-page capture:
|
|
|
59
67
|
agentshot "http://127.0.0.1:5173" ".agents/screenshots/home.png"
|
|
60
68
|
```
|
|
61
69
|
|
|
70
|
+
Fast development capture in the OS temp directory:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
agentshot "http://127.0.0.1:5173" "home.png" --temp
|
|
74
|
+
```
|
|
75
|
+
|
|
62
76
|
Lazy-loaded page:
|
|
63
77
|
|
|
64
78
|
```bash
|
|
@@ -91,13 +105,31 @@ agentshot "http://127.0.0.1:5173" ".agents/screenshots/mobile.png" \
|
|
|
91
105
|
--viewport 390x844 --scroll --wait 1000
|
|
92
106
|
```
|
|
93
107
|
|
|
108
|
+
Dismiss a cookie banner or simple overlay before capture:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
agentshot "https://example.com" ".agents/screenshots/home.png" \
|
|
112
|
+
--click-if-present "button:has-text('Reject all')" --wait 500
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Reveal hover UI before capture:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
agentshot "http://127.0.0.1:5173" ".agents/screenshots/menu-hover.png" \
|
|
119
|
+
--hover ".nav-item" --selector ".nav-region" --padding 24
|
|
120
|
+
```
|
|
121
|
+
|
|
94
122
|
## Commands
|
|
95
123
|
|
|
96
124
|
```bash
|
|
97
125
|
agentshot URL OUTPUT [options]
|
|
126
|
+
agentshot URL --temp [options]
|
|
127
|
+
agentshot URL NAME --temp [options]
|
|
98
128
|
agentshot auth LICENSE_KEY [--api-url URL]
|
|
99
129
|
agentshot status
|
|
100
130
|
agentshot doctor
|
|
131
|
+
agentshot install-browser
|
|
132
|
+
agentshot instructions
|
|
101
133
|
agentshot feedback "MESSAGE" [--kind feedback|bug|idea]
|
|
102
134
|
agentshot logout
|
|
103
135
|
agentshot help
|
|
@@ -114,22 +146,52 @@ Important capture flags:
|
|
|
114
146
|
- `--height PX`: capture from `--from` or page top to a fixed height.
|
|
115
147
|
- `--from PX --to PX`: capture a vertical page slice.
|
|
116
148
|
- `--viewport WIDTHxHEIGHT`: set viewport size.
|
|
149
|
+
- `--device-scale-factor N`: capture higher-density pixels without changing CSS layout. Defaults to `2`; use `1` for smaller files.
|
|
117
150
|
- `--wait-for CSS`: wait for an element before capture.
|
|
151
|
+
- `--click SELECTOR`: click a required Playwright/CSS selector before capture. Searches the main page and child frames. Repeatable.
|
|
152
|
+
- `--click-if-present SELECTOR`: click a Playwright/CSS selector before capture if it appears. Searches the main page and child frames. Repeatable.
|
|
153
|
+
- `--hover SELECTOR`: hover a required Playwright/CSS selector before capture. Searches the main page and child frames. Repeatable.
|
|
154
|
+
- `--hover-if-present SELECTOR`: hover a Playwright/CSS selector before capture if it appears. Searches the main page and child frames. Repeatable.
|
|
118
155
|
- `--wait-until STATE`: `load`, `domcontentloaded`, or `networkidle`.
|
|
156
|
+
- `--temp`: save in the OS temp directory. With a filename hint, for example `pricing.png --temp`, the output is named like `pricing-temp-df2d.png`; without a filename hint, the name is derived from the URL. It does not add timed deletion or cleanup tracking.
|
|
119
157
|
- `--json`: print machine-readable output.
|
|
120
158
|
- `--no-report`: skip usage reporting.
|
|
121
159
|
|
|
122
160
|
Selectors are Playwright locators, so plain CSS, `text=Pricing`, `section:has-text('Pricing')`, and `xpath=...` work.
|
|
123
161
|
|
|
162
|
+
For live production websites, prefer `--wait-until load --wait 1500` over `--wait-until networkidle` unless you know the page settles cleanly. Analytics, chat widgets, and background requests can keep `networkidle` from completing.
|
|
163
|
+
|
|
164
|
+
Screenshots default to `--device-scale-factor 2` for sharper image understanding. Use `--device-scale-factor 1` for large batch or matrix runs, such as dozens or hundreds of screenshots, when reducing disk/CI artifact size or upload time matters more than visual fidelity. Browser/page zoom is not currently a first-class CLI option; use `--viewport` for responsive width checks.
|
|
165
|
+
|
|
166
|
+
For cookie banners and other one-click overlays, use `--click-if-present` before capture. Prefer reject or close actions when available:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
agentshot "https://example.com" ".agents/screenshots/page.png" \
|
|
170
|
+
--click-if-present "button:has-text('Reject all')" \
|
|
171
|
+
--click-if-present "button[aria-label='Close']"
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Use `--click` or `--click-if-present` for click-driven UI such as modals, drawers, accordions, tabs, dropdown menus, and collapsed content. Use `--hover` or `--hover-if-present` for hover-driven UI such as tooltips, hover cards, menu flyouts, hover-revealed controls, and CSS `:hover` states.
|
|
175
|
+
|
|
124
176
|
## Agent Workflow
|
|
125
177
|
|
|
126
178
|
Tell your coding agent:
|
|
127
179
|
|
|
128
|
-
1. Save captures to `.agents/screenshots
|
|
180
|
+
1. Save project captures to `.agents/screenshots/`, unless the user provides another path or asks for `--temp`.
|
|
129
181
|
2. Use `agentshot` after meaningful UI changes.
|
|
130
182
|
3. Prefer selector or slice captures over huge full-page captures when possible.
|
|
131
183
|
4. Open and inspect the saved PNG before judging the UI.
|
|
132
184
|
|
|
185
|
+
Output path best practices:
|
|
186
|
+
|
|
187
|
+
- For project captures, pass an explicit output path so the capture lands exactly where the user or agent expects.
|
|
188
|
+
- For project work, save screenshots inside the active repo, preferably under `.agents/screenshots/`.
|
|
189
|
+
- Use task-specific subfolders for larger runs, such as `.agents/screenshots/qc/`, `.agents/screenshots/mobile/`, or `.agents/screenshots/2026-05-17-pricing/`.
|
|
190
|
+
- Use descriptive filenames that include the page, viewport, section, or state, for example `pricing-mobile.png`, `hero-hover-menu.png`, or `checkout-modal-closed.png`.
|
|
191
|
+
- Add `.agents/screenshots/` to `.gitignore` unless the screenshots are intentionally part of the repo.
|
|
192
|
+
- For fast development captures that do not need versioned screenshot history, use `--temp` when available.
|
|
193
|
+
- If the user provides a specific output path or saving instructions, use that path and those instructions exactly.
|
|
194
|
+
|
|
133
195
|
The package includes [`AGENT-INSTRUCTIONS.md`](./AGENT-INSTRUCTIONS.md), a copy-paste prompt for Claude Code, Codex, Cursor, Windsurf, and OpenCode.
|
|
134
196
|
|
|
135
197
|
## Usage Reporting
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { delimiter, isAbsolute, join } from 'node:path';
|
|
5
|
+
import { execFile, spawn } from 'node:child_process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { chromium } from 'playwright';
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
function hasPathSeparator(value) {
|
|
11
|
+
return value.includes('/') || value.includes('\\');
|
|
12
|
+
}
|
|
13
|
+
function truthyEnv(value) {
|
|
14
|
+
return value === '1' || value === 'true' || value === 'yes';
|
|
15
|
+
}
|
|
16
|
+
function getEnvBrowserCandidate() {
|
|
17
|
+
return process.env.AGENTSHOT_BROWSER_PATH || process.env.AGENTSHOT_CHROME_PATH || null;
|
|
18
|
+
}
|
|
19
|
+
async function canAccess(path) {
|
|
20
|
+
try {
|
|
21
|
+
await access(path, constants.F_OK);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function resolveCommand(command) {
|
|
29
|
+
const resolver = process.platform === 'win32' ? 'where.exe' : 'which';
|
|
30
|
+
try {
|
|
31
|
+
const { stdout } = await execFileAsync(resolver, [command]);
|
|
32
|
+
return stdout
|
|
33
|
+
.split(/\r?\n/)
|
|
34
|
+
.map((line) => line.trim())
|
|
35
|
+
.find(Boolean) ?? null;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function getKnownBrowserPaths() {
|
|
42
|
+
if (process.platform === 'darwin') {
|
|
43
|
+
return [
|
|
44
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
45
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
46
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
47
|
+
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser'
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
if (process.platform === 'win32') {
|
|
51
|
+
const prefixes = [
|
|
52
|
+
process.env.LOCALAPPDATA,
|
|
53
|
+
process.env.PROGRAMFILES,
|
|
54
|
+
process.env['PROGRAMFILES(X86)']
|
|
55
|
+
].filter((value) => Boolean(value));
|
|
56
|
+
return prefixes.flatMap((prefix) => [
|
|
57
|
+
join(prefix, 'Google/Chrome/Application/chrome.exe'),
|
|
58
|
+
join(prefix, 'Chromium/Application/chrome.exe'),
|
|
59
|
+
join(prefix, 'Microsoft/Edge/Application/msedge.exe'),
|
|
60
|
+
join(prefix, 'BraveSoftware/Brave-Browser/Application/brave.exe')
|
|
61
|
+
]);
|
|
62
|
+
}
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
function getPathBrowserCommands() {
|
|
66
|
+
if (process.platform === 'win32') {
|
|
67
|
+
return ['chrome.exe', 'msedge.exe', 'brave.exe'];
|
|
68
|
+
}
|
|
69
|
+
return [
|
|
70
|
+
'google-chrome-stable',
|
|
71
|
+
'google-chrome',
|
|
72
|
+
'chrome',
|
|
73
|
+
'chromium',
|
|
74
|
+
'chromium-browser',
|
|
75
|
+
'microsoft-edge-stable',
|
|
76
|
+
'microsoft-edge',
|
|
77
|
+
'brave-browser',
|
|
78
|
+
'brave'
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
async function resolveBrowserCandidate(candidate) {
|
|
82
|
+
if (isAbsolute(candidate) || hasPathSeparator(candidate)) {
|
|
83
|
+
return (await canAccess(candidate)) ? candidate : null;
|
|
84
|
+
}
|
|
85
|
+
return resolveCommand(candidate);
|
|
86
|
+
}
|
|
87
|
+
function labelForPath(path) {
|
|
88
|
+
const parts = path.split(/[\\/]/);
|
|
89
|
+
return parts.at(-1) || path;
|
|
90
|
+
}
|
|
91
|
+
export async function findSystemBrowser() {
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
const envCandidate = getEnvBrowserCandidate();
|
|
94
|
+
if (envCandidate) {
|
|
95
|
+
const executablePath = await resolveBrowserCandidate(envCandidate);
|
|
96
|
+
if (executablePath) {
|
|
97
|
+
return {
|
|
98
|
+
executablePath,
|
|
99
|
+
label: 'configured browser',
|
|
100
|
+
source: 'env'
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for (const command of getPathBrowserCommands()) {
|
|
105
|
+
const executablePath = await resolveBrowserCandidate(command);
|
|
106
|
+
if (executablePath && !seen.has(executablePath)) {
|
|
107
|
+
seen.add(executablePath);
|
|
108
|
+
return {
|
|
109
|
+
executablePath,
|
|
110
|
+
label: command,
|
|
111
|
+
source: 'path'
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const path of getKnownBrowserPaths()) {
|
|
116
|
+
if (seen.has(path)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (await canAccess(path)) {
|
|
120
|
+
return {
|
|
121
|
+
executablePath: path,
|
|
122
|
+
label: labelForPath(path),
|
|
123
|
+
source: 'known-path'
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
export function shouldSkipBrowserInstall() {
|
|
130
|
+
return truthyEnv(process.env.AGENTSHOT_SKIP_BROWSER_INSTALL);
|
|
131
|
+
}
|
|
132
|
+
export async function installPlaywrightChromium() {
|
|
133
|
+
const playwrightCliPath = require.resolve('playwright/cli');
|
|
134
|
+
await new Promise((resolve, reject) => {
|
|
135
|
+
const child = spawn(process.execPath, [playwrightCliPath, 'install', 'chromium'], {
|
|
136
|
+
stdio: 'inherit',
|
|
137
|
+
env: {
|
|
138
|
+
...process.env,
|
|
139
|
+
PATH: process.env.PATH?.split(delimiter).join(delimiter)
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
child.on('error', reject);
|
|
143
|
+
child.on('exit', (code) => {
|
|
144
|
+
if (code === 0) {
|
|
145
|
+
resolve();
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
reject(new Error(`playwright install chromium exited with code ${code ?? 'unknown'}.`));
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
export async function ensureBrowserInstalled(options = {}) {
|
|
154
|
+
if (options.respectSkipEnv && shouldSkipBrowserInstall()) {
|
|
155
|
+
return {
|
|
156
|
+
status: 'skipped',
|
|
157
|
+
reason: 'env_skip',
|
|
158
|
+
message: 'Skipped browser install because AGENTSHOT_SKIP_BROWSER_INSTALL is set. Run `agentshot install-browser` later if needed.'
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (!options.force) {
|
|
162
|
+
const systemBrowser = await findSystemBrowser();
|
|
163
|
+
if (systemBrowser) {
|
|
164
|
+
return {
|
|
165
|
+
status: 'skipped',
|
|
166
|
+
reason: 'system_browser_found',
|
|
167
|
+
executablePath: systemBrowser.executablePath,
|
|
168
|
+
message: `Found local browser (${systemBrowser.label}) at ${systemBrowser.executablePath}; skipping Playwright Chromium download.`
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
await installPlaywrightChromium();
|
|
173
|
+
return {
|
|
174
|
+
status: 'installed',
|
|
175
|
+
message: 'Playwright Chromium installed.'
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function browserLaunchError(error) {
|
|
179
|
+
const original = error instanceof Error ? error.message : String(error);
|
|
180
|
+
return new Error(`Could not launch a local browser. Run \`agentshot doctor\` for details or \`agentshot install-browser\` to download Playwright Chromium.\n\nOriginal error: ${original}`);
|
|
181
|
+
}
|
|
182
|
+
export async function launchAgentshotBrowser(options) {
|
|
183
|
+
if (options.browser === 'chrome') {
|
|
184
|
+
try {
|
|
185
|
+
return {
|
|
186
|
+
browser: await chromium.launch({
|
|
187
|
+
channel: 'chrome',
|
|
188
|
+
headless: !options.headed
|
|
189
|
+
}),
|
|
190
|
+
source: 'Chrome channel'
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
throw browserLaunchError(error);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const systemBrowser = await findSystemBrowser();
|
|
198
|
+
const errors = [];
|
|
199
|
+
if (systemBrowser) {
|
|
200
|
+
try {
|
|
201
|
+
return {
|
|
202
|
+
browser: await chromium.launch({
|
|
203
|
+
executablePath: systemBrowser.executablePath,
|
|
204
|
+
headless: !options.headed
|
|
205
|
+
}),
|
|
206
|
+
source: `${systemBrowser.label} at ${systemBrowser.executablePath}`
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
return {
|
|
215
|
+
browser: await chromium.launch({
|
|
216
|
+
headless: !options.headed
|
|
217
|
+
}),
|
|
218
|
+
source: 'Playwright Chromium'
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
223
|
+
throw browserLaunchError(errors.join('\n\n'));
|
|
224
|
+
}
|
|
225
|
+
}
|
package/dist/capture.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { mkdir } from 'node:fs/promises';
|
|
2
2
|
import { dirname, extname, resolve } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { launchAgentshotBrowser } from './browser.js';
|
|
4
4
|
import { getFileSize, getTargetKind, reportCheck } from './reporting.js';
|
|
5
|
-
const PACKAGE_VERSION = '0.
|
|
5
|
+
const PACKAGE_VERSION = '0.2.0';
|
|
6
6
|
function sleep(ms) {
|
|
7
7
|
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
8
8
|
}
|
|
@@ -17,6 +17,45 @@ function getFormat(output) {
|
|
|
17
17
|
const extension = extname(output).toLowerCase();
|
|
18
18
|
return extension === '.jpg' || extension === '.jpeg' ? 'jpeg' : 'png';
|
|
19
19
|
}
|
|
20
|
+
function getPngDimensions(image) {
|
|
21
|
+
if (image.length < 24 || image.toString('ascii', 1, 4) !== 'PNG') {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
width: image.readUInt32BE(16),
|
|
26
|
+
height: image.readUInt32BE(20)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function getJpegDimensions(image) {
|
|
30
|
+
let offset = 2;
|
|
31
|
+
while (offset < image.length - 9) {
|
|
32
|
+
if (image[offset] !== 0xff) {
|
|
33
|
+
offset += 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const marker = image[offset + 1];
|
|
37
|
+
offset += 2;
|
|
38
|
+
if (marker === 0xd8 || marker === 0xd9 || (marker >= 0xd0 && marker <= 0xd7)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const segmentLength = image.readUInt16BE(offset);
|
|
42
|
+
const isStartOfFrame = (marker >= 0xc0 && marker <= 0xc3) ||
|
|
43
|
+
(marker >= 0xc5 && marker <= 0xc7) ||
|
|
44
|
+
(marker >= 0xc9 && marker <= 0xcb) ||
|
|
45
|
+
(marker >= 0xcd && marker <= 0xcf);
|
|
46
|
+
if (isStartOfFrame) {
|
|
47
|
+
return {
|
|
48
|
+
height: image.readUInt16BE(offset + 3),
|
|
49
|
+
width: image.readUInt16BE(offset + 5)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
offset += segmentLength;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function getImageDimensions(image, format) {
|
|
57
|
+
return format === 'png' ? getPngDimensions(image) : getJpegDimensions(image);
|
|
58
|
+
}
|
|
20
59
|
async function getPageSize(page) {
|
|
21
60
|
return page.evaluate(() => {
|
|
22
61
|
const root = document.scrollingElement || document.documentElement;
|
|
@@ -49,6 +88,48 @@ async function triggerLazyLoadedContent(page) {
|
|
|
49
88
|
await sleepInPage(250);
|
|
50
89
|
});
|
|
51
90
|
}
|
|
91
|
+
async function findVisibleLocatorInFrames(page, selector, timeoutMs) {
|
|
92
|
+
const deadline = Date.now() + timeoutMs;
|
|
93
|
+
let lastError = null;
|
|
94
|
+
while (Date.now() <= deadline) {
|
|
95
|
+
for (const frame of page.frames()) {
|
|
96
|
+
const locator = frame.locator(selector).first();
|
|
97
|
+
try {
|
|
98
|
+
if (await locator.isVisible({ timeout: 100 })) {
|
|
99
|
+
return locator;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
lastError = error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
await sleep(100);
|
|
107
|
+
}
|
|
108
|
+
if (lastError instanceof Error) {
|
|
109
|
+
throw lastError;
|
|
110
|
+
}
|
|
111
|
+
throw new Error(`Timed out waiting for visible selector before click: ${selector}`);
|
|
112
|
+
}
|
|
113
|
+
async function runPreCaptureActions(page, actions, timeoutMs) {
|
|
114
|
+
for (const action of actions) {
|
|
115
|
+
const timeout = action.optional ? Math.min(timeoutMs, 2500) : timeoutMs;
|
|
116
|
+
try {
|
|
117
|
+
const locator = await findVisibleLocatorInFrames(page, action.selector, timeout);
|
|
118
|
+
if (action.type === 'click') {
|
|
119
|
+
await locator.click({ timeout });
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
await locator.hover({ timeout });
|
|
123
|
+
}
|
|
124
|
+
await sleep(250);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (!action.optional) {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
52
133
|
async function computeSelectorClip(page, selector, index, padding, timeoutMs) {
|
|
53
134
|
const locator = page.locator(selector).nth(index);
|
|
54
135
|
await locator.waitFor({ state: 'visible', timeout: timeoutMs });
|
|
@@ -83,12 +164,11 @@ function computeVerticalClip(options, pageWidth, pageHeight) {
|
|
|
83
164
|
return clampClip({ x: 0, y, width: pageWidth, height }, pageWidth, pageHeight);
|
|
84
165
|
}
|
|
85
166
|
async function launchBrowser(options) {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
executablePath,
|
|
90
|
-
headless: !options.headed
|
|
167
|
+
const result = await launchAgentshotBrowser({
|
|
168
|
+
browser: options.browser,
|
|
169
|
+
headed: options.headed
|
|
91
170
|
});
|
|
171
|
+
return result.browser;
|
|
92
172
|
}
|
|
93
173
|
export async function capture(options) {
|
|
94
174
|
const startedAt = Date.now();
|
|
@@ -114,6 +194,7 @@ export async function capture(options) {
|
|
|
114
194
|
timeout: options.timeoutMs
|
|
115
195
|
});
|
|
116
196
|
}
|
|
197
|
+
await runPreCaptureActions(page, options.actions, options.timeoutMs);
|
|
117
198
|
if (options.scroll) {
|
|
118
199
|
await triggerLazyLoadedContent(page);
|
|
119
200
|
}
|
|
@@ -124,11 +205,12 @@ export async function capture(options) {
|
|
|
124
205
|
const format = getFormat(output);
|
|
125
206
|
let screenshotWidth = pageSize.width;
|
|
126
207
|
let screenshotHeight = pageSize.height;
|
|
208
|
+
let screenshotBuffer = null;
|
|
127
209
|
let mode = 'full-page';
|
|
128
210
|
if (options.selector) {
|
|
129
211
|
const selectorClip = await computeSelectorClip(page, options.selector, options.selectorIndex, options.padding, options.timeoutMs);
|
|
130
212
|
const clip = clampClip(selectorClip, pageSize.width, pageSize.height);
|
|
131
|
-
await page.screenshot({ path: output, fullPage: true, clip, type: format });
|
|
213
|
+
screenshotBuffer = await page.screenshot({ path: output, fullPage: true, clip, type: format });
|
|
132
214
|
screenshotWidth = clip.width;
|
|
133
215
|
screenshotHeight = clip.height;
|
|
134
216
|
mode = 'selector';
|
|
@@ -136,17 +218,22 @@ export async function capture(options) {
|
|
|
136
218
|
else {
|
|
137
219
|
const verticalClip = computeVerticalClip(options, pageSize.width, pageSize.height);
|
|
138
220
|
if (verticalClip) {
|
|
139
|
-
await page.screenshot({ path: output, fullPage: true, clip: verticalClip, type: format });
|
|
221
|
+
screenshotBuffer = await page.screenshot({ path: output, fullPage: true, clip: verticalClip, type: format });
|
|
140
222
|
screenshotWidth = verticalClip.width;
|
|
141
223
|
screenshotHeight = verticalClip.height;
|
|
142
224
|
mode = 'clip';
|
|
143
225
|
}
|
|
144
226
|
else {
|
|
145
|
-
await page.screenshot({ path: output, fullPage: options.fullPage, type: format });
|
|
227
|
+
screenshotBuffer = await page.screenshot({ path: output, fullPage: options.fullPage, type: format });
|
|
146
228
|
screenshotWidth = options.fullPage ? pageSize.width : options.width;
|
|
147
229
|
screenshotHeight = options.fullPage ? pageSize.height : options.viewportHeight;
|
|
148
230
|
}
|
|
149
231
|
}
|
|
232
|
+
const imageDimensions = screenshotBuffer ? getImageDimensions(screenshotBuffer, format) : null;
|
|
233
|
+
if (imageDimensions) {
|
|
234
|
+
screenshotWidth = imageDimensions.width;
|
|
235
|
+
screenshotHeight = imageDimensions.height;
|
|
236
|
+
}
|
|
150
237
|
const durationMs = Date.now() - startedAt;
|
|
151
238
|
const fileSizeBytes = await getFileSize(output);
|
|
152
239
|
let reported = false;
|
|
@@ -175,6 +262,7 @@ export async function capture(options) {
|
|
|
175
262
|
return {
|
|
176
263
|
output,
|
|
177
264
|
url: options.url,
|
|
265
|
+
temporary: options.temporary,
|
|
178
266
|
width: screenshotWidth,
|
|
179
267
|
height: screenshotHeight,
|
|
180
268
|
fileSizeBytes,
|
package/dist/doctor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { findSystemBrowser, launchAgentshotBrowser } from './browser.js';
|
|
5
5
|
import { getConfigPath, readConfig, resolveApiUrl, resolveLicenseKey } from './config.js';
|
|
6
6
|
import { validateLicense } from './reporting.js';
|
|
7
7
|
function getOverallStatus(checks) {
|
|
@@ -32,14 +32,18 @@ async function checkWritableTempFile() {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
async function checkBrowser() {
|
|
35
|
-
const
|
|
35
|
+
const launch = await launchAgentshotBrowser({
|
|
36
|
+
browser: 'chromium',
|
|
37
|
+
headed: false
|
|
38
|
+
});
|
|
39
|
+
const { browser } = launch;
|
|
36
40
|
try {
|
|
37
41
|
const page = await browser.newPage();
|
|
38
42
|
await page.setContent('<!doctype html><title>agentshot doctor</title><p>ok</p>');
|
|
39
43
|
return {
|
|
40
44
|
name: 'browser',
|
|
41
45
|
status: 'ok',
|
|
42
|
-
message:
|
|
46
|
+
message: `Local browser launches successfully (${launch.source}).`
|
|
43
47
|
};
|
|
44
48
|
}
|
|
45
49
|
finally {
|
|
@@ -93,6 +97,14 @@ export async function runDoctor(options) {
|
|
|
93
97
|
: 'No license key configured. Run `agentshot auth ags_live_...` when ready.'
|
|
94
98
|
});
|
|
95
99
|
try {
|
|
100
|
+
const systemBrowser = await findSystemBrowser();
|
|
101
|
+
checks.push({
|
|
102
|
+
name: 'browser-detection',
|
|
103
|
+
status: systemBrowser ? 'ok' : 'warn',
|
|
104
|
+
message: systemBrowser
|
|
105
|
+
? `Found local browser at ${systemBrowser.executablePath}.`
|
|
106
|
+
: 'No system Chrome/Chromium browser found. Playwright Chromium will be used if installed.'
|
|
107
|
+
});
|
|
96
108
|
checks.push(await checkBrowser());
|
|
97
109
|
}
|
|
98
110
|
catch (error) {
|
|
@@ -101,7 +113,7 @@ export async function runDoctor(options) {
|
|
|
101
113
|
status: 'fail',
|
|
102
114
|
message: error instanceof Error
|
|
103
115
|
? error.message
|
|
104
|
-
: '
|
|
116
|
+
: 'No local browser could launch. Run `agentshot install-browser` to download Playwright Chromium.'
|
|
105
117
|
});
|
|
106
118
|
}
|
|
107
119
|
try {
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
3
5
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { dirname, join } from 'node:path';
|
|
6
|
+
import { basename, dirname, extname, join } from 'node:path';
|
|
7
|
+
import { ensureBrowserInstalled } from './browser.js';
|
|
5
8
|
import { capture } from './capture.js';
|
|
6
9
|
import { clearLicenseKey, readConfig, writeConfig } from './config.js';
|
|
7
10
|
import { runDoctor } from './doctor.js';
|
|
@@ -12,14 +15,20 @@ function printHelp() {
|
|
|
12
15
|
|
|
13
16
|
Usage:
|
|
14
17
|
agentshot URL OUTPUT [options]
|
|
18
|
+
agentshot URL --temp [options]
|
|
19
|
+
agentshot URL NAME --temp [options]
|
|
15
20
|
agentshot auth LICENSE_KEY [--api-url URL] [--json]
|
|
16
21
|
agentshot status [--api-url URL] [--license-key KEY]
|
|
17
22
|
agentshot doctor [--api-url URL] [--license-key KEY]
|
|
23
|
+
agentshot install-browser [--force] [--json]
|
|
24
|
+
agentshot instructions
|
|
18
25
|
agentshot feedback "MESSAGE" [--kind feedback|bug|idea]
|
|
19
26
|
agentshot logout
|
|
20
27
|
|
|
21
28
|
Examples:
|
|
22
29
|
agentshot "http://127.0.0.1:5200/design" "./shots/design.png"
|
|
30
|
+
agentshot "http://127.0.0.1:5200/design" --temp
|
|
31
|
+
agentshot "http://127.0.0.1:5200/design" "hero.png" --temp
|
|
23
32
|
agentshot "http://127.0.0.1:5200/design" "./shots/hero.png" --height 1200
|
|
24
33
|
agentshot "http://127.0.0.1:5200/design" "./shots/cards.png" --from 1800 --to 2500
|
|
25
34
|
agentshot "http://127.0.0.1:5200/design" "./shots/borders.png" --selector "section:has-text('Borders')" --padding 24
|
|
@@ -39,12 +48,19 @@ Capture options:
|
|
|
39
48
|
--viewport-height PX Browser viewport height (default: 900)
|
|
40
49
|
--viewport WIDTHxHEIGHT Set viewport width and height together
|
|
41
50
|
--wait-for CSS Wait for a selector before capture
|
|
51
|
+
--click SELECTOR Click a required selector before capture; repeatable
|
|
52
|
+
--click-if-present SELECTOR
|
|
53
|
+
Try to click a selector before capture; repeatable
|
|
54
|
+
--hover SELECTOR Hover a required selector before capture; repeatable
|
|
55
|
+
--hover-if-present SELECTOR
|
|
56
|
+
Try to hover a selector before capture; repeatable
|
|
42
57
|
--wait-until STATE load, domcontentloaded, or networkidle (default: load)
|
|
43
58
|
--timeout MS Navigation/action timeout (default: 30000)
|
|
44
|
-
--browser NAME chromium or chrome
|
|
59
|
+
--browser NAME chromium auto-detects local Chrome/Chromium, or use chrome channel
|
|
45
60
|
--headed Show the browser window
|
|
46
61
|
--no-full-page Capture only viewport when no selector/crop is used
|
|
47
|
-
--device-scale-factor N Device scale factor (default:
|
|
62
|
+
--device-scale-factor N Device scale factor (default: 2)
|
|
63
|
+
--temp Save to OS temp dir; optional OUTPUT becomes filename hint
|
|
48
64
|
--json Print machine-readable JSON
|
|
49
65
|
--no-report Do not report a visual check to AgentScreenshots
|
|
50
66
|
--api-url URL Override AgentScreenshots API URL
|
|
@@ -54,11 +70,13 @@ Environment:
|
|
|
54
70
|
AGENTSHOT_API_URL
|
|
55
71
|
AGENTSHOT_LICENSE_KEY
|
|
56
72
|
AGENTSHOT_CONFIG
|
|
73
|
+
AGENTSHOT_BROWSER_PATH
|
|
74
|
+
AGENTSHOT_SKIP_BROWSER_INSTALL=1
|
|
57
75
|
`);
|
|
58
76
|
}
|
|
59
77
|
function parsePositiveNumber(value, name) {
|
|
60
78
|
const parsed = Number(value);
|
|
61
|
-
if (!Number.isFinite(parsed) || parsed
|
|
79
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
62
80
|
throw new Error(`${name} must be a positive number.`);
|
|
63
81
|
}
|
|
64
82
|
return parsed;
|
|
@@ -81,12 +99,14 @@ function getDefaultCaptureOptions(url, output) {
|
|
|
81
99
|
return {
|
|
82
100
|
url,
|
|
83
101
|
output,
|
|
102
|
+
temporary: false,
|
|
84
103
|
width: 1280,
|
|
85
104
|
viewportHeight: 900,
|
|
86
|
-
deviceScaleFactor:
|
|
105
|
+
deviceScaleFactor: 2,
|
|
87
106
|
waitMs: 0,
|
|
88
107
|
timeoutMs: 30_000,
|
|
89
108
|
scroll: false,
|
|
109
|
+
actions: [],
|
|
90
110
|
selector: null,
|
|
91
111
|
selectorIndex: 0,
|
|
92
112
|
padding: 0,
|
|
@@ -104,6 +124,40 @@ function getDefaultCaptureOptions(url, output) {
|
|
|
104
124
|
licenseKey: null
|
|
105
125
|
};
|
|
106
126
|
}
|
|
127
|
+
function slugifyFilename(value) {
|
|
128
|
+
return (value
|
|
129
|
+
.toLowerCase()
|
|
130
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
131
|
+
.replace(/^-+|-+$/g, '')
|
|
132
|
+
.slice(0, 64) || 'capture');
|
|
133
|
+
}
|
|
134
|
+
function getTempCaptureOutput(url, nameHint = null) {
|
|
135
|
+
const id = randomUUID().replace(/-/g, '').slice(0, 4);
|
|
136
|
+
if (nameHint) {
|
|
137
|
+
const name = basename(nameHint);
|
|
138
|
+
const extension = extname(name).toLowerCase();
|
|
139
|
+
const outputExtension = extension === '.jpg' || extension === '.jpeg' ? extension : '.png';
|
|
140
|
+
const stem = extension ? name.slice(0, -extension.length) : name;
|
|
141
|
+
return join(tmpdir(), 'agentshot', `${slugifyFilename(stem)}-temp-${id}${outputExtension}`);
|
|
142
|
+
}
|
|
143
|
+
let source = 'capture';
|
|
144
|
+
try {
|
|
145
|
+
const parsed = new URL(url);
|
|
146
|
+
if (parsed.protocol === 'data:') {
|
|
147
|
+
source = 'data-url';
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const host = parsed.hostname || parsed.protocol.replace(':', '') || 'capture';
|
|
151
|
+
const path = parsed.pathname === '/' ? '' : parsed.pathname;
|
|
152
|
+
source = `${host}${path}`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
source = url;
|
|
157
|
+
}
|
|
158
|
+
const slug = slugifyFilename(source);
|
|
159
|
+
return join(tmpdir(), 'agentshot', `${slug}-temp-${id}.png`);
|
|
160
|
+
}
|
|
107
161
|
function parseCommonAuthOptions(args, startIndex = 0) {
|
|
108
162
|
let apiUrl = null;
|
|
109
163
|
let licenseKey = null;
|
|
@@ -186,12 +240,35 @@ function parseLogout(args) {
|
|
|
186
240
|
}
|
|
187
241
|
return { name: 'logout', json };
|
|
188
242
|
}
|
|
243
|
+
function parseInstallBrowser(args) {
|
|
244
|
+
let force = false;
|
|
245
|
+
let json = false;
|
|
246
|
+
for (const arg of args) {
|
|
247
|
+
if (arg === '--force') {
|
|
248
|
+
force = true;
|
|
249
|
+
}
|
|
250
|
+
else if (arg === '--json') {
|
|
251
|
+
json = true;
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return { name: 'install-browser', force, json };
|
|
258
|
+
}
|
|
189
259
|
function parseCapture(args) {
|
|
190
|
-
if (args.length <
|
|
191
|
-
throw new Error('Capture requires URL and OUTPUT.');
|
|
260
|
+
if (args.length < 1) {
|
|
261
|
+
throw new Error('Capture requires URL and OUTPUT, or URL with --temp.');
|
|
262
|
+
}
|
|
263
|
+
const [url, maybeOutput, ...maybeRest] = args;
|
|
264
|
+
const output = maybeOutput && !maybeOutput.startsWith('--') ? maybeOutput : null;
|
|
265
|
+
const rest = output ? maybeRest : args.slice(1);
|
|
266
|
+
const temporary = rest.includes('--temp');
|
|
267
|
+
if (!output && !temporary) {
|
|
268
|
+
throw new Error('Capture requires OUTPUT unless --temp is used.');
|
|
192
269
|
}
|
|
193
|
-
const
|
|
194
|
-
|
|
270
|
+
const options = getDefaultCaptureOptions(url, temporary ? getTempCaptureOutput(url, output) : output);
|
|
271
|
+
options.temporary = temporary;
|
|
195
272
|
for (let index = 0; index < rest.length; index += 1) {
|
|
196
273
|
const arg = rest[index];
|
|
197
274
|
switch (arg) {
|
|
@@ -250,6 +327,38 @@ function parseCapture(args) {
|
|
|
250
327
|
options.waitForSelector = readValue(rest, index, arg);
|
|
251
328
|
index += 1;
|
|
252
329
|
break;
|
|
330
|
+
case '--click':
|
|
331
|
+
options.actions.push({
|
|
332
|
+
type: 'click',
|
|
333
|
+
selector: readValue(rest, index, arg),
|
|
334
|
+
optional: false
|
|
335
|
+
});
|
|
336
|
+
index += 1;
|
|
337
|
+
break;
|
|
338
|
+
case '--click-if-present':
|
|
339
|
+
options.actions.push({
|
|
340
|
+
type: 'click',
|
|
341
|
+
selector: readValue(rest, index, arg),
|
|
342
|
+
optional: true
|
|
343
|
+
});
|
|
344
|
+
index += 1;
|
|
345
|
+
break;
|
|
346
|
+
case '--hover':
|
|
347
|
+
options.actions.push({
|
|
348
|
+
type: 'hover',
|
|
349
|
+
selector: readValue(rest, index, arg),
|
|
350
|
+
optional: false
|
|
351
|
+
});
|
|
352
|
+
index += 1;
|
|
353
|
+
break;
|
|
354
|
+
case '--hover-if-present':
|
|
355
|
+
options.actions.push({
|
|
356
|
+
type: 'hover',
|
|
357
|
+
selector: readValue(rest, index, arg),
|
|
358
|
+
optional: true
|
|
359
|
+
});
|
|
360
|
+
index += 1;
|
|
361
|
+
break;
|
|
253
362
|
case '--wait-until': {
|
|
254
363
|
const value = readValue(rest, index, arg);
|
|
255
364
|
if (value !== 'load' && value !== 'domcontentloaded' && value !== 'networkidle') {
|
|
@@ -282,6 +391,8 @@ function parseCapture(args) {
|
|
|
282
391
|
options.deviceScaleFactor = parsePositiveNumber(readValue(rest, index, arg), arg);
|
|
283
392
|
index += 1;
|
|
284
393
|
break;
|
|
394
|
+
case '--temp':
|
|
395
|
+
break;
|
|
285
396
|
case '--json':
|
|
286
397
|
options.json = true;
|
|
287
398
|
break;
|
|
@@ -310,6 +421,12 @@ function parseArgs(args) {
|
|
|
310
421
|
if (first === '--version' || first === '-v' || first === 'version') {
|
|
311
422
|
return { name: 'version' };
|
|
312
423
|
}
|
|
424
|
+
if (first === 'instructions') {
|
|
425
|
+
if (args.length > 1) {
|
|
426
|
+
throw new Error('Usage: agentshot instructions');
|
|
427
|
+
}
|
|
428
|
+
return { name: 'instructions' };
|
|
429
|
+
}
|
|
313
430
|
if (first === 'auth') {
|
|
314
431
|
const key = args[1];
|
|
315
432
|
if (!key || key.startsWith('--')) {
|
|
@@ -324,6 +441,9 @@ function parseArgs(args) {
|
|
|
324
441
|
if (first === 'doctor') {
|
|
325
442
|
return { name: 'doctor', ...parseCommonAuthOptions(args, 1) };
|
|
326
443
|
}
|
|
444
|
+
if (first === 'install-browser') {
|
|
445
|
+
return parseInstallBrowser(args.slice(1));
|
|
446
|
+
}
|
|
327
447
|
if (first === 'feedback') {
|
|
328
448
|
return parseFeedback(args.slice(1));
|
|
329
449
|
}
|
|
@@ -343,6 +463,11 @@ async function readPackageVersion() {
|
|
|
343
463
|
return '0.1.0';
|
|
344
464
|
}
|
|
345
465
|
}
|
|
466
|
+
async function readAgentInstructions() {
|
|
467
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
468
|
+
const instructionsPath = join(dirname(currentFile), '..', 'AGENT-INSTRUCTIONS.md');
|
|
469
|
+
return readFile(instructionsPath, 'utf8');
|
|
470
|
+
}
|
|
346
471
|
async function run() {
|
|
347
472
|
const command = parseArgs(process.argv.slice(2));
|
|
348
473
|
if (command.name === 'help') {
|
|
@@ -353,6 +478,10 @@ async function run() {
|
|
|
353
478
|
console.log(await readPackageVersion());
|
|
354
479
|
return;
|
|
355
480
|
}
|
|
481
|
+
if (command.name === 'instructions') {
|
|
482
|
+
console.log((await readAgentInstructions()).trim());
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
356
485
|
if (command.name === 'auth') {
|
|
357
486
|
const config = await readConfig();
|
|
358
487
|
const apiUrl = command.apiUrl ?? config.apiUrl;
|
|
@@ -463,6 +592,19 @@ async function run() {
|
|
|
463
592
|
}
|
|
464
593
|
return;
|
|
465
594
|
}
|
|
595
|
+
if (command.name === 'install-browser') {
|
|
596
|
+
const result = await ensureBrowserInstalled({ force: command.force });
|
|
597
|
+
if (command.json) {
|
|
598
|
+
console.log(JSON.stringify(result, null, 2));
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
console.log(result.message);
|
|
602
|
+
if (result.status === 'skipped' && result.reason === 'system_browser_found') {
|
|
603
|
+
console.log('Use `agentshot install-browser --force` to download Playwright Chromium anyway.');
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
466
608
|
if (command.name === 'feedback') {
|
|
467
609
|
const result = await sendFeedback({
|
|
468
610
|
apiUrl: command.apiUrl,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ensureBrowserInstalled } from './browser.js';
|
|
2
|
+
async function run() {
|
|
3
|
+
const result = await ensureBrowserInstalled({ respectSkipEnv: true });
|
|
4
|
+
console.log(`[agentshot] ${result.message}`);
|
|
5
|
+
}
|
|
6
|
+
run().catch((error) => {
|
|
7
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8
|
+
console.warn(`[agentshot] Browser install did not complete: ${message}`);
|
|
9
|
+
console.warn('[agentshot] Install finished anyway. Run `agentshot install-browser` or `agentshot doctor` before your first capture.');
|
|
10
|
+
});
|
package/package.json
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentscreenshots",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Local-first website screenshots for AI coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://agentscreenshots.com",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/ArchMiha/agentscreenshots.git",
|
|
10
|
-
"directory": "cli"
|
|
11
|
-
},
|
|
12
|
-
"bugs": {
|
|
13
|
-
"url": "https://github.com/ArchMiha/agentscreenshots/issues"
|
|
14
|
-
},
|
|
15
7
|
"bin": {
|
|
16
8
|
"agentshot": "dist/index.js"
|
|
17
9
|
},
|
|
@@ -25,7 +17,7 @@
|
|
|
25
17
|
"build": "tsc -p tsconfig.json",
|
|
26
18
|
"check": "tsc -p tsconfig.json --noEmit",
|
|
27
19
|
"dev": "tsx src/index.ts",
|
|
28
|
-
"postinstall": "
|
|
20
|
+
"postinstall": "node dist/postinstall.js",
|
|
29
21
|
"prepublishOnly": "npm run check && npm run build"
|
|
30
22
|
},
|
|
31
23
|
"keywords": [
|