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.
@@ -1,102 +1,110 @@
1
- # AgentScreenshots Agent Prompt
1
+ # AgentScreenshots CLI agent instructions
2
2
 
3
- Copy this into your coding agent's project instructions, custom instructions, skill, or rules file.
3
+ Use `agentshot` for rendered webpage screenshots and visual UI checks.
4
4
 
5
- ```text
6
- You have access to the `agentshot` CLI for visual UI checks.
7
-
8
- Use it whenever you need to inspect a rendered webpage, verify frontend work, debug layout/CSS issues, check responsive behavior, or confirm that a visual change actually looks correct in the browser.
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
- 1. Identify the page URL to inspect.
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
- Selector notes:
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
- When to use `--scroll`:
70
-
71
- - Use it for full-page screenshots.
72
- - Use it when images, animations, lazy-loaded sections, or scroll-triggered content may not render until the page is scrolled.
73
- - You usually do not need it for a small selector capture unless that section is lazy-loaded.
74
-
75
- When to use `--wait`:
76
-
77
- - Use `--wait 500` for normal UI pages.
78
- - Use `--wait 1000` or `--wait 2000` for pages with animations, remote images, or client-side data loading.
79
- - Use `--wait-for "<selector>"` if a specific element must exist before capture.
80
-
81
- Good commands:
82
-
83
- agentshot "http://127.0.0.1:5173" ".agents/screenshots/home.png" --scroll --wait 1000
84
- agentshot "http://127.0.0.1:5173/pricing" ".agents/screenshots/pricing.png" --selector "main" --padding 16 --wait 500
85
- agentshot "http://127.0.0.1:5173" ".agents/screenshots/mobile-home.png" --viewport 390x844 --scroll --wait 1000
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 take huge full-page screenshots when a selector or slice would answer the question faster.
92
- - Do not keep re-capturing without making a concrete fix or forming a specific visual hypothesis.
93
- - Do not send feedback unless the user asks or the issue is clearly about the `agentshot` tool itself.
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
- ```text
101
- Use `agentshot` for visual UI checks. Capture rendered pages to `.agents/screenshots/`, then inspect the saved PNG before judging the UI. For full pages run `agentshot "<url>" ".agents/screenshots/page.png" --scroll --wait 1000`. For specific sections run `agentshot "<url>" ".agents/screenshots/section.png" --selector "section:has-text('Pricing')" --padding 24 --wait 500`. For mobile run `agentshot "<url>" ".agents/screenshots/mobile.png" --viewport 390x844 --scroll --wait 1000`. Prefer selector or vertical slice captures when possible. If the tool fails, run `agentshot doctor`. Do not paste base64 images into chat.
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 downloads Playwright Chromium during `postinstall`. If browser installation is blocked in your environment, run this manually after install:
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
- npx playwright install chromium
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
@@ -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 { chromium } from 'playwright';
3
+ import { launchAgentshotBrowser } from './browser.js';
4
4
  import { getFileSize, getTargetKind, reportCheck } from './reporting.js';
5
- const PACKAGE_VERSION = '0.1.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 executablePath = options.browser === 'chrome' ? undefined : undefined;
87
- return chromium.launch({
88
- channel: options.browser === 'chrome' ? 'chrome' : undefined,
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 { chromium } from 'playwright';
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 browser = await chromium.launch();
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: 'Playwright Chromium launches successfully.'
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
- : 'Playwright Chromium could not launch.'
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 (default: chromium)
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: 1)
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 < 0) {
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: 1,
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 < 2) {
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 [url, output, ...rest] = args;
194
- const options = getDefaultCaptureOptions(url, output);
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.1.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": "playwright install chromium",
20
+ "postinstall": "node dist/postinstall.js",
29
21
  "prepublishOnly": "npm run check && npm run build"
30
22
  },
31
23
  "keywords": [