agentscreenshots 0.1.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 +102 -0
- package/LICENSE +11 -0
- package/README.md +159 -0
- package/dist/capture.js +191 -0
- package/dist/config.js +52 -0
- package/dist/doctor.js +148 -0
- package/dist/feedback.js +35 -0
- package/dist/index.js +503 -0
- package/dist/reporting.js +169 -0
- package/dist/types.js +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# AgentScreenshots Agent Prompt
|
|
2
|
+
|
|
3
|
+
Copy this into your coding agent's project instructions, custom instructions, skill, or rules file.
|
|
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.
|
|
11
|
+
|
|
12
|
+
Default workflow:
|
|
13
|
+
|
|
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:
|
|
35
|
+
|
|
36
|
+
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
|
+
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
|
+
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
|
+
|
|
52
|
+
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
|
+
agentshot "<url>" ".agents/screenshots/mobile.png" --viewport 390x844 --scroll --wait 1000
|
|
57
|
+
|
|
58
|
+
Use desktop viewport checks when layout width matters:
|
|
59
|
+
|
|
60
|
+
agentshot "<url>" ".agents/screenshots/desktop.png" --viewport 1440x1000 --scroll --wait 1000
|
|
61
|
+
|
|
62
|
+
Selector notes:
|
|
63
|
+
|
|
64
|
+
- Plain CSS selectors work: `.hero`, `#pricing`, `[data-testid='nav']`.
|
|
65
|
+
- Playwright text selectors work: `text=Pricing`.
|
|
66
|
+
- Playwright text filters work: `section:has-text('Pricing')`.
|
|
67
|
+
- XPath works when needed: `xpath=//section[.//h2[contains(., 'Pricing')]]`.
|
|
68
|
+
|
|
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
|
|
86
|
+
|
|
87
|
+
Do not:
|
|
88
|
+
|
|
89
|
+
- Do not paste base64 screenshots into chat.
|
|
90
|
+
- 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:
|
|
99
|
+
|
|
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
|
+
```
|
package/LICENSE
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
AgentScreenshots CLI License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Miha Cacic.
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
The AgentScreenshots CLI is proprietary software. You may install and use the
|
|
7
|
+
package to access the AgentScreenshots service subject to the terms published
|
|
8
|
+
at https://agentscreenshots.com/terms.
|
|
9
|
+
|
|
10
|
+
You may not sell, sublicense, redistribute, host, or modify the package as a
|
|
11
|
+
competing product without written permission.
|
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# AgentScreenshots CLI
|
|
2
|
+
|
|
3
|
+
`agentshot` is a local-first screenshot CLI for AI coding agents. It runs Playwright on your machine, captures a rendered web page, writes a PNG/JPEG to disk, and reports one successful visual check to AgentScreenshots when a license key is configured.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install -g agentscreenshots
|
|
7
|
+
agentshot auth ags_live_xxx
|
|
8
|
+
agentshot "http://127.0.0.1:5173" ".agents/screenshots/home.png" --scroll --wait 1000
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
Requirements:
|
|
14
|
+
|
|
15
|
+
- Node.js 20+
|
|
16
|
+
- macOS, Linux, or Windows/WSL
|
|
17
|
+
- A reachable URL to capture, including localhost URLs
|
|
18
|
+
|
|
19
|
+
Install globally:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g agentscreenshots
|
|
23
|
+
```
|
|
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:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx playwright install chromium
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Authenticate
|
|
32
|
+
|
|
33
|
+
Create a free or paid license in the AgentScreenshots dashboard, then save it locally:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
agentshot auth ags_live_xxx
|
|
37
|
+
agentshot status
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The config file is stored at:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
~/.config/agentshot/config.json
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
You can override config for CI or one-off runs:
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
AGENTSHOT_API_URL
|
|
50
|
+
AGENTSHOT_LICENSE_KEY
|
|
51
|
+
AGENTSHOT_CONFIG
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Capture
|
|
55
|
+
|
|
56
|
+
Basic full-page capture:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
agentshot "http://127.0.0.1:5173" ".agents/screenshots/home.png"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Lazy-loaded page:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
agentshot "http://127.0.0.1:5173" ".agents/screenshots/home.png" --scroll --wait 1000
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Fixed top slice:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
agentshot "http://127.0.0.1:5173" ".agents/screenshots/hero.png" --height 1200
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Vertical slice:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
agentshot "http://127.0.0.1:5173" ".agents/screenshots/slice.png" --from 1600 --to 2400
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Specific section or component:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
agentshot "http://127.0.0.1:5173" ".agents/screenshots/pricing.png" \
|
|
84
|
+
--selector "section:has-text('Pricing')" --padding 24
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Mobile viewport:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
agentshot "http://127.0.0.1:5173" ".agents/screenshots/mobile.png" \
|
|
91
|
+
--viewport 390x844 --scroll --wait 1000
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Commands
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
agentshot URL OUTPUT [options]
|
|
98
|
+
agentshot auth LICENSE_KEY [--api-url URL]
|
|
99
|
+
agentshot status
|
|
100
|
+
agentshot doctor
|
|
101
|
+
agentshot feedback "MESSAGE" [--kind feedback|bug|idea]
|
|
102
|
+
agentshot logout
|
|
103
|
+
agentshot help
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Important capture flags:
|
|
107
|
+
|
|
108
|
+
- `--scroll`: scroll before capture to trigger lazy-loaded content.
|
|
109
|
+
- `--wait MS`: wait after navigation/scroll before capture.
|
|
110
|
+
- `--selector SELECTOR`: capture a Playwright/CSS selector.
|
|
111
|
+
- `--section SELECTOR`: alias for `--selector`.
|
|
112
|
+
- `--nth INDEX`: capture another selector match.
|
|
113
|
+
- `--padding PX`: add padding around selector captures.
|
|
114
|
+
- `--height PX`: capture from `--from` or page top to a fixed height.
|
|
115
|
+
- `--from PX --to PX`: capture a vertical page slice.
|
|
116
|
+
- `--viewport WIDTHxHEIGHT`: set viewport size.
|
|
117
|
+
- `--wait-for CSS`: wait for an element before capture.
|
|
118
|
+
- `--wait-until STATE`: `load`, `domcontentloaded`, or `networkidle`.
|
|
119
|
+
- `--json`: print machine-readable output.
|
|
120
|
+
- `--no-report`: skip usage reporting.
|
|
121
|
+
|
|
122
|
+
Selectors are Playwright locators, so plain CSS, `text=Pricing`, `section:has-text('Pricing')`, and `xpath=...` work.
|
|
123
|
+
|
|
124
|
+
## Agent Workflow
|
|
125
|
+
|
|
126
|
+
Tell your coding agent:
|
|
127
|
+
|
|
128
|
+
1. Save captures to `.agents/screenshots/`.
|
|
129
|
+
2. Use `agentshot` after meaningful UI changes.
|
|
130
|
+
3. Prefer selector or slice captures over huge full-page captures when possible.
|
|
131
|
+
4. Open and inspect the saved PNG before judging the UI.
|
|
132
|
+
|
|
133
|
+
The package includes [`AGENT-INSTRUCTIONS.md`](./AGENT-INSTRUCTIONS.md), a copy-paste prompt for Claude Code, Codex, Cursor, Windsurf, and OpenCode.
|
|
134
|
+
|
|
135
|
+
## Usage Reporting
|
|
136
|
+
|
|
137
|
+
Successful screenshots report one visual check when a license key is configured. Failed captures do not count. If the backend is temporarily unreachable, the event is queued at:
|
|
138
|
+
|
|
139
|
+
```text
|
|
140
|
+
~/.config/agentshot/usage-queue.jsonl
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Queued events sync on the next successful online capture.
|
|
144
|
+
|
|
145
|
+
## Development
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npm install
|
|
149
|
+
npm run check
|
|
150
|
+
npm run build
|
|
151
|
+
npm link
|
|
152
|
+
agentshot --version
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Package smoke test:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
npm pack --dry-run
|
|
159
|
+
```
|
package/dist/capture.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
|
+
import { dirname, extname, resolve } from 'node:path';
|
|
3
|
+
import { chromium } from 'playwright';
|
|
4
|
+
import { getFileSize, getTargetKind, reportCheck } from './reporting.js';
|
|
5
|
+
const PACKAGE_VERSION = '0.1.0';
|
|
6
|
+
function sleep(ms) {
|
|
7
|
+
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
8
|
+
}
|
|
9
|
+
function clampClip(clip, pageWidth, pageHeight) {
|
|
10
|
+
const x = Math.max(0, Math.floor(clip.x));
|
|
11
|
+
const y = Math.max(0, Math.floor(clip.y));
|
|
12
|
+
const width = Math.max(1, Math.min(Math.floor(clip.width), Math.floor(pageWidth - x)));
|
|
13
|
+
const height = Math.max(1, Math.min(Math.floor(clip.height), Math.floor(pageHeight - y)));
|
|
14
|
+
return { x, y, width, height };
|
|
15
|
+
}
|
|
16
|
+
function getFormat(output) {
|
|
17
|
+
const extension = extname(output).toLowerCase();
|
|
18
|
+
return extension === '.jpg' || extension === '.jpeg' ? 'jpeg' : 'png';
|
|
19
|
+
}
|
|
20
|
+
async function getPageSize(page) {
|
|
21
|
+
return page.evaluate(() => {
|
|
22
|
+
const root = document.scrollingElement || document.documentElement;
|
|
23
|
+
const body = document.body;
|
|
24
|
+
return {
|
|
25
|
+
width: Math.max(root.scrollWidth, body?.scrollWidth ?? 0, window.innerWidth),
|
|
26
|
+
height: Math.max(root.scrollHeight, body?.scrollHeight ?? 0, window.innerHeight)
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async function triggerLazyLoadedContent(page) {
|
|
31
|
+
await page.evaluate(async () => {
|
|
32
|
+
const sleepInPage = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
33
|
+
document
|
|
34
|
+
.querySelectorAll('img[loading="lazy"]')
|
|
35
|
+
.forEach((image) => (image.loading = 'eager'));
|
|
36
|
+
const root = document.scrollingElement || document.documentElement;
|
|
37
|
+
const viewport = window.innerHeight || 800;
|
|
38
|
+
const step = Math.max(240, Math.floor(viewport * 0.75));
|
|
39
|
+
const height = () => Math.max(root.scrollHeight, document.body?.scrollHeight ?? 0);
|
|
40
|
+
for (let y = 0; y < height(); y += step) {
|
|
41
|
+
window.scrollTo(0, y);
|
|
42
|
+
window.dispatchEvent(new Event('scroll'));
|
|
43
|
+
await sleepInPage(180);
|
|
44
|
+
}
|
|
45
|
+
window.scrollTo(0, height());
|
|
46
|
+
window.dispatchEvent(new Event('scroll'));
|
|
47
|
+
await sleepInPage(350);
|
|
48
|
+
window.scrollTo(0, 0);
|
|
49
|
+
await sleepInPage(250);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async function computeSelectorClip(page, selector, index, padding, timeoutMs) {
|
|
53
|
+
const locator = page.locator(selector).nth(index);
|
|
54
|
+
await locator.waitFor({ state: 'visible', timeout: timeoutMs });
|
|
55
|
+
await locator.scrollIntoViewIfNeeded();
|
|
56
|
+
await sleep(100);
|
|
57
|
+
const box = await locator.evaluate((element, pad) => {
|
|
58
|
+
const rect = element.getBoundingClientRect();
|
|
59
|
+
return {
|
|
60
|
+
x: rect.left + window.scrollX - pad,
|
|
61
|
+
y: rect.top + window.scrollY - pad,
|
|
62
|
+
width: rect.width + pad * 2,
|
|
63
|
+
height: rect.height + pad * 2
|
|
64
|
+
};
|
|
65
|
+
}, padding);
|
|
66
|
+
return box;
|
|
67
|
+
}
|
|
68
|
+
function computeVerticalClip(options, pageWidth, pageHeight) {
|
|
69
|
+
const y = options.fromY ?? 0;
|
|
70
|
+
let height = null;
|
|
71
|
+
if (options.toY !== null) {
|
|
72
|
+
height = options.toY - y;
|
|
73
|
+
}
|
|
74
|
+
else if (options.clipHeight !== null) {
|
|
75
|
+
height = options.clipHeight;
|
|
76
|
+
}
|
|
77
|
+
if (height === null) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (height <= 0) {
|
|
81
|
+
throw new Error('Clip height must be greater than 0.');
|
|
82
|
+
}
|
|
83
|
+
return clampClip({ x: 0, y, width: pageWidth, height }, pageWidth, pageHeight);
|
|
84
|
+
}
|
|
85
|
+
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
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
export async function capture(options) {
|
|
94
|
+
const startedAt = Date.now();
|
|
95
|
+
const output = resolve(options.output);
|
|
96
|
+
await mkdir(dirname(output), { recursive: true });
|
|
97
|
+
const browser = await launchBrowser(options);
|
|
98
|
+
try {
|
|
99
|
+
const page = await browser.newPage({
|
|
100
|
+
viewport: {
|
|
101
|
+
width: options.width,
|
|
102
|
+
height: options.viewportHeight
|
|
103
|
+
},
|
|
104
|
+
deviceScaleFactor: options.deviceScaleFactor
|
|
105
|
+
});
|
|
106
|
+
page.setDefaultTimeout(options.timeoutMs);
|
|
107
|
+
await page.goto(options.url, {
|
|
108
|
+
waitUntil: options.waitForLoadState,
|
|
109
|
+
timeout: options.timeoutMs
|
|
110
|
+
});
|
|
111
|
+
if (options.waitForSelector) {
|
|
112
|
+
await page.locator(options.waitForSelector).first().waitFor({
|
|
113
|
+
state: 'visible',
|
|
114
|
+
timeout: options.timeoutMs
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (options.scroll) {
|
|
118
|
+
await triggerLazyLoadedContent(page);
|
|
119
|
+
}
|
|
120
|
+
if (options.waitMs > 0) {
|
|
121
|
+
await sleep(options.waitMs);
|
|
122
|
+
}
|
|
123
|
+
const pageSize = await getPageSize(page);
|
|
124
|
+
const format = getFormat(output);
|
|
125
|
+
let screenshotWidth = pageSize.width;
|
|
126
|
+
let screenshotHeight = pageSize.height;
|
|
127
|
+
let mode = 'full-page';
|
|
128
|
+
if (options.selector) {
|
|
129
|
+
const selectorClip = await computeSelectorClip(page, options.selector, options.selectorIndex, options.padding, options.timeoutMs);
|
|
130
|
+
const clip = clampClip(selectorClip, pageSize.width, pageSize.height);
|
|
131
|
+
await page.screenshot({ path: output, fullPage: true, clip, type: format });
|
|
132
|
+
screenshotWidth = clip.width;
|
|
133
|
+
screenshotHeight = clip.height;
|
|
134
|
+
mode = 'selector';
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
const verticalClip = computeVerticalClip(options, pageSize.width, pageSize.height);
|
|
138
|
+
if (verticalClip) {
|
|
139
|
+
await page.screenshot({ path: output, fullPage: true, clip: verticalClip, type: format });
|
|
140
|
+
screenshotWidth = verticalClip.width;
|
|
141
|
+
screenshotHeight = verticalClip.height;
|
|
142
|
+
mode = 'clip';
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
await page.screenshot({ path: output, fullPage: options.fullPage, type: format });
|
|
146
|
+
screenshotWidth = options.fullPage ? pageSize.width : options.width;
|
|
147
|
+
screenshotHeight = options.fullPage ? pageSize.height : options.viewportHeight;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const durationMs = Date.now() - startedAt;
|
|
151
|
+
const fileSizeBytes = await getFileSize(output);
|
|
152
|
+
let reported = false;
|
|
153
|
+
let reportStatus = 'skipped';
|
|
154
|
+
let reportReason;
|
|
155
|
+
if (options.report) {
|
|
156
|
+
const report = await reportCheck({
|
|
157
|
+
apiUrl: options.apiUrl,
|
|
158
|
+
licenseKey: options.licenseKey,
|
|
159
|
+
metadata: {
|
|
160
|
+
cliVersion: PACKAGE_VERSION,
|
|
161
|
+
targetKind: getTargetKind(options.url),
|
|
162
|
+
viewportWidth: options.width,
|
|
163
|
+
viewportHeight: options.viewportHeight,
|
|
164
|
+
waitMs: options.waitMs,
|
|
165
|
+
scroll: options.scroll,
|
|
166
|
+
durationMs,
|
|
167
|
+
fileSizeBytes,
|
|
168
|
+
format
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
reported = report.reported;
|
|
172
|
+
reportStatus = report.status;
|
|
173
|
+
reportReason = report.reason;
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
output,
|
|
177
|
+
url: options.url,
|
|
178
|
+
width: screenshotWidth,
|
|
179
|
+
height: screenshotHeight,
|
|
180
|
+
fileSizeBytes,
|
|
181
|
+
durationMs,
|
|
182
|
+
mode,
|
|
183
|
+
reported,
|
|
184
|
+
reportStatus,
|
|
185
|
+
reportReason
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
await browser.close();
|
|
190
|
+
}
|
|
191
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
const DEFAULT_API_URL = 'https://agentscreenshots.com';
|
|
5
|
+
export function getConfigPath() {
|
|
6
|
+
if (process.env.AGENTSHOT_CONFIG) {
|
|
7
|
+
return process.env.AGENTSHOT_CONFIG;
|
|
8
|
+
}
|
|
9
|
+
const configHome = process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
|
|
10
|
+
return join(configHome, 'agentshot', 'config.json');
|
|
11
|
+
}
|
|
12
|
+
export async function readConfig() {
|
|
13
|
+
const path = getConfigPath();
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error.code === 'ENOENT') {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function writeConfig(config) {
|
|
25
|
+
const path = getConfigPath();
|
|
26
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
27
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
28
|
+
return path;
|
|
29
|
+
}
|
|
30
|
+
export async function clearLicenseKey() {
|
|
31
|
+
const config = await readConfig();
|
|
32
|
+
const nextConfig = { ...config };
|
|
33
|
+
const hadLicenseKey = Boolean(nextConfig.licenseKey);
|
|
34
|
+
delete nextConfig.licenseKey;
|
|
35
|
+
const path = await writeConfig(nextConfig);
|
|
36
|
+
return {
|
|
37
|
+
path,
|
|
38
|
+
hadLicenseKey,
|
|
39
|
+
envOverrideActive: Boolean(process.env.AGENTSHOT_LICENSE_KEY)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export async function resolveApiUrl(explicitApiUrl) {
|
|
43
|
+
const config = await readConfig();
|
|
44
|
+
return stripTrailingSlash(explicitApiUrl || process.env.AGENTSHOT_API_URL || config.apiUrl || DEFAULT_API_URL);
|
|
45
|
+
}
|
|
46
|
+
export async function resolveLicenseKey(explicitLicenseKey) {
|
|
47
|
+
const config = await readConfig();
|
|
48
|
+
return explicitLicenseKey || process.env.AGENTSHOT_LICENSE_KEY || config.licenseKey || null;
|
|
49
|
+
}
|
|
50
|
+
export function stripTrailingSlash(value) {
|
|
51
|
+
return value.replace(/\/+$/, '');
|
|
52
|
+
}
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { chromium } from 'playwright';
|
|
5
|
+
import { getConfigPath, readConfig, resolveApiUrl, resolveLicenseKey } from './config.js';
|
|
6
|
+
import { validateLicense } from './reporting.js';
|
|
7
|
+
function getOverallStatus(checks) {
|
|
8
|
+
if (checks.some((check) => check.status === 'fail')) {
|
|
9
|
+
return 'fail';
|
|
10
|
+
}
|
|
11
|
+
if (checks.some((check) => check.status === 'warn')) {
|
|
12
|
+
return 'warn';
|
|
13
|
+
}
|
|
14
|
+
return 'ok';
|
|
15
|
+
}
|
|
16
|
+
function getNodeMajor() {
|
|
17
|
+
return Number.parseInt(process.versions.node.split('.')[0] ?? '0', 10);
|
|
18
|
+
}
|
|
19
|
+
async function checkWritableTempFile() {
|
|
20
|
+
const directory = await mkdtemp(join(tmpdir(), 'agentshot-'));
|
|
21
|
+
const path = join(directory, 'doctor.txt');
|
|
22
|
+
try {
|
|
23
|
+
await writeFile(path, 'ok\n');
|
|
24
|
+
return {
|
|
25
|
+
name: 'output',
|
|
26
|
+
status: 'ok',
|
|
27
|
+
message: `Can write screenshot output files (${directory}).`
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
await rm(directory, { recursive: true, force: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function checkBrowser() {
|
|
35
|
+
const browser = await chromium.launch();
|
|
36
|
+
try {
|
|
37
|
+
const page = await browser.newPage();
|
|
38
|
+
await page.setContent('<!doctype html><title>agentshot doctor</title><p>ok</p>');
|
|
39
|
+
return {
|
|
40
|
+
name: 'browser',
|
|
41
|
+
status: 'ok',
|
|
42
|
+
message: 'Playwright Chromium launches successfully.'
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
await browser.close();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function runDoctor(options) {
|
|
50
|
+
const checks = [];
|
|
51
|
+
checks.push({
|
|
52
|
+
name: 'node',
|
|
53
|
+
status: getNodeMajor() >= 20 ? 'ok' : 'fail',
|
|
54
|
+
message: `Node ${process.versions.node} detected. agentshot requires Node 20+.`
|
|
55
|
+
});
|
|
56
|
+
try {
|
|
57
|
+
await readConfig();
|
|
58
|
+
checks.push({
|
|
59
|
+
name: 'config',
|
|
60
|
+
status: 'ok',
|
|
61
|
+
message: `Config path is readable: ${getConfigPath()}`
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
checks.push({
|
|
66
|
+
name: 'config',
|
|
67
|
+
status: 'fail',
|
|
68
|
+
message: error instanceof Error ? error.message : 'Config could not be read.'
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
let apiUrl = '';
|
|
72
|
+
try {
|
|
73
|
+
apiUrl = await resolveApiUrl(options.apiUrl);
|
|
74
|
+
checks.push({
|
|
75
|
+
name: 'api-url',
|
|
76
|
+
status: 'ok',
|
|
77
|
+
message: `API URL: ${apiUrl}`
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
checks.push({
|
|
82
|
+
name: 'api-url',
|
|
83
|
+
status: 'fail',
|
|
84
|
+
message: error instanceof Error ? error.message : 'API URL could not be resolved.'
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const licenseKey = await resolveLicenseKey(options.licenseKey);
|
|
88
|
+
checks.push({
|
|
89
|
+
name: 'license-key',
|
|
90
|
+
status: licenseKey ? 'ok' : 'warn',
|
|
91
|
+
message: licenseKey
|
|
92
|
+
? 'License key is configured.'
|
|
93
|
+
: 'No license key configured. Run `agentshot auth ags_live_...` when ready.'
|
|
94
|
+
});
|
|
95
|
+
try {
|
|
96
|
+
checks.push(await checkBrowser());
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
checks.push({
|
|
100
|
+
name: 'browser',
|
|
101
|
+
status: 'fail',
|
|
102
|
+
message: error instanceof Error
|
|
103
|
+
? error.message
|
|
104
|
+
: 'Playwright Chromium could not launch.'
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
checks.push(await checkWritableTempFile());
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
checks.push({
|
|
112
|
+
name: 'output',
|
|
113
|
+
status: 'fail',
|
|
114
|
+
message: error instanceof Error ? error.message : 'Could not write a temp output file.'
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (licenseKey && apiUrl) {
|
|
118
|
+
try {
|
|
119
|
+
const result = await validateLicense(options);
|
|
120
|
+
const license = result.body?.license;
|
|
121
|
+
checks.push({
|
|
122
|
+
name: 'api-license',
|
|
123
|
+
status: result.ok ? 'ok' : 'fail',
|
|
124
|
+
message: result.ok
|
|
125
|
+
? `License ${license?.keyPrefix ?? ''} validates: ${license?.usedChecks ?? 0}/${license?.quotaChecks ?? 0} checks used.`
|
|
126
|
+
: `License validation failed with HTTP ${result.status}.`
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
checks.push({
|
|
131
|
+
name: 'api-license',
|
|
132
|
+
status: 'fail',
|
|
133
|
+
message: error instanceof Error ? error.message : 'License validation failed.'
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
checks.push({
|
|
139
|
+
name: 'api-license',
|
|
140
|
+
status: 'skip',
|
|
141
|
+
message: 'Skipped because no license key is configured.'
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
status: getOverallStatus(checks),
|
|
146
|
+
checks
|
|
147
|
+
};
|
|
148
|
+
}
|