agentgrade-cli 1.0.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 (5) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +71 -0
  3. package/cli.mjs +337 -0
  4. package/lib/probe.mjs +1383 -0
  5. package/package.json +32 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Agentic Adventures
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,71 @@
1
+ # agentgrade
2
+
3
+ Scan any website for AI agent readiness.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx agentgrade https://example.com
9
+ ```
10
+
11
+ No install needed. Runs directly with `npx`.
12
+
13
+ ## What It Checks
14
+
15
+ - **Payment protocols** -- x402 (Coinbase), MPP (Machine Payments Protocol), L402 (Lightning), Stripe SPT
16
+ - **Discovery endpoints** -- `/.well-known/mpp.json`, `/.well-known/x402.json`, payment info headers
17
+ - **Bazaar discovery** -- x402 service catalogs, probe endpoints, Link headers
18
+ - **MCP servers** -- Model Context Protocol endpoints and tool counts
19
+ - **Claude plugins** -- `/.well-known/claude-plugin.json`
20
+ - **OpenAI plugins** -- `/.well-known/ai-plugin.json`
21
+ - **OpenAPI specs** -- `/openapi.json`, `/swagger.json`
22
+ - **llms.txt** -- LLM-friendly site descriptions
23
+ - **agents.txt** -- Agent permission declarations
24
+ - **robots.txt** -- AI agent-specific directives
25
+ - **Homepage scan** -- Mentions of payment protocols in page content
26
+
27
+ ## Usage
28
+
29
+ Scan a specific endpoint for 402 payment challenges plus all discovery:
30
+
31
+ ```bash
32
+ agentgrade https://api.example.com/v1/submit POST '{"title":"test"}'
33
+ ```
34
+
35
+ Run discovery checks only (skip the 402 probe):
36
+
37
+ ```bash
38
+ agentgrade https://example.com --discover-only
39
+ ```
40
+
41
+ Default method is POST. Pass GET explicitly for read endpoints:
42
+
43
+ ```bash
44
+ agentgrade https://api.example.com/v1/feed GET
45
+ ```
46
+
47
+ ## Output
48
+
49
+ The scanner prints color-coded terminal output organized into sections:
50
+
51
+ - **402 Probe** -- Sends the request and inspects response headers for x402, MPP, and L402 challenges
52
+ - **Discovery** -- Checks well-known endpoints for payment configuration files
53
+ - **Bazaar Discovery** -- Looks for x402 service catalogs and probe endpoints
54
+ - **Homepage Scan** -- Scans the homepage HTML for protocol mentions
55
+ - **Agent Capabilities** -- Finds MCP servers, plugins, OpenAPI specs, llms.txt, and more
56
+ - **Summary** -- Lists confirmed payment rails, documented protocols, and detected capabilities
57
+
58
+ Green checkmarks indicate detected features. Dimmed X marks indicate features not found.
59
+
60
+ ## Web Scanner
61
+
62
+ For a full web-based scan with a visual report, visit [agentgrade.com](https://agentgrade.com).
63
+
64
+ ## Links
65
+
66
+ - [AgentGrade web scanner](https://agentgrade.com)
67
+ - [Agentic Adventures](https://github.com/agentic-adventures)
68
+
69
+ ## License
70
+
71
+ MIT
package/cli.mjs ADDED
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * agentgrade CLI — scan any site for agent capabilities and payment protocols
5
+ *
6
+ * Usage:
7
+ * node cli.mjs <url> [method] [body]
8
+ * node cli.mjs <url> --discover-only
9
+ *
10
+ * Examples:
11
+ * node cli.mjs https://agentnews.xyz/api/v1/submit POST '{"title":"test"}'
12
+ * node cli.mjs https://agentnews.xyz --discover-only
13
+ */
14
+
15
+ import { probe402, probeDiscovery, probeBazaar, probeHomepage, probeCapabilities } from './lib/probe.mjs';
16
+
17
+ const BOLD = '\x1b[1m';
18
+ const DIM = '\x1b[2m';
19
+ const GREEN = '\x1b[32m';
20
+ const YELLOW = '\x1b[33m';
21
+ const CYAN = '\x1b[36m';
22
+ const RED = '\x1b[31m';
23
+ const RESET = '\x1b[0m';
24
+
25
+ const args = process.argv.slice(2).filter(a => !a.startsWith('--'));
26
+ const flags = new Set(process.argv.slice(2).filter(a => a.startsWith('--')));
27
+ const discoverOnly = flags.has('--discover-only');
28
+
29
+ if (args.length === 0) {
30
+ console.log(`\n${BOLD}agentgrade${RESET} — scan any site for agent capabilities\n`);
31
+ console.log(`Usage:`);
32
+ console.log(` node cli.mjs <url> [method] [body]`);
33
+ console.log(` node cli.mjs <url> --discover-only\n`);
34
+ console.log(`Examples:`);
35
+ console.log(` node cli.mjs https://agentnews.xyz/api/v1/submit POST '{"title":"test"}'`);
36
+ console.log(` node cli.mjs https://agentnews.xyz --discover-only\n`);
37
+ process.exit(0);
38
+ }
39
+
40
+ const url = new URL(args[0]);
41
+ const method = (args[1] || 'POST').toUpperCase();
42
+ const body = args[2] || undefined;
43
+ const origin = url.origin;
44
+ const base = origin + url.pathname.replace(/\/$/, '');
45
+
46
+ // ─── Formatting helpers ────────────────────────────────────────────────────
47
+
48
+ function section(title) { console.log(`\n${BOLD}${title}${RESET}`); }
49
+ function found(protocol, details) { console.log(` ${GREEN}✓ ${protocol}${RESET} ${details}`); }
50
+ function notFound(protocol, reason) { console.log(` ${DIM}✗ ${protocol} ${reason}${RESET}`); }
51
+ function info(label, value) { console.log(` ${CYAN}${label}:${RESET} ${value}`); }
52
+ function warn(msg) { console.log(` ${YELLOW}${msg}${RESET}`); }
53
+
54
+ // ─── 402 Probe ──────────────────────────────────────────────────────────────
55
+
56
+ const rails = [];
57
+ let cfBlocked = false;
58
+
59
+ if (!discoverOnly) {
60
+ section(`Probing ${method} ${url.href}`);
61
+
62
+ const result = await probe402(url.href, method, body);
63
+ info('Status', `${result.status}${result.error ? ` (${result.error})` : ''}`);
64
+
65
+ if (result.status !== 402) {
66
+ if (result.cfBlocked) {
67
+ cfBlocked = true;
68
+ console.log(` ${RED}Cloudflare blocked${RESET} — bot challenge intercepted the request. Agents cannot reach this endpoint.`);
69
+ } else if (result.status >= 200 && result.status < 300) {
70
+ warn(`No payment required (${result.status}) — may use quota-based 402`);
71
+ } else if (result.error) {
72
+ console.log(` ${RED}Error:${RESET} ${result.error}`);
73
+ } else {
74
+ warn(`Got ${result.status} — not a 402. Try a different method or path.`);
75
+ }
76
+ if (result.bodyMentions.length > 0) {
77
+ found('Body scan', `mentions: ${result.bodyMentions.join(', ')}`);
78
+ }
79
+ } else {
80
+ // x402
81
+ if (result.x402) {
82
+ if (result.x402.decodeFailed) {
83
+ found('x402', `Payment-Required header present (could not decode)`);
84
+ info(' Raw', result.x402.raw);
85
+ } else {
86
+ found('x402', `V${result.x402.version} — ${result.x402.options.length} payment option(s)`);
87
+ if (result.x402.description) info(' Description', result.x402.description);
88
+ for (const opt of result.x402.options) {
89
+ info(' Network', opt.network);
90
+ info(' Amount', `${opt.amount}${opt.assetName ? ` (${opt.assetName})` : ''}`);
91
+ info(' Asset', opt.asset);
92
+ info(' Pay to', opt.payTo);
93
+ if (opt.scheme) info(' Scheme', opt.scheme);
94
+ if (opt.maxTimeoutSeconds) info(' Timeout', `${opt.maxTimeoutSeconds}s`);
95
+ console.log();
96
+ }
97
+ if (result.x402.extensions) {
98
+ info(' Extensions', Object.keys(result.x402.extensions).join(', '));
99
+ if (result.x402.bazaar) {
100
+ const bz = result.x402.bazaar;
101
+ found(' Bazaar', `discoverable=${bz.discoverable ?? '?'}`);
102
+ if (bz.description) info(' Description', bz.description);
103
+ if (bz.inputSchema) info(' Input schema', JSON.stringify(bz.inputSchema));
104
+ if (bz.outputSchema) info(' Output schema', JSON.stringify(bz.outputSchema));
105
+ }
106
+ }
107
+ }
108
+ rails.push('x402');
109
+ } else {
110
+ notFound('x402', 'no Payment-Required header');
111
+ }
112
+
113
+ // MPP
114
+ if (result.mpp) {
115
+ found('MPP', 'WWW-Authenticate: Payment');
116
+ for (const [k, v] of Object.entries(result.mpp.fields)) info(` ${k}`, v);
117
+ rails.push('MPP');
118
+ } else {
119
+ notFound('MPP', 'no WWW-Authenticate: Payment header');
120
+ }
121
+
122
+ // L402
123
+ if (result.l402) {
124
+ found('L402', `WWW-Authenticate: ${result.l402.header}`);
125
+ if (result.l402.macaroon) info(' Macaroon', result.l402.macaroon.slice(0, 60) + '...');
126
+ if (result.l402.invoice) info(' Invoice', result.l402.invoice.slice(0, 60) + '...');
127
+ rails.push('L402');
128
+ } else {
129
+ notFound('L402', 'no L402/LSAT in WWW-Authenticate');
130
+ }
131
+
132
+ // Other headers
133
+ if (result.otherHeaders.length > 0) {
134
+ console.log(`\n ${DIM}Other payment-related headers:${RESET}`);
135
+ for (const h of result.otherHeaders) info(` ${h.name}`, h.value);
136
+ }
137
+ }
138
+ }
139
+
140
+ // ─── Discovery ──────────────────────────────────────────────────────────────
141
+
142
+ section(`Discovery (${origin})`);
143
+
144
+ const discovery = await probeDiscovery(origin);
145
+ for (const ep of discovery.endpoints) {
146
+ if (ep.hasPayment || ep.mentions.length > 0) {
147
+ const detail = ep.mentions.length > 0 ? `mentions: ${ep.mentions.join(', ')}` : 'payment info found';
148
+ found(ep.path, `${ep.status} — ${detail}`);
149
+ if (ep.settlementRails) {
150
+ for (const rail of ep.settlementRails) {
151
+ info(` ${rail.name}`, `${rail.protocol} — ${rail.currency} (${rail.status})`);
152
+ if (!rails.includes(rail.protocol)) rails.push(rail.protocol);
153
+ }
154
+ }
155
+ if (ep.paymentMode) info(' payment_mode', ep.paymentMode);
156
+ if (ep.paymentInfo) info(' payment_info', ep.paymentInfo);
157
+ } else {
158
+ info(ep.path, `${ep.status} (no payment info)`);
159
+ }
160
+ }
161
+
162
+ // ─── Bazaar Discovery ───────────────────────────────────────────────────────
163
+
164
+ section(`Bazaar Discovery (${origin})`);
165
+
166
+ const bazaar = await probeBazaar(origin);
167
+
168
+ if (bazaar.x402Json) {
169
+ found('/.well-known/x402.json', 'x402 service catalog found');
170
+ const bf = bazaar.x402Json.fields || {};
171
+ const showField = (label, key) => {
172
+ const d = bf[key];
173
+ if (!d || d.status === 'ok') { if (bazaar.x402Json[key]) info(` ${label}`, bazaar.x402Json[key]); return; }
174
+ if (d.status === 'misnamed') warn(` ${label}: ${d.hint}`);
175
+ else if (d.status === 'empty') warn(` ${label}: ${d.hint}`);
176
+ else warn(` ${label}: ✗ ${d.hint}`);
177
+ };
178
+ showField('Provider', 'name');
179
+ showField('Network', 'network');
180
+ showField('Facilitator', 'facilitator');
181
+ showField('Pay to', 'payTo');
182
+ if (bazaar.x402Json.testnet) warn(' ⚠ Testnet mode');
183
+ if (bazaar.x402Json.services.length > 0) {
184
+ info(' Services', `${bazaar.x402Json.services.length} paid endpoint(s)`);
185
+ for (const svc of bazaar.x402Json.services) {
186
+ const bazaarTag = svc.bazaar?.discoverable ? ` ${GREEN}[Bazaar: discoverable]${RESET}` : '';
187
+ info(` ${svc.method} ${svc.path}`, `${svc.amount}${bazaarTag}`);
188
+ if (svc.description) info(' ', svc.description);
189
+ if (svc.bazaar) {
190
+ if (svc.bazaar.inputSchema) info(' Input', JSON.stringify(svc.bazaar.inputSchema));
191
+ if (svc.bazaar.outputSchema) info(' Output', JSON.stringify(svc.bazaar.outputSchema));
192
+ }
193
+ }
194
+ }
195
+ if (bazaar.x402Json.freeEndpoints.length > 0) {
196
+ info(' Free endpoints', `${bazaar.x402Json.freeEndpoints.length}`);
197
+ for (const ep of bazaar.x402Json.freeEndpoints) {
198
+ info(` ${ep.method} ${ep.path}`, ep.description);
199
+ }
200
+ }
201
+ if (!rails.includes('x402')) rails.push('x402');
202
+ } else {
203
+ notFound('/.well-known/x402.json', 'not found');
204
+ }
205
+
206
+ if (bazaar.x402Probe) {
207
+ found('/.well-known/x402-probe', 'returns 402 — live challenges');
208
+ if (bazaar.x402Probe.version) info(' Payment-Required', `x402 V${bazaar.x402Probe.version}`);
209
+ if (bazaar.x402Probe.headerBazaar) found(' Bazaar in header', `discoverable=${bazaar.x402Probe.headerBazaar.discoverable}`);
210
+ if (bazaar.x402Probe.challenges.length > 0) {
211
+ info(' Challenges', `${bazaar.x402Probe.challenges.length} endpoint(s)`);
212
+ for (const ch of bazaar.x402Probe.challenges) {
213
+ const tag = ch.bazaar ? ` ${GREEN}[Bazaar]${RESET}` : '';
214
+ info(` ${ch.endpoint}`, `${ch.amount} ${ch.assetName || ''}${tag}`);
215
+ }
216
+ }
217
+ if (!rails.includes('x402')) rails.push('x402');
218
+ } else {
219
+ notFound('/.well-known/x402-probe', 'not found');
220
+ }
221
+
222
+ if (bazaar.linkHeader) {
223
+ found('Link header', bazaar.linkHeader);
224
+ } else {
225
+ notFound('Link header', 'no x402 discovery link');
226
+ }
227
+
228
+ // ─── Homepage Scan ──────────────────────────────────────────────────────────
229
+
230
+ if (url.pathname === '/' || discoverOnly) {
231
+ section(`Homepage scan (${origin})`);
232
+ const homepage = await probeHomepage(origin);
233
+ const keys = Object.keys(homepage.protocols);
234
+ if (keys.length > 0) {
235
+ for (const [proto, hints] of Object.entries(homepage.protocols)) {
236
+ const detail = hints.length > 0 ? ` (${hints.join(', ')})` : '';
237
+ found('Homepage', `mentions ${proto}${detail}`);
238
+ if (proto !== '402 flow' && !rails.includes(proto) && !rails.includes(`${proto} (documented)`)) {
239
+ rails.push(`${proto} (documented)`);
240
+ }
241
+ }
242
+ } else {
243
+ notFound('Homepage', 'no payment protocol mentions found');
244
+ }
245
+ }
246
+
247
+ // ─── Agent Capabilities ────────────────────────────────────────────────────
248
+
249
+ section(`Agent Capabilities (${base})`);
250
+
251
+ const rawCapabilities = await probeCapabilities(origin, base);
252
+ const capabilities = rawCapabilities.filter(c => !c.cfBlocked);
253
+ const cfBlockedCaps = rawCapabilities.filter(c => c.cfBlocked);
254
+ const capTypes = new Set();
255
+
256
+ for (const cap of cfBlockedCaps) {
257
+ console.log(` ${RED}✗ ${cap.type}${RESET} ${DIM}blocked by Cloudflare${RESET}`);
258
+ }
259
+
260
+ for (const cap of capabilities) {
261
+ capTypes.add(cap.type);
262
+ switch (cap.type) {
263
+ case 'MCP':
264
+ found('MCP', `${cap.path} — ${cap.name || cap.detail || 'endpoint found'}`);
265
+ if (cap.tools) info(' Tools', `${cap.tools} tool(s)`);
266
+ if (cap.name) info(' Name', cap.name);
267
+ if (cap.description) info(' Description', cap.description);
268
+ if (cap.transport) info(' Transport', cap.transport);
269
+ if (cap.protocolVersion) info(' Protocol', cap.protocolVersion);
270
+ if (cap.sseSupported) info(' SSE', 'supported');
271
+ if (cap.cors) info(' CORS', 'enabled');
272
+ break;
273
+ case 'Claude Plugin':
274
+ found('Claude Plugin', cap.path);
275
+ if (cap.name) info(' Name', cap.name);
276
+ if (cap.description) info(' Description', cap.description);
277
+ if (cap.version) info(' Version', cap.version);
278
+ if (cap.skills) info(' Skills', `${cap.skills} skill(s)`);
279
+ if (cap.tools) info(' Tools', `${cap.tools} tool(s)`);
280
+ break;
281
+ case 'AI Plugin':
282
+ found('AI Plugin', cap.path);
283
+ if (cap.name) info(' Name', cap.name);
284
+ if (cap.modelName) info(' Model name', cap.modelName);
285
+ if (cap.description) info(' Description', cap.description);
286
+ if (cap.apiUrl) info(' API spec', cap.apiUrl);
287
+ if (cap.auth) info(' Auth', cap.auth);
288
+ break;
289
+ case 'Skills':
290
+ found('Skills', `${cap.path} — ${cap.count ? cap.count + ' skill(s)' : cap.bytes + ' bytes'}`);
291
+ break;
292
+ case 'OpenAPI':
293
+ found('OpenAPI', `${cap.path} — ${cap.version} "${cap.title}" (${cap.paths} paths)`);
294
+ break;
295
+ case 'llms.txt':
296
+ found('llms.txt', `${cap.path} — ${cap.lines} lines`);
297
+ if (cap.preview) info(' ', cap.preview);
298
+ break;
299
+ case 'agents.txt':
300
+ found('agents.txt', `${cap.lines} lines`);
301
+ break;
302
+ case 'robots.txt (agent-aware)':
303
+ found('robots.txt', 'agent-relevant directives found');
304
+ for (const d of cap.directives) info(' ', d);
305
+ break;
306
+ default:
307
+ found(cap.type, cap.path);
308
+ }
309
+ }
310
+
311
+ // Not-found for types we checked but didn't find
312
+ const cfBlockedCapTypes = new Set(cfBlockedCaps.map(c => c.type));
313
+ for (const type of ['MCP', 'Claude Plugin', 'AI Plugin', 'Skills', 'OpenAPI', 'llms.txt', 'agents.txt']) {
314
+ if (!capTypes.has(type) && !cfBlockedCapTypes.has(type)) notFound(type, `no ${type.toLowerCase()} found`);
315
+ }
316
+
317
+ // ─── Summary ────────────────────────────────────────────────────────────────
318
+
319
+ section('Summary');
320
+
321
+ const confirmed = rails.filter(r => !r.includes('(documented)'));
322
+ const documented = rails.filter(r => r.includes('(documented)'));
323
+ if (confirmed.length > 0) console.log(` ${GREEN}${BOLD}Confirmed (402 headers): ${confirmed.join(', ')}${RESET}`);
324
+ if (documented.length > 0) console.log(` ${YELLOW}${BOLD}Documented (mentioned in pages): ${documented.join(', ')}${RESET}`);
325
+ if (bazaar.bazaarServices > 0) {
326
+ console.log(` ${GREEN}${BOLD}Bazaar: ${bazaar.bazaarServices} discoverable service(s)${RESET}`);
327
+ } else {
328
+ console.log(` ${DIM}Bazaar: no discoverable services${RESET}`);
329
+ }
330
+ if (rails.length === 0 && !cfBlocked) console.log(` ${YELLOW}No payment rails detected${RESET}`);
331
+ if (rails.length === 0 && cfBlocked) console.log(` ${RED}No payment rails detected — Cloudflare blocked all probes${RESET}`);
332
+ if (capTypes.size > 0) {
333
+ console.log(` ${GREEN}${BOLD}Agent capabilities: ${[...capTypes].join(', ')}${RESET}`);
334
+ } else {
335
+ console.log(` ${DIM}No agent capabilities detected${RESET}`);
336
+ }
337
+ console.log();