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.
@@ -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
+ ```
@@ -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
+ }