@asvanevik/mcli 0.0.5
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 +76 -0
- package/bin/mcli.js +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +258 -0
- package/dist/format.d.ts +33 -0
- package/dist/format.js +137 -0
- package/dist/lib.d.ts +77 -0
- package/dist/lib.js +226 -0
- package/dist/registry.d.ts +10 -0
- package/dist/registry.js +46 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +1 -0
- package/package.json +39 -0
- package/registry/tools.json +14767 -0
package/dist/lib.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute overall agentScore from detailed agentScores.
|
|
3
|
+
* Weighted average: json(3) + nonInteractive(3) + tokenEfficiency(2) + safety(1) + pipeline(1)
|
|
4
|
+
* Returns 1-10 scale.
|
|
5
|
+
*/
|
|
6
|
+
export function computeAgentScore(scores) {
|
|
7
|
+
const weighted = scores.jsonOutput * 3 +
|
|
8
|
+
scores.nonInteractive * 3 +
|
|
9
|
+
scores.tokenEfficiency * 2 +
|
|
10
|
+
scores.safetyFeatures * 1 +
|
|
11
|
+
scores.pipelineFriendly * 1;
|
|
12
|
+
// Max possible = 5*10 = 50, scale to 1-10
|
|
13
|
+
return Math.round((weighted / 50) * 10);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Validate agentScores object.
|
|
17
|
+
*/
|
|
18
|
+
export function validateAgentScores(scores) {
|
|
19
|
+
const errors = [];
|
|
20
|
+
if (!scores || typeof scores !== 'object') {
|
|
21
|
+
return { valid: false, errors: ['agentScores must be an object'] };
|
|
22
|
+
}
|
|
23
|
+
const s = scores;
|
|
24
|
+
const dimensions = ['jsonOutput', 'nonInteractive', 'tokenEfficiency', 'safetyFeatures', 'pipelineFriendly'];
|
|
25
|
+
for (const dim of dimensions) {
|
|
26
|
+
const val = s[dim];
|
|
27
|
+
if (typeof val !== 'number' || val < 1 || val > 5 || !Number.isInteger(val)) {
|
|
28
|
+
errors.push(`agentScores.${dim} must be an integer 1-5`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { valid: errors.length === 0, errors };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Search tools by query string.
|
|
35
|
+
* Matches against slug, name, description, and categories.
|
|
36
|
+
*/
|
|
37
|
+
export function searchTools(registry, query) {
|
|
38
|
+
const q = query.toLowerCase().trim();
|
|
39
|
+
if (!q)
|
|
40
|
+
return [];
|
|
41
|
+
return registry.tools.filter(tool => tool.slug.toLowerCase().includes(q) ||
|
|
42
|
+
tool.name.toLowerCase().includes(q) ||
|
|
43
|
+
tool.description.toLowerCase().includes(q) ||
|
|
44
|
+
tool.categories.some(c => c.toLowerCase().includes(q)));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Find a tool by exact slug match.
|
|
48
|
+
*/
|
|
49
|
+
export function findTool(registry, slug) {
|
|
50
|
+
return registry.tools.find(t => t.slug === slug);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get all unique categories from the registry.
|
|
54
|
+
*/
|
|
55
|
+
export function getCategories(registry) {
|
|
56
|
+
const cats = new Set();
|
|
57
|
+
registry.tools.forEach(t => t.categories.forEach(c => cats.add(c)));
|
|
58
|
+
return [...cats].sort();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Sort tools by agent score (descending).
|
|
62
|
+
*/
|
|
63
|
+
export function sortByAgentScore(tools) {
|
|
64
|
+
return [...tools].sort((a, b) => b.agentScore - a.agentScore);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Filter tools by minimum agent score.
|
|
68
|
+
*/
|
|
69
|
+
export function filterByMinScore(tools, minScore) {
|
|
70
|
+
return tools.filter(t => t.agentScore >= minScore);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Filter tools by category.
|
|
74
|
+
*/
|
|
75
|
+
export function filterByCategory(tools, category) {
|
|
76
|
+
const cat = category.toLowerCase();
|
|
77
|
+
return tools.filter(t => t.categories.some(c => c.toLowerCase() === cat));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Filter tools by verification tier.
|
|
81
|
+
*/
|
|
82
|
+
export function filterByTier(tools, tier) {
|
|
83
|
+
return tools.filter(t => t.tier === tier);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get tier badge character.
|
|
87
|
+
*/
|
|
88
|
+
export function tierBadge(tier) {
|
|
89
|
+
switch (tier) {
|
|
90
|
+
case 'verified': return '✓';
|
|
91
|
+
case 'community': return '○';
|
|
92
|
+
default: return '?';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Validate a tool object has required fields.
|
|
97
|
+
*/
|
|
98
|
+
export function validateTool(tool) {
|
|
99
|
+
const errors = [];
|
|
100
|
+
if (!tool || typeof tool !== 'object') {
|
|
101
|
+
return { valid: false, errors: ['Tool must be an object'] };
|
|
102
|
+
}
|
|
103
|
+
const t = tool;
|
|
104
|
+
// Required string fields
|
|
105
|
+
const requiredStrings = ['slug', 'name', 'description'];
|
|
106
|
+
for (const field of requiredStrings) {
|
|
107
|
+
if (typeof t[field] !== 'string' || !t[field]) {
|
|
108
|
+
errors.push(`Missing or invalid field: ${field}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Slug format: lowercase, alphanumeric, hyphens
|
|
112
|
+
if (typeof t.slug === 'string' && !/^[a-z0-9-]+$/.test(t.slug)) {
|
|
113
|
+
errors.push('Slug must be lowercase alphanumeric with hyphens only');
|
|
114
|
+
}
|
|
115
|
+
// Agent score: 1-10
|
|
116
|
+
if (typeof t.agentScore !== 'number' || t.agentScore < 1 || t.agentScore > 10) {
|
|
117
|
+
errors.push('agentScore must be a number between 1 and 10');
|
|
118
|
+
}
|
|
119
|
+
// Tier validation
|
|
120
|
+
const validTiers = ['verified', 'community', 'unverified'];
|
|
121
|
+
if (!validTiers.includes(t.tier)) {
|
|
122
|
+
errors.push(`tier must be one of: ${validTiers.join(', ')}`);
|
|
123
|
+
}
|
|
124
|
+
// Categories: non-empty array
|
|
125
|
+
if (!Array.isArray(t.categories) || t.categories.length === 0) {
|
|
126
|
+
errors.push('categories must be a non-empty array');
|
|
127
|
+
}
|
|
128
|
+
// Vendor object
|
|
129
|
+
if (!t.vendor || typeof t.vendor !== 'object') {
|
|
130
|
+
errors.push('vendor must be an object');
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const v = t.vendor;
|
|
134
|
+
if (typeof v.name !== 'string' || !v.name)
|
|
135
|
+
errors.push('vendor.name is required');
|
|
136
|
+
if (typeof v.domain !== 'string' || !v.domain)
|
|
137
|
+
errors.push('vendor.domain is required');
|
|
138
|
+
if (typeof v.verified !== 'boolean')
|
|
139
|
+
errors.push('vendor.verified must be boolean');
|
|
140
|
+
}
|
|
141
|
+
// Install object
|
|
142
|
+
if (!t.install || typeof t.install !== 'object') {
|
|
143
|
+
errors.push('install must be an object');
|
|
144
|
+
}
|
|
145
|
+
// Capabilities object
|
|
146
|
+
if (!t.capabilities || typeof t.capabilities !== 'object') {
|
|
147
|
+
errors.push('capabilities must be an object');
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const c = t.capabilities;
|
|
151
|
+
if (typeof c.jsonOutput !== 'boolean')
|
|
152
|
+
errors.push('capabilities.jsonOutput must be boolean');
|
|
153
|
+
if (typeof c.idempotent !== 'boolean')
|
|
154
|
+
errors.push('capabilities.idempotent must be boolean');
|
|
155
|
+
if (typeof c.interactive !== 'boolean')
|
|
156
|
+
errors.push('capabilities.interactive must be boolean');
|
|
157
|
+
if (typeof c.streaming !== 'boolean')
|
|
158
|
+
errors.push('capabilities.streaming must be boolean');
|
|
159
|
+
if (!Array.isArray(c.auth))
|
|
160
|
+
errors.push('capabilities.auth must be an array');
|
|
161
|
+
}
|
|
162
|
+
return { valid: errors.length === 0, errors };
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Validate entire registry.
|
|
166
|
+
*/
|
|
167
|
+
export function validateRegistry(registry) {
|
|
168
|
+
const errors = [];
|
|
169
|
+
if (!registry || typeof registry !== 'object') {
|
|
170
|
+
return { valid: false, errors: ['Registry must be an object'] };
|
|
171
|
+
}
|
|
172
|
+
const r = registry;
|
|
173
|
+
if (typeof r.version !== 'string') {
|
|
174
|
+
errors.push('Registry must have a version string');
|
|
175
|
+
}
|
|
176
|
+
if (!Array.isArray(r.tools)) {
|
|
177
|
+
errors.push('Registry must have a tools array');
|
|
178
|
+
return { valid: false, errors };
|
|
179
|
+
}
|
|
180
|
+
// Check for duplicate slugs
|
|
181
|
+
const slugs = new Set();
|
|
182
|
+
for (const tool of r.tools) {
|
|
183
|
+
const t = tool;
|
|
184
|
+
if (typeof t.slug === 'string') {
|
|
185
|
+
if (slugs.has(t.slug)) {
|
|
186
|
+
errors.push(`Duplicate slug: ${t.slug}`);
|
|
187
|
+
}
|
|
188
|
+
slugs.add(t.slug);
|
|
189
|
+
}
|
|
190
|
+
const toolValidation = validateTool(tool);
|
|
191
|
+
if (!toolValidation.valid) {
|
|
192
|
+
errors.push(`Tool "${t.slug || 'unknown'}": ${toolValidation.errors.join(', ')}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return { valid: errors.length === 0, errors };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Generate install command string for a tool and platform.
|
|
199
|
+
*/
|
|
200
|
+
export function getInstallCommand(tool, platform) {
|
|
201
|
+
const cmd = tool.install[platform];
|
|
202
|
+
if (!cmd)
|
|
203
|
+
return null;
|
|
204
|
+
switch (platform) {
|
|
205
|
+
case 'brew': return `brew install ${cmd}`;
|
|
206
|
+
case 'apt': return `sudo apt install ${cmd}`;
|
|
207
|
+
case 'npm': return `npm install -g ${cmd}`;
|
|
208
|
+
case 'cargo': return `cargo install ${cmd}`;
|
|
209
|
+
case 'go': return `go install ${cmd}`;
|
|
210
|
+
case 'binary': return cmd;
|
|
211
|
+
case 'script': return cmd;
|
|
212
|
+
default: return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
export function compareTools(tools) {
|
|
216
|
+
if (tools.length === 0)
|
|
217
|
+
return [];
|
|
218
|
+
return [
|
|
219
|
+
{ field: 'Agent Score', values: tools.map(t => t.agentScore) },
|
|
220
|
+
{ field: 'JSON Output', values: tools.map(t => t.capabilities.jsonOutput) },
|
|
221
|
+
{ field: 'Idempotent', values: tools.map(t => t.capabilities.idempotent) },
|
|
222
|
+
{ field: 'Interactive', values: tools.map(t => t.capabilities.interactive) },
|
|
223
|
+
{ field: 'Streaming', values: tools.map(t => t.capabilities.streaming) },
|
|
224
|
+
{ field: 'Tier', values: tools.map(t => t.tier) },
|
|
225
|
+
];
|
|
226
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Registry } from './types.js';
|
|
2
|
+
export declare class RegistryError extends Error {
|
|
3
|
+
cause?: Error | undefined;
|
|
4
|
+
constructor(message: string, cause?: Error | undefined);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Load and validate registry from a JSON file.
|
|
8
|
+
* @throws {RegistryError} If file is missing, malformed, or invalid
|
|
9
|
+
*/
|
|
10
|
+
export declare function loadRegistry(path: string): Registry;
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry loading with proper error handling.
|
|
3
|
+
* Extracted for testability.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, existsSync } from 'fs';
|
|
6
|
+
import { validateRegistry } from './lib.js';
|
|
7
|
+
export class RegistryError extends Error {
|
|
8
|
+
cause;
|
|
9
|
+
constructor(message, cause) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.cause = cause;
|
|
12
|
+
this.name = 'RegistryError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Load and validate registry from a JSON file.
|
|
17
|
+
* @throws {RegistryError} If file is missing, malformed, or invalid
|
|
18
|
+
*/
|
|
19
|
+
export function loadRegistry(path) {
|
|
20
|
+
// Check file exists
|
|
21
|
+
if (!existsSync(path)) {
|
|
22
|
+
throw new RegistryError(`Registry file not found: ${path}`);
|
|
23
|
+
}
|
|
24
|
+
// Read file
|
|
25
|
+
let content;
|
|
26
|
+
try {
|
|
27
|
+
content = readFileSync(path, 'utf-8');
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
throw new RegistryError(`Failed to read registry file: ${path}`, err instanceof Error ? err : undefined);
|
|
31
|
+
}
|
|
32
|
+
// Parse JSON
|
|
33
|
+
let data;
|
|
34
|
+
try {
|
|
35
|
+
data = JSON.parse(content);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
throw new RegistryError(`Registry file contains invalid JSON: ${path}`, err instanceof Error ? err : undefined);
|
|
39
|
+
}
|
|
40
|
+
// Validate schema
|
|
41
|
+
const validation = validateRegistry(data);
|
|
42
|
+
if (!validation.valid) {
|
|
43
|
+
throw new RegistryError(`Registry validation failed: ${validation.errors.join('; ')}`);
|
|
44
|
+
}
|
|
45
|
+
return data;
|
|
46
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface Vendor {
|
|
2
|
+
name: string;
|
|
3
|
+
domain: string;
|
|
4
|
+
verified: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface InstallMethods {
|
|
7
|
+
brew?: string;
|
|
8
|
+
apt?: string;
|
|
9
|
+
npm?: string;
|
|
10
|
+
cargo?: string;
|
|
11
|
+
go?: string;
|
|
12
|
+
binary?: string;
|
|
13
|
+
script?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface Capabilities {
|
|
16
|
+
jsonOutput: boolean;
|
|
17
|
+
auth: string[];
|
|
18
|
+
idempotent: boolean;
|
|
19
|
+
interactive: boolean;
|
|
20
|
+
streaming: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Multi-dimensional agent-friendliness scores.
|
|
24
|
+
* Each dimension is 1-5 (5 = best).
|
|
25
|
+
*/
|
|
26
|
+
export interface AgentScores {
|
|
27
|
+
/** Structured output: --json, --output=json, parseable formats */
|
|
28
|
+
jsonOutput: number;
|
|
29
|
+
/** Non-interactive: --yes, env auth, no prompts, no TTY required */
|
|
30
|
+
nonInteractive: number;
|
|
31
|
+
/** Token efficiency: --quiet, --compact, --id-only, minimal output */
|
|
32
|
+
tokenEfficiency: number;
|
|
33
|
+
/** Safety features: --dry-run, structured exit codes (0/1/2), confirmation flags */
|
|
34
|
+
safetyFeatures: number;
|
|
35
|
+
/** Pipeline friendly: clean stderr/stdout separation, chainable output */
|
|
36
|
+
pipelineFriendly: number;
|
|
37
|
+
}
|
|
38
|
+
export interface CliTool {
|
|
39
|
+
slug: string;
|
|
40
|
+
name: string;
|
|
41
|
+
vendor: Vendor;
|
|
42
|
+
repo?: string;
|
|
43
|
+
docs?: string;
|
|
44
|
+
install: InstallMethods;
|
|
45
|
+
capabilities: Capabilities;
|
|
46
|
+
agentScore: number;
|
|
47
|
+
agentScores?: AgentScores;
|
|
48
|
+
categories: string[];
|
|
49
|
+
description: string;
|
|
50
|
+
tier: 'verified' | 'community' | 'unverified';
|
|
51
|
+
}
|
|
52
|
+
export interface Registry {
|
|
53
|
+
version: string;
|
|
54
|
+
updated: string;
|
|
55
|
+
tools: CliTool[];
|
|
56
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@asvanevik/mcli",
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"description": "A CLI for discovering and comparing CLI tools",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcli": "./bin/mcli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"dist",
|
|
12
|
+
"registry"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"dev": "tsx src/cli.ts",
|
|
17
|
+
"start": "node dist/cli.js",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest",
|
|
20
|
+
"test:coverage": "vitest run --coverage",
|
|
21
|
+
"prepublishOnly": "npm test && npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"cli",
|
|
25
|
+
"tools",
|
|
26
|
+
"discovery",
|
|
27
|
+
"registry"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@anthropic-ai/sdk": "^0.78.0",
|
|
32
|
+
"@types/node": "^25.3.2",
|
|
33
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
34
|
+
"openai": "^6.25.0",
|
|
35
|
+
"tsx": "^4.21.0",
|
|
36
|
+
"typescript": "^5.9.3",
|
|
37
|
+
"vitest": "^4.0.18"
|
|
38
|
+
}
|
|
39
|
+
}
|