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 +189 -0
- package/install.sh +104 -0
- package/package.json +38 -0
- package/src/cli.js +770 -0
- package/src/config.js +29 -0
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
|
+

|
|
6
|
+

|
|
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
|
+
};
|