agent-ready-scanner 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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +130 -0
  3. package/dist/cli.mjs +543 -0
  4. package/package.json +55 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Agent Ready
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # agent-ready-cli
2
+
3
+ Command-line client for [Agent Ready](https://agent-ready.dev) — scan any URL for **AI-agent readability** against the Vercel Agent Readability Spec, the [llmstxt.org](https://llmstxt.org) standard, and agent-protocol manifests (MCP server cards, A2A, `agents.json`, `agent-permissions.json`, UCP, x402, NLWeb).
4
+
5
+ It's a thin wrapper over the hosted [agent-ready.dev REST API](https://agent-ready.dev/api/v1/openapi.json) — no scanning happens locally. For tool-native access from an AI assistant, see [`agent-ready-mcp`](https://github.com/mlava/agent-ready-mcp) instead.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g agent-ready-scanner
11
+ ```
12
+
13
+ This installs the `agent-ready` command. Or run without installing:
14
+
15
+ ```bash
16
+ npx agent-ready-scanner scan https://example.com
17
+ ```
18
+
19
+ > **Why `agent-ready-scanner`?** The bare `agent-ready` name is blocked by
20
+ > npm's package-name similarity policy (it collides with an unrelated
21
+ > `agentready` package). The installed command is still `agent-ready`.
22
+
23
+ Requires Node.js ≥ 20.10.
24
+
25
+ ## Authentication
26
+
27
+ `scan`, `get`, and `list` require a **Pro API key**. Issue one from the
28
+ [dashboard](https://agent-ready.dev/dashboard/api-keys), then either:
29
+
30
+ ```bash
31
+ export AGENT_READY_API_KEY="ar_live_..."
32
+ # or pass per-command:
33
+ agent-ready scan https://example.com --api-key ar_live_...
34
+ ```
35
+
36
+ `ask` is public and needs no key.
37
+
38
+ ## Commands
39
+
40
+ ### `scan <url>`
41
+
42
+ Starts a scan, polls until it finishes, and prints a readability summary.
43
+
44
+ ```bash
45
+ agent-ready scan https://example.com
46
+ agent-ready scan https://example.com --page-limit 25
47
+ agent-ready scan https://example.com --no-wait # queue only, print the id
48
+ agent-ready scan https://example.com --json # raw JSON
49
+ ```
50
+
51
+ | Option | Description |
52
+ | --- | --- |
53
+ | `--page-limit <n>` | Max pages to crawl |
54
+ | `--no-wait` | Queue the scan and print its id without polling |
55
+ | `--poll-interval <s>` | Seconds between status polls (default 2) |
56
+ | `--timeout <s>` | Max seconds to wait for completion (default 120) |
57
+
58
+ ### `get <id>`
59
+
60
+ Fetch a scan by id (e.g. one started earlier with `--no-wait`).
61
+
62
+ ```bash
63
+ agent-ready get V1StGXR8_Z
64
+ agent-ready get V1StGXR8_Z --json
65
+ ```
66
+
67
+ ### `list`
68
+
69
+ List your recent scans, newest first.
70
+
71
+ ```bash
72
+ agent-ready list
73
+ agent-ready list --limit 5
74
+ agent-ready list --cursor 2026-05-30T00:00:00.000Z # next page
75
+ ```
76
+
77
+ ### `ask <query...>`
78
+
79
+ Natural-language search over Agent Ready's own docs (methodology, the check
80
+ registry, supported specs). Public — no API key.
81
+
82
+ ```bash
83
+ agent-ready ask "how is the score calculated?"
84
+ agent-ready ask "what does check S4 do?" --type checks
85
+ agent-ready ask "summarize the llms.txt requirements" --mode summarize
86
+ ```
87
+
88
+ ## Global options
89
+
90
+ | Option | Description |
91
+ | --- | --- |
92
+ | `--json` | Output raw JSON instead of formatted text |
93
+ | `--api-key <key>` | Override `AGENT_READY_API_KEY` |
94
+ | `--base-url <url>` | Override `AGENT_READY_API_URL` (e.g. for local dev) |
95
+ | `--no-color` | Disable coloured output ([`NO_COLOR`](https://no-color.org) is also honoured) |
96
+ | `-h, --help` | Show help |
97
+ | `-v, --version` | Show version |
98
+
99
+ ## Environment variables
100
+
101
+ | Variable | Default | Purpose |
102
+ | --- | --- | --- |
103
+ | `AGENT_READY_API_KEY` | — | Pro API key for `scan`/`get`/`list` |
104
+ | `AGENT_READY_API_URL` | `https://agent-ready.dev` | API base URL |
105
+ | `AGENT_READY_SCAN_TIMEOUT_MS` | `120000` | Overall scan wait budget |
106
+ | `AGENT_READY_GET_TIMEOUT_MS` | `10000` | Per-request timeout |
107
+
108
+ ## Exit codes
109
+
110
+ | Code | Meaning |
111
+ | --- | --- |
112
+ | `0` | Success |
113
+ | `1` | API error, scan failed, or scan timed out |
114
+ | `2` | Usage error (bad arguments) |
115
+
116
+ `--json` output goes to stdout; progress and errors go to stderr, so you can
117
+ safely pipe JSON into other tools.
118
+
119
+ ## Development
120
+
121
+ ```bash
122
+ npm install
123
+ npm test # vitest
124
+ npm run typecheck # tsc --noEmit
125
+ npm run build # bundle to dist/cli.mjs
126
+ ```
127
+
128
+ ## License
129
+
130
+ MIT © Agent Ready
package/dist/cli.mjs ADDED
@@ -0,0 +1,543 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { parseArgs } from "node:util";
5
+
6
+ // src/client.ts
7
+ var DEFAULT_BASE_URL = "https://agent-ready.dev";
8
+ var DEFAULT_SCAN_TIMEOUT_MS = 12e4;
9
+ var DEFAULT_GET_TIMEOUT_MS = 1e4;
10
+ function createConfig(env = process.env) {
11
+ const rawBase = env.AGENT_READY_API_URL ?? DEFAULT_BASE_URL;
12
+ const baseUrl = rawBase.replace(/\/+$/, "");
13
+ const apiKey = (env.AGENT_READY_API_KEY?.trim() ?? "") || null;
14
+ const scanTimeoutMs = positiveIntOr(
15
+ env.AGENT_READY_SCAN_TIMEOUT_MS,
16
+ DEFAULT_SCAN_TIMEOUT_MS
17
+ );
18
+ const getTimeoutMs = positiveIntOr(
19
+ env.AGENT_READY_GET_TIMEOUT_MS,
20
+ DEFAULT_GET_TIMEOUT_MS
21
+ );
22
+ return { baseUrl, apiKey, scanTimeoutMs, getTimeoutMs };
23
+ }
24
+ function positiveIntOr(raw, fallback) {
25
+ const parsed = Number(raw);
26
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
27
+ }
28
+ var ApiError = class extends Error {
29
+ constructor(code, message, status = null) {
30
+ super(message);
31
+ this.code = code;
32
+ this.status = status;
33
+ this.name = "ApiError";
34
+ }
35
+ };
36
+ async function call(config, opts) {
37
+ if (!config.apiKey) {
38
+ throw new ApiError(
39
+ "missing_api_key",
40
+ "No API key set. Issue a Pro API key from https://agent-ready.dev/dashboard/api-keys, then pass --api-key or set AGENT_READY_API_KEY."
41
+ );
42
+ }
43
+ const url = `${config.baseUrl}${opts.path}`;
44
+ const headers = {
45
+ Authorization: `Bearer ${config.apiKey}`,
46
+ Accept: "application/json"
47
+ };
48
+ if (opts.body !== void 0) {
49
+ headers["Content-Type"] = "application/json";
50
+ }
51
+ let res;
52
+ try {
53
+ res = await fetch(url, {
54
+ method: opts.method,
55
+ headers,
56
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
57
+ signal: AbortSignal.timeout(opts.timeoutMs)
58
+ });
59
+ } catch (err) {
60
+ if (err instanceof Error && err.name === "TimeoutError") {
61
+ throw new ApiError(
62
+ "timeout",
63
+ `Request to ${opts.path} timed out after ${opts.timeoutMs}ms.`
64
+ );
65
+ }
66
+ const message = err instanceof Error ? err.message : String(err);
67
+ throw new ApiError(
68
+ "network_error",
69
+ `Network error calling ${opts.path}: ${message}`
70
+ );
71
+ }
72
+ const text = await res.text();
73
+ let payload = null;
74
+ if (text.length > 0) {
75
+ try {
76
+ payload = JSON.parse(text);
77
+ } catch {
78
+ }
79
+ }
80
+ if (!res.ok) {
81
+ const detail = payload && typeof payload === "object" && "error" in payload ? payload.error : null;
82
+ const code = detail?.code ?? `http_${res.status}`;
83
+ const message = (detail?.message ?? text) || `HTTP ${res.status} from ${opts.path}`;
84
+ throw new ApiError(code, message, res.status);
85
+ }
86
+ return payload;
87
+ }
88
+ async function postScan(config, body) {
89
+ return call(config, {
90
+ method: "POST",
91
+ path: "/api/v1/scans",
92
+ body,
93
+ timeoutMs: config.getTimeoutMs
94
+ });
95
+ }
96
+ async function getScan(config, id) {
97
+ return call(config, {
98
+ method: "GET",
99
+ path: `/api/v1/scans/${encodeURIComponent(id)}`,
100
+ timeoutMs: config.getTimeoutMs
101
+ });
102
+ }
103
+ async function listScans(config, opts = {}) {
104
+ const params = new URLSearchParams();
105
+ if (opts.limit !== void 0) params.set("limit", String(opts.limit));
106
+ if (opts.cursor) params.set("cursor", opts.cursor);
107
+ const query = params.toString();
108
+ return call(config, {
109
+ method: "GET",
110
+ path: `/api/v1/scans${query ? `?${query}` : ""}`,
111
+ timeoutMs: config.getTimeoutMs
112
+ });
113
+ }
114
+ async function postAsk(config, opts) {
115
+ const url = `${config.baseUrl}/api/v1/ask`;
116
+ const headers = {
117
+ "Content-Type": "application/json",
118
+ Accept: "application/json"
119
+ };
120
+ if (config.apiKey) headers.Authorization = `Bearer ${config.apiKey}`;
121
+ const payloadBody = {
122
+ query: { q: opts.q, itemType: opts.itemType },
123
+ prefer: opts.mode ? { mode: opts.mode } : void 0
124
+ };
125
+ let res;
126
+ try {
127
+ res = await fetch(url, {
128
+ method: "POST",
129
+ headers,
130
+ body: JSON.stringify(payloadBody),
131
+ signal: AbortSignal.timeout(config.getTimeoutMs)
132
+ });
133
+ } catch (err) {
134
+ if (err instanceof Error && err.name === "TimeoutError") {
135
+ throw new ApiError(
136
+ "timeout",
137
+ `Request to /api/v1/ask timed out after ${config.getTimeoutMs}ms.`
138
+ );
139
+ }
140
+ const message = err instanceof Error ? err.message : String(err);
141
+ throw new ApiError(
142
+ "network_error",
143
+ `Network error calling /api/v1/ask: ${message}`
144
+ );
145
+ }
146
+ const text = await res.text();
147
+ let payload = null;
148
+ if (text.length > 0) {
149
+ try {
150
+ payload = JSON.parse(text);
151
+ } catch {
152
+ }
153
+ }
154
+ if (payload && typeof payload === "object" && "_meta" in payload) {
155
+ return payload;
156
+ }
157
+ if (!res.ok) {
158
+ throw new ApiError(`http_${res.status}`, text || `HTTP ${res.status}`, res.status);
159
+ }
160
+ return payload;
161
+ }
162
+
163
+ // src/format.ts
164
+ var CODES = {
165
+ reset: "\x1B[0m",
166
+ bold: "\x1B[1m",
167
+ dim: "\x1B[2m",
168
+ red: "\x1B[31m",
169
+ green: "\x1B[32m",
170
+ yellow: "\x1B[33m",
171
+ cyan: "\x1B[36m",
172
+ gray: "\x1B[90m"
173
+ };
174
+ function makePainter(enabled) {
175
+ if (!enabled) return (_code, text) => text;
176
+ return (code, text) => `${CODES[code]}${text}${CODES.reset}`;
177
+ }
178
+ var STATUS_ICON = {
179
+ pass: "\u2714",
180
+ fail: "\u2718",
181
+ warn: "\u25B2",
182
+ error: "\u2718"
183
+ };
184
+ var STATUS_COLOR = {
185
+ pass: "green",
186
+ fail: "red",
187
+ warn: "yellow",
188
+ error: "red"
189
+ };
190
+ function scoreColor(score) {
191
+ if (score >= 80) return "green";
192
+ if (score >= 50) return "yellow";
193
+ return "red";
194
+ }
195
+ function formatScan(scan, paint) {
196
+ const lines = [];
197
+ lines.push("");
198
+ lines.push(
199
+ `${paint("bold", scan.rootUrl)} ${paint("gray", `(${scan.id})`)}`
200
+ );
201
+ lines.push("");
202
+ lines.push(
203
+ ` Vercel readability ${paint(
204
+ scoreColor(scan.vercelScore),
205
+ `${scan.vercelScore}/100`
206
+ )} ${paint("gray", scan.vercelRating.replace(/_/g, " "))}`
207
+ );
208
+ lines.push(
209
+ ` llms.txt ${paint(
210
+ scoreColor(scan.llmstxtScore),
211
+ `${scan.llmstxtScore}/100`
212
+ )}`
213
+ );
214
+ lines.push(
215
+ paint(
216
+ "gray",
217
+ ` ${scan.pagesScanned}/${scan.pagesDiscovered} pages scanned`
218
+ )
219
+ );
220
+ const sections = [
221
+ ["Site checks", scan.siteChecks],
222
+ ["llms.txt checks", scan.llmstxtChecks]
223
+ ];
224
+ for (const [title, checks] of sections) {
225
+ const failed = checks.filter(
226
+ (c) => c.status === "fail" || c.status === "error" || c.status === "warn"
227
+ );
228
+ if (failed.length === 0) continue;
229
+ lines.push("");
230
+ lines.push(paint("bold", ` ${title} \u2014 ${failed.length} need attention`));
231
+ for (const c of failed) {
232
+ lines.push(formatCheckLine(c, paint));
233
+ }
234
+ }
235
+ const totalIssues = countNonPassing(scan.siteChecks) + countNonPassing(scan.llmstxtChecks);
236
+ lines.push("");
237
+ if (totalIssues === 0) {
238
+ lines.push(paint("green", " All checks passed. \u{1F389}"));
239
+ } else {
240
+ lines.push(
241
+ paint("gray", ` ${totalIssues} check(s) need attention. `) + paint("cyan", `https://agent-ready.dev/scan/${scan.shareToken}`)
242
+ );
243
+ }
244
+ lines.push("");
245
+ return lines.join("\n");
246
+ }
247
+ function formatCheckLine(c, paint) {
248
+ const icon = paint(STATUS_COLOR[c.status], STATUS_ICON[c.status]);
249
+ const id = paint("gray", c.checkId.padEnd(4));
250
+ return ` ${icon} ${id} ${c.name}${c.message ? paint("dim", ` \u2014 ${c.message}`) : ""}`;
251
+ }
252
+ function countNonPassing(checks) {
253
+ return checks.filter((c) => c.status !== "pass").length;
254
+ }
255
+ function formatQueued(id, url, paint) {
256
+ return [
257
+ `${paint("green", "\u2714")} Scan queued for ${paint("bold", url)}`,
258
+ ` id: ${id}`,
259
+ paint("gray", ` poll: agent-ready get ${id}`)
260
+ ].join("\n");
261
+ }
262
+ function formatScanList(rows, paint) {
263
+ if (rows.length === 0) {
264
+ return paint("gray", "No scans yet. Run `agent-ready scan <url>` to start one.");
265
+ }
266
+ const lines = rows.map((r) => {
267
+ const score = r.vercelScore === null ? paint("gray", " --") : paint(scoreColor(r.vercelScore), String(r.vercelScore).padStart(3));
268
+ const id = paint("gray", r.id.padEnd(12));
269
+ const when = paint("dim", formatDate(r.createdAt));
270
+ return ` ${score} ${id} ${r.domain} ${when}`;
271
+ });
272
+ return [paint("bold", " score id domain created"), ...lines].join(
273
+ "\n"
274
+ );
275
+ }
276
+ function formatDate(iso) {
277
+ return iso.replace("T", " ").replace(/:\d\d\.\d+Z$/, "Z").slice(0, 16);
278
+ }
279
+ function formatAsk(payload, paint) {
280
+ if (!payload || typeof payload !== "object") {
281
+ return JSON.stringify(payload, null, 2);
282
+ }
283
+ const obj = payload;
284
+ const meta = obj._meta ?? {};
285
+ const results = Array.isArray(obj.results) ? obj.results : [];
286
+ const metaMessage = typeof meta.message === "string" ? meta.message : void 0;
287
+ if (results.length === 0) {
288
+ return paint("yellow", metaMessage ?? "No results.");
289
+ }
290
+ const lines = [];
291
+ for (const r of results) {
292
+ if (!r || typeof r !== "object") continue;
293
+ const item = r;
294
+ const name = typeof item.name === "string" ? item.name : "(untitled)";
295
+ const url = typeof item.url === "string" ? item.url : "";
296
+ const desc = typeof item.description === "string" ? item.description : "";
297
+ lines.push(paint("bold", `\u2022 ${name}`));
298
+ if (url) lines.push(paint("cyan", ` ${url}`));
299
+ if (desc) lines.push(paint("dim", ` ${desc}`));
300
+ }
301
+ return lines.join("\n");
302
+ }
303
+
304
+ // src/cli.ts
305
+ var VERSION = "0.1.0";
306
+ var realApi = { postScan, getScan, listScans, postAsk };
307
+ var HELP = `agent-ready \u2014 scan any URL for AI-agent readability (agent-ready.dev)
308
+
309
+ USAGE
310
+ agent-ready <command> [options]
311
+
312
+ COMMANDS
313
+ scan <url> Start a scan and wait for the result
314
+ get <id> Fetch a completed (or in-progress) scan by id
315
+ list List your recent scans
316
+ ask <query...> Natural-language search of Agent Ready's docs (no key needed)
317
+
318
+ GLOBAL OPTIONS
319
+ --json Output raw JSON instead of formatted text
320
+ --api-key <key> API key (overrides AGENT_READY_API_KEY)
321
+ --base-url <url> API base URL (overrides AGENT_READY_API_URL)
322
+ --no-color Disable coloured output
323
+ -h, --help Show this help
324
+ -v, --version Show version
325
+
326
+ SCAN OPTIONS
327
+ --page-limit <n> Max pages to crawl
328
+ --no-wait Queue the scan and print its id without polling
329
+ --poll-interval <s> Seconds between status polls (default 2)
330
+ --timeout <s> Max seconds to wait for completion (default 120)
331
+
332
+ LIST OPTIONS
333
+ --limit <n> Number of scans to return (1\u2013100, default 20)
334
+ --cursor <iso> Pagination cursor (nextCursor from a prior response)
335
+
336
+ ASK OPTIONS
337
+ --mode <list|summarize> Result style
338
+ --type <itemType> Restrict to: methodology | checks | specs | llms-txt | check
339
+
340
+ AUTH
341
+ scan, get, and list need a Pro API key \u2014 get one at
342
+ https://agent-ready.dev/dashboard/api-keys. ask is public.
343
+
344
+ EXAMPLES
345
+ agent-ready scan https://example.com
346
+ agent-ready scan https://example.com --json --page-limit 25
347
+ agent-ready get V1StGXR8_Z
348
+ agent-ready list --limit 5
349
+ agent-ready ask "how is the score calculated?"
350
+ `;
351
+ function parse(argv) {
352
+ const { values, positionals } = parseArgs({
353
+ args: argv,
354
+ allowPositionals: true,
355
+ options: {
356
+ json: { type: "boolean" },
357
+ "api-key": { type: "string" },
358
+ "base-url": { type: "string" },
359
+ "no-color": { type: "boolean" },
360
+ "no-wait": { type: "boolean" },
361
+ "page-limit": { type: "string" },
362
+ "poll-interval": { type: "string" },
363
+ timeout: { type: "string" },
364
+ limit: { type: "string" },
365
+ cursor: { type: "string" },
366
+ mode: { type: "string" },
367
+ type: { type: "string" },
368
+ help: { type: "boolean", short: "h" },
369
+ version: { type: "boolean", short: "v" }
370
+ }
371
+ });
372
+ return { values, positionals };
373
+ }
374
+ function resolveConfig(env, values) {
375
+ const config = createConfig(env);
376
+ if (typeof values["api-key"] === "string") config.apiKey = values["api-key"];
377
+ if (typeof values["base-url"] === "string") {
378
+ config.baseUrl = values["base-url"].replace(/\/+$/, "");
379
+ }
380
+ if (typeof values.timeout === "string") {
381
+ const secs = Number(values.timeout);
382
+ if (Number.isFinite(secs) && secs > 0) config.scanTimeoutMs = secs * 1e3;
383
+ }
384
+ return config;
385
+ }
386
+ function intOption(raw, name) {
387
+ if (typeof raw !== "string") return void 0;
388
+ const n = Number(raw);
389
+ if (!Number.isInteger(n) || n <= 0) {
390
+ throw new ApiError("invalid_request", `--${name} must be a positive integer.`);
391
+ }
392
+ return n;
393
+ }
394
+ async function run(argv, env, io2, api = realApi) {
395
+ let parsed;
396
+ try {
397
+ parsed = parse(argv);
398
+ } catch (err) {
399
+ io2.err(`Error: ${err instanceof Error ? err.message : String(err)}`);
400
+ io2.err("Run `agent-ready --help` for usage.");
401
+ return 2;
402
+ }
403
+ const { values, positionals } = parsed;
404
+ const command = positionals[0];
405
+ if (values.version) {
406
+ io2.out(VERSION);
407
+ return 0;
408
+ }
409
+ if (values.help || !command) {
410
+ io2.out(HELP);
411
+ return command ? 0 : values.help ? 0 : 1;
412
+ }
413
+ const color = io2.color && !values["no-color"];
414
+ const paint = makePainter(color);
415
+ const json = Boolean(values.json);
416
+ try {
417
+ switch (command) {
418
+ case "scan":
419
+ return await cmdScan(positionals.slice(1), values, env, io2, api, json, paint);
420
+ case "get":
421
+ return await cmdGet(positionals.slice(1), values, env, io2, api, json, paint);
422
+ case "list":
423
+ return await cmdList(values, env, io2, api, json, paint);
424
+ case "ask":
425
+ return await cmdAsk(positionals.slice(1), values, env, io2, api, json, paint);
426
+ default:
427
+ io2.err(`Unknown command: ${command}`);
428
+ io2.err("Run `agent-ready --help` for usage.");
429
+ return 2;
430
+ }
431
+ } catch (err) {
432
+ if (err instanceof ApiError) {
433
+ io2.err(`Error (${err.code}): ${err.message}`);
434
+ return 1;
435
+ }
436
+ io2.err(`Error: ${err instanceof Error ? err.message : String(err)}`);
437
+ return 1;
438
+ }
439
+ }
440
+ async function cmdScan(args, values, env, io2, api, json, paint) {
441
+ const url = args[0];
442
+ if (!url) {
443
+ io2.err("Usage: agent-ready scan <url> [--page-limit n] [--no-wait]");
444
+ return 2;
445
+ }
446
+ const config = resolveConfig(env, values);
447
+ const pageLimit = intOption(values["page-limit"], "page-limit");
448
+ const queued = await api.postScan(config, { url, pageLimit });
449
+ if (values["no-wait"]) {
450
+ if (json) io2.out(JSON.stringify(queued, null, 2));
451
+ else io2.out(formatQueued(queued.id, queued.url, paint));
452
+ return 0;
453
+ }
454
+ const pollSecs = intOption(values["poll-interval"], "poll-interval") ?? 2;
455
+ const deadline = Date.now() + config.scanTimeoutMs;
456
+ if (!json) io2.err(paint("gray", `Scanning ${url}\u2026`));
457
+ let scan = await api.getScan(config, queued.id);
458
+ while (scan.status === "running") {
459
+ if (Date.now() >= deadline) {
460
+ io2.err(
461
+ `Timed out waiting for scan ${queued.id} after ${Math.round(
462
+ config.scanTimeoutMs / 1e3
463
+ )}s. It may still finish \u2014 try \`agent-ready get ${queued.id}\`.`
464
+ );
465
+ return 1;
466
+ }
467
+ await io2.sleep(pollSecs * 1e3);
468
+ scan = await api.getScan(config, queued.id);
469
+ }
470
+ if (scan.status === "failed") {
471
+ io2.err(`Scan ${queued.id} failed.`);
472
+ if (json) io2.out(JSON.stringify(scan, null, 2));
473
+ return 1;
474
+ }
475
+ if (json) io2.out(JSON.stringify(scan, null, 2));
476
+ else io2.out(formatScan(scan, paint));
477
+ return 0;
478
+ }
479
+ async function cmdGet(args, values, env, io2, api, json, paint) {
480
+ const id = args[0];
481
+ if (!id) {
482
+ io2.err("Usage: agent-ready get <id>");
483
+ return 2;
484
+ }
485
+ const config = resolveConfig(env, values);
486
+ const scan = await api.getScan(config, id);
487
+ if (json) io2.out(JSON.stringify(scan, null, 2));
488
+ else if (scan.status === "running")
489
+ io2.out(paint("yellow", `Scan ${id} is still running. Try again shortly.`));
490
+ else io2.out(formatScan(scan, paint));
491
+ return 0;
492
+ }
493
+ async function cmdList(values, env, io2, api, json, paint) {
494
+ const config = resolveConfig(env, values);
495
+ const limit = intOption(values.limit, "limit");
496
+ const cursor = typeof values.cursor === "string" ? values.cursor : void 0;
497
+ const res = await api.listScans(config, { limit, cursor });
498
+ if (json) {
499
+ io2.out(JSON.stringify(res, null, 2));
500
+ return 0;
501
+ }
502
+ io2.out(formatScanList(res.data, paint));
503
+ if (res.nextCursor) {
504
+ io2.out(paint("gray", `
505
+ More: agent-ready list --cursor ${res.nextCursor}`));
506
+ }
507
+ return 0;
508
+ }
509
+ async function cmdAsk(args, values, env, io2, api, json, paint) {
510
+ const q = args.join(" ").trim();
511
+ if (!q) {
512
+ io2.err('Usage: agent-ready ask "<question>"');
513
+ return 2;
514
+ }
515
+ const config = resolveConfig(env, values);
516
+ const mode = values.mode === "list" || values.mode === "summarize" ? values.mode : void 0;
517
+ const itemType = typeof values.type === "string" ? values.type : void 0;
518
+ const payload = await api.postAsk(config, { q, mode, itemType });
519
+ if (json) io2.out(JSON.stringify(payload, null, 2));
520
+ else io2.out(formatAsk(payload, paint));
521
+ return 0;
522
+ }
523
+
524
+ // src/bin.ts
525
+ var io = {
526
+ out: (s) => process.stdout.write(s.endsWith("\n") ? s : `${s}
527
+ `),
528
+ err: (s) => process.stderr.write(s.endsWith("\n") ? s : `${s}
529
+ `),
530
+ // Colour when stdout is a TTY and NO_COLOR is unset (https://no-color.org).
531
+ color: process.stdout.isTTY === true && !process.env.NO_COLOR,
532
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
533
+ };
534
+ run(process.argv.slice(2), process.env, io).then((code) => {
535
+ process.exitCode = code;
536
+ }).catch((err) => {
537
+ process.stderr.write(
538
+ `Fatal: ${err instanceof Error ? err.message : String(err)}
539
+ `
540
+ );
541
+ process.exitCode = 1;
542
+ });
543
+ //# sourceMappingURL=cli.mjs.map
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "agent-ready-scanner",
3
+ "version": "0.1.0",
4
+ "description": "Command-line client for Agent Ready — scan any URL for AI-readability against the Vercel Agent Readability Spec, the llmstxt.org standard, and agent-protocol manifests. Wraps the agent-ready.dev REST API.",
5
+ "license": "MIT",
6
+ "author": "Agent Ready",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/mlava/agent-ready-cli.git"
10
+ },
11
+ "homepage": "https://agent-ready.dev",
12
+ "bugs": {
13
+ "url": "https://github.com/mlava/agent-ready-cli/issues"
14
+ },
15
+ "keywords": [
16
+ "cli",
17
+ "agent-readability",
18
+ "ai-search",
19
+ "llms-txt",
20
+ "agents-md",
21
+ "scanner",
22
+ "validator",
23
+ "vercel-spec",
24
+ "seo",
25
+ "nlweb"
26
+ ],
27
+ "type": "module",
28
+ "engines": {
29
+ "node": ">=20.10"
30
+ },
31
+ "bin": {
32
+ "agent-ready": "dist/cli.mjs"
33
+ },
34
+ "files": [
35
+ "dist/cli.mjs",
36
+ "README.md",
37
+ "LICENSE"
38
+ ],
39
+ "scripts": {
40
+ "build": "node scripts/build.mjs",
41
+ "dev": "node --experimental-strip-types src/bin.ts",
42
+ "test": "vitest run",
43
+ "test:coverage": "vitest run --coverage",
44
+ "typecheck": "tsc --noEmit",
45
+ "prepublishOnly": "npm run build"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^20.11.30",
49
+ "@vitest/coverage-v8": "^2.1.9",
50
+ "esbuild": "^0.25.8",
51
+ "typescript": "^5.9.3",
52
+ "vite-tsconfig-paths": "^5.1.4",
53
+ "vitest": "^2.1.9"
54
+ }
55
+ }