claude-manager 1.5.0 → 1.5.1

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 ADDED
@@ -0,0 +1,590 @@
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
+ const VERSION = "1.5.1";
11
+ const PROFILES_DIR = path.join(os.homedir(), ".claude", "profiles");
12
+ const SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
13
+ const CLAUDE_JSON_PATH = path.join(os.homedir(), ".claude.json");
14
+ const LAST_PROFILE_PATH = path.join(os.homedir(), ".claude", ".last-profile");
15
+ const MCP_REGISTRY_URL = "https://registry.modelcontextprotocol.io/v0/servers";
16
+ const args = process.argv.slice(2);
17
+ const cmd = args[0];
18
+ if (!fs.existsSync(PROFILES_DIR)) fs.mkdirSync(PROFILES_DIR, { recursive: true });
19
+ if (args.includes("-v") || args.includes("--version")) {
20
+ console.log(`cm v${VERSION}`);
21
+ process.exit(0);
22
+ }
23
+ if (args.includes("-h") || args.includes("--help")) {
24
+ console.log(`cm v${VERSION} - Claude Settings Manager
25
+
26
+ Usage: cm [command] [options]
27
+
28
+ Commands:
29
+ (none) Select profile interactively
30
+ new Create a new profile
31
+ edit <n> Edit profile (by name or number)
32
+ delete <n> Delete profile (by name or number)
33
+ status Show current settings
34
+ list List all profiles
35
+ mcp [query] Search and add MCP servers
36
+ skills Browse and add Anthropic skills
37
+
38
+ Options:
39
+ --last, -l Use last profile without menu
40
+ --skip-update Skip update check
41
+ --yolo Run claude with --dangerously-skip-permissions
42
+ -v, --version Show version
43
+ -h, --help Show help`);
44
+ process.exit(0);
45
+ }
46
+ const skipUpdate = args.includes("--skip-update");
47
+ const useLast = args.includes("--last") || args.includes("-l");
48
+ const dangerMode = args.includes("--dangerously-skip-permissions") || args.includes("--yolo");
49
+ const loadProfiles = () => {
50
+ const profiles = [];
51
+ if (fs.existsSync(PROFILES_DIR)) {
52
+ for (const file of fs.readdirSync(PROFILES_DIR).sort()) {
53
+ if (file.endsWith(".json")) {
54
+ try {
55
+ const content = JSON.parse(fs.readFileSync(path.join(PROFILES_DIR, file), "utf8"));
56
+ profiles.push({
57
+ label: content.name || file.replace(".json", ""),
58
+ value: file,
59
+ key: file,
60
+ group: content.group || null,
61
+ data: content
62
+ });
63
+ } catch {
64
+ }
65
+ }
66
+ }
67
+ }
68
+ return profiles;
69
+ };
70
+ const applyProfile = (filename) => {
71
+ const profilePath = path.join(PROFILES_DIR, filename);
72
+ const profile = JSON.parse(fs.readFileSync(profilePath, "utf8"));
73
+ const { name, group, mcpServers, ...settings } = profile;
74
+ fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
75
+ if (mcpServers !== void 0) {
76
+ try {
77
+ const claudeJson = fs.existsSync(CLAUDE_JSON_PATH) ? JSON.parse(fs.readFileSync(CLAUDE_JSON_PATH, "utf8")) : {};
78
+ claudeJson.mcpServers = mcpServers;
79
+ fs.writeFileSync(CLAUDE_JSON_PATH, JSON.stringify(claudeJson, null, 2));
80
+ } catch {
81
+ }
82
+ }
83
+ fs.writeFileSync(LAST_PROFILE_PATH, filename);
84
+ return name || filename;
85
+ };
86
+ const getLastProfile = () => {
87
+ try {
88
+ return fs.readFileSync(LAST_PROFILE_PATH, "utf8").trim();
89
+ } catch {
90
+ return null;
91
+ }
92
+ };
93
+ const checkProjectProfile = () => {
94
+ const localProfile = path.join(process.cwd(), ".claude-profile");
95
+ if (fs.existsSync(localProfile)) {
96
+ return fs.readFileSync(localProfile, "utf8").trim();
97
+ }
98
+ return null;
99
+ };
100
+ const checkForUpdate = () => {
101
+ if (skipUpdate) return { needsUpdate: false };
102
+ try {
103
+ const current = execSync("claude --version 2>/dev/null", { encoding: "utf8" }).match(/(\d+\.\d+\.\d+)/)?.[1];
104
+ const output = execSync("brew outdated claude-code 2>&1 || true", { encoding: "utf8" }).trim();
105
+ return { current, needsUpdate: output.includes("claude-code") };
106
+ } catch {
107
+ return { needsUpdate: false };
108
+ }
109
+ };
110
+ const launchClaude = () => {
111
+ try {
112
+ const claudeArgs = dangerMode ? "--dangerously-skip-permissions" : "";
113
+ execSync(`claude ${claudeArgs}`, { stdio: "inherit" });
114
+ } catch (e) {
115
+ process.exit(e.status || 1);
116
+ }
117
+ process.exit(0);
118
+ };
119
+ if (useLast) {
120
+ const last = getLastProfile();
121
+ if (last && fs.existsSync(path.join(PROFILES_DIR, last))) {
122
+ const name = applyProfile(last);
123
+ console.log(`\x1B[32m\u2713\x1B[0m Applied: ${name}
124
+ `);
125
+ launchClaude();
126
+ } else {
127
+ console.log("\x1B[31mNo last profile found\x1B[0m");
128
+ process.exit(1);
129
+ }
130
+ }
131
+ const projectProfile = checkProjectProfile();
132
+ if (projectProfile && !cmd) {
133
+ const profiles = loadProfiles();
134
+ const match = profiles.find((p) => p.label === projectProfile || p.value === projectProfile + ".json");
135
+ if (match) {
136
+ console.log(`\x1B[36mUsing project profile: ${match.label}\x1B[0m`);
137
+ applyProfile(match.value);
138
+ launchClaude();
139
+ }
140
+ }
141
+ if (cmd === "status") {
142
+ const last = getLastProfile();
143
+ const profiles = loadProfiles();
144
+ const current = profiles.find((p) => p.value === last);
145
+ console.log(`\x1B[1m\x1B[36mClaude Settings Manager v${VERSION}\x1B[0m`);
146
+ console.log(`\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
147
+ if (current) {
148
+ console.log(`Current profile: \x1B[32m${current.label}\x1B[0m`);
149
+ console.log(`Model: ${current.data.env?.ANTHROPIC_MODEL || "default"}`);
150
+ console.log(`Provider: ${current.data.env?.ANTHROPIC_BASE_URL || "Anthropic Direct"}`);
151
+ const mcpServers = current.data.mcpServers || {};
152
+ if (Object.keys(mcpServers).length > 0) {
153
+ console.log(`
154
+ Profile MCP Servers (${Object.keys(mcpServers).length}):`);
155
+ Object.keys(mcpServers).forEach((s) => console.log(` - ${s}`));
156
+ }
157
+ } else {
158
+ console.log("No profile active");
159
+ }
160
+ const skillsDir = path.join(os.homedir(), ".claude", "skills");
161
+ try {
162
+ if (fs.existsSync(skillsDir)) {
163
+ const installedSkills = fs.readdirSync(skillsDir).filter((f) => {
164
+ const p = path.join(skillsDir, f);
165
+ return fs.statSync(p).isDirectory() && !f.startsWith(".");
166
+ });
167
+ if (installedSkills.length > 0) {
168
+ console.log(`
169
+ Installed Skills (${installedSkills.length}):`);
170
+ installedSkills.forEach((s) => console.log(` - ${s}`));
171
+ }
172
+ }
173
+ } catch {
174
+ }
175
+ try {
176
+ const claudeJson = JSON.parse(fs.readFileSync(CLAUDE_JSON_PATH, "utf8"));
177
+ const globalMcp = claudeJson.mcpServers || {};
178
+ if (Object.keys(globalMcp).length > 0) {
179
+ console.log(`
180
+ Global MCP Servers (${Object.keys(globalMcp).length}):`);
181
+ Object.keys(globalMcp).forEach((s) => console.log(` - ${s}`));
182
+ }
183
+ } catch {
184
+ }
185
+ try {
186
+ const ver = execSync("claude --version 2>/dev/null", { encoding: "utf8" }).trim();
187
+ console.log(`
188
+ Claude: ${ver}`);
189
+ } catch {
190
+ }
191
+ process.exit(0);
192
+ }
193
+ if (cmd === "list") {
194
+ const profiles = loadProfiles();
195
+ console.log(`\x1B[1m\x1B[36mProfiles\x1B[0m (${profiles.length})`);
196
+ console.log(`\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
197
+ profiles.forEach((p, i) => {
198
+ const group = p.group ? `\x1B[33m[${p.group}]\x1B[0m ` : "";
199
+ console.log(`${i + 1}. ${group}${p.label}`);
200
+ });
201
+ process.exit(0);
202
+ }
203
+ if (cmd === "delete") {
204
+ const profiles = loadProfiles();
205
+ const target = args[1];
206
+ const idx = parseInt(target) - 1;
207
+ const match = profiles[idx] || profiles.find((p) => p.label.toLowerCase() === target?.toLowerCase());
208
+ if (match) {
209
+ fs.unlinkSync(path.join(PROFILES_DIR, match.value));
210
+ console.log(`\x1B[32m\u2713\x1B[0m Deleted: ${match.label}`);
211
+ } else {
212
+ console.log(`\x1B[31mProfile not found: ${target}\x1B[0m`);
213
+ }
214
+ process.exit(0);
215
+ }
216
+ if (cmd === "edit") {
217
+ const profiles = loadProfiles();
218
+ const target = args[1];
219
+ const idx = parseInt(target) - 1;
220
+ const match = profiles[idx] || profiles.find((p) => p.label.toLowerCase() === target?.toLowerCase());
221
+ if (match) {
222
+ const editor = process.env.EDITOR || "nano";
223
+ spawnSync(editor, [path.join(PROFILES_DIR, match.value)], { stdio: "inherit" });
224
+ } else {
225
+ console.log(`\x1B[31mProfile not found: ${target}\x1B[0m`);
226
+ }
227
+ process.exit(0);
228
+ }
229
+ const searchMcpServers = (query) => {
230
+ try {
231
+ const res = execSync(`curl -s "${MCP_REGISTRY_URL}?limit=100"`, { encoding: "utf8", timeout: 1e4 });
232
+ const data = JSON.parse(res);
233
+ const seen = /* @__PURE__ */ new Set();
234
+ return data.servers.filter((s) => {
235
+ if (seen.has(s.server.name)) return false;
236
+ seen.add(s.server.name);
237
+ const isLatest = s._meta?.["io.modelcontextprotocol.registry/official"]?.isLatest !== false;
238
+ const matchesQuery = !query || s.server.name.toLowerCase().includes(query.toLowerCase()) || s.server.description?.toLowerCase().includes(query.toLowerCase());
239
+ return isLatest && matchesQuery;
240
+ }).slice(0, 15);
241
+ } catch {
242
+ return [];
243
+ }
244
+ };
245
+ const addMcpToProfile = (server, profileFile) => {
246
+ const profilePath = path.join(PROFILES_DIR, profileFile);
247
+ const profile = JSON.parse(fs.readFileSync(profilePath, "utf8"));
248
+ if (!profile.mcpServers) profile.mcpServers = {};
249
+ const s = server.server;
250
+ const name = s.name.split("/").pop();
251
+ if (s.remotes?.[0]) {
252
+ const remote = s.remotes[0];
253
+ profile.mcpServers[name] = {
254
+ type: remote.type === "streamable-http" ? "http" : remote.type,
255
+ url: remote.url
256
+ };
257
+ } else if (s.packages?.[0]) {
258
+ const pkg = s.packages[0];
259
+ if (pkg.registryType === "npm") {
260
+ profile.mcpServers[name] = {
261
+ type: "stdio",
262
+ command: "npx",
263
+ args: ["-y", pkg.identifier]
264
+ };
265
+ } else if (pkg.registryType === "pypi") {
266
+ profile.mcpServers[name] = {
267
+ type: "stdio",
268
+ command: "uvx",
269
+ args: [pkg.identifier]
270
+ };
271
+ }
272
+ }
273
+ fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2));
274
+ return name;
275
+ };
276
+ const McpSearch = () => {
277
+ const { exit } = useApp();
278
+ const [step, setStep] = useState(args[1] ? "loading" : "search");
279
+ const [query, setQuery] = useState(args[1] || "");
280
+ const [servers, setServers] = useState([]);
281
+ const [selectedServer, setSelectedServer] = useState(null);
282
+ const profiles = loadProfiles();
283
+ useEffect(() => {
284
+ if (args[1] && step === "loading") {
285
+ const results = searchMcpServers(args[1]);
286
+ setServers(results);
287
+ setStep("results");
288
+ }
289
+ }, []);
290
+ const doSearch = () => {
291
+ const results = searchMcpServers(query);
292
+ setServers(results);
293
+ setStep("results");
294
+ };
295
+ const serverItems = servers.map((s) => ({
296
+ label: `${s.server.name} - ${s.server.description?.slice(0, 50) || ""}`,
297
+ value: s,
298
+ key: s.server.name + s.server.version
299
+ }));
300
+ const profileItems = profiles.map((p) => ({ label: p.label, value: p.value, key: p.key }));
301
+ if (step === "search") {
302
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, "MCP Server Search"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Search: "), /* @__PURE__ */ React.createElement(TextInput, { value: query, onChange: setQuery, onSubmit: doSearch })));
303
+ }
304
+ if (step === "loading") {
305
+ return /* @__PURE__ */ React.createElement(Box, { padding: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Searching MCP registry..."));
306
+ }
307
+ if (step === "results") {
308
+ if (servers.length === 0) {
309
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, 'No servers found for "', query, '"'));
310
+ }
311
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, "MCP Servers"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Found ", servers.length, " servers"), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React.createElement(
312
+ SelectInput,
313
+ {
314
+ items: serverItems,
315
+ onSelect: (item) => {
316
+ setSelectedServer(item.value);
317
+ setStep("profile");
318
+ },
319
+ limit: 10
320
+ }
321
+ )));
322
+ }
323
+ if (step === "profile") {
324
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, "Add to Profile"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"), /* @__PURE__ */ React.createElement(Text, null, "Server: ", selectedServer.server.name), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Select profile:"), /* @__PURE__ */ React.createElement(
325
+ SelectInput,
326
+ {
327
+ items: profileItems,
328
+ onSelect: (item) => {
329
+ const name = addMcpToProfile(selectedServer, item.value);
330
+ console.log(`
331
+ \x1B[32m\u2713\x1B[0m Added ${name} to ${item.label}`);
332
+ exit();
333
+ }
334
+ }
335
+ )));
336
+ }
337
+ return null;
338
+ };
339
+ const SKILL_SOURCES = [
340
+ { url: "https://api.github.com/repos/anthropics/skills/contents/skills", base: "https://github.com/anthropics/skills/tree/main/skills" },
341
+ { url: "https://api.github.com/repos/Prat011/awesome-llm-skills/contents/skills", base: "https://github.com/Prat011/awesome-llm-skills/tree/main/skills" },
342
+ { url: "https://api.github.com/repos/skillcreatorai/Ai-Agent-Skills/contents/skills", base: "https://github.com/skillcreatorai/Ai-Agent-Skills/tree/main/skills" }
343
+ ];
344
+ const fetchSkills = () => {
345
+ const seen = /* @__PURE__ */ new Set();
346
+ const skills = [];
347
+ for (const source of SKILL_SOURCES) {
348
+ try {
349
+ const res = execSync(`curl -s "${source.url}"`, { encoding: "utf8", timeout: 1e4 });
350
+ const data = JSON.parse(res);
351
+ if (Array.isArray(data)) {
352
+ for (const s of data.filter((s2) => s2.type === "dir")) {
353
+ if (!seen.has(s.name)) {
354
+ seen.add(s.name);
355
+ skills.push({
356
+ label: s.name,
357
+ value: `${source.base}/${s.name}`,
358
+ key: s.name
359
+ });
360
+ }
361
+ }
362
+ }
363
+ } catch {
364
+ }
365
+ }
366
+ return skills.sort((a, b) => a.label.localeCompare(b.label));
367
+ };
368
+ const SKILLS_DIR = path.join(os.homedir(), ".claude", "skills");
369
+ const addSkillToClaudeJson = (skillName, skillUrl) => {
370
+ try {
371
+ if (!fs.existsSync(SKILLS_DIR)) fs.mkdirSync(SKILLS_DIR, { recursive: true });
372
+ const skillPath = path.join(SKILLS_DIR, skillName);
373
+ if (fs.existsSync(skillPath)) {
374
+ return { success: false, message: "Skill already installed" };
375
+ }
376
+ const match = skillUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\/tree\/([^\/]+)\/(.+)/);
377
+ if (!match) return { success: false, message: "Invalid skill URL" };
378
+ const [, owner, repo, branch, skillSubPath] = match;
379
+ const tempDir = `/tmp/skill-clone-${Date.now()}`;
380
+ execSync(`git clone --depth 1 --filter=blob:none --sparse "https://github.com/${owner}/${repo}.git" "${tempDir}" 2>/dev/null`, { timeout: 3e4 });
381
+ execSync(`cd "${tempDir}" && git sparse-checkout set "${skillSubPath}" 2>/dev/null`, { timeout: 1e4 });
382
+ execSync(`mv "${tempDir}/${skillSubPath}" "${skillPath}"`, { timeout: 5e3 });
383
+ execSync(`rm -rf "${tempDir}"`, { timeout: 5e3 });
384
+ return { success: true };
385
+ } catch (e) {
386
+ return { success: false, message: "Failed to download skill" };
387
+ }
388
+ };
389
+ const SkillsBrowser = () => {
390
+ const { exit } = useApp();
391
+ const [skills, setSkills] = useState([]);
392
+ const [loading, setLoading] = useState(true);
393
+ useEffect(() => {
394
+ const s = fetchSkills();
395
+ setSkills(s);
396
+ setLoading(false);
397
+ }, []);
398
+ if (loading) {
399
+ return /* @__PURE__ */ React.createElement(Box, { padding: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Loading skills..."));
400
+ }
401
+ if (skills.length === 0) {
402
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "Could not fetch skills"));
403
+ }
404
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, "Anthropic Skills"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Found ", skills.length, " skills"), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React.createElement(
405
+ SelectInput,
406
+ {
407
+ items: skills,
408
+ onSelect: (item) => {
409
+ const result = addSkillToClaudeJson(item.label, item.value);
410
+ if (result.success) {
411
+ console.log(`
412
+ \x1B[32m\u2713\x1B[0m Installed skill: ${item.label}`);
413
+ console.log(`\x1B[36mLocation: ~/.claude/skills/${item.label}/\x1B[0m`);
414
+ } else {
415
+ console.log(`
416
+ \x1B[31m\u2717\x1B[0m ${result.message || "Failed to install skill"}`);
417
+ }
418
+ exit();
419
+ }
420
+ }
421
+ )));
422
+ };
423
+ if (cmd === "skills") {
424
+ render(/* @__PURE__ */ React.createElement(SkillsBrowser, null));
425
+ } else if (cmd === "mcp") {
426
+ render(/* @__PURE__ */ React.createElement(McpSearch, null));
427
+ } else if (cmd === "new") {
428
+ const NewProfileWizard = () => {
429
+ const { exit } = useApp();
430
+ const [step, setStep] = useState("name");
431
+ const [name, setName] = useState("");
432
+ const [provider, setProvider] = useState("");
433
+ const [apiKey, setApiKey] = useState("");
434
+ const [model, setModel] = useState("");
435
+ const [group, setGroup] = useState("");
436
+ const providers = [
437
+ { label: "Anthropic (Direct)", value: "anthropic", url: "", needsKey: true },
438
+ { label: "Amazon Bedrock", value: "bedrock", url: "", needsKey: false },
439
+ { label: "Z.AI", value: "zai", url: "https://api.z.ai/api/anthropic", needsKey: true },
440
+ { label: "MiniMax", value: "minimax", url: "https://api.minimax.io/anthropic", needsKey: true },
441
+ { label: "Custom", value: "custom", url: "", needsKey: true }
442
+ ];
443
+ const handleSave = () => {
444
+ const prov = providers.find((p) => p.value === provider);
445
+ const profile = {
446
+ name,
447
+ group: group || void 0,
448
+ env: {
449
+ ...apiKey && { ANTHROPIC_AUTH_TOKEN: apiKey },
450
+ ...model && { ANTHROPIC_MODEL: model },
451
+ ...prov?.url && { ANTHROPIC_BASE_URL: prov.url },
452
+ API_TIMEOUT_MS: "3000000"
453
+ },
454
+ model: "opus",
455
+ alwaysThinkingEnabled: true,
456
+ defaultMode: "bypassPermissions"
457
+ };
458
+ const filename = name.toLowerCase().replace(/\s+/g, "-") + ".json";
459
+ fs.writeFileSync(path.join(PROFILES_DIR, filename), JSON.stringify(profile, null, 2));
460
+ console.log(`
461
+ \x1B[32m\u2713\x1B[0m Created: ${name}`);
462
+ exit();
463
+ };
464
+ const handleProviderSelect = (item) => {
465
+ setProvider(item.value);
466
+ const prov = providers.find((p) => p.value === item.value);
467
+ setStep(prov.needsKey ? "apikey" : "model");
468
+ };
469
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, "New Profile"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"), step === "name" && /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Name: "), /* @__PURE__ */ React.createElement(TextInput, { value: name, onChange: setName, onSubmit: () => setStep("provider") })), step === "provider" && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Provider:"), /* @__PURE__ */ React.createElement(SelectInput, { items: providers, onSelect: handleProviderSelect })), step === "apikey" && /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "API Key: "), /* @__PURE__ */ React.createElement(TextInput, { value: apiKey, onChange: setApiKey, onSubmit: () => setStep("model"), mask: "*" })), step === "model" && /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Model ID (optional): "), /* @__PURE__ */ React.createElement(TextInput, { value: model, onChange: setModel, onSubmit: () => setStep("group") })), step === "group" && /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Group (optional): "), /* @__PURE__ */ React.createElement(TextInput, { value: group, onChange: setGroup, onSubmit: handleSave })));
470
+ };
471
+ render(/* @__PURE__ */ React.createElement(NewProfileWizard, null));
472
+ } else {
473
+ const LoadingScreen = ({ message = "Loading..." }) => {
474
+ const [dots, setDots] = useState("");
475
+ const [colorIdx, setColorIdx] = useState(0);
476
+ const colors = ["cyan", "blue", "magenta", "red", "yellow", "green"];
477
+ useEffect(() => {
478
+ const dotsInterval = setInterval(() => {
479
+ setDots((d) => d.length >= 3 ? "" : d + ".");
480
+ }, 500);
481
+ const colorInterval = setInterval(() => {
482
+ setColorIdx((i) => (i + 1) % colors.length);
483
+ }, 200);
484
+ return () => {
485
+ clearInterval(dotsInterval);
486
+ clearInterval(colorInterval);
487
+ };
488
+ }, []);
489
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: colors[colorIdx] }, `\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
490
+ \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D
491
+ \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557
492
+ \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D
493
+ \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
494
+ \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D`), /* @__PURE__ */ React.createElement(Text, { bold: true, color: colors[(colorIdx + 3) % colors.length] }, "MANAGER v", VERSION), /* @__PURE__ */ React.createElement(Text, { color: "yellow", marginTop: 1 }, message, dots));
495
+ };
496
+ const App = () => {
497
+ const [step, setStep] = useState("select");
498
+ const [updateInfo, setUpdateInfo] = useState(null);
499
+ const [filter, setFilter] = useState("");
500
+ const profiles = loadProfiles();
501
+ useEffect(() => {
502
+ setTimeout(() => setStep("select"), 1500);
503
+ if (!skipUpdate) {
504
+ Promise.resolve().then(() => {
505
+ const info = checkForUpdate();
506
+ setUpdateInfo(info);
507
+ });
508
+ }
509
+ }, []);
510
+ useInput((input, key) => {
511
+ if (step === "select") {
512
+ const num = parseInt(input);
513
+ if (num >= 1 && num <= 9 && num <= filteredProfiles.length) {
514
+ const profile = filteredProfiles[num - 1];
515
+ applyProfile(profile.value);
516
+ console.log(`
517
+ \x1B[32m\u2713\x1B[0m Applied: ${profile.label}
518
+ `);
519
+ launchClaude();
520
+ }
521
+ if (input === "u" && updateInfo?.needsUpdate) {
522
+ console.log("\n\x1B[33mUpdating Claude...\x1B[0m\n");
523
+ try {
524
+ execSync("brew upgrade claude-code", { stdio: "inherit" });
525
+ console.log("\n\x1B[32m\u2713 Updated!\x1B[0m\n");
526
+ setUpdateInfo({ ...updateInfo, needsUpdate: false });
527
+ } catch {
528
+ }
529
+ }
530
+ if (input.match(/^[a-zA-Z]$/) && input !== "u") {
531
+ setFilter((f) => f + input);
532
+ }
533
+ if (key.backspace || key.delete) {
534
+ setFilter((f) => f.slice(0, -1));
535
+ }
536
+ if (key.escape) {
537
+ setFilter("");
538
+ }
539
+ }
540
+ });
541
+ const filteredProfiles = profiles.filter(
542
+ (p) => !filter || p.label.toLowerCase().includes(filter.toLowerCase())
543
+ );
544
+ const groupedItems = [];
545
+ const groups = [...new Set(filteredProfiles.map((p) => p.group).filter(Boolean))];
546
+ if (groups.length > 0) {
547
+ groups.forEach((g) => {
548
+ groupedItems.push({ label: `\u2500\u2500 ${g} \u2500\u2500`, value: `group-${g}`, key: `group-${g}`, disabled: true });
549
+ filteredProfiles.filter((p) => p.group === g).forEach((p, i) => {
550
+ groupedItems.push({ ...p, label: `${i + 1}. ${p.label}` });
551
+ });
552
+ });
553
+ const ungrouped = filteredProfiles.filter((p) => !p.group);
554
+ if (ungrouped.length > 0) {
555
+ groupedItems.push({ label: "\u2500\u2500 Other \u2500\u2500", value: "group-other", key: "group-other", disabled: true });
556
+ ungrouped.forEach((p, i) => groupedItems.push({ ...p, label: `${i + 1}. ${p.label}` }));
557
+ }
558
+ } else {
559
+ filteredProfiles.forEach((p, i) => groupedItems.push({ ...p, label: `${i + 1}. ${p.label}` }));
560
+ }
561
+ if (step === "loading") {
562
+ return /* @__PURE__ */ React.createElement(LoadingScreen, { message: "Initializing Claude Manager" });
563
+ }
564
+ if (profiles.length === 0) {
565
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, "CLAUDE MANAGER"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"), /* @__PURE__ */ React.createElement(Text, { color: "yellow", marginTop: 1 }, "No profiles found!"), /* @__PURE__ */ React.createElement(Text, null, "Run: cm new"));
566
+ }
567
+ const handleSelect = (item) => {
568
+ if (item.disabled) return;
569
+ applyProfile(item.value);
570
+ console.log(`
571
+ \x1B[32m\u2713\x1B[0m Applied: ${item.label.replace(/^\d+\.\s*/, "")}
572
+ `);
573
+ launchClaude();
574
+ };
575
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, `\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
576
+ \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D
577
+ \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557
578
+ \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D
579
+ \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
580
+ \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D`), /* @__PURE__ */ React.createElement(Text, { bold: true, color: "magenta" }, "MANAGER v", VERSION), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"), updateInfo?.current && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Claude v", updateInfo.current), updateInfo?.needsUpdate && /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "\u26A0 Update available! Press 'u' to upgrade"), filter && /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "Filter: ", filter), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Select Profile: ", /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "(1-9 quick select, type to filter", updateInfo?.needsUpdate ? ", u to update" : "", ")")), /* @__PURE__ */ React.createElement(
581
+ SelectInput,
582
+ {
583
+ items: groupedItems,
584
+ onSelect: handleSelect,
585
+ itemComponent: ({ isSelected, label, disabled }) => /* @__PURE__ */ React.createElement(Text, { color: disabled ? "gray" : isSelected ? "cyan" : "white", dimColor: disabled }, disabled ? label : (isSelected ? "\u276F " : " ") + label)
586
+ }
587
+ )));
588
+ };
589
+ render(/* @__PURE__ */ React.createElement(App, null));
590
+ }
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "claude-manager",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Terminal app for managing Claude Code settings, profiles, MCP servers, and skills",
5
5
  "type": "module",
6
6
  "bin": {
7
- "cm": "./src/cli.js",
8
- "claude-manager": "./src/cli.js"
7
+ "cm": "./dist/cli.js",
8
+ "claude-manager": "./dist/cli.js"
9
9
  },
10
10
  "scripts": {
11
- "start": "node src/cli.js"
11
+ "build": "esbuild src/cli.js --platform=node --format=esm --loader:.js=jsx --outfile=dist/cli.js --packages=external && echo '#!/usr/bin/env node' | cat - dist/cli.js > dist/tmp && mv dist/tmp dist/cli.js && chmod +x dist/cli.js",
12
+ "prepublishOnly": "npm run build"
12
13
  },
14
+ "files": ["dist"],
13
15
  "keywords": [
14
16
  "claude",
15
17
  "claude-code",
@@ -34,5 +36,8 @@
34
36
  "ink-select-input": "^6.0.0",
35
37
  "ink-text-input": "^6.0.0",
36
38
  "react": "^18.3.1"
39
+ },
40
+ "devDependencies": {
41
+ "esbuild": "^0.24.2"
37
42
  }
38
43
  }