claude-manager 1.5.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 ADDED
@@ -0,0 +1,189 @@
1
+ # Claude Manager (cm)
2
+
3
+ A powerful terminal app for managing Claude Code settings, profiles, MCP servers, and skills. Switch between different AI providers, models, and configurations with a single command.
4
+
5
+ ![Version](https://img.shields.io/badge/version-1.4.0-blue)
6
+ ![License](https://img.shields.io/badge/license-MIT-green)
7
+
8
+ ## Features
9
+
10
+ - 🔄 **Profile Management** - Create, edit, and switch between multiple Claude configurations
11
+ - 🚀 **Quick Launch** - Select profile and launch Claude in one command
12
+ - 🔌 **MCP Server Registry** - Search and add MCP servers from the official registry
13
+ - 🎯 **Skills Browser** - Browse and install skills from 3 repositories
14
+ - 📁 **Per-Project Profiles** - Auto-select profiles based on project directory
15
+ - 🔢 **Quick Select** - Press 1-9 to instantly select profiles
16
+ - 🔍 **Fuzzy Search** - Type to filter profiles
17
+ - 📦 **Profile Groups** - Organize profiles by category
18
+ - 🔄 **Auto-Update Check** - Get notified when Claude updates are available
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ curl -fsSL https://raw.githubusercontent.com/faisalnazir/claude-manager/main/install.sh | bash
24
+ ```
25
+
26
+ ### Requirements
27
+ - [Bun](https://bun.sh) (auto-installed if missing)
28
+ - [Claude Code](https://docs.anthropic.com/en/docs/claude-code)
29
+
30
+ ## Quick Start
31
+
32
+ ```bash
33
+ cm # Select profile interactively and launch Claude
34
+ cm -l # Use last profile instantly
35
+ cm --yolo # Launch with --dangerously-skip-permissions
36
+ ```
37
+
38
+ ## Commands
39
+
40
+ ### Profile Management
41
+
42
+ | Command | Description |
43
+ |---------|-------------|
44
+ | `cm` | Interactive profile selection (1-9 quick select, type to filter) |
45
+ | `cm list` | List all profiles |
46
+ | `cm new` | Create new profile wizard |
47
+ | `cm edit <name\|num>` | Edit profile in $EDITOR |
48
+ | `cm delete <name\|num>` | Delete profile |
49
+ | `cm status` | Show current settings, MCP servers, and skills |
50
+
51
+ ### MCP Servers
52
+
53
+ ```bash
54
+ cm mcp # Search MCP registry interactively
55
+ cm mcp github # Search for "github" servers
56
+ cm mcp web # Search for "web" servers
57
+ ```
58
+
59
+ Select a server → Choose profile → Server config added to profile.
60
+
61
+ ### Skills
62
+
63
+ ```bash
64
+ cm skills # Browse and install skills
65
+ ```
66
+
67
+ Skills are downloaded from 3 repositories:
68
+ - [anthropics/skills](https://github.com/anthropics/skills) (official)
69
+ - [Prat011/awesome-llm-skills](https://github.com/Prat011/awesome-llm-skills)
70
+ - [skillcreatorai/Ai-Agent-Skills](https://github.com/skillcreatorai/Ai-Agent-Skills)
71
+
72
+ ### Options
73
+
74
+ | Flag | Description |
75
+ |------|-------------|
76
+ | `--last`, `-l` | Use last profile without menu |
77
+ | `--skip-update` | Skip Claude update check |
78
+ | `--yolo` | Run Claude with `--dangerously-skip-permissions` |
79
+ | `-v`, `--version` | Show version |
80
+ | `-h`, `--help` | Show help |
81
+
82
+ ## Profiles
83
+
84
+ Profiles are stored in `~/.claude/profiles/*.json`
85
+
86
+ ### Example Profile
87
+
88
+ ```json
89
+ {
90
+ "name": "Z.AI (GLM)",
91
+ "group": "providers",
92
+ "env": {
93
+ "ANTHROPIC_AUTH_TOKEN": "your-api-key",
94
+ "ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
95
+ "API_TIMEOUT_MS": "3000000"
96
+ },
97
+ "model": "opus",
98
+ "enabledPlugins": {
99
+ "glm-plan-usage@zai-coding-plugins": true
100
+ },
101
+ "alwaysThinkingEnabled": true,
102
+ "defaultMode": "bypassPermissions",
103
+ "mcpServers": {
104
+ "web-search": {
105
+ "type": "http",
106
+ "url": "https://api.z.ai/api/mcp/web_search_prime/mcp",
107
+ "headers": {
108
+ "Authorization": "Bearer your-api-key"
109
+ }
110
+ }
111
+ }
112
+ }
113
+ ```
114
+
115
+ ### Profile Fields
116
+
117
+ | Field | Description |
118
+ |-------|-------------|
119
+ | `name` | Display name |
120
+ | `group` | Optional grouping (e.g., "providers", "work") |
121
+ | `env` | Environment variables for Claude |
122
+ | `model` | Model tier: "opus", "sonnet", "haiku" |
123
+ | `enabledPlugins` | Plugins to enable |
124
+ | `alwaysThinkingEnabled` | Enable extended thinking |
125
+ | `defaultMode` | Permission mode |
126
+ | `mcpServers` | MCP server configurations (per-profile) |
127
+
128
+ ## Supported Providers
129
+
130
+ Pre-configured in `cm new`:
131
+
132
+ | Provider | Base URL |
133
+ |----------|----------|
134
+ | Anthropic (Direct) | Default |
135
+ | Amazon Bedrock | Default |
136
+ | Z.AI | `https://api.z.ai/api/anthropic` |
137
+ | MiniMax | `https://api.minimax.io/anthropic` |
138
+ | Custom | Your URL |
139
+
140
+ ## Per-Project Profiles
141
+
142
+ Create `.claude-profile` in any directory with a profile name:
143
+
144
+ ```bash
145
+ echo "Z.AI (GLM)" > /path/to/project/.claude-profile
146
+ ```
147
+
148
+ When you run `cm` from that directory, it auto-selects that profile.
149
+
150
+ ## File Locations
151
+
152
+ | File | Purpose |
153
+ |------|---------|
154
+ | `~/.claude/profiles/*.json` | Profile configurations |
155
+ | `~/.claude/settings.json` | Active Claude settings |
156
+ | `~/.claude/skills/` | Installed skills |
157
+ | `~/.claude.json` | Global MCP servers |
158
+ | `~/.claude/.last-profile` | Last used profile |
159
+ | `.claude-profile` | Per-project profile selector |
160
+
161
+ ## Tips
162
+
163
+ ```bash
164
+ # Quick launch aliases
165
+ alias c="cm --skip-update"
166
+ alias cy="cm --skip-update --yolo"
167
+ alias cl="cm -l --skip-update"
168
+
169
+ # Edit profile by number
170
+ cm edit 1
171
+
172
+ # Check what's installed
173
+ cm status
174
+ ```
175
+
176
+ ## How It Works
177
+
178
+ 1. **Profile Selection**: Choose a profile from the interactive menu
179
+ 2. **Settings Applied**: Profile config is written to `~/.claude/settings.json`
180
+ 3. **MCP Servers**: Profile's MCP servers are written to `~/.claude.json`
181
+ 4. **Claude Launched**: Claude Code starts with your selected configuration
182
+
183
+ ## Contributing
184
+
185
+ Pull requests welcome! Please ensure no API keys or sensitive data in commits.
186
+
187
+ ## License
188
+
189
+ MIT
package/install.sh ADDED
@@ -0,0 +1,104 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ echo "Installing Claude Manager (cm)..."
5
+ echo ""
6
+
7
+ # Detect OS
8
+ OS="$(uname -s)"
9
+ case "$OS" in
10
+ Linux*) OS_TYPE="linux";;
11
+ Darwin*) OS_TYPE="mac";;
12
+ *) echo "❌ Unsupported OS: $OS"; exit 1;;
13
+ esac
14
+
15
+ # Check dependencies
16
+ check_dep() {
17
+ if ! command -v "$1" &> /dev/null; then
18
+ echo "❌ $1 is required but not installed."
19
+ if [ "$OS_TYPE" = "mac" ]; then
20
+ echo " Install with: brew install $1"
21
+ else
22
+ echo " Install with: sudo apt install $1 (or your package manager)"
23
+ fi
24
+ exit 1
25
+ fi
26
+ }
27
+
28
+ check_dep "curl"
29
+ check_dep "git"
30
+
31
+ # Check for bun, install if missing
32
+ if ! command -v bun &> /dev/null; then
33
+ echo "Installing bun..."
34
+ curl -fsSL https://bun.sh/install | bash
35
+ export BUN_INSTALL="$HOME/.bun"
36
+ export PATH="$BUN_INSTALL/bin:$PATH"
37
+ # Source for current session
38
+ if [ -f "$HOME/.bashrc" ]; then
39
+ source "$HOME/.bashrc" 2>/dev/null || true
40
+ fi
41
+ fi
42
+
43
+ # Verify bun is available
44
+ if ! command -v bun &> /dev/null; then
45
+ export PATH="$HOME/.bun/bin:$PATH"
46
+ fi
47
+
48
+ # Check for claude
49
+ if ! command -v claude &> /dev/null; then
50
+ echo "⚠️ Claude Code not found. Install with:"
51
+ if [ "$OS_TYPE" = "mac" ]; then
52
+ echo " brew install claude-code"
53
+ else
54
+ echo " npm install -g @anthropic-ai/claude-code"
55
+ fi
56
+ echo ""
57
+ fi
58
+
59
+ # Install to ~/.cm
60
+ CM_DIR="$HOME/.cm"
61
+ rm -rf "$CM_DIR"
62
+ mkdir -p "$CM_DIR"
63
+
64
+ echo "Downloading cm..."
65
+ curl -fsSL https://raw.githubusercontent.com/faisalnazir/claude-manager/main/src/cli.js -o "$CM_DIR/cli.js"
66
+ curl -fsSL https://raw.githubusercontent.com/faisalnazir/claude-manager/main/src/config.js -o "$CM_DIR/config.js"
67
+ curl -fsSL https://raw.githubusercontent.com/faisalnazir/claude-manager/main/package.json -o "$CM_DIR/package.json"
68
+
69
+ echo "Installing dependencies..."
70
+ cd "$CM_DIR" && bun install --silent
71
+
72
+ # Create wrapper script
73
+ BIN_DIR="/usr/local/bin"
74
+ if [ ! -d "$BIN_DIR" ]; then
75
+ sudo mkdir -p "$BIN_DIR"
76
+ fi
77
+
78
+ if [ -w "$BIN_DIR" ]; then
79
+ cat > "$BIN_DIR/cm" << 'EOF'
80
+ #!/bin/bash
81
+ export PATH="$HOME/.bun/bin:$PATH"
82
+ bun ~/.cm/cli.js "$@"
83
+ EOF
84
+ chmod +x "$BIN_DIR/cm"
85
+ else
86
+ sudo tee "$BIN_DIR/cm" > /dev/null << 'EOF'
87
+ #!/bin/bash
88
+ export PATH="$HOME/.bun/bin:$PATH"
89
+ bun ~/.cm/cli.js "$@"
90
+ EOF
91
+ sudo chmod +x "$BIN_DIR/cm"
92
+ fi
93
+
94
+ # Create profiles directory
95
+ mkdir -p ~/.claude/profiles
96
+
97
+ echo ""
98
+ echo "✅ cm installed successfully!"
99
+ echo ""
100
+ echo "Usage:"
101
+ echo " cm # Select profile and launch Claude"
102
+ echo " cm new # Create a new profile"
103
+ echo " cm --help # Show all commands"
104
+ echo ""
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "claude-manager",
3
+ "version": "1.5.0",
4
+ "description": "Terminal app for managing Claude Code settings, profiles, MCP servers, and skills",
5
+ "type": "module",
6
+ "bin": {
7
+ "cm": "./src/cli.js",
8
+ "claude-manager": "./src/cli.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node src/cli.js"
12
+ },
13
+ "keywords": [
14
+ "claude",
15
+ "claude-code",
16
+ "anthropic",
17
+ "ai",
18
+ "cli",
19
+ "mcp",
20
+ "profile-manager"
21
+ ],
22
+ "author": "Faisal Nazir",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/faisalnazir/claude-manager.git"
27
+ },
28
+ "homepage": "https://github.com/faisalnazir/claude-manager#readme",
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "dependencies": {
33
+ "ink": "^5.1.0",
34
+ "ink-select-input": "^6.0.0",
35
+ "ink-text-input": "^6.0.0",
36
+ "react": "^18.3.1"
37
+ }
38
+ }
package/src/cli.js ADDED
@@ -0,0 +1,770 @@
1
+ #!/usr/bin/env node
2
+ import React, { useState, useEffect } from 'react';
3
+ import { render, Box, Text, useInput, useApp } from 'ink';
4
+ import SelectInput from 'ink-select-input';
5
+ import TextInput from 'ink-text-input';
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import os from 'os';
9
+ import { execSync, spawnSync } from 'child_process';
10
+
11
+ const VERSION = "1.5.0";
12
+ const PROFILES_DIR = path.join(os.homedir(), '.claude', 'profiles');
13
+ const SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
14
+ const CLAUDE_JSON_PATH = path.join(os.homedir(), '.claude.json');
15
+ const LAST_PROFILE_PATH = path.join(os.homedir(), '.claude', '.last-profile');
16
+ const MCP_REGISTRY_URL = 'https://registry.modelcontextprotocol.io/v0/servers';
17
+
18
+ const args = process.argv.slice(2);
19
+ const cmd = args[0];
20
+
21
+ // Ensure profiles directory exists
22
+ if (!fs.existsSync(PROFILES_DIR)) fs.mkdirSync(PROFILES_DIR, { recursive: true });
23
+
24
+ // CLI flags
25
+ if (args.includes('-v') || args.includes('--version')) {
26
+ console.log(`cm v${VERSION}`);
27
+ process.exit(0);
28
+ }
29
+
30
+ if (args.includes('-h') || args.includes('--help')) {
31
+ console.log(`cm v${VERSION} - Claude Settings Manager
32
+
33
+ Usage: cm [command] [options]
34
+
35
+ Commands:
36
+ (none) Select profile interactively
37
+ new Create a new profile
38
+ edit <n> Edit profile (by name or number)
39
+ delete <n> Delete profile (by name or number)
40
+ status Show current settings
41
+ list List all profiles
42
+ mcp [query] Search and add MCP servers
43
+ skills Browse and add Anthropic skills
44
+
45
+ Options:
46
+ --last, -l Use last profile without menu
47
+ --skip-update Skip update check
48
+ --yolo Run claude with --dangerously-skip-permissions
49
+ -v, --version Show version
50
+ -h, --help Show help`);
51
+ process.exit(0);
52
+ }
53
+
54
+ const skipUpdate = args.includes('--skip-update');
55
+ const useLast = args.includes('--last') || args.includes('-l');
56
+ const dangerMode = args.includes('--dangerously-skip-permissions') || args.includes('--yolo');
57
+
58
+ // Helper functions
59
+ const loadProfiles = () => {
60
+ const profiles = [];
61
+ if (fs.existsSync(PROFILES_DIR)) {
62
+ for (const file of fs.readdirSync(PROFILES_DIR).sort()) {
63
+ if (file.endsWith('.json')) {
64
+ try {
65
+ const content = JSON.parse(fs.readFileSync(path.join(PROFILES_DIR, file), 'utf8'));
66
+ profiles.push({
67
+ label: content.name || file.replace('.json', ''),
68
+ value: file,
69
+ key: file,
70
+ group: content.group || null,
71
+ data: content,
72
+ });
73
+ } catch {}
74
+ }
75
+ }
76
+ }
77
+ return profiles;
78
+ };
79
+
80
+ const applyProfile = (filename) => {
81
+ const profilePath = path.join(PROFILES_DIR, filename);
82
+ const profile = JSON.parse(fs.readFileSync(profilePath, 'utf8'));
83
+ const { name, group, mcpServers, ...settings } = profile;
84
+
85
+ // Write settings.json
86
+ fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
87
+
88
+ // Update MCP servers in .claude.json if specified
89
+ if (mcpServers !== undefined) {
90
+ try {
91
+ const claudeJson = fs.existsSync(CLAUDE_JSON_PATH)
92
+ ? JSON.parse(fs.readFileSync(CLAUDE_JSON_PATH, 'utf8'))
93
+ : {};
94
+ claudeJson.mcpServers = mcpServers;
95
+ fs.writeFileSync(CLAUDE_JSON_PATH, JSON.stringify(claudeJson, null, 2));
96
+ } catch {}
97
+ }
98
+
99
+ fs.writeFileSync(LAST_PROFILE_PATH, filename);
100
+ return name || filename;
101
+ };
102
+
103
+ const getLastProfile = () => {
104
+ try { return fs.readFileSync(LAST_PROFILE_PATH, 'utf8').trim(); } catch { return null; }
105
+ };
106
+
107
+ const checkProjectProfile = () => {
108
+ const localProfile = path.join(process.cwd(), '.claude-profile');
109
+ if (fs.existsSync(localProfile)) {
110
+ return fs.readFileSync(localProfile, 'utf8').trim();
111
+ }
112
+ return null;
113
+ };
114
+
115
+ const checkForUpdate = () => {
116
+ if (skipUpdate) return { needsUpdate: false };
117
+ try {
118
+ const current = execSync('claude --version 2>/dev/null', { encoding: 'utf8' }).match(/(\d+\.\d+\.\d+)/)?.[1];
119
+ const output = execSync('brew outdated claude-code 2>&1 || true', { encoding: 'utf8' }).trim();
120
+ return { current, needsUpdate: output.includes('claude-code') };
121
+ } catch {
122
+ return { needsUpdate: false };
123
+ }
124
+ };
125
+
126
+ const launchClaude = () => {
127
+ try {
128
+ const claudeArgs = dangerMode ? '--dangerously-skip-permissions' : '';
129
+ execSync(`claude ${claudeArgs}`, { stdio: 'inherit' });
130
+ } catch (e) {
131
+ process.exit(e.status || 1);
132
+ }
133
+ process.exit(0);
134
+ };
135
+
136
+ // Handle --last flag
137
+ if (useLast) {
138
+ const last = getLastProfile();
139
+ if (last && fs.existsSync(path.join(PROFILES_DIR, last))) {
140
+ const name = applyProfile(last);
141
+ console.log(`\x1b[32m✓\x1b[0m Applied: ${name}\n`);
142
+ launchClaude();
143
+ } else {
144
+ console.log('\x1b[31mNo last profile found\x1b[0m');
145
+ process.exit(1);
146
+ }
147
+ }
148
+
149
+ // Handle project profile
150
+ const projectProfile = checkProjectProfile();
151
+ if (projectProfile && !cmd) {
152
+ const profiles = loadProfiles();
153
+ const match = profiles.find(p => p.label === projectProfile || p.value === projectProfile + '.json');
154
+ if (match) {
155
+ console.log(`\x1b[36mUsing project profile: ${match.label}\x1b[0m`);
156
+ applyProfile(match.value);
157
+ launchClaude();
158
+ }
159
+ }
160
+
161
+ // Handle commands
162
+ if (cmd === 'status') {
163
+ const last = getLastProfile();
164
+ const profiles = loadProfiles();
165
+ const current = profiles.find(p => p.value === last);
166
+ console.log(`\x1b[1m\x1b[36mClaude Settings Manager v${VERSION}\x1b[0m`);
167
+ console.log(`─────────────────────────`);
168
+ if (current) {
169
+ console.log(`Current profile: \x1b[32m${current.label}\x1b[0m`);
170
+ console.log(`Model: ${current.data.env?.ANTHROPIC_MODEL || 'default'}`);
171
+ console.log(`Provider: ${current.data.env?.ANTHROPIC_BASE_URL || 'Anthropic Direct'}`);
172
+ const mcpServers = current.data.mcpServers || {};
173
+ if (Object.keys(mcpServers).length > 0) {
174
+ console.log(`\nProfile MCP Servers (${Object.keys(mcpServers).length}):`);
175
+ Object.keys(mcpServers).forEach(s => console.log(` - ${s}`));
176
+ }
177
+ } else {
178
+ console.log('No profile active');
179
+ }
180
+ // Show installed skills from ~/.claude/skills/
181
+ const skillsDir = path.join(os.homedir(), '.claude', 'skills');
182
+ try {
183
+ if (fs.existsSync(skillsDir)) {
184
+ const installedSkills = fs.readdirSync(skillsDir).filter(f => {
185
+ const p = path.join(skillsDir, f);
186
+ return fs.statSync(p).isDirectory() && !f.startsWith('.');
187
+ });
188
+ if (installedSkills.length > 0) {
189
+ console.log(`\nInstalled Skills (${installedSkills.length}):`);
190
+ installedSkills.forEach(s => console.log(` - ${s}`));
191
+ }
192
+ }
193
+ } catch {}
194
+ // Show global MCP servers from ~/.claude.json
195
+ try {
196
+ const claudeJson = JSON.parse(fs.readFileSync(CLAUDE_JSON_PATH, 'utf8'));
197
+ const globalMcp = claudeJson.mcpServers || {};
198
+ if (Object.keys(globalMcp).length > 0) {
199
+ console.log(`\nGlobal MCP Servers (${Object.keys(globalMcp).length}):`);
200
+ Object.keys(globalMcp).forEach(s => console.log(` - ${s}`));
201
+ }
202
+ } catch {}
203
+ try {
204
+ const ver = execSync('claude --version 2>/dev/null', { encoding: 'utf8' }).trim();
205
+ console.log(`\nClaude: ${ver}`);
206
+ } catch {}
207
+ process.exit(0);
208
+ }
209
+
210
+ if (cmd === 'list') {
211
+ const profiles = loadProfiles();
212
+ console.log(`\x1b[1m\x1b[36mProfiles\x1b[0m (${profiles.length})`);
213
+ console.log(`─────────────────────────`);
214
+ profiles.forEach((p, i) => {
215
+ const group = p.group ? `\x1b[33m[${p.group}]\x1b[0m ` : '';
216
+ console.log(`${i + 1}. ${group}${p.label}`);
217
+ });
218
+ process.exit(0);
219
+ }
220
+
221
+ if (cmd === 'delete') {
222
+ const profiles = loadProfiles();
223
+ const target = args[1];
224
+ const idx = parseInt(target) - 1;
225
+ const match = profiles[idx] || profiles.find(p => p.label.toLowerCase() === target?.toLowerCase());
226
+ if (match) {
227
+ fs.unlinkSync(path.join(PROFILES_DIR, match.value));
228
+ console.log(`\x1b[32m✓\x1b[0m Deleted: ${match.label}`);
229
+ } else {
230
+ console.log(`\x1b[31mProfile not found: ${target}\x1b[0m`);
231
+ }
232
+ process.exit(0);
233
+ }
234
+
235
+ if (cmd === 'edit') {
236
+ const profiles = loadProfiles();
237
+ const target = args[1];
238
+ const idx = parseInt(target) - 1;
239
+ const match = profiles[idx] || profiles.find(p => p.label.toLowerCase() === target?.toLowerCase());
240
+ if (match) {
241
+ const editor = process.env.EDITOR || 'nano';
242
+ spawnSync(editor, [path.join(PROFILES_DIR, match.value)], { stdio: 'inherit' });
243
+ } else {
244
+ console.log(`\x1b[31mProfile not found: ${target}\x1b[0m`);
245
+ }
246
+ process.exit(0);
247
+ }
248
+
249
+ // MCP server search and add
250
+ const searchMcpServers = (query) => {
251
+ try {
252
+ const res = execSync(`curl -s "${MCP_REGISTRY_URL}?limit=100"`, { encoding: 'utf8', timeout: 10000 });
253
+ const data = JSON.parse(res);
254
+ const seen = new Set();
255
+ return data.servers.filter(s => {
256
+ if (seen.has(s.server.name)) return false;
257
+ seen.add(s.server.name);
258
+ const isLatest = s._meta?.['io.modelcontextprotocol.registry/official']?.isLatest !== false;
259
+ const matchesQuery = !query ||
260
+ s.server.name.toLowerCase().includes(query.toLowerCase()) ||
261
+ s.server.description?.toLowerCase().includes(query.toLowerCase());
262
+ return isLatest && matchesQuery;
263
+ }).slice(0, 15);
264
+ } catch {
265
+ return [];
266
+ }
267
+ };
268
+
269
+ const addMcpToProfile = (server, profileFile) => {
270
+ const profilePath = path.join(PROFILES_DIR, profileFile);
271
+ const profile = JSON.parse(fs.readFileSync(profilePath, 'utf8'));
272
+ if (!profile.mcpServers) profile.mcpServers = {};
273
+
274
+ const s = server.server;
275
+ const name = s.name.split('/').pop();
276
+
277
+ if (s.remotes?.[0]) {
278
+ const remote = s.remotes[0];
279
+ profile.mcpServers[name] = {
280
+ type: remote.type === 'streamable-http' ? 'http' : remote.type,
281
+ url: remote.url,
282
+ };
283
+ } else if (s.packages?.[0]) {
284
+ const pkg = s.packages[0];
285
+ if (pkg.registryType === 'npm') {
286
+ profile.mcpServers[name] = {
287
+ type: 'stdio',
288
+ command: 'npx',
289
+ args: ['-y', pkg.identifier],
290
+ };
291
+ } else if (pkg.registryType === 'pypi') {
292
+ profile.mcpServers[name] = {
293
+ type: 'stdio',
294
+ command: 'uvx',
295
+ args: [pkg.identifier],
296
+ };
297
+ }
298
+ }
299
+
300
+ fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2));
301
+ return name;
302
+ };
303
+
304
+ const McpSearch = () => {
305
+ const { exit } = useApp();
306
+ const [step, setStep] = useState(args[1] ? 'loading' : 'search');
307
+ const [query, setQuery] = useState(args[1] || '');
308
+ const [servers, setServers] = useState([]);
309
+ const [selectedServer, setSelectedServer] = useState(null);
310
+ const profiles = loadProfiles();
311
+
312
+ useEffect(() => {
313
+ if (args[1] && step === 'loading') {
314
+ const results = searchMcpServers(args[1]);
315
+ setServers(results);
316
+ setStep('results');
317
+ }
318
+ }, []);
319
+
320
+ const doSearch = () => {
321
+ const results = searchMcpServers(query);
322
+ setServers(results);
323
+ setStep('results');
324
+ };
325
+
326
+ const serverItems = servers.map(s => ({
327
+ label: `${s.server.name} - ${s.server.description?.slice(0, 50) || ''}`,
328
+ value: s,
329
+ key: s.server.name + s.server.version,
330
+ }));
331
+
332
+ const profileItems = profiles.map(p => ({ label: p.label, value: p.value, key: p.key }));
333
+
334
+ if (step === 'search') {
335
+ return (
336
+ <Box flexDirection="column" padding={1}>
337
+ <Text bold color="cyan">MCP Server Search</Text>
338
+ <Text dimColor>─────────────────────────</Text>
339
+ <Box marginTop={1}>
340
+ <Text>Search: </Text>
341
+ <TextInput value={query} onChange={setQuery} onSubmit={doSearch} />
342
+ </Box>
343
+ </Box>
344
+ );
345
+ }
346
+
347
+ if (step === 'loading') {
348
+ return <Box padding={1}><Text>Searching MCP registry...</Text></Box>;
349
+ }
350
+
351
+ if (step === 'results') {
352
+ if (servers.length === 0) {
353
+ return (
354
+ <Box flexDirection="column" padding={1}>
355
+ <Text color="yellow">No servers found for "{query}"</Text>
356
+ </Box>
357
+ );
358
+ }
359
+ return (
360
+ <Box flexDirection="column" padding={1}>
361
+ <Text bold color="cyan">MCP Servers</Text>
362
+ <Text dimColor>─────────────────────────</Text>
363
+ <Text dimColor>Found {servers.length} servers</Text>
364
+ <Box flexDirection="column" marginTop={1}>
365
+ <SelectInput
366
+ items={serverItems}
367
+ onSelect={(item) => { setSelectedServer(item.value); setStep('profile'); }}
368
+ limit={10}
369
+ />
370
+ </Box>
371
+ </Box>
372
+ );
373
+ }
374
+
375
+ if (step === 'profile') {
376
+ return (
377
+ <Box flexDirection="column" padding={1}>
378
+ <Text bold color="cyan">Add to Profile</Text>
379
+ <Text dimColor>─────────────────────────</Text>
380
+ <Text>Server: {selectedServer.server.name}</Text>
381
+ <Box flexDirection="column" marginTop={1}>
382
+ <Text>Select profile:</Text>
383
+ <SelectInput
384
+ items={profileItems}
385
+ onSelect={(item) => {
386
+ const name = addMcpToProfile(selectedServer, item.value);
387
+ console.log(`\n\x1b[32m✓\x1b[0m Added ${name} to ${item.label}`);
388
+ exit();
389
+ }}
390
+ />
391
+ </Box>
392
+ </Box>
393
+ );
394
+ }
395
+
396
+ return null;
397
+ };
398
+
399
+ // Skills browser
400
+ const SKILL_SOURCES = [
401
+ { url: 'https://api.github.com/repos/anthropics/skills/contents/skills', base: 'https://github.com/anthropics/skills/tree/main/skills' },
402
+ { url: 'https://api.github.com/repos/Prat011/awesome-llm-skills/contents/skills', base: 'https://github.com/Prat011/awesome-llm-skills/tree/main/skills' },
403
+ { url: 'https://api.github.com/repos/skillcreatorai/Ai-Agent-Skills/contents/skills', base: 'https://github.com/skillcreatorai/Ai-Agent-Skills/tree/main/skills' },
404
+ ];
405
+
406
+ const fetchSkills = () => {
407
+ const seen = new Set();
408
+ const skills = [];
409
+ for (const source of SKILL_SOURCES) {
410
+ try {
411
+ const res = execSync(`curl -s "${source.url}"`, { encoding: 'utf8', timeout: 10000 });
412
+ const data = JSON.parse(res);
413
+ if (Array.isArray(data)) {
414
+ for (const s of data.filter(s => s.type === 'dir')) {
415
+ if (!seen.has(s.name)) {
416
+ seen.add(s.name);
417
+ skills.push({
418
+ label: s.name,
419
+ value: `${source.base}/${s.name}`,
420
+ key: s.name,
421
+ });
422
+ }
423
+ }
424
+ }
425
+ } catch {}
426
+ }
427
+ return skills.sort((a, b) => a.label.localeCompare(b.label));
428
+ };
429
+
430
+ const SKILLS_DIR = path.join(os.homedir(), '.claude', 'skills');
431
+
432
+ const addSkillToClaudeJson = (skillName, skillUrl) => {
433
+ try {
434
+ // Ensure skills directory exists
435
+ if (!fs.existsSync(SKILLS_DIR)) fs.mkdirSync(SKILLS_DIR, { recursive: true });
436
+
437
+ const skillPath = path.join(SKILLS_DIR, skillName);
438
+ if (fs.existsSync(skillPath)) {
439
+ return { success: false, message: 'Skill already installed' };
440
+ }
441
+
442
+ // Convert GitHub URL to clone-friendly format
443
+ // https://github.com/anthropics/skills/tree/main/skills/frontend-design
444
+ // -> git clone with sparse checkout
445
+ const match = skillUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\/tree\/([^\/]+)\/(.+)/);
446
+ if (!match) return { success: false, message: 'Invalid skill URL' };
447
+
448
+ const [, owner, repo, branch, skillSubPath] = match;
449
+ const tempDir = `/tmp/skill-clone-${Date.now()}`;
450
+
451
+ // Sparse clone just the skill folder
452
+ execSync(`git clone --depth 1 --filter=blob:none --sparse "https://github.com/${owner}/${repo}.git" "${tempDir}" 2>/dev/null`, { timeout: 30000 });
453
+ execSync(`cd "${tempDir}" && git sparse-checkout set "${skillSubPath}" 2>/dev/null`, { timeout: 10000 });
454
+
455
+ // Move skill to destination
456
+ execSync(`mv "${tempDir}/${skillSubPath}" "${skillPath}"`, { timeout: 5000 });
457
+ execSync(`rm -rf "${tempDir}"`, { timeout: 5000 });
458
+
459
+ return { success: true };
460
+ } catch (e) {
461
+ return { success: false, message: 'Failed to download skill' };
462
+ }
463
+ };
464
+
465
+ const SkillsBrowser = () => {
466
+ const { exit } = useApp();
467
+ const [skills, setSkills] = useState([]);
468
+ const [loading, setLoading] = useState(true);
469
+
470
+ useEffect(() => {
471
+ const s = fetchSkills();
472
+ setSkills(s);
473
+ setLoading(false);
474
+ }, []);
475
+
476
+ if (loading) {
477
+ return <Box padding={1}><Text>Loading skills...</Text></Box>;
478
+ }
479
+
480
+ if (skills.length === 0) {
481
+ return (
482
+ <Box flexDirection="column" padding={1}>
483
+ <Text color="yellow">Could not fetch skills</Text>
484
+ </Box>
485
+ );
486
+ }
487
+
488
+ return (
489
+ <Box flexDirection="column" padding={1}>
490
+ <Text bold color="cyan">Anthropic Skills</Text>
491
+ <Text dimColor>─────────────────────────</Text>
492
+ <Text dimColor>Found {skills.length} skills</Text>
493
+ <Box flexDirection="column" marginTop={1}>
494
+ <SelectInput
495
+ items={skills}
496
+ onSelect={(item) => {
497
+ const result = addSkillToClaudeJson(item.label, item.value);
498
+ if (result.success) {
499
+ console.log(`\n\x1b[32m✓\x1b[0m Installed skill: ${item.label}`);
500
+ console.log(`\x1b[36mLocation: ~/.claude/skills/${item.label}/\x1b[0m`);
501
+ } else {
502
+ console.log(`\n\x1b[31m✗\x1b[0m ${result.message || 'Failed to install skill'}`);
503
+ }
504
+ exit();
505
+ }}
506
+ />
507
+ </Box>
508
+ </Box>
509
+ );
510
+ };
511
+
512
+ if (cmd === 'skills') {
513
+ render(<SkillsBrowser />);
514
+ } else if (cmd === 'mcp') {
515
+ render(<McpSearch />);
516
+ } else if (cmd === 'new') {
517
+ // New profile wizard
518
+ const NewProfileWizard = () => {
519
+ const { exit } = useApp();
520
+ const [step, setStep] = useState('name');
521
+ const [name, setName] = useState('');
522
+ const [provider, setProvider] = useState('');
523
+ const [apiKey, setApiKey] = useState('');
524
+ const [model, setModel] = useState('');
525
+ const [group, setGroup] = useState('');
526
+
527
+ const providers = [
528
+ { label: 'Anthropic (Direct)', value: 'anthropic', url: '', needsKey: true },
529
+ { label: 'Amazon Bedrock', value: 'bedrock', url: '', needsKey: false },
530
+ { label: 'Z.AI', value: 'zai', url: 'https://api.z.ai/api/anthropic', needsKey: true },
531
+ { label: 'MiniMax', value: 'minimax', url: 'https://api.minimax.io/anthropic', needsKey: true },
532
+ { label: 'Custom', value: 'custom', url: '', needsKey: true },
533
+ ];
534
+
535
+ const handleSave = () => {
536
+ const prov = providers.find(p => p.value === provider);
537
+ const profile = {
538
+ name,
539
+ group: group || undefined,
540
+ env: {
541
+ ...(apiKey && { ANTHROPIC_AUTH_TOKEN: apiKey }),
542
+ ...(model && { ANTHROPIC_MODEL: model }),
543
+ ...(prov?.url && { ANTHROPIC_BASE_URL: prov.url }),
544
+ API_TIMEOUT_MS: '3000000',
545
+ },
546
+ model: 'opus',
547
+ alwaysThinkingEnabled: true,
548
+ defaultMode: 'bypassPermissions',
549
+ };
550
+ const filename = name.toLowerCase().replace(/\s+/g, '-') + '.json';
551
+ fs.writeFileSync(path.join(PROFILES_DIR, filename), JSON.stringify(profile, null, 2));
552
+ console.log(`\n\x1b[32m✓\x1b[0m Created: ${name}`);
553
+ exit();
554
+ };
555
+
556
+ const handleProviderSelect = (item) => {
557
+ setProvider(item.value);
558
+ const prov = providers.find(p => p.value === item.value);
559
+ setStep(prov.needsKey ? 'apikey' : 'model');
560
+ };
561
+
562
+ return (
563
+ <Box flexDirection="column" padding={1}>
564
+ <Text bold color="cyan">New Profile</Text>
565
+ <Text dimColor>─────────────────────────</Text>
566
+
567
+ {step === 'name' && (
568
+ <Box marginTop={1}>
569
+ <Text>Name: </Text>
570
+ <TextInput value={name} onChange={setName} onSubmit={() => setStep('provider')} />
571
+ </Box>
572
+ )}
573
+
574
+ {step === 'provider' && (
575
+ <Box flexDirection="column" marginTop={1}>
576
+ <Text>Provider:</Text>
577
+ <SelectInput items={providers} onSelect={handleProviderSelect} />
578
+ </Box>
579
+ )}
580
+
581
+ {step === 'apikey' && (
582
+ <Box marginTop={1}>
583
+ <Text>API Key: </Text>
584
+ <TextInput value={apiKey} onChange={setApiKey} onSubmit={() => setStep('model')} mask="*" />
585
+ </Box>
586
+ )}
587
+
588
+ {step === 'model' && (
589
+ <Box marginTop={1}>
590
+ <Text>Model ID (optional): </Text>
591
+ <TextInput value={model} onChange={setModel} onSubmit={() => setStep('group')} />
592
+ </Box>
593
+ )}
594
+
595
+ {step === 'group' && (
596
+ <Box marginTop={1}>
597
+ <Text>Group (optional): </Text>
598
+ <TextInput value={group} onChange={setGroup} onSubmit={handleSave} />
599
+ </Box>
600
+ )}
601
+ </Box>
602
+ );
603
+ };
604
+ render(<NewProfileWizard />);
605
+ } else {
606
+ // Loading animation component
607
+ const LoadingScreen = ({ message = 'Loading...' }) => {
608
+ const [dots, setDots] = useState('');
609
+ const [colorIdx, setColorIdx] = useState(0);
610
+ const colors = ['cyan', 'blue', 'magenta', 'red', 'yellow', 'green'];
611
+
612
+ useEffect(() => {
613
+ const dotsInterval = setInterval(() => {
614
+ setDots(d => d.length >= 3 ? '' : d + '.');
615
+ }, 500);
616
+ const colorInterval = setInterval(() => {
617
+ setColorIdx(i => (i + 1) % colors.length);
618
+ }, 200);
619
+ return () => { clearInterval(dotsInterval); clearInterval(colorInterval); };
620
+ }, []);
621
+
622
+ return (
623
+ <Box flexDirection="column" padding={1}>
624
+ <Text bold color={colors[colorIdx]}>
625
+ {`██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗
626
+ ██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝
627
+ ██║ ██║ ███████║██║ ██║██║ ██║█████╗
628
+ ██║ ██║ ██╔══██║██║ ██║██║ ██║██╔══╝
629
+ ╚██████╗███████╗██║ ██║╚██████╔╝██████╔╝███████╗
630
+ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝`}
631
+ </Text>
632
+ <Text bold color={colors[(colorIdx + 3) % colors.length]}>MANAGER v{VERSION}</Text>
633
+ <Text color="yellow" marginTop={1}>{message}{dots}</Text>
634
+ </Box>
635
+ );
636
+ };
637
+
638
+ // Main app
639
+ const App = () => {
640
+ const [step, setStep] = useState('select');
641
+ const [updateInfo, setUpdateInfo] = useState(null);
642
+ const [filter, setFilter] = useState('');
643
+ const profiles = loadProfiles();
644
+
645
+ useEffect(() => {
646
+ // Show loading screen briefly, then go to select
647
+ setTimeout(() => setStep('select'), 1500);
648
+
649
+ // Check for updates in parallel (non-blocking)
650
+ if (!skipUpdate) {
651
+ Promise.resolve().then(() => {
652
+ const info = checkForUpdate();
653
+ setUpdateInfo(info);
654
+ });
655
+ }
656
+ }, []);
657
+
658
+ useInput((input, key) => {
659
+ if (step === 'select') {
660
+ // Number shortcuts
661
+ const num = parseInt(input);
662
+ if (num >= 1 && num <= 9 && num <= filteredProfiles.length) {
663
+ const profile = filteredProfiles[num - 1];
664
+ applyProfile(profile.value);
665
+ console.log(`\n\x1b[32m✓\x1b[0m Applied: ${profile.label}\n`);
666
+ launchClaude();
667
+ }
668
+ // Update shortcut
669
+ if (input === 'u' && updateInfo?.needsUpdate) {
670
+ console.log('\n\x1b[33mUpdating Claude...\x1b[0m\n');
671
+ try {
672
+ execSync('brew upgrade claude-code', { stdio: 'inherit' });
673
+ console.log('\n\x1b[32m✓ Updated!\x1b[0m\n');
674
+ setUpdateInfo({ ...updateInfo, needsUpdate: false });
675
+ } catch {}
676
+ }
677
+ // Fuzzy filter
678
+ if (input.match(/^[a-zA-Z]$/) && input !== 'u') {
679
+ setFilter(f => f + input);
680
+ }
681
+ if (key.backspace || key.delete) {
682
+ setFilter(f => f.slice(0, -1));
683
+ }
684
+ if (key.escape) {
685
+ setFilter('');
686
+ }
687
+ }
688
+ });
689
+
690
+ // Group and filter profiles
691
+ const filteredProfiles = profiles.filter(p =>
692
+ !filter || p.label.toLowerCase().includes(filter.toLowerCase())
693
+ );
694
+
695
+ const groupedItems = [];
696
+ const groups = [...new Set(filteredProfiles.map(p => p.group).filter(Boolean))];
697
+
698
+ if (groups.length > 0) {
699
+ groups.forEach(g => {
700
+ groupedItems.push({ label: `── ${g} ──`, value: `group-${g}`, key: `group-${g}`, disabled: true });
701
+ filteredProfiles.filter(p => p.group === g).forEach((p, i) => {
702
+ groupedItems.push({ ...p, label: `${i + 1}. ${p.label}` });
703
+ });
704
+ });
705
+ const ungrouped = filteredProfiles.filter(p => !p.group);
706
+ if (ungrouped.length > 0) {
707
+ groupedItems.push({ label: '── Other ──', value: 'group-other', key: 'group-other', disabled: true });
708
+ ungrouped.forEach((p, i) => groupedItems.push({ ...p, label: `${i + 1}. ${p.label}` }));
709
+ }
710
+ } else {
711
+ filteredProfiles.forEach((p, i) => groupedItems.push({ ...p, label: `${i + 1}. ${p.label}` }));
712
+ }
713
+
714
+ if (step === 'loading') {
715
+ return <LoadingScreen message="Initializing Claude Manager" />;
716
+ }
717
+
718
+ if (profiles.length === 0) {
719
+ return (
720
+ <Box flexDirection="column" padding={1}>
721
+ <Text bold color="cyan">CLAUDE MANAGER</Text>
722
+ <Text dimColor>─────────────────────────</Text>
723
+ <Text color="yellow" marginTop={1}>No profiles found!</Text>
724
+ <Text>Run: cm new</Text>
725
+ </Box>
726
+ );
727
+ }
728
+
729
+ const handleSelect = (item) => {
730
+ if (item.disabled) return;
731
+ applyProfile(item.value);
732
+ console.log(`\n\x1b[32m✓\x1b[0m Applied: ${item.label.replace(/^\d+\.\s*/, '')}\n`);
733
+ launchClaude();
734
+ };
735
+
736
+ return (
737
+ <Box flexDirection="column" padding={1}>
738
+ <Text bold color="cyan">
739
+ {`██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗
740
+ ██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝
741
+ ██║ ██║ ███████║██║ ██║██║ ██║█████╗
742
+ ██║ ██║ ██╔══██║██║ ██║██║ ██║██╔══╝
743
+ ╚██████╗███████╗██║ ██║╚██████╔╝██████╔╝███████╗
744
+ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝`}
745
+ </Text>
746
+ <Text bold color="magenta">MANAGER v{VERSION}</Text>
747
+ <Text dimColor>─────────────────────────</Text>
748
+ {updateInfo?.current && <Text dimColor>Claude v{updateInfo.current}</Text>}
749
+ {updateInfo?.needsUpdate && (
750
+ <Text color="yellow">⚠ Update available! Press 'u' to upgrade</Text>
751
+ )}
752
+ {filter && <Text color="yellow">Filter: {filter}</Text>}
753
+ <Box flexDirection="column" marginTop={1}>
754
+ <Text>Select Profile: <Text dimColor>(1-9 quick select, type to filter{updateInfo?.needsUpdate ? ', u to update' : ''})</Text></Text>
755
+ <SelectInput
756
+ items={groupedItems}
757
+ onSelect={handleSelect}
758
+ itemComponent={({ isSelected, label, disabled }) => (
759
+ <Text color={disabled ? 'gray' : isSelected ? 'cyan' : 'white'} dimColor={disabled}>
760
+ {disabled ? label : (isSelected ? '❯ ' : ' ') + label}
761
+ </Text>
762
+ )}
763
+ />
764
+ </Box>
765
+ </Box>
766
+ );
767
+ };
768
+
769
+ render(<App />);
770
+ }
package/src/config.js ADDED
@@ -0,0 +1,29 @@
1
+ export const VENDORS = [
2
+ { label: 'Anthropic (Direct)', value: 'anthropic' },
3
+ { label: 'Amazon Bedrock', value: 'bedrock' },
4
+ { label: 'Google Vertex AI', value: 'vertex' },
5
+ { label: 'Z.AI', value: 'zai' },
6
+ { label: 'MiniMax', value: 'minimax' },
7
+ ];
8
+
9
+ export const MODELS = {
10
+ anthropic: [
11
+ { label: 'Claude Sonnet 4', value: 'claude-sonnet-4-20250514' },
12
+ { label: 'Claude Opus 4', value: 'claude-opus-4-20250514' },
13
+ { label: 'Claude 3.5 Sonnet', value: 'claude-3-5-sonnet-20241022' },
14
+ ],
15
+ bedrock: [
16
+ { label: 'Claude Sonnet 4', value: 'anthropic.claude-sonnet-4-20250514-v1:0' },
17
+ { label: 'Claude 3.5 Sonnet', value: 'anthropic.claude-3-5-sonnet-20241022-v2:0' },
18
+ ],
19
+ vertex: [
20
+ { label: 'Claude Sonnet 4', value: 'claude-sonnet-4@20250514' },
21
+ { label: 'Claude 3.5 Sonnet', value: 'claude-3-5-sonnet-v2@20241022' },
22
+ ],
23
+ zai: [
24
+ { label: 'GLM 4.6', value: 'glm-4.6' },
25
+ ],
26
+ minimax: [
27
+ { label: 'MiniMax-01', value: 'minimax-01' },
28
+ ],
29
+ };