claude-manager 1.5.0 → 1.5.3
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/dist/cli.js +985 -0
- package/package.json +12 -4
- package/install.sh +0 -104
- package/src/cli.js +0 -770
- package/src/config.js +0 -29
package/src/cli.js
DELETED
|
@@ -1,770 +0,0 @@
|
|
|
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
|
-
}
|