@getvetai/cli 0.1.0 → 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 +32 -12
- package/dist/commands/find.d.ts +0 -1
- package/dist/commands/find.js +3 -9
- package/dist/commands/scan.d.ts +2 -0
- package/dist/commands/scan.js +56 -1
- package/dist/index.js +3 -2
- package/dist/utils/api.d.ts +3 -0
- package/dist/utils/api.js +46 -0
- package/dist/utils/config.js +54 -0
- package/dist/utils/display.d.ts +5 -1
- package/dist/utils/display.js +11 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,19 +10,35 @@ Security audit CLI for AI skills and MCP servers. Scan, audit, and score tools b
|
|
|
10
10
|
npm install -g @getvetai/cli
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## What's New in v0.2.0
|
|
14
|
+
|
|
15
|
+
- **Registry integration** — instant results for known tools from the getvet.ai catalog
|
|
16
|
+
- **Expanded MCP config discovery** — Claude, Cursor, VS Code, Windsurf, Cline, Zed, Continue, OpenClaw
|
|
17
|
+
- **`--offline` flag** — skip registry lookup for air-gapped environments
|
|
18
|
+
- **`--deep` flag** — request a deep scan from the registry
|
|
19
|
+
- **Better display** — trust score bars, registry links, badge indicators
|
|
20
|
+
|
|
13
21
|
## Commands
|
|
14
22
|
|
|
15
23
|
### `vet scan <target>`
|
|
16
24
|
|
|
17
25
|
Scan a single tool for security issues. Accepts file paths, URLs, npm packages, or GitHub repos.
|
|
18
26
|
|
|
27
|
+
For npm packages, the CLI first checks the getvet.ai registry for existing scan results. If a deep scan is available, it returns instantly without local analysis.
|
|
28
|
+
|
|
19
29
|
```bash
|
|
30
|
+
# Scan an npm package (checks registry first)
|
|
31
|
+
vet scan @modelcontextprotocol/server-filesystem
|
|
32
|
+
|
|
33
|
+
# Skip registry, local analysis only
|
|
34
|
+
vet scan @modelcontextprotocol/server-filesystem --offline
|
|
35
|
+
|
|
36
|
+
# Request a deep scan from registry
|
|
37
|
+
vet scan @modelcontextprotocol/server-filesystem --deep
|
|
38
|
+
|
|
20
39
|
# Scan a local SKILL.md
|
|
21
40
|
vet scan ./my-skill/SKILL.md
|
|
22
41
|
|
|
23
|
-
# Scan an npm package
|
|
24
|
-
vet scan @modelcontextprotocol/server-filesystem
|
|
25
|
-
|
|
26
42
|
# Scan a GitHub repo
|
|
27
43
|
vet scan https://github.com/modelcontextprotocol/servers
|
|
28
44
|
|
|
@@ -30,11 +46,22 @@ vet scan https://github.com/modelcontextprotocol/servers
|
|
|
30
46
|
vet scan ./SKILL.md --json
|
|
31
47
|
```
|
|
32
48
|
|
|
33
|
-
**Output includes:** trust score, badge (certified/reviewed/unverified/flagged), detected permissions, security issues, risk factors, and
|
|
49
|
+
**Output includes:** trust score, badge (certified/reviewed/unverified/flagged), detected permissions, security issues, risk factors, tools list, and registry link.
|
|
34
50
|
|
|
35
51
|
### `vet audit [path]`
|
|
36
52
|
|
|
37
|
-
Audit all AI tools in a project. Discovers tools from
|
|
53
|
+
Audit all AI tools in a project. Discovers tools from:
|
|
54
|
+
|
|
55
|
+
- `package.json` (MCP dependencies)
|
|
56
|
+
- Cursor (`.cursor/mcp.json`)
|
|
57
|
+
- Claude Desktop (`claude_desktop_config.json`)
|
|
58
|
+
- VS Code (`settings.json` → `mcp.servers`)
|
|
59
|
+
- Windsurf (`mcp.json`)
|
|
60
|
+
- Cline (`mcp_settings.json`)
|
|
61
|
+
- Zed (`settings.json` → `mcp`)
|
|
62
|
+
- Continue (`config.json` → `mcpServers`)
|
|
63
|
+
- OpenClaw (`openclaw.json`)
|
|
64
|
+
- `SKILL.md` files
|
|
38
65
|
|
|
39
66
|
```bash
|
|
40
67
|
# Audit current directory
|
|
@@ -61,9 +88,6 @@ vet find "database query tool"
|
|
|
61
88
|
|
|
62
89
|
# JSON output
|
|
63
90
|
vet find "weather" --json
|
|
64
|
-
|
|
65
|
-
# Use local API
|
|
66
|
-
vet find "search" --api http://localhost:3300
|
|
67
91
|
```
|
|
68
92
|
|
|
69
93
|
### `vet install <package>`
|
|
@@ -98,10 +122,6 @@ vet install --skill weather
|
|
|
98
122
|
|
|
99
123
|
**MCP-specific:** tool parameter analysis, transport detection (stdio/http/sse), runtime detection, environment variable scanning.
|
|
100
124
|
|
|
101
|
-
## API
|
|
102
|
-
|
|
103
|
-
By default, `vet find` uses the public API at `https://getvet.ai`. Use `--api` to point to a different instance.
|
|
104
|
-
|
|
105
125
|
## License
|
|
106
126
|
|
|
107
127
|
MIT — [getvet.ai](https://getvet.ai)
|
package/dist/commands/find.d.ts
CHANGED
package/dist/commands/find.js
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import ora from 'ora';
|
|
2
|
+
import { searchTools } from '../utils/api.js';
|
|
2
3
|
import { displayFindResults } from '../utils/display.js';
|
|
3
4
|
export async function findCommand(query, options) {
|
|
4
5
|
const spinner = ora(`Searching "${query}"...`).start();
|
|
5
|
-
const api = options.api || 'https://getvet.ai';
|
|
6
6
|
try {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
throw new Error(`API returned ${r.status}`);
|
|
10
|
-
const data = await r.json();
|
|
11
|
-
// Handle both array response and { results: [...] } response
|
|
12
|
-
const items = Array.isArray(data) ? data : (data.results || []);
|
|
13
|
-
const results = items.map(x => ({
|
|
7
|
+
const items = await searchTools(query);
|
|
8
|
+
const results = items.map((x) => ({
|
|
14
9
|
name: x.name,
|
|
15
10
|
slug: x.slug,
|
|
16
11
|
description: x.description,
|
|
@@ -28,7 +23,6 @@ export async function findCommand(query, options) {
|
|
|
28
23
|
}
|
|
29
24
|
catch (err) {
|
|
30
25
|
spinner.fail(`Search failed: ${err.message}`);
|
|
31
|
-
console.log(' Tip: use --api http://localhost:3300 for local dev');
|
|
32
26
|
process.exitCode = 1;
|
|
33
27
|
}
|
|
34
28
|
}
|
package/dist/commands/scan.d.ts
CHANGED
package/dist/commands/scan.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import chalk from 'chalk';
|
|
2
3
|
import ora from 'ora';
|
|
3
4
|
import { analyzeSkill } from '../utils/analyzer.js';
|
|
4
5
|
import { analyzeMcp } from '../utils/mcp-analyzer.js';
|
|
5
6
|
import { displayScanReport } from '../utils/display.js';
|
|
7
|
+
import { lookupTool, requestDeepScan } from '../utils/api.js';
|
|
6
8
|
function detect(t) {
|
|
7
9
|
if (/^https?:\/\/github\.com\//.test(t))
|
|
8
10
|
return 'github';
|
|
@@ -17,6 +19,59 @@ export async function scanCommand(target, options) {
|
|
|
17
19
|
try {
|
|
18
20
|
const tt = detect(target);
|
|
19
21
|
let result;
|
|
22
|
+
let registrySlug;
|
|
23
|
+
// Try registry lookup first for npm packages (unless --offline)
|
|
24
|
+
if (tt === 'npm' && !options.offline) {
|
|
25
|
+
spinner.text = `Checking Vet registry for ${target}...`;
|
|
26
|
+
const registryData = await lookupTool(target);
|
|
27
|
+
if (registryData && registryData.scanTier === 'deep') {
|
|
28
|
+
spinner.succeed(chalk.green('✓ Found in Vet registry (deep scan available)'));
|
|
29
|
+
registrySlug = registryData.slug || target;
|
|
30
|
+
result = {
|
|
31
|
+
name: registryData.name || target,
|
|
32
|
+
description: registryData.description,
|
|
33
|
+
type: registryData.type || 'mcp',
|
|
34
|
+
transport: registryData.transport,
|
|
35
|
+
runtime: registryData.runtime,
|
|
36
|
+
tools: registryData.tools || [],
|
|
37
|
+
permissions: registryData.permissions || [],
|
|
38
|
+
issues: registryData.issues || [],
|
|
39
|
+
trustScore: registryData.trustScore ?? registryData.trust_score ?? 50,
|
|
40
|
+
badge: registryData.badge || 'unverified',
|
|
41
|
+
codeQuality: registryData.codeQuality || { hasTests: false, hasDocs: false, linesOfCode: 0 },
|
|
42
|
+
riskFactors: registryData.riskFactors || [],
|
|
43
|
+
overview: registryData.overview,
|
|
44
|
+
envVars: registryData.envVars,
|
|
45
|
+
};
|
|
46
|
+
if (options.json)
|
|
47
|
+
console.log(JSON.stringify(result, null, 2));
|
|
48
|
+
else
|
|
49
|
+
displayScanReport(result, registrySlug);
|
|
50
|
+
process.exitCode = result.badge === 'flagged' ? 1 : 0;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (registryData) {
|
|
54
|
+
registrySlug = registryData.slug || target;
|
|
55
|
+
console.log(chalk.cyan(` ℹ Found in Vet registry (indexed) — running local analysis too`));
|
|
56
|
+
console.log(chalk.gray(` View: https://getvet.ai/catalog/${registrySlug}`));
|
|
57
|
+
console.log();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Deep scan request
|
|
61
|
+
if (options.deep && tt === 'npm') {
|
|
62
|
+
spinner.text = `Requesting deep scan for ${target}...`;
|
|
63
|
+
const deepResult = await requestDeepScan(target);
|
|
64
|
+
if (deepResult) {
|
|
65
|
+
spinner.succeed(chalk.green('✓ Deep scan requested'));
|
|
66
|
+
if (deepResult.status === 'queued') {
|
|
67
|
+
console.log(chalk.gray(` Deep scan queued. Check back soon at https://getvet.ai/catalog/${target}`));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
spinner.info('Deep scan request failed — proceeding with local analysis');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Local analysis (current behavior)
|
|
20
75
|
if (tt === 'npm') {
|
|
21
76
|
spinner.text = `Fetching npm: ${target}`;
|
|
22
77
|
result = await analyzeMcp({ npmPackage: target });
|
|
@@ -43,7 +98,7 @@ export async function scanCommand(target, options) {
|
|
|
43
98
|
if (options.json)
|
|
44
99
|
console.log(JSON.stringify(result, null, 2));
|
|
45
100
|
else
|
|
46
|
-
displayScanReport(result);
|
|
101
|
+
displayScanReport(result, registrySlug);
|
|
47
102
|
process.exitCode = result.badge === 'flagged' ? 1 : 0;
|
|
48
103
|
}
|
|
49
104
|
catch (err) {
|
package/dist/index.js
CHANGED
|
@@ -8,13 +8,15 @@ const program = new Command();
|
|
|
8
8
|
program
|
|
9
9
|
.name('vet')
|
|
10
10
|
.description('Security audit CLI for AI skills & MCP servers')
|
|
11
|
-
.version('0.
|
|
11
|
+
.version('0.2.0');
|
|
12
12
|
program
|
|
13
13
|
.command('scan')
|
|
14
14
|
.description('Scan a single tool for security issues')
|
|
15
15
|
.argument('<target>', 'URL, npm package, file path, or GitHub repo')
|
|
16
16
|
.option('--json', 'Output JSON instead of formatted report')
|
|
17
17
|
.option('--overview', 'Include AI-generated overview')
|
|
18
|
+
.option('--offline', 'Skip registry lookup')
|
|
19
|
+
.option('--deep', 'Request deep scan from registry')
|
|
18
20
|
.action(scanCommand);
|
|
19
21
|
program
|
|
20
22
|
.command('audit')
|
|
@@ -29,7 +31,6 @@ program
|
|
|
29
31
|
.description('Search for tools by description')
|
|
30
32
|
.argument('<query>', 'Natural language search query')
|
|
31
33
|
.option('--json', 'Output JSON')
|
|
32
|
-
.option('--api <url>', 'API endpoint (default: https://getvet.ai)')
|
|
33
34
|
.action(findCommand);
|
|
34
35
|
program
|
|
35
36
|
.command('install')
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const API_BASE = 'https://getvet.ai';
|
|
2
|
+
export async function lookupTool(slug) {
|
|
3
|
+
try {
|
|
4
|
+
const resp = await fetch(`${API_BASE}/api/skills/${encodeURIComponent(slug)}`, {
|
|
5
|
+
headers: { 'User-Agent': 'vet-cli/0.2.0' },
|
|
6
|
+
signal: AbortSignal.timeout(5000),
|
|
7
|
+
});
|
|
8
|
+
if (resp.ok)
|
|
9
|
+
return await resp.json();
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function searchTools(query) {
|
|
17
|
+
try {
|
|
18
|
+
const resp = await fetch(`${API_BASE}/api/skills/search?q=${encodeURIComponent(query)}`, {
|
|
19
|
+
headers: { 'User-Agent': 'vet-cli/0.2.0' },
|
|
20
|
+
signal: AbortSignal.timeout(5000),
|
|
21
|
+
});
|
|
22
|
+
if (resp.ok) {
|
|
23
|
+
const data = await resp.json();
|
|
24
|
+
return Array.isArray(data) ? data : data.results || data.items || [];
|
|
25
|
+
}
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function requestDeepScan(slug) {
|
|
33
|
+
try {
|
|
34
|
+
const resp = await fetch(`${API_BASE}/api/tools/${encodeURIComponent(slug)}/deep-scan`, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { 'User-Agent': 'vet-cli/0.2.0', 'Content-Type': 'application/json' },
|
|
37
|
+
signal: AbortSignal.timeout(10000),
|
|
38
|
+
});
|
|
39
|
+
if (resp.ok)
|
|
40
|
+
return await resp.json();
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
package/dist/utils/config.js
CHANGED
|
@@ -21,11 +21,15 @@ export function discoverTools(projectPath) {
|
|
|
21
21
|
}
|
|
22
22
|
catch { /* skip */ }
|
|
23
23
|
}
|
|
24
|
+
// Standard MCP config files (mcpServers or servers key at top level)
|
|
24
25
|
const mcpPaths = [
|
|
25
26
|
join(projectPath, '.cursor', 'mcp.json'),
|
|
26
27
|
join(projectPath, 'mcp.json'),
|
|
27
28
|
join(homedir(), '.config', 'claude', 'claude_desktop_config.json'),
|
|
28
29
|
join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
|
|
30
|
+
join(homedir(), '.windsurf', 'mcp.json'),
|
|
31
|
+
join(homedir(), '.config', 'windsurf', 'mcp.json'),
|
|
32
|
+
join(homedir(), '.config', 'cline', 'mcp_settings.json'),
|
|
29
33
|
];
|
|
30
34
|
for (const cp of mcpPaths) {
|
|
31
35
|
if (!existsSync(cp))
|
|
@@ -43,6 +47,56 @@ export function discoverTools(projectPath) {
|
|
|
43
47
|
}
|
|
44
48
|
catch { /* skip */ }
|
|
45
49
|
}
|
|
50
|
+
// VS Code settings.json — look for mcp.servers key
|
|
51
|
+
const vscodePath = join(homedir(), '.config', 'Code', 'User', 'settings.json');
|
|
52
|
+
if (existsSync(vscodePath)) {
|
|
53
|
+
try {
|
|
54
|
+
const cfg = JSON.parse(readFileSync(vscodePath, 'utf-8'));
|
|
55
|
+
const svrs = cfg['mcp.servers'] || cfg?.mcp?.servers || {};
|
|
56
|
+
let c = 0;
|
|
57
|
+
for (const [n, v] of Object.entries(svrs)) {
|
|
58
|
+
const target = (v.command === 'npx' ? v.args?.[0] : undefined) || n;
|
|
59
|
+
tools.push({ name: n, source: vscodePath.replace(homedir(), '~'), type: 'mcp-config', target });
|
|
60
|
+
c++;
|
|
61
|
+
}
|
|
62
|
+
add(vscodePath.replace(homedir(), '~'), c);
|
|
63
|
+
}
|
|
64
|
+
catch { /* skip */ }
|
|
65
|
+
}
|
|
66
|
+
// Zed settings.json — look for mcp key
|
|
67
|
+
const zedPath = join(homedir(), '.config', 'zed', 'settings.json');
|
|
68
|
+
if (existsSync(zedPath)) {
|
|
69
|
+
try {
|
|
70
|
+
const cfg = JSON.parse(readFileSync(zedPath, 'utf-8'));
|
|
71
|
+
const svrs = cfg?.mcp?.servers || cfg?.mcp || {};
|
|
72
|
+
let c = 0;
|
|
73
|
+
for (const [n, v] of Object.entries(svrs)) {
|
|
74
|
+
if (typeof v !== 'object' || v === null)
|
|
75
|
+
continue;
|
|
76
|
+
const target = (v.command === 'npx' ? v.args?.[0] : undefined) || n;
|
|
77
|
+
tools.push({ name: n, source: zedPath.replace(homedir(), '~'), type: 'mcp-config', target });
|
|
78
|
+
c++;
|
|
79
|
+
}
|
|
80
|
+
add(zedPath.replace(homedir(), '~'), c);
|
|
81
|
+
}
|
|
82
|
+
catch { /* skip */ }
|
|
83
|
+
}
|
|
84
|
+
// Continue config.json — look for mcpServers key
|
|
85
|
+
const continuePath = join(homedir(), '.continue', 'config.json');
|
|
86
|
+
if (existsSync(continuePath)) {
|
|
87
|
+
try {
|
|
88
|
+
const cfg = JSON.parse(readFileSync(continuePath, 'utf-8'));
|
|
89
|
+
const svrs = cfg.mcpServers || {};
|
|
90
|
+
let c = 0;
|
|
91
|
+
for (const [n, v] of Object.entries(svrs)) {
|
|
92
|
+
const target = (v.command === 'npx' ? v.args?.[0] : undefined) || n;
|
|
93
|
+
tools.push({ name: n, source: continuePath.replace(homedir(), '~'), type: 'mcp-config', target });
|
|
94
|
+
c++;
|
|
95
|
+
}
|
|
96
|
+
add(continuePath.replace(homedir(), '~'), c);
|
|
97
|
+
}
|
|
98
|
+
catch { /* skip */ }
|
|
99
|
+
}
|
|
46
100
|
for (const op of [join(projectPath, 'openclaw.json'), join(homedir(), '.openclaw', 'openclaw.json')]) {
|
|
47
101
|
if (!existsSync(op))
|
|
48
102
|
continue;
|
package/dist/utils/display.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type { AnalysisResult, BadgeType } from '../types.js';
|
|
2
|
-
export declare function displayScanReport(r: AnalysisResult): void;
|
|
2
|
+
export declare function displayScanReport(r: AnalysisResult, registrySlug?: string): void;
|
|
3
3
|
export declare function displayAuditReport(results: AnalysisResult[], sources: {
|
|
4
4
|
source: string;
|
|
5
5
|
count: number;
|
|
6
6
|
}[]): void;
|
|
7
7
|
export declare function displayFindResults(results: Array<{
|
|
8
8
|
name: string;
|
|
9
|
+
slug?: string;
|
|
9
10
|
description?: string;
|
|
10
11
|
trustScore?: number;
|
|
11
12
|
badge?: BadgeType;
|
|
13
|
+
author?: string;
|
|
14
|
+
version?: string;
|
|
15
|
+
installs?: number;
|
|
12
16
|
}>): void;
|
package/dist/utils/display.js
CHANGED
|
@@ -23,7 +23,7 @@ function overallRisk(r) {
|
|
|
23
23
|
return 'medium';
|
|
24
24
|
return 'low';
|
|
25
25
|
}
|
|
26
|
-
export function displayScanReport(r) {
|
|
26
|
+
export function displayScanReport(r, registrySlug) {
|
|
27
27
|
const b = BADGE[r.badge], rk = overallRisk(r);
|
|
28
28
|
console.log();
|
|
29
29
|
console.log(chalk.bold(' 🔍 Vet Security Report'));
|
|
@@ -86,6 +86,10 @@ export function displayScanReport(r) {
|
|
|
86
86
|
for (const l of r.overview.split('\n'))
|
|
87
87
|
console.log(` ${chalk.gray(l)}`);
|
|
88
88
|
}
|
|
89
|
+
if (registrySlug) {
|
|
90
|
+
console.log();
|
|
91
|
+
console.log(` ${chalk.gray('View full report:')} ${chalk.cyan(`https://getvet.ai/catalog/${registrySlug}`)}`);
|
|
92
|
+
}
|
|
89
93
|
console.log();
|
|
90
94
|
console.log(chalk.gray(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
91
95
|
console.log();
|
|
@@ -128,10 +132,14 @@ export function displayFindResults(results) {
|
|
|
128
132
|
for (const r of results) {
|
|
129
133
|
const b = r.badge ? BADGE[r.badge] : BADGE.unverified;
|
|
130
134
|
console.log();
|
|
131
|
-
console.log(` ${b.emoji} ${chalk.bold(r.name)}`);
|
|
135
|
+
console.log(` ${b.emoji} ${chalk.bold(r.name)}${r.version ? chalk.gray(` v${r.version}`) : ''}${r.author ? chalk.gray(` by ${r.author}`) : ''}`);
|
|
132
136
|
if (r.description)
|
|
133
137
|
console.log(` ${chalk.gray(r.description.slice(0, 80))}`);
|
|
134
|
-
console.log(` Score: ${r.trustScore != null ? bar(r.trustScore) : chalk.gray('N/A')}`);
|
|
138
|
+
console.log(` Score: ${r.trustScore != null ? bar(r.trustScore) : chalk.gray('N/A')} Badge: ${b.emoji} ${b.color(b.label)}`);
|
|
139
|
+
if (r.installs != null)
|
|
140
|
+
console.log(` ${chalk.gray(`${r.installs.toLocaleString()} installs`)}`);
|
|
141
|
+
if (r.slug)
|
|
142
|
+
console.log(` ${chalk.cyan(`https://getvet.ai/catalog/${r.slug}`)}`);
|
|
135
143
|
}
|
|
136
144
|
console.log();
|
|
137
145
|
}
|