ainative-ax-audit 0.0.1 → 0.2.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 CHANGED
@@ -1,5 +1,26 @@
1
- # AX Audit by AINative
1
+ # ainative-ax
2
2
 
3
- Score any site's agent readiness — how well AI agents can discover, understand, authenticate with, and transact with it.
3
+ Score how ready a site is for AI agents, from your terminal.
4
4
 
5
- Full CLI launching soon. Try the free scan now at **https://ainative.ax**.
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).
package/ainative-ax.js ADDED
@@ -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,25 +1,12 @@
1
1
  {
2
2
  "name": "ainative-ax-audit",
3
- "version": "0.0.1",
4
- "description": "AX Audit by AINative — score any site's agent readiness. Full CLI coming soon: https://ainative.ax",
5
- "bin": {
6
- "ainative-ax-audit": "./index.js"
7
- },
8
- "main": "index.js",
3
+ "version": "0.2.0",
4
+ "description": "Score how ready a site is for AI agents — CLI for AINative's AX Audit. npx ainative-ax-audit audit <url>",
5
+ "bin": { "ainative-ax-audit": "ainative-ax.js" },
6
+ "main": "./ainative-ax.js",
7
+ "engines": { "node": ">=18" },
9
8
  "license": "MIT",
10
- "repository": {
11
- "type": "git",
12
- "url": "https://github.com/AINative-Studio/ainative-ax"
13
- },
9
+ "repository": { "type": "git", "url": "git+https://github.com/AINative-Studio/ainative-ax.git" },
14
10
  "homepage": "https://ainative.ax",
15
- "keywords": [
16
- "ai",
17
- "agents",
18
- "agent-readiness",
19
- "ax-audit",
20
- "seo",
21
- "aeo",
22
- "llms-txt",
23
- "mcp"
24
- ]
11
+ "keywords": ["ai", "agents", "agent-readiness", "ax-audit", "llms-txt", "mcp", "seo", "aeo"]
25
12
  }
package/index.js DELETED
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- console.log("AX Audit by AINative is launching soon.");
3
- console.log("In the meantime, run a scan at https://ainative.ax");