@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 ADDED
@@ -0,0 +1,76 @@
1
+ # mcli
2
+
3
+ A CLI for discovering and comparing CLI tools — optimized for AI agents.
4
+
5
+ ## Why?
6
+
7
+ Package managers tell you *how* to install tools. mcli tells you *which* tools are best for your use case, with structured metadata about agent-friendliness.
8
+
9
+ ## Features
10
+
11
+ - **Discovery** — Search and compare CLI tools across categories
12
+ - **Agent Scores** — Rate tools on JSON output, idempotency, auth complexity
13
+ - **Verification Tiers** — Know if a tool is officially verified, community-vetted, or unverified
14
+ - **Install Commands** — Get the right install command for your platform
15
+
16
+ ## Quick Start
17
+
18
+ ```bash
19
+ npx mcli search cloud
20
+ npx mcli info hcloud
21
+ npx mcli compare aws gcloud hcloud
22
+ npx mcli install gh
23
+ ```
24
+
25
+ ## Commands
26
+
27
+ | Command | Description |
28
+ |---------|-------------|
29
+ | `search <query>` | Search for CLI tools |
30
+ | `info <slug>` | Show detailed info about a tool |
31
+ | `compare <slug> [slug...]` | Compare multiple tools side-by-side |
32
+ | `install <slug>` | Show install commands for a tool |
33
+ | `list` | List all tools |
34
+ | `list --agent-friendly` | List tools sorted by agent score |
35
+ | `categories` | List all categories |
36
+
37
+ ## Agent Score
38
+
39
+ Tools are rated 1-10 on agent-friendliness based on:
40
+
41
+ - **JSON Output** — Parseable structured output
42
+ - **Idempotency** — Same command, same result
43
+ - **Auth Simplicity** — Env vars vs interactive flows
44
+ - **Error Clarity** — Actionable error messages
45
+ - **Streaming** — Handles long-running operations
46
+
47
+ ## Verification Tiers
48
+
49
+ - ✓ **Verified** — Traced to official vendor source
50
+ - ○ **Community** — Submitted and reviewed by community
51
+ - ? **Unverified** — Auto-indexed, use with caution
52
+
53
+ ## Roadmap
54
+
55
+ - [ ] Agent reviews — Let AI agents rate tools based on real usage
56
+ - [ ] PR reputation — Agents earn trust by contributing to CLI repos
57
+ - [ ] Web interface — Browse the registry online
58
+ - [ ] Skill integration — Link to skills.sh for usage instructions
59
+
60
+ ## Development
61
+
62
+ This project follows **test-driven development (TDD)**. Tests must pass before merging.
63
+
64
+ ```bash
65
+ npm test # Run tests
66
+ npm run test:watch # Watch mode
67
+ npm run test:coverage # Coverage report
68
+ ```
69
+
70
+ ## Contributing
71
+
72
+ PRs welcome! To add a tool, edit `registry/tools.json`. All tools are validated by tests.
73
+
74
+ ## License
75
+
76
+ MIT
package/bin/mcli.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/cli.js');
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname, join } from 'path';
4
+ import { searchTools, findTool, getCategories, sortByAgentScore, tierBadge, filterByMinScore, filterByCategory } from './lib.js';
5
+ import { loadRegistry, RegistryError } from './registry.js';
6
+ // Parse --flag and --flag=value from args
7
+ function parseFlag(args, flag) {
8
+ for (const arg of args) {
9
+ if (arg === flag)
10
+ return 'true';
11
+ if (arg.startsWith(`${flag}=`))
12
+ return arg.slice(flag.length + 1);
13
+ }
14
+ return null;
15
+ }
16
+ function parseNumericFlag(args, flag) {
17
+ const val = parseFlag(args, flag);
18
+ if (val === null)
19
+ return null;
20
+ const num = parseInt(val, 10);
21
+ return isNaN(num) ? null : num;
22
+ }
23
+ const __dirname = dirname(fileURLToPath(import.meta.url));
24
+ const registryPath = join(__dirname, '..', 'registry', 'tools.json');
25
+ function scoreColor(score) {
26
+ if (score >= 8)
27
+ return '\x1b[32m'; // green
28
+ if (score >= 5)
29
+ return '\x1b[33m'; // yellow
30
+ return '\x1b[31m'; // red
31
+ }
32
+ function reset() {
33
+ return '\x1b[0m';
34
+ }
35
+ function printTool(tool, verbose = false) {
36
+ const badge = tierBadge(tool.tier);
37
+ const color = scoreColor(tool.agentScore);
38
+ console.log(`${badge} ${tool.name} (${tool.slug}) - Agent Score: ${color}${tool.agentScore}/10${reset()}`);
39
+ console.log(` ${tool.description}`);
40
+ if (verbose) {
41
+ console.log(` Vendor: ${tool.vendor.name} (${tool.vendor.domain})`);
42
+ console.log(` Categories: ${tool.categories.join(', ')}`);
43
+ // Show agent score breakdown if available
44
+ if (tool.agentScores) {
45
+ console.log(` Agent Scores (1-5 each):`);
46
+ console.log(` JSON Output: ${tool.agentScores.jsonOutput}/5`);
47
+ console.log(` Non-Interactive: ${tool.agentScores.nonInteractive}/5`);
48
+ console.log(` Token Efficiency:${tool.agentScores.tokenEfficiency}/5`);
49
+ console.log(` Safety Features: ${tool.agentScores.safetyFeatures}/5`);
50
+ console.log(` Pipeline Friend: ${tool.agentScores.pipelineFriendly}/5`);
51
+ }
52
+ console.log(` Install:`);
53
+ for (const [method, cmd] of Object.entries(tool.install)) {
54
+ if (cmd)
55
+ console.log(` ${method}: ${cmd}`);
56
+ }
57
+ console.log(` Capabilities:`);
58
+ console.log(` JSON output: ${tool.capabilities.jsonOutput ? 'yes' : 'no'}`);
59
+ console.log(` Idempotent: ${tool.capabilities.idempotent ? 'yes' : 'no'}`);
60
+ console.log(` Interactive: ${tool.capabilities.interactive ? 'yes' : 'no'}`);
61
+ if (tool.capabilities.auth.length > 0) {
62
+ console.log(` Auth: ${tool.capabilities.auth.join(', ')}`);
63
+ }
64
+ if (tool.repo)
65
+ console.log(` Repo: ${tool.repo}`);
66
+ if (tool.docs)
67
+ console.log(` Docs: ${tool.docs}`);
68
+ }
69
+ console.log();
70
+ }
71
+ // search function moved to lib.ts as searchTools
72
+ function compare(registry, slugs) {
73
+ const tools = slugs.map(slug => registry.tools.find(t => t.slug === slug)).filter((t) => t !== undefined);
74
+ if (tools.length === 0) {
75
+ console.log('No tools found');
76
+ return;
77
+ }
78
+ // Header
79
+ console.log('Tool'.padEnd(20) + tools.map(t => t.slug.padEnd(15)).join(''));
80
+ console.log('-'.repeat(20 + tools.length * 15));
81
+ // Rows
82
+ console.log('Agent Score'.padEnd(20) + tools.map(t => `${t.agentScore}/10`.padEnd(15)).join(''));
83
+ console.log('JSON Output'.padEnd(20) + tools.map(t => (t.capabilities.jsonOutput ? 'yes' : 'no').padEnd(15)).join(''));
84
+ console.log('Idempotent'.padEnd(20) + tools.map(t => (t.capabilities.idempotent ? 'yes' : 'no').padEnd(15)).join(''));
85
+ console.log('Interactive'.padEnd(20) + tools.map(t => (t.capabilities.interactive ? 'yes' : 'no').padEnd(15)).join(''));
86
+ console.log('Verified'.padEnd(20) + tools.map(t => (t.tier === 'verified' ? '✓' : '○').padEnd(15)).join(''));
87
+ console.log('Install (brew)'.padEnd(20) + tools.map(t => (t.install.brew || '-').padEnd(15)).join(''));
88
+ }
89
+ function installCmd(tool) {
90
+ console.log(`# Install ${tool.name}\n`);
91
+ if (tool.install.brew)
92
+ console.log(`# macOS (Homebrew)\nbrew install ${tool.install.brew}\n`);
93
+ if (tool.install.apt)
94
+ console.log(`# Debian/Ubuntu\nsudo apt install ${tool.install.apt}\n`);
95
+ if (tool.install.npm)
96
+ console.log(`# npm\nnpm install -g ${tool.install.npm}\n`);
97
+ if (tool.install.cargo)
98
+ console.log(`# Cargo (Rust)\ncargo install ${tool.install.cargo}\n`);
99
+ if (tool.install.go)
100
+ console.log(`# Go\ngo install ${tool.install.go}\n`);
101
+ if (tool.install.binary)
102
+ console.log(`# Binary download\n${tool.install.binary}\n`);
103
+ if (tool.install.script)
104
+ console.log(`# Install script\n${tool.install.script}\n`);
105
+ }
106
+ function main() {
107
+ const args = process.argv.slice(2);
108
+ const command = args[0];
109
+ if (!command || command === 'help' || command === '--help') {
110
+ console.log(`
111
+ mcli - A CLI for discovering CLI tools
112
+
113
+ Commands:
114
+ search <query> Search for CLI tools
115
+ info <slug> Show detailed info about a tool
116
+ compare <slug> [slug...] Compare multiple tools
117
+ install <slug> Show install commands for a tool
118
+ list List all tools
119
+ categories List all categories
120
+
121
+ Filters (for search and list):
122
+ --min-score=N Only show tools with agent score >= N
123
+ --category=NAME Filter by category
124
+ --agent-friendly Sort by agent score (highest first)
125
+
126
+ Tier Badges:
127
+ ✓ = Verified (traced to official vendor source)
128
+ ○ = Community (reviewed but not vendor-verified)
129
+ ? = Unverified (auto-indexed, use with caution)
130
+
131
+ Examples:
132
+ mcli search cloud
133
+ mcli search git --category=git
134
+ mcli list --agent-friendly --min-score=8
135
+ mcli info gh
136
+ mcli compare aws gcloud hcloud
137
+ `);
138
+ return;
139
+ }
140
+ let registry;
141
+ try {
142
+ registry = loadRegistry(registryPath);
143
+ }
144
+ catch (err) {
145
+ if (err instanceof RegistryError) {
146
+ console.error(`Error: ${err.message}`);
147
+ if (err.cause) {
148
+ console.error(` Caused by: ${err.cause.message}`);
149
+ }
150
+ }
151
+ else {
152
+ console.error('Unexpected error loading registry:', err);
153
+ }
154
+ process.exit(1);
155
+ }
156
+ switch (command) {
157
+ case 'search': {
158
+ // Extract query (non-flag args after command)
159
+ const queryParts = args.slice(1).filter(a => !a.startsWith('--'));
160
+ const query = queryParts.join(' ');
161
+ if (!query) {
162
+ console.log('Usage: mcli search <query> [--min-score=N] [--category=NAME]');
163
+ return;
164
+ }
165
+ let results = searchTools(registry, query);
166
+ // Apply filters
167
+ const minScore = parseNumericFlag(args, '--min-score');
168
+ if (minScore !== null) {
169
+ results = filterByMinScore(results, minScore);
170
+ }
171
+ const category = parseFlag(args, '--category');
172
+ if (category && category !== 'true') {
173
+ results = filterByCategory(results, category);
174
+ }
175
+ // Sort by score if requested
176
+ if (args.includes('--agent-friendly')) {
177
+ results = sortByAgentScore(results);
178
+ }
179
+ if (results.length === 0) {
180
+ console.log('No tools found');
181
+ }
182
+ else {
183
+ results.forEach(t => printTool(t));
184
+ }
185
+ break;
186
+ }
187
+ case 'info': {
188
+ const slug = args[1];
189
+ if (!slug) {
190
+ console.log('Usage: mcli info <slug>');
191
+ return;
192
+ }
193
+ const tool = findTool(registry, slug);
194
+ if (!tool) {
195
+ console.log(`Tool not found: ${slug}`);
196
+ }
197
+ else {
198
+ printTool(tool, true);
199
+ }
200
+ break;
201
+ }
202
+ case 'compare': {
203
+ const slugs = args.slice(1);
204
+ if (slugs.length < 2) {
205
+ console.log('Usage: mcli compare <slug> <slug> [slug...]');
206
+ return;
207
+ }
208
+ compare(registry, slugs);
209
+ break;
210
+ }
211
+ case 'install': {
212
+ const slug = args[1];
213
+ if (!slug) {
214
+ console.log('Usage: mcli install <slug>');
215
+ return;
216
+ }
217
+ const tool = findTool(registry, slug);
218
+ if (!tool) {
219
+ console.log(`Tool not found: ${slug}`);
220
+ }
221
+ else {
222
+ installCmd(tool);
223
+ }
224
+ break;
225
+ }
226
+ case 'list': {
227
+ let tools = [...registry.tools];
228
+ // Apply filters
229
+ const minScore = parseNumericFlag(args, '--min-score');
230
+ if (minScore !== null) {
231
+ tools = filterByMinScore(tools, minScore);
232
+ }
233
+ const category = parseFlag(args, '--category');
234
+ if (category && category !== 'true') {
235
+ tools = filterByCategory(tools, category);
236
+ }
237
+ // Sort by score if requested
238
+ if (args.includes('--agent-friendly')) {
239
+ tools = sortByAgentScore(tools);
240
+ }
241
+ if (tools.length === 0) {
242
+ console.log('No tools found matching filters');
243
+ }
244
+ else {
245
+ tools.forEach(t => printTool(t));
246
+ }
247
+ break;
248
+ }
249
+ case 'categories': {
250
+ const cats = getCategories(registry);
251
+ console.log('Categories:', cats.join(', '));
252
+ break;
253
+ }
254
+ default:
255
+ console.log(`Unknown command: ${command}\nRun 'mcli help' for usage.`);
256
+ }
257
+ }
258
+ main();
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Formatting helpers for CLI output.
3
+ * Extracted for testability.
4
+ */
5
+ import type { CliTool } from './types.js';
6
+ /**
7
+ * ANSI color code for agent score.
8
+ */
9
+ export declare function scoreColor(score: number): string;
10
+ /**
11
+ * ANSI reset code.
12
+ */
13
+ export declare function reset(): string;
14
+ /**
15
+ * Format a tool's header line.
16
+ */
17
+ export declare function formatToolHeader(tool: CliTool): string;
18
+ /**
19
+ * Format a tool for brief display.
20
+ */
21
+ export declare function formatToolBrief(tool: CliTool): string;
22
+ /**
23
+ * Format a tool for detailed display.
24
+ */
25
+ export declare function formatToolDetailed(tool: CliTool): string;
26
+ /**
27
+ * Format a comparison table for multiple tools.
28
+ */
29
+ export declare function formatCompareTable(tools: CliTool[]): string;
30
+ /**
31
+ * Format install commands for a tool.
32
+ */
33
+ export declare function formatInstallCommands(tool: CliTool): string;
package/dist/format.js ADDED
@@ -0,0 +1,137 @@
1
+ import { tierBadge, compareTools } from './lib.js';
2
+ /**
3
+ * ANSI color code for agent score.
4
+ */
5
+ export function scoreColor(score) {
6
+ if (score >= 8)
7
+ return '\x1b[32m'; // green
8
+ if (score >= 5)
9
+ return '\x1b[33m'; // yellow
10
+ return '\x1b[31m'; // red
11
+ }
12
+ /**
13
+ * ANSI reset code.
14
+ */
15
+ export function reset() {
16
+ return '\x1b[0m';
17
+ }
18
+ /**
19
+ * Format a tool's header line.
20
+ */
21
+ export function formatToolHeader(tool) {
22
+ const badge = tierBadge(tool.tier);
23
+ const color = scoreColor(tool.agentScore);
24
+ return `${badge} ${tool.name} (${tool.slug}) - Agent Score: ${color}${tool.agentScore}/10${reset()}`;
25
+ }
26
+ /**
27
+ * Format a tool for brief display.
28
+ */
29
+ export function formatToolBrief(tool) {
30
+ return `${formatToolHeader(tool)}\n ${tool.description}`;
31
+ }
32
+ /**
33
+ * Format a tool for detailed display.
34
+ */
35
+ export function formatToolDetailed(tool) {
36
+ const lines = [formatToolHeader(tool), ` ${tool.description}`];
37
+ lines.push(` Vendor: ${tool.vendor.name} (${tool.vendor.domain})`);
38
+ lines.push(` Categories: ${tool.categories.join(', ')}`);
39
+ lines.push(` Install:`);
40
+ for (const [method, cmd] of Object.entries(tool.install)) {
41
+ if (cmd)
42
+ lines.push(` ${method}: ${cmd}`);
43
+ }
44
+ lines.push(` Capabilities:`);
45
+ lines.push(` JSON output: ${tool.capabilities.jsonOutput ? 'yes' : 'no'}`);
46
+ lines.push(` Idempotent: ${tool.capabilities.idempotent ? 'yes' : 'no'}`);
47
+ lines.push(` Interactive: ${tool.capabilities.interactive ? 'yes' : 'no'}`);
48
+ if (tool.capabilities.auth.length > 0) {
49
+ lines.push(` Auth: ${tool.capabilities.auth.join(', ')}`);
50
+ }
51
+ if (tool.repo)
52
+ lines.push(` Repo: ${tool.repo}`);
53
+ if (tool.docs)
54
+ lines.push(` Docs: ${tool.docs}`);
55
+ return lines.join('\n');
56
+ }
57
+ /**
58
+ * Format a comparison table for multiple tools.
59
+ */
60
+ export function formatCompareTable(tools) {
61
+ if (tools.length === 0) {
62
+ return 'No tools found';
63
+ }
64
+ const rows = compareTools(tools);
65
+ const lines = [];
66
+ // Header
67
+ lines.push('Tool'.padEnd(20) + tools.map(t => t.slug.padEnd(15)).join(''));
68
+ lines.push('-'.repeat(20 + tools.length * 15));
69
+ // Format values
70
+ const formatValue = (v) => {
71
+ if (typeof v === 'boolean')
72
+ return v ? 'yes' : 'no';
73
+ if (v === 'verified')
74
+ return '✓';
75
+ if (v === 'community')
76
+ return '○';
77
+ if (v === 'unverified')
78
+ return '?';
79
+ return String(v);
80
+ };
81
+ for (const row of rows) {
82
+ const label = row.field === 'Agent Score' ? 'Agent Score' :
83
+ row.field === 'JSON Output' ? 'JSON Output' :
84
+ row.field === 'Idempotent' ? 'Idempotent' :
85
+ row.field === 'Interactive' ? 'Interactive' :
86
+ row.field === 'Streaming' ? 'Streaming' :
87
+ row.field === 'Tier' ? 'Verified' : row.field;
88
+ const values = row.values.map(v => {
89
+ const formatted = formatValue(v);
90
+ return (row.field === 'Agent Score' ? `${formatted}/10` : formatted).padEnd(15);
91
+ }).join('');
92
+ lines.push(label.padEnd(20) + values);
93
+ }
94
+ return lines.join('\n');
95
+ }
96
+ /**
97
+ * Format install commands for a tool.
98
+ */
99
+ export function formatInstallCommands(tool) {
100
+ const lines = [`# Install ${tool.name}`, ''];
101
+ if (tool.install.brew) {
102
+ lines.push('# macOS (Homebrew)');
103
+ lines.push(`brew install ${tool.install.brew}`);
104
+ lines.push('');
105
+ }
106
+ if (tool.install.apt) {
107
+ lines.push('# Debian/Ubuntu');
108
+ lines.push(`sudo apt install ${tool.install.apt}`);
109
+ lines.push('');
110
+ }
111
+ if (tool.install.npm) {
112
+ lines.push('# npm');
113
+ lines.push(`npm install -g ${tool.install.npm}`);
114
+ lines.push('');
115
+ }
116
+ if (tool.install.cargo) {
117
+ lines.push('# Cargo (Rust)');
118
+ lines.push(`cargo install ${tool.install.cargo}`);
119
+ lines.push('');
120
+ }
121
+ if (tool.install.go) {
122
+ lines.push('# Go');
123
+ lines.push(`go install ${tool.install.go}`);
124
+ lines.push('');
125
+ }
126
+ if (tool.install.binary) {
127
+ lines.push('# Binary download');
128
+ lines.push(tool.install.binary);
129
+ lines.push('');
130
+ }
131
+ if (tool.install.script) {
132
+ lines.push('# Install script');
133
+ lines.push(tool.install.script);
134
+ lines.push('');
135
+ }
136
+ return lines.join('\n');
137
+ }
package/dist/lib.d.ts ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Core library functions for mcli.
3
+ * Extracted from CLI for testability.
4
+ */
5
+ import type { Registry, CliTool, AgentScores } from './types.js';
6
+ /**
7
+ * Compute overall agentScore from detailed agentScores.
8
+ * Weighted average: json(3) + nonInteractive(3) + tokenEfficiency(2) + safety(1) + pipeline(1)
9
+ * Returns 1-10 scale.
10
+ */
11
+ export declare function computeAgentScore(scores: AgentScores): number;
12
+ /**
13
+ * Validate agentScores object.
14
+ */
15
+ export declare function validateAgentScores(scores: unknown): {
16
+ valid: boolean;
17
+ errors: string[];
18
+ };
19
+ /**
20
+ * Search tools by query string.
21
+ * Matches against slug, name, description, and categories.
22
+ */
23
+ export declare function searchTools(registry: Registry, query: string): CliTool[];
24
+ /**
25
+ * Find a tool by exact slug match.
26
+ */
27
+ export declare function findTool(registry: Registry, slug: string): CliTool | undefined;
28
+ /**
29
+ * Get all unique categories from the registry.
30
+ */
31
+ export declare function getCategories(registry: Registry): string[];
32
+ /**
33
+ * Sort tools by agent score (descending).
34
+ */
35
+ export declare function sortByAgentScore(tools: CliTool[]): CliTool[];
36
+ /**
37
+ * Filter tools by minimum agent score.
38
+ */
39
+ export declare function filterByMinScore(tools: CliTool[], minScore: number): CliTool[];
40
+ /**
41
+ * Filter tools by category.
42
+ */
43
+ export declare function filterByCategory(tools: CliTool[], category: string): CliTool[];
44
+ /**
45
+ * Filter tools by verification tier.
46
+ */
47
+ export declare function filterByTier(tools: CliTool[], tier: CliTool['tier']): CliTool[];
48
+ /**
49
+ * Get tier badge character.
50
+ */
51
+ export declare function tierBadge(tier: string): string;
52
+ /**
53
+ * Validate a tool object has required fields.
54
+ */
55
+ export declare function validateTool(tool: unknown): {
56
+ valid: boolean;
57
+ errors: string[];
58
+ };
59
+ /**
60
+ * Validate entire registry.
61
+ */
62
+ export declare function validateRegistry(registry: unknown): {
63
+ valid: boolean;
64
+ errors: string[];
65
+ };
66
+ /**
67
+ * Generate install command string for a tool and platform.
68
+ */
69
+ export declare function getInstallCommand(tool: CliTool, platform: keyof CliTool['install']): string | null;
70
+ /**
71
+ * Compare tools and return a comparison matrix.
72
+ */
73
+ export interface ComparisonRow {
74
+ field: string;
75
+ values: (string | boolean | number)[];
76
+ }
77
+ export declare function compareTools(tools: CliTool[]): ComparisonRow[];