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
package/dist/feedback.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { resolveApiUrl, resolveLicenseKey } from './config.js';
|
|
2
|
+
export async function sendFeedback(options) {
|
|
3
|
+
const apiUrl = await resolveApiUrl(options.apiUrl);
|
|
4
|
+
const licenseKey = await resolveLicenseKey(options.licenseKey);
|
|
5
|
+
const headers = {
|
|
6
|
+
'content-type': 'application/json'
|
|
7
|
+
};
|
|
8
|
+
if (licenseKey) {
|
|
9
|
+
headers.authorization = `Bearer ${licenseKey}`;
|
|
10
|
+
}
|
|
11
|
+
const response = await fetch(`${apiUrl}/api/cli/feedback`, {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers,
|
|
14
|
+
body: JSON.stringify({
|
|
15
|
+
kind: options.kind,
|
|
16
|
+
message: options.message,
|
|
17
|
+
metadata: {
|
|
18
|
+
cliVersion: options.cliVersion,
|
|
19
|
+
nodeVersion: process.versions.node,
|
|
20
|
+
platform: process.platform,
|
|
21
|
+
arch: process.arch,
|
|
22
|
+
apiUrl,
|
|
23
|
+
licenseKeyConfigured: Boolean(licenseKey)
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
});
|
|
27
|
+
const body = await response.json().catch(() => null);
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
const error = body && typeof body === 'object' && 'error' in body
|
|
30
|
+
? String(body.error)
|
|
31
|
+
: `HTTP ${response.status}`;
|
|
32
|
+
throw new Error(`Feedback failed: ${error}`);
|
|
33
|
+
}
|
|
34
|
+
return body;
|
|
35
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { capture } from './capture.js';
|
|
6
|
+
import { clearLicenseKey, readConfig, writeConfig } from './config.js';
|
|
7
|
+
import { runDoctor } from './doctor.js';
|
|
8
|
+
import { sendFeedback } from './feedback.js';
|
|
9
|
+
import { validateLicense } from './reporting.js';
|
|
10
|
+
function printHelp() {
|
|
11
|
+
console.log(`agentshot
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
agentshot URL OUTPUT [options]
|
|
15
|
+
agentshot auth LICENSE_KEY [--api-url URL] [--json]
|
|
16
|
+
agentshot status [--api-url URL] [--license-key KEY]
|
|
17
|
+
agentshot doctor [--api-url URL] [--license-key KEY]
|
|
18
|
+
agentshot feedback "MESSAGE" [--kind feedback|bug|idea]
|
|
19
|
+
agentshot logout
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
agentshot "http://127.0.0.1:5200/design" "./shots/design.png"
|
|
23
|
+
agentshot "http://127.0.0.1:5200/design" "./shots/hero.png" --height 1200
|
|
24
|
+
agentshot "http://127.0.0.1:5200/design" "./shots/cards.png" --from 1800 --to 2500
|
|
25
|
+
agentshot "http://127.0.0.1:5200/design" "./shots/borders.png" --selector "section:has-text('Borders')" --padding 24
|
|
26
|
+
agentshot "http://127.0.0.1:5200/design" "./shots/page.png" --scroll --wait 1000
|
|
27
|
+
|
|
28
|
+
Capture options:
|
|
29
|
+
--wait MS Wait after load/scroll before capture
|
|
30
|
+
--scroll Scroll through the page first to trigger lazy-loaded content
|
|
31
|
+
--selector SELECTOR Capture the first matching Playwright/CSS selector
|
|
32
|
+
--section SELECTOR Alias for --selector
|
|
33
|
+
--nth INDEX Capture selector match by zero-based index (default: 0)
|
|
34
|
+
--padding PX Add padding around selector screenshots (default: 0)
|
|
35
|
+
--height PX Capture from --from/top to this height; defaults from 0
|
|
36
|
+
--from PX Vertical crop start, page pixels
|
|
37
|
+
--to PX Vertical crop end, page pixels
|
|
38
|
+
--width PX Browser viewport width (default: 1280)
|
|
39
|
+
--viewport-height PX Browser viewport height (default: 900)
|
|
40
|
+
--viewport WIDTHxHEIGHT Set viewport width and height together
|
|
41
|
+
--wait-for CSS Wait for a selector before capture
|
|
42
|
+
--wait-until STATE load, domcontentloaded, or networkidle (default: load)
|
|
43
|
+
--timeout MS Navigation/action timeout (default: 30000)
|
|
44
|
+
--browser NAME chromium or chrome (default: chromium)
|
|
45
|
+
--headed Show the browser window
|
|
46
|
+
--no-full-page Capture only viewport when no selector/crop is used
|
|
47
|
+
--device-scale-factor N Device scale factor (default: 1)
|
|
48
|
+
--json Print machine-readable JSON
|
|
49
|
+
--no-report Do not report a visual check to AgentScreenshots
|
|
50
|
+
--api-url URL Override AgentScreenshots API URL
|
|
51
|
+
--license-key KEY Override configured license key
|
|
52
|
+
|
|
53
|
+
Environment:
|
|
54
|
+
AGENTSHOT_API_URL
|
|
55
|
+
AGENTSHOT_LICENSE_KEY
|
|
56
|
+
AGENTSHOT_CONFIG
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
function parsePositiveNumber(value, name) {
|
|
60
|
+
const parsed = Number(value);
|
|
61
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
62
|
+
throw new Error(`${name} must be a positive number.`);
|
|
63
|
+
}
|
|
64
|
+
return parsed;
|
|
65
|
+
}
|
|
66
|
+
function parseInteger(value, name) {
|
|
67
|
+
const parsed = Number.parseInt(value, 10);
|
|
68
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
69
|
+
throw new Error(`${name} must be a positive integer.`);
|
|
70
|
+
}
|
|
71
|
+
return parsed;
|
|
72
|
+
}
|
|
73
|
+
function readValue(args, index, option) {
|
|
74
|
+
const value = args[index + 1];
|
|
75
|
+
if (!value || value.startsWith('--')) {
|
|
76
|
+
throw new Error(`${option} requires a value.`);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function getDefaultCaptureOptions(url, output) {
|
|
81
|
+
return {
|
|
82
|
+
url,
|
|
83
|
+
output,
|
|
84
|
+
width: 1280,
|
|
85
|
+
viewportHeight: 900,
|
|
86
|
+
deviceScaleFactor: 1,
|
|
87
|
+
waitMs: 0,
|
|
88
|
+
timeoutMs: 30_000,
|
|
89
|
+
scroll: false,
|
|
90
|
+
selector: null,
|
|
91
|
+
selectorIndex: 0,
|
|
92
|
+
padding: 0,
|
|
93
|
+
fromY: null,
|
|
94
|
+
toY: null,
|
|
95
|
+
clipHeight: null,
|
|
96
|
+
fullPage: true,
|
|
97
|
+
waitForSelector: null,
|
|
98
|
+
waitForLoadState: 'load',
|
|
99
|
+
browser: 'chromium',
|
|
100
|
+
headed: false,
|
|
101
|
+
json: false,
|
|
102
|
+
report: true,
|
|
103
|
+
apiUrl: null,
|
|
104
|
+
licenseKey: null
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function parseCommonAuthOptions(args, startIndex = 0) {
|
|
108
|
+
let apiUrl = null;
|
|
109
|
+
let licenseKey = null;
|
|
110
|
+
let json = false;
|
|
111
|
+
for (let index = startIndex; index < args.length; index += 1) {
|
|
112
|
+
const arg = args[index];
|
|
113
|
+
if (arg === '--api-url') {
|
|
114
|
+
apiUrl = readValue(args, index, arg);
|
|
115
|
+
index += 1;
|
|
116
|
+
}
|
|
117
|
+
else if (arg === '--license-key') {
|
|
118
|
+
licenseKey = readValue(args, index, arg);
|
|
119
|
+
index += 1;
|
|
120
|
+
}
|
|
121
|
+
else if (arg === '--json') {
|
|
122
|
+
json = true;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { apiUrl, licenseKey, json };
|
|
129
|
+
}
|
|
130
|
+
function parseFeedback(args) {
|
|
131
|
+
const messageParts = [];
|
|
132
|
+
let kind = 'feedback';
|
|
133
|
+
let apiUrl = null;
|
|
134
|
+
let licenseKey = null;
|
|
135
|
+
let json = false;
|
|
136
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
137
|
+
const arg = args[index];
|
|
138
|
+
if (arg === '--kind') {
|
|
139
|
+
const value = readValue(args, index, arg);
|
|
140
|
+
if (value !== 'feedback' && value !== 'bug' && value !== 'idea') {
|
|
141
|
+
throw new Error('--kind must be feedback, bug, or idea.');
|
|
142
|
+
}
|
|
143
|
+
kind = value;
|
|
144
|
+
index += 1;
|
|
145
|
+
}
|
|
146
|
+
else if (arg === '--api-url') {
|
|
147
|
+
apiUrl = readValue(args, index, arg);
|
|
148
|
+
index += 1;
|
|
149
|
+
}
|
|
150
|
+
else if (arg === '--license-key') {
|
|
151
|
+
licenseKey = readValue(args, index, arg);
|
|
152
|
+
index += 1;
|
|
153
|
+
}
|
|
154
|
+
else if (arg === '--json') {
|
|
155
|
+
json = true;
|
|
156
|
+
}
|
|
157
|
+
else if (arg.startsWith('--')) {
|
|
158
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
messageParts.push(arg);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const message = messageParts.join(' ').trim();
|
|
165
|
+
if (!message) {
|
|
166
|
+
throw new Error('Usage: agentshot feedback "MESSAGE" [--kind feedback|bug|idea]');
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
name: 'feedback',
|
|
170
|
+
kind,
|
|
171
|
+
message,
|
|
172
|
+
apiUrl,
|
|
173
|
+
licenseKey,
|
|
174
|
+
json
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function parseLogout(args) {
|
|
178
|
+
let json = false;
|
|
179
|
+
for (const arg of args) {
|
|
180
|
+
if (arg === '--json') {
|
|
181
|
+
json = true;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return { name: 'logout', json };
|
|
188
|
+
}
|
|
189
|
+
function parseCapture(args) {
|
|
190
|
+
if (args.length < 2) {
|
|
191
|
+
throw new Error('Capture requires URL and OUTPUT.');
|
|
192
|
+
}
|
|
193
|
+
const [url, output, ...rest] = args;
|
|
194
|
+
const options = getDefaultCaptureOptions(url, output);
|
|
195
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
196
|
+
const arg = rest[index];
|
|
197
|
+
switch (arg) {
|
|
198
|
+
case '--wait':
|
|
199
|
+
options.waitMs = parseInteger(readValue(rest, index, arg), arg);
|
|
200
|
+
index += 1;
|
|
201
|
+
break;
|
|
202
|
+
case '--scroll':
|
|
203
|
+
options.scroll = true;
|
|
204
|
+
break;
|
|
205
|
+
case '--selector':
|
|
206
|
+
case '--section':
|
|
207
|
+
options.selector = readValue(rest, index, arg);
|
|
208
|
+
index += 1;
|
|
209
|
+
break;
|
|
210
|
+
case '--nth':
|
|
211
|
+
options.selectorIndex = parseInteger(readValue(rest, index, arg), arg);
|
|
212
|
+
index += 1;
|
|
213
|
+
break;
|
|
214
|
+
case '--padding':
|
|
215
|
+
options.padding = parseInteger(readValue(rest, index, arg), arg);
|
|
216
|
+
index += 1;
|
|
217
|
+
break;
|
|
218
|
+
case '--height':
|
|
219
|
+
options.clipHeight = parseInteger(readValue(rest, index, arg), arg);
|
|
220
|
+
index += 1;
|
|
221
|
+
break;
|
|
222
|
+
case '--from':
|
|
223
|
+
options.fromY = parseInteger(readValue(rest, index, arg), arg);
|
|
224
|
+
index += 1;
|
|
225
|
+
break;
|
|
226
|
+
case '--to':
|
|
227
|
+
options.toY = parseInteger(readValue(rest, index, arg), arg);
|
|
228
|
+
index += 1;
|
|
229
|
+
break;
|
|
230
|
+
case '--width':
|
|
231
|
+
options.width = parseInteger(readValue(rest, index, arg), arg);
|
|
232
|
+
index += 1;
|
|
233
|
+
break;
|
|
234
|
+
case '--viewport-height':
|
|
235
|
+
options.viewportHeight = parseInteger(readValue(rest, index, arg), arg);
|
|
236
|
+
index += 1;
|
|
237
|
+
break;
|
|
238
|
+
case '--viewport': {
|
|
239
|
+
const value = readValue(rest, index, arg);
|
|
240
|
+
const match = value.match(/^(\d+)x(\d+)$/i);
|
|
241
|
+
if (!match) {
|
|
242
|
+
throw new Error('--viewport must look like 1280x900.');
|
|
243
|
+
}
|
|
244
|
+
options.width = parseInteger(match[1], '--viewport width');
|
|
245
|
+
options.viewportHeight = parseInteger(match[2], '--viewport height');
|
|
246
|
+
index += 1;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
case '--wait-for':
|
|
250
|
+
options.waitForSelector = readValue(rest, index, arg);
|
|
251
|
+
index += 1;
|
|
252
|
+
break;
|
|
253
|
+
case '--wait-until': {
|
|
254
|
+
const value = readValue(rest, index, arg);
|
|
255
|
+
if (value !== 'load' && value !== 'domcontentloaded' && value !== 'networkidle') {
|
|
256
|
+
throw new Error('--wait-until must be load, domcontentloaded, or networkidle.');
|
|
257
|
+
}
|
|
258
|
+
options.waitForLoadState = value;
|
|
259
|
+
index += 1;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
case '--timeout':
|
|
263
|
+
options.timeoutMs = parseInteger(readValue(rest, index, arg), arg);
|
|
264
|
+
index += 1;
|
|
265
|
+
break;
|
|
266
|
+
case '--browser': {
|
|
267
|
+
const value = readValue(rest, index, arg);
|
|
268
|
+
if (value !== 'chromium' && value !== 'chrome') {
|
|
269
|
+
throw new Error('--browser must be chromium or chrome.');
|
|
270
|
+
}
|
|
271
|
+
options.browser = value;
|
|
272
|
+
index += 1;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
case '--headed':
|
|
276
|
+
options.headed = true;
|
|
277
|
+
break;
|
|
278
|
+
case '--no-full-page':
|
|
279
|
+
options.fullPage = false;
|
|
280
|
+
break;
|
|
281
|
+
case '--device-scale-factor':
|
|
282
|
+
options.deviceScaleFactor = parsePositiveNumber(readValue(rest, index, arg), arg);
|
|
283
|
+
index += 1;
|
|
284
|
+
break;
|
|
285
|
+
case '--json':
|
|
286
|
+
options.json = true;
|
|
287
|
+
break;
|
|
288
|
+
case '--no-report':
|
|
289
|
+
options.report = false;
|
|
290
|
+
break;
|
|
291
|
+
case '--api-url':
|
|
292
|
+
options.apiUrl = readValue(rest, index, arg);
|
|
293
|
+
index += 1;
|
|
294
|
+
break;
|
|
295
|
+
case '--license-key':
|
|
296
|
+
options.licenseKey = readValue(rest, index, arg);
|
|
297
|
+
index += 1;
|
|
298
|
+
break;
|
|
299
|
+
default:
|
|
300
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return { name: 'capture', options };
|
|
304
|
+
}
|
|
305
|
+
function parseArgs(args) {
|
|
306
|
+
const first = args[0];
|
|
307
|
+
if (!first || first === '--help' || first === '-h' || first === 'help') {
|
|
308
|
+
return { name: 'help' };
|
|
309
|
+
}
|
|
310
|
+
if (first === '--version' || first === '-v' || first === 'version') {
|
|
311
|
+
return { name: 'version' };
|
|
312
|
+
}
|
|
313
|
+
if (first === 'auth') {
|
|
314
|
+
const key = args[1];
|
|
315
|
+
if (!key || key.startsWith('--')) {
|
|
316
|
+
throw new Error('Usage: agentshot auth LICENSE_KEY [--api-url URL]');
|
|
317
|
+
}
|
|
318
|
+
const { apiUrl, json } = parseCommonAuthOptions(args, 2);
|
|
319
|
+
return { name: 'auth', key, apiUrl, json };
|
|
320
|
+
}
|
|
321
|
+
if (first === 'status') {
|
|
322
|
+
return { name: 'status', ...parseCommonAuthOptions(args, 1) };
|
|
323
|
+
}
|
|
324
|
+
if (first === 'doctor') {
|
|
325
|
+
return { name: 'doctor', ...parseCommonAuthOptions(args, 1) };
|
|
326
|
+
}
|
|
327
|
+
if (first === 'feedback') {
|
|
328
|
+
return parseFeedback(args.slice(1));
|
|
329
|
+
}
|
|
330
|
+
if (first === 'logout') {
|
|
331
|
+
return parseLogout(args.slice(1));
|
|
332
|
+
}
|
|
333
|
+
return parseCapture(args);
|
|
334
|
+
}
|
|
335
|
+
async function readPackageVersion() {
|
|
336
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
337
|
+
const packageJsonPath = join(dirname(currentFile), '..', 'package.json');
|
|
338
|
+
try {
|
|
339
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
|
340
|
+
return packageJson.version ?? '0.1.0';
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return '0.1.0';
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async function run() {
|
|
347
|
+
const command = parseArgs(process.argv.slice(2));
|
|
348
|
+
if (command.name === 'help') {
|
|
349
|
+
printHelp();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (command.name === 'version') {
|
|
353
|
+
console.log(await readPackageVersion());
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (command.name === 'auth') {
|
|
357
|
+
const config = await readConfig();
|
|
358
|
+
const apiUrl = command.apiUrl ?? config.apiUrl;
|
|
359
|
+
const validation = await validateLicense({
|
|
360
|
+
apiUrl,
|
|
361
|
+
licenseKey: command.key
|
|
362
|
+
});
|
|
363
|
+
if (!validation.ok) {
|
|
364
|
+
if (command.json) {
|
|
365
|
+
console.log(JSON.stringify(validation.body, null, 2));
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
console.error(`License validation failed (${validation.status}).`);
|
|
369
|
+
if (validation.body) {
|
|
370
|
+
console.error(JSON.stringify(validation.body, null, 2));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
process.exitCode = 1;
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const path = await writeConfig({
|
|
377
|
+
...config,
|
|
378
|
+
apiUrl,
|
|
379
|
+
licenseKey: command.key
|
|
380
|
+
});
|
|
381
|
+
if (command.json) {
|
|
382
|
+
console.log(JSON.stringify({
|
|
383
|
+
ok: true,
|
|
384
|
+
path,
|
|
385
|
+
license: validation.body?.license
|
|
386
|
+
}, null, 2));
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
const license = validation.body?.license;
|
|
390
|
+
console.log(`Saved license key to ${path}`);
|
|
391
|
+
console.log(`License ${license?.keyPrefix ?? ''} valid: ${license?.usedChecks ?? 0}/${license?.quotaChecks ?? 0} checks used`);
|
|
392
|
+
}
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (command.name === 'logout') {
|
|
396
|
+
const result = await clearLicenseKey();
|
|
397
|
+
if (command.json) {
|
|
398
|
+
console.log(JSON.stringify(result, null, 2));
|
|
399
|
+
}
|
|
400
|
+
else if (result.hadLicenseKey) {
|
|
401
|
+
console.log(`Removed saved license key from ${result.path}`);
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
console.log(`No saved license key found in ${result.path}`);
|
|
405
|
+
}
|
|
406
|
+
if (result.envOverrideActive && !command.json) {
|
|
407
|
+
console.error('AGENTSHOT_LICENSE_KEY is still set in the environment.');
|
|
408
|
+
}
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (command.name === 'status') {
|
|
412
|
+
let result;
|
|
413
|
+
try {
|
|
414
|
+
result = await validateLicense({
|
|
415
|
+
apiUrl: command.apiUrl,
|
|
416
|
+
licenseKey: command.licenseKey
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
421
|
+
if (command.json) {
|
|
422
|
+
console.log(JSON.stringify({
|
|
423
|
+
ok: false,
|
|
424
|
+
reason: 'no_license_key',
|
|
425
|
+
message
|
|
426
|
+
}, null, 2));
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
console.error(message);
|
|
430
|
+
}
|
|
431
|
+
process.exitCode = 1;
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (command.json) {
|
|
435
|
+
console.log(JSON.stringify(result.body, null, 2));
|
|
436
|
+
}
|
|
437
|
+
else if (result.ok) {
|
|
438
|
+
const license = result.body?.license;
|
|
439
|
+
console.log(`License ${license?.keyPrefix ?? ''} valid: ${license?.usedChecks ?? 0}/${license?.quotaChecks ?? 0} checks used`);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
console.error(`License check failed (${result.status}).`);
|
|
443
|
+
if (result.body) {
|
|
444
|
+
console.error(JSON.stringify(result.body, null, 2));
|
|
445
|
+
}
|
|
446
|
+
process.exitCode = 1;
|
|
447
|
+
}
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (command.name === 'doctor') {
|
|
451
|
+
const result = await runDoctor(command);
|
|
452
|
+
if (command.json) {
|
|
453
|
+
console.log(JSON.stringify(result, null, 2));
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
console.log(`agentshot doctor: ${result.status}`);
|
|
457
|
+
for (const check of result.checks) {
|
|
458
|
+
console.log(`${check.status.padEnd(4)} ${check.name}: ${check.message}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (result.status === 'fail') {
|
|
462
|
+
process.exitCode = 1;
|
|
463
|
+
}
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (command.name === 'feedback') {
|
|
467
|
+
const result = await sendFeedback({
|
|
468
|
+
apiUrl: command.apiUrl,
|
|
469
|
+
licenseKey: command.licenseKey,
|
|
470
|
+
json: command.json,
|
|
471
|
+
kind: command.kind,
|
|
472
|
+
message: command.message,
|
|
473
|
+
cliVersion: await readPackageVersion()
|
|
474
|
+
});
|
|
475
|
+
if (command.json) {
|
|
476
|
+
console.log(JSON.stringify(result, null, 2));
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
const feedback = result?.feedback;
|
|
480
|
+
console.log(`Feedback sent${feedback?.id ? ` (${feedback.id})` : ''}.`);
|
|
481
|
+
}
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const result = await capture(command.options);
|
|
485
|
+
if (command.options.json) {
|
|
486
|
+
console.log(JSON.stringify(result, null, 2));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
console.log(`Captured ${result.width}x${result.height} -> ${result.output} (${(result.durationMs / 1000).toFixed(1)}s)`);
|
|
490
|
+
if (result.reportStatus === 'failed') {
|
|
491
|
+
console.error(`Usage report failed: ${result.reportReason}`);
|
|
492
|
+
}
|
|
493
|
+
else if (result.reportStatus === 'queued') {
|
|
494
|
+
console.error(`Usage report queued for retry: ${result.reportReason}`);
|
|
495
|
+
}
|
|
496
|
+
else if (result.reportStatus === 'skipped' && result.reportReason === 'no_license_key') {
|
|
497
|
+
console.error('Usage report skipped: no license key configured.');
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
run().catch((error) => {
|
|
501
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
502
|
+
process.exit(1);
|
|
503
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { appendFile, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { getConfigPath, resolveApiUrl, resolveLicenseKey } from './config.js';
|
|
4
|
+
export async function getFileSize(path) {
|
|
5
|
+
const file = await stat(path);
|
|
6
|
+
return file.size;
|
|
7
|
+
}
|
|
8
|
+
export function getTargetKind(url) {
|
|
9
|
+
try {
|
|
10
|
+
const parsed = new URL(url);
|
|
11
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
12
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {
|
|
13
|
+
return 'localhost';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return 'localhost';
|
|
18
|
+
}
|
|
19
|
+
return 'public';
|
|
20
|
+
}
|
|
21
|
+
function getQueuePath() {
|
|
22
|
+
return join(dirname(getConfigPath()), 'usage-queue.jsonl');
|
|
23
|
+
}
|
|
24
|
+
async function readQueuedEvents() {
|
|
25
|
+
const path = getQueuePath();
|
|
26
|
+
try {
|
|
27
|
+
const content = await readFile(path, 'utf8');
|
|
28
|
+
return content
|
|
29
|
+
.split('\n')
|
|
30
|
+
.map((line) => line.trim())
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.map((line) => JSON.parse(line));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error.code === 'ENOENT') {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function writeQueuedEvents(events) {
|
|
42
|
+
const path = getQueuePath();
|
|
43
|
+
if (events.length === 0) {
|
|
44
|
+
await rm(path, { force: true });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
48
|
+
await writeFile(path, `${events.map((event) => JSON.stringify(event)).join('\n')}\n`, {
|
|
49
|
+
mode: 0o600
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async function queueUsageEvent(event) {
|
|
53
|
+
const path = getQueuePath();
|
|
54
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
55
|
+
await appendFile(path, `${JSON.stringify({ ...event, queuedAt: event.queuedAt ?? new Date().toISOString() })}\n`, { mode: 0o600 });
|
|
56
|
+
}
|
|
57
|
+
async function postUsageEvent(apiUrl, licenseKey, event) {
|
|
58
|
+
const controller = new AbortController();
|
|
59
|
+
const timeout = setTimeout(() => controller.abort(), 4_000);
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(`${apiUrl}/api/cli/report-check`, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: {
|
|
64
|
+
authorization: `Bearer ${licenseKey}`,
|
|
65
|
+
'content-type': 'application/json'
|
|
66
|
+
},
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
eventId: event.eventId,
|
|
69
|
+
metadata: event.metadata
|
|
70
|
+
}),
|
|
71
|
+
signal: controller.signal
|
|
72
|
+
});
|
|
73
|
+
const payload = (await response.json().catch(() => null));
|
|
74
|
+
const reason = payload?.reason ?? `http_${response.status}`;
|
|
75
|
+
if (response.ok) {
|
|
76
|
+
return { ok: true, retryable: false, reason };
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
retryable: response.status >= 500 || response.status === 429,
|
|
81
|
+
reason
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
clearTimeout(timeout);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function flushQueuedEvents(apiUrl, licenseKey) {
|
|
89
|
+
const queued = await readQueuedEvents();
|
|
90
|
+
if (queued.length === 0) {
|
|
91
|
+
return { flushed: 0, kept: 0 };
|
|
92
|
+
}
|
|
93
|
+
const remaining = [];
|
|
94
|
+
let flushed = 0;
|
|
95
|
+
for (const event of queued) {
|
|
96
|
+
try {
|
|
97
|
+
const result = await postUsageEvent(apiUrl, licenseKey, event);
|
|
98
|
+
if (result.ok || !result.retryable) {
|
|
99
|
+
flushed += 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
remaining.push(event);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
remaining.push(event);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
await writeQueuedEvents(remaining);
|
|
109
|
+
return {
|
|
110
|
+
flushed,
|
|
111
|
+
kept: remaining.length
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export async function reportCheck(options) {
|
|
115
|
+
const licenseKey = await resolveLicenseKey(options.licenseKey);
|
|
116
|
+
if (!licenseKey) {
|
|
117
|
+
return {
|
|
118
|
+
reported: false,
|
|
119
|
+
status: 'skipped',
|
|
120
|
+
reason: 'no_license_key'
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const apiUrl = await resolveApiUrl(options.apiUrl);
|
|
124
|
+
const event = {
|
|
125
|
+
eventId: crypto.randomUUID(),
|
|
126
|
+
metadata: options.metadata
|
|
127
|
+
};
|
|
128
|
+
try {
|
|
129
|
+
await flushQueuedEvents(apiUrl, licenseKey);
|
|
130
|
+
const result = await postUsageEvent(apiUrl, licenseKey, event);
|
|
131
|
+
if (!result.ok) {
|
|
132
|
+
return {
|
|
133
|
+
reported: false,
|
|
134
|
+
status: 'failed',
|
|
135
|
+
reason: result.reason
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
reported: true,
|
|
140
|
+
status: 'ok',
|
|
141
|
+
reason: result.reason
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
await queueUsageEvent(event);
|
|
146
|
+
return {
|
|
147
|
+
reported: false,
|
|
148
|
+
status: 'queued',
|
|
149
|
+
reason: error instanceof Error ? error.message : 'queued_for_retry'
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export async function validateLicense(options) {
|
|
154
|
+
const licenseKey = await resolveLicenseKey(options.licenseKey);
|
|
155
|
+
if (!licenseKey) {
|
|
156
|
+
throw new Error('No license key configured. Run `agentshot auth ags_live_...` first.');
|
|
157
|
+
}
|
|
158
|
+
const apiUrl = await resolveApiUrl(options.apiUrl);
|
|
159
|
+
const response = await fetch(`${apiUrl}/api/cli/license`, {
|
|
160
|
+
headers: {
|
|
161
|
+
authorization: `Bearer ${licenseKey}`
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
ok: response.ok,
|
|
166
|
+
status: response.status,
|
|
167
|
+
body: await response.json().catch(() => null)
|
|
168
|
+
};
|
|
169
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|