ainative-ax 0.0.0-stage → 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/README.md +25 -2
- package/bin/ainative-ax.js +393 -0
- package/package.json +29 -4
package/README.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ainative-ax
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Score how ready a site is for AI agents, from your terminal.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx ainative-ax audit example.com
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Wraps the same scoring engine behind [ainative.ax](https://ainative.ax) — no separate backend, no API key required for a quick scan.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx ainative-ax audit <url> Run a quick agent-readiness scan
|
|
15
|
+
npx ainative-ax --help Show help
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Exits with status `1` if the scan is incomplete (score below max) or fails, so it can gate CI.
|
|
19
|
+
|
|
20
|
+
## What it checks
|
|
21
|
+
|
|
22
|
+
Discoverability, structured data, API documentation, authentication standards, security transparency, and more — see the full breakdown at [ainative.ax](https://ainative.ax) or run a scan yourself.
|
|
23
|
+
|
|
24
|
+
## Full reports
|
|
25
|
+
|
|
26
|
+
This CLI covers the quick scan. For stored reports, industry benchmarks, and remediation, visit [ainative.ax](https://ainative.ax).
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ainative-ax CLI — thin client for the real, already-live
|
|
6
|
+
* POST /api/v1/public/ax-audit endpoint. No new backend logic here;
|
|
7
|
+
* this mirrors services/auditService.ts's runAudit() response shape
|
|
8
|
+
* (AuditResult / AuditCheck) so CLI and web output never drift apart.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const https = require('https');
|
|
12
|
+
const { URL } = require('url');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const readline = require('readline');
|
|
17
|
+
|
|
18
|
+
const API_BASE = process.env.AINATIVE_AX_API_URL || 'https://ainative-browser-builder.up.railway.app';
|
|
19
|
+
const TIMEOUT_MS = 60000;
|
|
20
|
+
|
|
21
|
+
// Session token storage: a plain file under the user's config dir, not an
|
|
22
|
+
// OS keychain — this CLI is deliberately dependency-free, and a native
|
|
23
|
+
// keychain binding would mean a compiled dependency. Documented tradeoff,
|
|
24
|
+
// not an oversight. Permissions locked to the owner only (0600).
|
|
25
|
+
const CONFIG_DIR = path.join(os.homedir(), '.config', 'ainative-ax');
|
|
26
|
+
const TOKEN_FILE = path.join(CONFIG_DIR, 'token');
|
|
27
|
+
|
|
28
|
+
function saveToken(token) {
|
|
29
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
30
|
+
fs.writeFileSync(TOKEN_FILE, token, { mode: 0o600 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function loadToken() {
|
|
34
|
+
try {
|
|
35
|
+
return fs.readFileSync(TOKEN_FILE, 'utf8').trim() || null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function clearStoredToken() {
|
|
42
|
+
try {
|
|
43
|
+
fs.unlinkSync(TOKEN_FILE);
|
|
44
|
+
} catch {
|
|
45
|
+
// nothing to clear
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// One shared readline interface + its line async-iterator for the whole
|
|
50
|
+
// process, created lazily. A second `readline.createInterface()` call
|
|
51
|
+
// against the same stdin reliably fails to read anything on piped
|
|
52
|
+
// (non-TTY) input — stdin's data gets consumed by the first interface's
|
|
53
|
+
// internal buffering before the second one attaches. Verified directly:
|
|
54
|
+
// two separate interfaces drop the second prompt's answer every time.
|
|
55
|
+
let _rl;
|
|
56
|
+
let _lineIterator;
|
|
57
|
+
function getLineSource() {
|
|
58
|
+
if (!_rl) {
|
|
59
|
+
_rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
|
|
60
|
+
_lineIterator = _rl[Symbol.asyncIterator]();
|
|
61
|
+
}
|
|
62
|
+
return _lineIterator;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function prompt(question, { hidden = false } = {}) {
|
|
66
|
+
const iterator = getLineSource();
|
|
67
|
+
if (!hidden) {
|
|
68
|
+
process.stdout.write(question);
|
|
69
|
+
const { value } = await iterator.next();
|
|
70
|
+
return value || '';
|
|
71
|
+
}
|
|
72
|
+
// Hidden input: mute the interface's own echo of the piped/typed line
|
|
73
|
+
// while reading it, matching the common Node CLI password-prompt
|
|
74
|
+
// pattern (no extra dependency for this).
|
|
75
|
+
const outputStream = _rl._writeToOutput;
|
|
76
|
+
process.stdout.write(question);
|
|
77
|
+
_rl._writeToOutput = () => {};
|
|
78
|
+
const { value } = await iterator.next();
|
|
79
|
+
_rl._writeToOutput = outputStream;
|
|
80
|
+
process.stdout.write('\n');
|
|
81
|
+
return value || '';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function closePrompt() {
|
|
85
|
+
if (_rl) _rl.close();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function printUsage() {
|
|
89
|
+
console.log(`
|
|
90
|
+
ainative-ax — score how ready a site is for AI agents
|
|
91
|
+
|
|
92
|
+
Usage:
|
|
93
|
+
npx ainative-ax audit <url> [options] Run an agent-readiness scan
|
|
94
|
+
npx ainative-ax login Sign in (stores a session locally)
|
|
95
|
+
npx ainative-ax logout Clear your stored session
|
|
96
|
+
npx ainative-ax history [--json] List your past scans while signed in
|
|
97
|
+
npx ainative-ax --help Show this help
|
|
98
|
+
|
|
99
|
+
Audit options:
|
|
100
|
+
--json Print the raw API response as JSON (no color) —
|
|
101
|
+
pipeable into jq or a CI step. Exit code unchanged.
|
|
102
|
+
--full Run the full audit (agent simulation + benchmark
|
|
103
|
+
percentile) instead of the quick scan.
|
|
104
|
+
--industry <name> Industry for the full audit's benchmark comparison
|
|
105
|
+
(default: saas). Only used with --full.
|
|
106
|
+
|
|
107
|
+
Examples:
|
|
108
|
+
npx ainative-ax audit example.com
|
|
109
|
+
npx ainative-ax audit https://example.com --json
|
|
110
|
+
npx ainative-ax audit example.com --full --industry saas
|
|
111
|
+
npx ainative-ax login
|
|
112
|
+
npx ainative-ax history
|
|
113
|
+
|
|
114
|
+
Full reports, benchmarks, and remediation: https://ainative.ax
|
|
115
|
+
`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function postJSON(path, body, extraHeaders) {
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
const target = new URL(path, API_BASE);
|
|
121
|
+
const payload = JSON.stringify(body);
|
|
122
|
+
const req = https.request(
|
|
123
|
+
target,
|
|
124
|
+
{
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: {
|
|
127
|
+
'Content-Type': 'application/json',
|
|
128
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
129
|
+
...extraHeaders,
|
|
130
|
+
},
|
|
131
|
+
timeout: TIMEOUT_MS,
|
|
132
|
+
},
|
|
133
|
+
(res) => {
|
|
134
|
+
let data = '';
|
|
135
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
136
|
+
res.on('end', () => {
|
|
137
|
+
let parsed;
|
|
138
|
+
try {
|
|
139
|
+
parsed = data ? JSON.parse(data) : {};
|
|
140
|
+
} catch {
|
|
141
|
+
reject(new Error(`Unexpected response from ${target}: ${data.slice(0, 200)}`));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
145
|
+
reject(new Error(parsed.detail || `API error: ${res.statusCode}`));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
resolve(parsed);
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
req.on('timeout', () => {
|
|
153
|
+
req.destroy();
|
|
154
|
+
reject(new Error(`Request timed out after ${TIMEOUT_MS / 1000}s`));
|
|
155
|
+
});
|
|
156
|
+
req.on('error', reject);
|
|
157
|
+
req.write(payload);
|
|
158
|
+
req.end();
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function getJSON(path, extraHeaders) {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
const target = new URL(path, API_BASE);
|
|
165
|
+
const req = https.request(
|
|
166
|
+
target,
|
|
167
|
+
{ method: 'GET', headers: { ...extraHeaders }, timeout: TIMEOUT_MS },
|
|
168
|
+
(res) => {
|
|
169
|
+
let data = '';
|
|
170
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
171
|
+
res.on('end', () => {
|
|
172
|
+
let parsed;
|
|
173
|
+
try {
|
|
174
|
+
parsed = data ? JSON.parse(data) : {};
|
|
175
|
+
} catch {
|
|
176
|
+
reject(new Error(`Unexpected response from ${target}: ${data.slice(0, 200)}`));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
180
|
+
reject(new Error(parsed.detail || `API error: ${res.statusCode}`));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
resolve(parsed);
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
req.on('timeout', () => {
|
|
188
|
+
req.destroy();
|
|
189
|
+
reject(new Error(`Request timed out after ${TIMEOUT_MS / 1000}s`));
|
|
190
|
+
});
|
|
191
|
+
req.on('error', reject);
|
|
192
|
+
req.end();
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function normalizeUrl(input) {
|
|
197
|
+
if (!input) return null;
|
|
198
|
+
return /^https?:\/\//i.test(input) ? input : `https://${input}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function gradeColor(grade) {
|
|
202
|
+
if (grade.startsWith('A')) return '\x1b[32m'; // green
|
|
203
|
+
if (grade === 'B') return '\x1b[34m'; // blue
|
|
204
|
+
if (grade === 'C') return '\x1b[33m'; // yellow
|
|
205
|
+
return '\x1b[31m'; // red (D, F)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const RESET = '\x1b[0m';
|
|
209
|
+
const DIM = '\x1b[2m';
|
|
210
|
+
const BOLD = '\x1b[1m';
|
|
211
|
+
|
|
212
|
+
function printResult(result) {
|
|
213
|
+
const color = gradeColor(result.grade);
|
|
214
|
+
console.log('');
|
|
215
|
+
console.log(`${BOLD}AX Audit${RESET} ${DIM}·${RESET} ${result.url}`);
|
|
216
|
+
console.log(`${color}${BOLD}${result.score}/${result.max_score}${RESET} ${color}Grade ${result.grade}${RESET}`);
|
|
217
|
+
console.log('');
|
|
218
|
+
|
|
219
|
+
for (const check of result.checks || []) {
|
|
220
|
+
const mark = check.passed ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
|
|
221
|
+
const score = `${check.score}/${check.max_score}`;
|
|
222
|
+
console.log(` ${mark} ${check.name.padEnd(28)} ${DIM}${score.padStart(6)}${RESET}`);
|
|
223
|
+
if (check.details) {
|
|
224
|
+
console.log(` ${DIM}${check.details}${RESET}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
console.log('');
|
|
229
|
+
console.log(`${DIM}Full report, benchmarks, and remediation: https://ainative.ax${RESET}`);
|
|
230
|
+
console.log('');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function printFullExtras(result) {
|
|
234
|
+
if (result.benchmark) {
|
|
235
|
+
const b = result.benchmark;
|
|
236
|
+
console.log(`${DIM}Benchmark (${b.industry}):${RESET} ${b.measured ? `${BOLD}${b.percentile}th percentile${RESET} — measured` : `${DIM}${b.label}${RESET}`}`);
|
|
237
|
+
console.log('');
|
|
238
|
+
}
|
|
239
|
+
if (result.simulation) {
|
|
240
|
+
const s = result.simulation;
|
|
241
|
+
const mark = s.success ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
|
|
242
|
+
console.log(`${BOLD}Agent simulation${RESET} ${DIM}·${RESET} ${s.task}`);
|
|
243
|
+
console.log(` ${mark} ${s.success ? 'Completed' : `Stalled — ${s.failure_point || 'unknown step'}`} ${DIM}(${s.total_steps} steps, ${s.total_time_ms}ms)${RESET}`);
|
|
244
|
+
console.log('');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function runAudit(rawUrl, { json, full, industry } = {}) {
|
|
249
|
+
const url = normalizeUrl(rawUrl);
|
|
250
|
+
if (!url) {
|
|
251
|
+
if (json) {
|
|
252
|
+
console.error(JSON.stringify({ error: 'A URL is required. Try: npx ainative-ax audit example.com --json' }));
|
|
253
|
+
} else {
|
|
254
|
+
console.error('Error: a URL is required. Try: npx ainative-ax audit example.com');
|
|
255
|
+
}
|
|
256
|
+
process.exitCode = 1;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!json) process.stdout.write(`Scanning ${url}${full ? ' (full audit)' : ''} ...`);
|
|
261
|
+
try {
|
|
262
|
+
const result = full
|
|
263
|
+
? await postJSON('/api/v1/public/ax-audit/full', { url, industry: industry || 'saas' })
|
|
264
|
+
: await postJSON('/api/v1/public/ax-audit', { url });
|
|
265
|
+
if (json) {
|
|
266
|
+
console.log(JSON.stringify(result));
|
|
267
|
+
} else {
|
|
268
|
+
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
269
|
+
printResult(result);
|
|
270
|
+
if (full) printFullExtras(result);
|
|
271
|
+
}
|
|
272
|
+
if (typeof result.score === 'number' && typeof result.max_score === 'number' && result.score < result.max_score) {
|
|
273
|
+
process.exitCode = 1;
|
|
274
|
+
}
|
|
275
|
+
} catch (err) {
|
|
276
|
+
if (json) {
|
|
277
|
+
console.error(JSON.stringify({ error: err.message }));
|
|
278
|
+
} else {
|
|
279
|
+
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
280
|
+
console.error(`Scan failed: ${err.message}`);
|
|
281
|
+
}
|
|
282
|
+
process.exitCode = 1;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function runLogin() {
|
|
287
|
+
try {
|
|
288
|
+
const email = await prompt('Email: ');
|
|
289
|
+
const password = await prompt('Password: ', { hidden: true });
|
|
290
|
+
try {
|
|
291
|
+
const result = await postJSON('/api/v1/auth/login', { email, password });
|
|
292
|
+
if (!result.access_token) {
|
|
293
|
+
console.error('Login succeeded but no session token was returned — your account may need email verification first.');
|
|
294
|
+
process.exitCode = 1;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
saveToken(result.access_token);
|
|
298
|
+
console.log(`Signed in as ${result.user && result.user.email ? result.user.email : email}.`);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
console.error(`Login failed: ${err.message}`);
|
|
301
|
+
process.exitCode = 1;
|
|
302
|
+
}
|
|
303
|
+
} finally {
|
|
304
|
+
closePrompt();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function runLogout() {
|
|
309
|
+
clearStoredToken();
|
|
310
|
+
console.log('Signed out.');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function runHistory({ json } = {}) {
|
|
314
|
+
const token = loadToken();
|
|
315
|
+
if (!token) {
|
|
316
|
+
if (json) {
|
|
317
|
+
console.error(JSON.stringify({ error: 'Not signed in. Run: npx ainative-ax login' }));
|
|
318
|
+
} else {
|
|
319
|
+
console.error('Not signed in. Run: npx ainative-ax login');
|
|
320
|
+
}
|
|
321
|
+
process.exitCode = 1;
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
const result = await getJSON('/api/v1/public/ax-audit/my-scans', { Authorization: `Bearer ${token}` });
|
|
326
|
+
if (json) {
|
|
327
|
+
console.log(JSON.stringify(result));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const scans = result.scans || [];
|
|
331
|
+
if (scans.length === 0) {
|
|
332
|
+
console.log('No scans yet while signed in. Run: npx ainative-ax audit <url>');
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
console.log('');
|
|
336
|
+
console.log(`${BOLD}Scan history${RESET} ${DIM}· ${result.total} total${RESET}`);
|
|
337
|
+
console.log('');
|
|
338
|
+
for (const s of scans) {
|
|
339
|
+
const color = gradeColor(s.grade);
|
|
340
|
+
console.log(` ${color}${String(s.score).padStart(3)}${RESET} ${DIM}${s.grade.padEnd(3)}${RESET} ${s.url.padEnd(38)} ${DIM}${s.scanned_at.slice(0, 10)}${RESET}`);
|
|
341
|
+
}
|
|
342
|
+
console.log('');
|
|
343
|
+
} catch (err) {
|
|
344
|
+
if (json) {
|
|
345
|
+
console.error(JSON.stringify({ error: err.message }));
|
|
346
|
+
} else {
|
|
347
|
+
console.error(`Could not load scan history: ${err.message}`);
|
|
348
|
+
}
|
|
349
|
+
process.exitCode = 1;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function main() {
|
|
354
|
+
const [, , command, ...rest] = process.argv;
|
|
355
|
+
|
|
356
|
+
if (!command || command === '--help' || command === '-h') {
|
|
357
|
+
printUsage();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (command === 'audit') {
|
|
362
|
+
const json = rest.includes('--json');
|
|
363
|
+
const full = rest.includes('--full');
|
|
364
|
+
const industryIdx = rest.indexOf('--industry');
|
|
365
|
+
const industry = industryIdx !== -1 ? rest[industryIdx + 1] : undefined;
|
|
366
|
+
const skipIndexes = new Set([industryIdx, industryIdx === -1 ? -1 : industryIdx + 1]);
|
|
367
|
+
const target = rest.find((a, i) => a !== '--json' && a !== '--full' && !skipIndexes.has(i));
|
|
368
|
+
await runAudit(target, { json, full, industry });
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (command === 'login') {
|
|
373
|
+
await runLogin();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (command === 'logout') {
|
|
378
|
+
runLogout();
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (command === 'history') {
|
|
383
|
+
const json = rest.includes('--json');
|
|
384
|
+
await runHistory({ json });
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
console.error(`Unknown command: ${command}`);
|
|
389
|
+
printUsage();
|
|
390
|
+
process.exitCode = 1;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ainative-ax",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Score how ready a site is for AI agents — CLI for AINative's AX Audit. npx ainative-ax audit <url>",
|
|
5
|
+
"bin": {
|
|
6
|
+
"ainative-ax": "./bin/ainative-ax.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "./bin/ainative-ax.js",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/AINative-Studio/ainative-ax.git"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://ainative.ax",
|
|
18
|
+
"keywords": [
|
|
19
|
+
"ai",
|
|
20
|
+
"agents",
|
|
21
|
+
"agent-readiness",
|
|
22
|
+
"ax-audit",
|
|
23
|
+
"llms-txt",
|
|
24
|
+
"mcp",
|
|
25
|
+
"seo",
|
|
26
|
+
"aeo"
|
|
27
|
+
],
|
|
28
|
+
"files": [
|
|
29
|
+
"bin"
|
|
30
|
+
]
|
|
31
|
+
}
|