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.
- package/LICENSE +21 -0
- package/README.md +130 -0
- package/dist/cli.mjs +543 -0
- 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
|
+
}
|