claude-manager 1.5.3 → 1.5.4

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.
Files changed (3) hide show
  1. package/README.md +20 -2
  2. package/dist/cli.js +427 -329
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
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
4
 
5
- ![Version](https://img.shields.io/badge/version-1.4.0-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.5.3-blue)
6
6
  ![License](https://img.shields.io/badge/license-MIT-green)
7
7
 
8
8
  ## Features
@@ -19,12 +19,27 @@ A powerful terminal app for managing Claude Code settings, profiles, MCP servers
19
19
 
20
20
  ## Installation
21
21
 
22
+ ### Option 1: Homebrew (macOS)
23
+
24
+ ```bash
25
+ brew tap faisalnazir/claude-manager
26
+ brew install claude-manager
27
+ ```
28
+
29
+ ### Option 2: npm
30
+
31
+ ```bash
32
+ npm install -g claude-manager
33
+ ```
34
+
35
+ ### Option 3: curl
36
+
22
37
  ```bash
23
38
  curl -fsSL https://raw.githubusercontent.com/faisalnazir/claude-manager/main/install.sh | bash
24
39
  ```
25
40
 
26
41
  ### Requirements
27
- - [Bun](https://bun.sh) (auto-installed if missing)
42
+ - [Node.js 18+](https://nodejs.org) (for npm install)
28
43
  - [Claude Code](https://docs.anthropic.com/en/docs/claude-code)
29
44
 
30
45
  ## Quick Start
@@ -171,6 +186,9 @@ cm edit 1
171
186
 
172
187
  # Check what's installed
173
188
  cm status
189
+
190
+ # Update cm via npm
191
+ npm update -g claude-manager
174
192
  ```
175
193
 
176
194
  ## How It Works
package/dist/cli.js CHANGED
@@ -1,88 +1,119 @@
1
1
  #!/usr/bin/env node
2
+ // src/cli.js
2
3
  import React, { useState, useEffect, useMemo } from "react";
3
4
  import { render, Box, Text, useInput, useApp } from "ink";
4
5
  import SelectInput from "ink-select-input";
5
6
  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
- import { createInterface } from "readline";
7
+ import { spawnSync, execSync as execSync2 } from "child_process";
8
+ import fs2 from "fs";
9
+ import path3 from "path";
11
10
  import Fuse from "fuse.js";
12
- const VERSION = "1.5.3";
13
- const LOGO = `\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
11
+
12
+ // src/constants.js
13
+ import os from "os";
14
+ import path from "path";
15
+ var VERSION = "1.5.4";
16
+ var LOGO = `\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
14
17
  \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
15
18
  \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
16
19
  \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
17
20
  \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
18
21
  \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`;
19
- const MCP_REGISTRY_URL = "https://registry.modelcontextprotocol.io/v0/servers";
20
- const PROFILES_DIR = path.join(os.homedir(), ".claude", "profiles");
21
- const SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
22
- const CLAUDE_JSON_PATH = path.join(os.homedir(), ".claude.json");
23
- const LAST_PROFILE_PATH = path.join(os.homedir(), ".claude", ".last-profile");
24
- const args = process.argv.slice(2);
25
- const cmd = args[0];
26
- if (!fs.existsSync(PROFILES_DIR)) fs.mkdirSync(PROFILES_DIR, { recursive: true });
27
- if (args.includes("-v") || args.includes("--version")) {
28
- console.log(`cm v${VERSION}`);
29
- process.exit(0);
30
- }
31
- if (args.includes("-h") || args.includes("--help")) {
32
- console.log(`cm v${VERSION} - Claude Settings Manager
33
-
34
- Usage: cm [command] [options]
35
-
36
- Commands:
37
- (none) Select profile interactively
38
- new Create a new profile
39
- edit <n> Edit profile (by name or number)
40
- copy <n> <new> Copy/duplicate a profile
41
- delete <n> Delete profile (by name or number)
42
- status Show current settings
43
- list List all profiles
44
- config Open Claude settings.json in editor
45
- mcp [query] Search and add MCP servers
46
- mcp remove Remove MCP server from profile
47
- skills Browse and add Anthropic skills
48
- skills list List installed skills
49
- skills remove Remove an installed skill
22
+ var PROFILES_DIR = path.join(os.homedir(), ".claude", "profiles");
23
+ var SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
24
+ var CLAUDE_JSON_PATH = path.join(os.homedir(), ".claude.json");
25
+ var LAST_PROFILE_PATH = path.join(os.homedir(), ".claude", ".last-profile");
26
+ var SKILLS_DIR = path.join(os.homedir(), ".claude", "skills");
27
+ var MCP_REGISTRY_URL = "https://registry.modelcontextprotocol.io/v0/servers";
28
+ var SKILL_SOURCES = [
29
+ { url: "https://api.github.com/repos/anthropics/skills/contents/skills", base: "https://github.com/anthropics/skills/tree/main/skills" },
30
+ { url: "https://api.github.com/repos/Prat011/awesome-llm-skills/contents/skills", base: "https://github.com/Prat011/awesome-llm-skills/tree/main/skills" },
31
+ { url: "https://api.github.com/repos/skillcreatorai/Ai-Agent-Skills/contents/skills", base: "https://github.com/skillcreatorai/Ai-Agent-Skills/tree/main/skills" }
32
+ ];
33
+ var PROVIDERS = [
34
+ { label: "Anthropic (Direct)", value: "anthropic", url: "", needsKey: true },
35
+ { label: "Amazon Bedrock", value: "bedrock", url: "", needsKey: false },
36
+ { label: "Z.AI", value: "zai", url: "https://api.z.ai/api/anthropic", needsKey: true },
37
+ { label: "MiniMax", value: "minimax", url: "https://api.minimax.io/anthropic", needsKey: true },
38
+ { label: "Custom", value: "custom", url: "", needsKey: true }
39
+ ];
40
+ var FETCH_TIMEOUT = 1e4;
41
+ var NPM_OUTDATED_TIMEOUT = 5e3;
42
+ var GIT_CLONE_TIMEOUT = 3e4;
43
+ var GIT_SPARSE_TIMEOUT = 1e4;
44
+ var GIT_MOVE_TIMEOUT = 5e3;
45
+ var GIT_CLEANUP_TIMEOUT = 5e3;
46
+ var MCP_PAGE_SIZE = 50;
47
+ var SKILLS_PAGE_SIZE = 50;
48
+ var DEFAULT_SETTINGS = {
49
+ env: {},
50
+ model: "opus",
51
+ alwaysThinkingEnabled: true,
52
+ defaultMode: "bypassPermissions"
53
+ };
54
+ var API_TIMEOUT_MS = "3000000";
55
+ var FUSE_THRESHOLD = 0.3;
50
56
 
51
- Options:
52
- --last, -l Use last profile without menu
53
- --skip-update Skip update check
54
- --yolo Run claude with --dangerously-skip-permissions
55
- --force, -f Skip confirmation prompts (e.g., for delete)
56
- -v, --version Show version
57
- -h, --help Show help`);
58
- process.exit(0);
59
- }
60
- const skipUpdate = args.includes("--skip-update");
61
- const useLast = args.includes("--last") || args.includes("-l");
62
- const dangerMode = args.includes("--dangerously-skip-permissions") || args.includes("--yolo");
63
- const loadProfiles = () => {
57
+ // src/utils.js
58
+ import fs from "fs";
59
+ import path2 from "path";
60
+ import { execSync } from "child_process";
61
+ import { createInterface } from "readline";
62
+ var ensureProfilesDir = () => {
63
+ if (!fs.existsSync(PROFILES_DIR)) {
64
+ fs.mkdirSync(PROFILES_DIR, { recursive: true });
65
+ }
66
+ };
67
+ var logError = (context, error) => {
68
+ if (process.env.DEBUG || process.env.CM_DEBUG) {
69
+ console.error(`[${context}]`, error?.message || error);
70
+ }
71
+ };
72
+ var safeParseInt = (value, defaultValue = null) => {
73
+ const parsed = parseInt(value, 10);
74
+ return Number.isNaN(parsed) ? defaultValue : parsed;
75
+ };
76
+ var sanitizeProfileName = (name) => {
77
+ return name.toLowerCase().replace(/[^a-z0-9-_]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
78
+ };
79
+ var sanitizeFilePath = (filename, baseDir) => {
80
+ const sanitized = path2.basename(filename);
81
+ const resolved = path2.resolve(baseDir, sanitized);
82
+ if (!resolved.startsWith(baseDir)) {
83
+ return null;
84
+ }
85
+ return sanitized;
86
+ };
87
+ var loadProfiles = () => {
64
88
  const profiles = [];
65
- if (fs.existsSync(PROFILES_DIR)) {
66
- for (const file of fs.readdirSync(PROFILES_DIR).sort()) {
67
- if (file.endsWith(".json")) {
68
- try {
69
- const content = JSON.parse(fs.readFileSync(path.join(PROFILES_DIR, file), "utf8"));
70
- profiles.push({
71
- label: content.name || file.replace(".json", ""),
72
- value: file,
73
- key: file,
74
- group: content.group || null,
75
- data: content
76
- });
77
- } catch {
78
- }
79
- }
89
+ ensureProfilesDir();
90
+ if (!fs.existsSync(PROFILES_DIR)) {
91
+ return profiles;
92
+ }
93
+ const files = fs.readdirSync(PROFILES_DIR).sort();
94
+ for (const file of files) {
95
+ if (!file.endsWith(".json")) continue;
96
+ const filePath = path2.join(PROFILES_DIR, file);
97
+ try {
98
+ const content = JSON.parse(fs.readFileSync(filePath, "utf8"));
99
+ profiles.push({
100
+ label: content.name || file.replace(".json", ""),
101
+ value: file,
102
+ key: file,
103
+ group: content.group || null,
104
+ data: content
105
+ });
106
+ } catch (error) {
107
+ logError("loadProfiles", error);
80
108
  }
81
109
  }
82
110
  return profiles;
83
111
  };
84
- const applyProfile = (filename) => {
85
- const profilePath = path.join(PROFILES_DIR, filename);
112
+ var applyProfile = (filename) => {
113
+ const profilePath = path2.join(PROFILES_DIR, filename);
114
+ if (!fs.existsSync(profilePath)) {
115
+ throw new Error(`Profile not found: ${filename}`);
116
+ }
86
117
  const profile = JSON.parse(fs.readFileSync(profilePath, "utf8"));
87
118
  const { name, group, mcpServers, ...settings } = profile;
88
119
  fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
@@ -91,32 +122,33 @@ const applyProfile = (filename) => {
91
122
  const claudeJson = fs.existsSync(CLAUDE_JSON_PATH) ? JSON.parse(fs.readFileSync(CLAUDE_JSON_PATH, "utf8")) : {};
92
123
  claudeJson.mcpServers = mcpServers;
93
124
  fs.writeFileSync(CLAUDE_JSON_PATH, JSON.stringify(claudeJson, null, 2));
94
- } catch {
125
+ } catch (error) {
126
+ logError("applyProfile-mcp", error);
95
127
  }
96
128
  }
97
129
  fs.writeFileSync(LAST_PROFILE_PATH, filename);
98
130
  return name || filename;
99
131
  };
100
- const getLastProfile = () => {
132
+ var getLastProfile = () => {
101
133
  try {
102
- return fs.readFileSync(LAST_PROFILE_PATH, "utf8").trim();
103
- } catch {
134
+ const content = fs.readFileSync(LAST_PROFILE_PATH, "utf8");
135
+ return content.trim() || null;
136
+ } catch (error) {
104
137
  return null;
105
138
  }
106
139
  };
107
- const checkProjectProfile = () => {
108
- const localProfile = path.join(process.cwd(), ".claude-profile");
140
+ var checkProjectProfile = () => {
141
+ const localProfile = path2.join(process.cwd(), ".claude-profile");
109
142
  if (fs.existsSync(localProfile)) {
110
- return fs.readFileSync(localProfile, "utf8").trim();
143
+ try {
144
+ return fs.readFileSync(localProfile, "utf8").trim();
145
+ } catch (error) {
146
+ logError("checkProjectProfile", error);
147
+ }
111
148
  }
112
149
  return null;
113
150
  };
114
- const logError = (context, error) => {
115
- if (process.env.DEBUG || process.env.CM_DEBUG) {
116
- console.error(`[${context}]`, error?.message || error);
117
- }
118
- };
119
- const confirm = async (message) => {
151
+ var confirm = async (message) => {
120
152
  const rl = createInterface({
121
153
  input: process.stdin,
122
154
  output: process.stdout
@@ -124,11 +156,12 @@ const confirm = async (message) => {
124
156
  return new Promise((resolve) => {
125
157
  rl.question(`${message} (y/N): `, (answer) => {
126
158
  rl.close();
127
- resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
159
+ const normalized = answer.toLowerCase().trim();
160
+ resolve(normalized === "y" || normalized === "yes");
128
161
  });
129
162
  });
130
163
  };
131
- const validateProfile = (profile) => {
164
+ var validateProfile = (profile) => {
132
165
  const errors = [];
133
166
  if (!profile.name || profile.name.trim().length === 0) {
134
167
  errors.push("Profile name is required");
@@ -163,28 +196,41 @@ const validateProfile = (profile) => {
163
196
  }
164
197
  return { valid: errors.length === 0, errors };
165
198
  };
166
- const getInstalledSkills = () => {
167
- const skillsDir = path.join(os.homedir(), ".claude", "skills");
168
- if (!fs.existsSync(skillsDir)) return [];
169
- return fs.readdirSync(skillsDir).filter((f) => {
170
- const p = path.join(skillsDir, f);
171
- return fs.statSync(p).isDirectory() && !f.startsWith(".");
172
- });
199
+ var getInstalledSkills = () => {
200
+ if (!fs.existsSync(SKILLS_DIR)) return [];
201
+ try {
202
+ return fs.readdirSync(SKILLS_DIR).filter((f) => {
203
+ const p = path2.join(SKILLS_DIR, f);
204
+ try {
205
+ return fs.statSync(p).isDirectory() && !f.startsWith(".");
206
+ } catch {
207
+ return false;
208
+ }
209
+ });
210
+ } catch (error) {
211
+ logError("getInstalledSkills", error);
212
+ return [];
213
+ }
173
214
  };
174
- const removeSkill = (skillName) => {
175
- const skillPath = path.join(os.homedir(), ".claude", "skills", skillName);
215
+ var removeSkill = (skillName) => {
216
+ const skillPath = path2.join(SKILLS_DIR, skillName);
176
217
  if (!fs.existsSync(skillPath)) {
177
218
  return { success: false, message: "Skill not found" };
178
219
  }
179
- fs.rmSync(skillPath, { recursive: true, force: true });
180
- return { success: true };
220
+ try {
221
+ fs.rmSync(skillPath, { recursive: true, force: true });
222
+ return { success: true };
223
+ } catch (error) {
224
+ logError("removeSkill", error);
225
+ return { success: false, message: "Failed to remove skill" };
226
+ }
181
227
  };
182
- const checkForUpdate = async () => {
183
- if (skipUpdate) return { needsUpdate: false };
228
+ var checkForUpdate = async (skipUpdate2) => {
229
+ if (skipUpdate2) return { needsUpdate: false };
230
+ const { exec } = await import("child_process");
231
+ const { promisify } = await import("util");
232
+ const execAsync = promisify(exec);
184
233
  try {
185
- const { exec } = await import("child_process");
186
- const { promisify } = await import("util");
187
- const execAsync = promisify(exec);
188
234
  const versionResult = await execAsync("claude --version 2>/dev/null").catch(() => ({ stdout: "" }));
189
235
  const current = versionResult.stdout.match(/(\d+\.\d+\.\d+)/)?.[1];
190
236
  if (!current) return { needsUpdate: false };
@@ -197,7 +243,7 @@ const checkForUpdate = async () => {
197
243
  const npmListResult = await execAsync("npm list -g @anthropic-ai/claude-code 2>/dev/null").catch(() => ({ stdout: "" }));
198
244
  if (npmListResult.stdout.includes("@anthropic-ai/claude-code")) {
199
245
  try {
200
- const npmOutdated = await execAsync("npm outdated -g @anthropic-ai/claude-code --json 2>/dev/null || true", { timeout: 5e3 });
246
+ const npmOutdated = await execAsync("npm outdated -g @anthropic-ai/claude-code --json 2>/dev/null || true", { timeout: NPM_OUTDATED_TIMEOUT });
201
247
  needsUpdate = npmOutdated.stdout.length > 0;
202
248
  } catch {
203
249
  needsUpdate = true;
@@ -210,35 +256,222 @@ const checkForUpdate = async () => {
210
256
  return { needsUpdate: false };
211
257
  }
212
258
  };
213
- const launchClaude = () => {
259
+ var launchClaude = (dangerMode2) => {
214
260
  try {
215
- const claudeArgs = dangerMode ? "--dangerously-skip-permissions" : "";
261
+ const claudeArgs = dangerMode2 ? "--dangerously-skip-permissions" : "";
216
262
  execSync(`claude ${claudeArgs}`, { stdio: "inherit" });
217
263
  } catch (e) {
218
264
  process.exit(e.status || 1);
219
265
  }
220
266
  process.exit(0);
221
267
  };
268
+ var searchMcpServers = async (query, offset = 0) => {
269
+ const controller = new AbortController();
270
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
271
+ try {
272
+ const res = await fetch(`${MCP_REGISTRY_URL}?limit=200`, { signal: controller.signal });
273
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
274
+ const data = await res.json();
275
+ const seen = /* @__PURE__ */ new Set();
276
+ const filtered = data.servers.filter((s) => {
277
+ if (seen.has(s.server.name)) return false;
278
+ seen.add(s.server.name);
279
+ const isLatest = s._meta?.["io.modelcontextprotocol.registry/official"]?.isLatest !== false;
280
+ const matchesQuery = !query || s.server.name.toLowerCase().includes(query.toLowerCase()) || s.server.description?.toLowerCase().includes(query.toLowerCase());
281
+ return isLatest && matchesQuery;
282
+ });
283
+ const MCP_PAGE_SIZE2 = 50;
284
+ return {
285
+ servers: filtered.slice(offset, offset + MCP_PAGE_SIZE2),
286
+ total: filtered.length,
287
+ hasMore: offset + MCP_PAGE_SIZE2 < filtered.length,
288
+ offset
289
+ };
290
+ } catch (error) {
291
+ logError("searchMcpServers", error);
292
+ return { servers: [], total: 0, hasMore: false, offset: 0 };
293
+ } finally {
294
+ clearTimeout(timeout);
295
+ }
296
+ };
297
+ var addMcpToProfile = (server, profileFile) => {
298
+ const sanitizedFile = sanitizeFilePath(profileFile, PROFILES_DIR);
299
+ if (!sanitizedFile) {
300
+ throw new Error("Invalid profile file");
301
+ }
302
+ const profilePath = path2.join(PROFILES_DIR, sanitizedFile);
303
+ const profile = JSON.parse(fs.readFileSync(profilePath, "utf8"));
304
+ if (!profile.mcpServers) profile.mcpServers = {};
305
+ const s = server.server;
306
+ const name = s.name.split("/").pop();
307
+ if (s.remotes?.[0]) {
308
+ const remote = s.remotes[0];
309
+ profile.mcpServers[name] = {
310
+ type: remote.type === "streamable-http" ? "http" : remote.type,
311
+ url: remote.url
312
+ };
313
+ } else if (s.packages?.[0]) {
314
+ const pkg = s.packages[0];
315
+ if (pkg.registryType === "npm") {
316
+ profile.mcpServers[name] = {
317
+ type: "stdio",
318
+ command: "npx",
319
+ args: ["-y", pkg.identifier]
320
+ };
321
+ } else if (pkg.registryType === "pypi") {
322
+ profile.mcpServers[name] = {
323
+ type: "stdio",
324
+ command: "uvx",
325
+ args: [pkg.identifier]
326
+ };
327
+ }
328
+ }
329
+ fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2));
330
+ return name;
331
+ };
332
+ var fetchSkills = async () => {
333
+ const seen = /* @__PURE__ */ new Set();
334
+ const skills = [];
335
+ const promises = SKILL_SOURCES.map(async (source) => {
336
+ const controller = new AbortController();
337
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
338
+ try {
339
+ const res = await fetch(source.url, {
340
+ signal: controller.signal,
341
+ headers: { "Accept": "application/vnd.github.v3+json" }
342
+ });
343
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
344
+ const data = await res.json();
345
+ if (Array.isArray(data)) {
346
+ for (const s of data.filter((s2) => s2.type === "dir")) {
347
+ if (!seen.has(s.name)) {
348
+ seen.add(s.name);
349
+ skills.push({
350
+ label: s.name,
351
+ value: `${source.base}/${s.name}`,
352
+ key: s.name
353
+ });
354
+ }
355
+ }
356
+ }
357
+ } catch (error) {
358
+ logError(`fetchSkills(${source.url})`, error);
359
+ } finally {
360
+ clearTimeout(timeout);
361
+ }
362
+ });
363
+ await Promise.all(promises);
364
+ return skills.sort((a, b) => a.label.localeCompare(b.label));
365
+ };
366
+ var addSkillToClaudeJson = (skillName, skillUrl) => {
367
+ try {
368
+ if (!fs.existsSync(SKILLS_DIR)) fs.mkdirSync(SKILLS_DIR, { recursive: true });
369
+ const skillPath = path2.join(SKILLS_DIR, skillName);
370
+ if (fs.existsSync(skillPath)) {
371
+ return { success: false, message: "Skill already installed" };
372
+ }
373
+ const match = skillUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\/tree\/([^\/]+)\/(.+)/);
374
+ if (!match) return { success: false, message: "Invalid skill URL" };
375
+ const [, owner, repo, branch, skillSubPath] = match;
376
+ const tempDir = `/tmp/skill-clone-${Date.now()}`;
377
+ const sanitizedTempDir = sanitizeFilePath(`skill-clone-${Date.now()}`, "/tmp");
378
+ const finalTempDir = path2.join("/tmp", sanitizedTempDir || "skill-clone");
379
+ execSync(`git clone --depth 1 --filter=blob:none --sparse "https://github.com/${owner}/${repo}.git" "${finalTempDir}" 2>/dev/null`, { timeout: GIT_CLONE_TIMEOUT });
380
+ execSync(`cd "${finalTempDir}" && git sparse-checkout set "${skillSubPath}" 2>/dev/null`, { timeout: GIT_SPARSE_TIMEOUT });
381
+ const sourcePath = path2.join(finalTempDir, skillSubPath);
382
+ if (fs.existsSync(sourcePath)) {
383
+ execSync(`mv "${sourcePath}" "${skillPath}"`, { timeout: GIT_MOVE_TIMEOUT });
384
+ }
385
+ execSync(`rm -rf "${finalTempDir}"`, { timeout: GIT_CLEANUP_TIMEOUT });
386
+ return { success: true };
387
+ } catch (e) {
388
+ logError("addSkillToClaudeJson", e);
389
+ return { success: false, message: "Failed to download skill" };
390
+ }
391
+ };
392
+ var createDefaultSettings = () => {
393
+ if (!fs.existsSync(SETTINGS_PATH)) {
394
+ fs.writeFileSync(SETTINGS_PATH, JSON.stringify(DEFAULT_SETTINGS, null, 2));
395
+ }
396
+ };
397
+ var buildProfileData = (name, provider, apiKey, model, group, providers) => {
398
+ const prov = providers.find((p) => p.value === provider);
399
+ return {
400
+ name,
401
+ group: group || void 0,
402
+ env: {
403
+ ...apiKey && { ANTHROPIC_AUTH_TOKEN: apiKey },
404
+ ...model && { ANTHROPIC_MODEL: model },
405
+ ...prov?.url && { ANTHROPIC_BASE_URL: prov.url },
406
+ API_TIMEOUT_MS
407
+ },
408
+ model: "opus",
409
+ alwaysThinkingEnabled: true,
410
+ defaultMode: "bypassPermissions"
411
+ };
412
+ };
413
+
414
+ // src/cli.js
415
+ ensureProfilesDir();
416
+ var args = process.argv.slice(2);
417
+ var cmd = args[0];
418
+ if (args.includes("-v") || args.includes("--version")) {
419
+ console.log(`cm v${VERSION}`);
420
+ process.exit(0);
421
+ }
422
+ if (args.includes("-h") || args.includes("--help")) {
423
+ console.log(`cm v${VERSION} - Claude Settings Manager
424
+
425
+ Usage: cm [command] [options]
426
+
427
+ Commands:
428
+ (none) Select profile interactively
429
+ new Create a new profile
430
+ edit <n> Edit profile (by name or number)
431
+ copy <n> <new> Copy/duplicate a profile
432
+ delete <n> Delete profile (by name or number)
433
+ status Show current settings
434
+ list List all profiles
435
+ config Open Claude settings.json in editor
436
+ mcp [query] Search and add MCP servers
437
+ mcp remove Remove MCP server from profile
438
+ skills Browse and add Anthropic skills
439
+ skills list List installed skills
440
+ skills remove Remove an installed skill
441
+
442
+ Options:
443
+ --last, -l Use last profile without menu
444
+ --skip-update Skip update check
445
+ --yolo Run claude with --dangerously-skip-permissions
446
+ --force, -f Skip confirmation prompts (e.g., for delete)
447
+ -v, --version Show version
448
+ -h, --help Show help`);
449
+ process.exit(0);
450
+ }
451
+ var skipUpdate = args.includes("--skip-update");
452
+ var useLast = args.includes("--last") || args.includes("-l");
453
+ var dangerMode = args.includes("--dangerously-skip-permissions") || args.includes("--yolo");
222
454
  if (useLast) {
223
455
  const last = getLastProfile();
224
- if (last && fs.existsSync(path.join(PROFILES_DIR, last))) {
456
+ const lastPath = last ? path3.join(PROFILES_DIR, last) : null;
457
+ if (last && lastPath && fs2.existsSync(lastPath)) {
225
458
  const name = applyProfile(last);
226
459
  console.log(`\x1B[32m\u2713\x1B[0m Applied: ${name}
227
460
  `);
228
- launchClaude();
461
+ launchClaude(dangerMode);
229
462
  } else {
230
463
  console.log("\x1B[31mNo last profile found\x1B[0m");
231
464
  process.exit(1);
232
465
  }
233
466
  }
234
- const projectProfile = checkProjectProfile();
467
+ var projectProfile = checkProjectProfile();
235
468
  if (projectProfile && !cmd) {
236
469
  const profiles = loadProfiles();
237
470
  const match = profiles.find((p) => p.label === projectProfile || p.value === projectProfile + ".json");
238
471
  if (match) {
239
472
  console.log(`\x1B[36mUsing project profile: ${match.label}\x1B[0m`);
240
473
  applyProfile(match.value);
241
- launchClaude();
474
+ launchClaude(dangerMode);
242
475
  }
243
476
  }
244
477
  if (cmd === "status") {
@@ -260,36 +493,29 @@ Profile MCP Servers (${Object.keys(mcpServers).length}):`);
260
493
  } else {
261
494
  console.log("No profile active");
262
495
  }
263
- const skillsDir = path.join(os.homedir(), ".claude", "skills");
264
- try {
265
- if (fs.existsSync(skillsDir)) {
266
- const installedSkills = fs.readdirSync(skillsDir).filter((f) => {
267
- const p = path.join(skillsDir, f);
268
- return fs.statSync(p).isDirectory() && !f.startsWith(".");
269
- });
270
- if (installedSkills.length > 0) {
271
- console.log(`
496
+ const installedSkills = getInstalledSkills();
497
+ if (installedSkills.length > 0) {
498
+ console.log(`
272
499
  Installed Skills (${installedSkills.length}):`);
273
- installedSkills.forEach((s) => console.log(` - ${s}`));
274
- }
275
- }
276
- } catch {
500
+ installedSkills.forEach((s) => console.log(` - ${s}`));
277
501
  }
278
502
  try {
279
- const claudeJson = JSON.parse(fs.readFileSync(CLAUDE_JSON_PATH, "utf8"));
503
+ const claudeJson = JSON.parse(fs2.readFileSync(CLAUDE_JSON_PATH, "utf8"));
280
504
  const globalMcp = claudeJson.mcpServers || {};
281
505
  if (Object.keys(globalMcp).length > 0) {
282
506
  console.log(`
283
507
  Global MCP Servers (${Object.keys(globalMcp).length}):`);
284
508
  Object.keys(globalMcp).forEach((s) => console.log(` - ${s}`));
285
509
  }
286
- } catch {
510
+ } catch (error) {
511
+ logError("status-mcp", error);
287
512
  }
288
513
  try {
289
- const ver = execSync("claude --version 2>/dev/null", { encoding: "utf8" }).trim();
514
+ const ver = execSync2("claude --version 2>/dev/null", { encoding: "utf8" }).trim();
290
515
  console.log(`
291
516
  Claude: ${ver}`);
292
- } catch {
517
+ } catch (error) {
518
+ logError("status-version", error);
293
519
  }
294
520
  process.exit(0);
295
521
  }
@@ -305,34 +531,35 @@ if (cmd === "list") {
305
531
  }
306
532
  if (cmd === "config") {
307
533
  const editor = process.env.EDITOR || "nano";
308
- const configPath = SETTINGS_PATH;
309
- if (!fs.existsSync(configPath)) {
310
- console.log(`\x1B[33mSettings file not found. Creating default settings...\x1B[0m`);
311
- fs.writeFileSync(configPath, JSON.stringify({
312
- env: {},
313
- model: "opus",
314
- alwaysThinkingEnabled: true,
315
- defaultMode: "bypassPermissions"
316
- }, null, 2));
317
- }
318
- console.log(`Opening ${configPath} in ${editor}...`);
319
- spawnSync(editor, [configPath], { stdio: "inherit" });
534
+ createDefaultSettings();
535
+ console.log(`Opening ${SETTINGS_PATH} in ${editor}...`);
536
+ spawnSync(editor, [SETTINGS_PATH], { stdio: "inherit" });
320
537
  process.exit(0);
321
538
  }
322
539
  if (cmd === "delete") {
323
540
  const forceDelete = args.includes("--force") || args.includes("-f");
324
541
  const profiles = loadProfiles();
325
542
  const target = args[1];
326
- const idx = parseInt(target) - 1;
327
- const match = profiles[idx] || profiles.find((p) => p.label.toLowerCase() === target?.toLowerCase());
543
+ if (!target) {
544
+ console.log("\x1B[31mUsage: cm delete <profile>\x1B[0m");
545
+ console.log(" profile: Profile name or number");
546
+ process.exit(1);
547
+ }
548
+ const idx = safeParseInt(target, -1);
549
+ const match = idx > 0 && idx <= profiles.length ? profiles[idx - 1] : profiles.find((p) => p.label.toLowerCase() === target?.toLowerCase());
328
550
  if (!match) {
329
551
  console.log(`\x1B[31mProfile not found: ${target}\x1B[0m`);
330
552
  process.exit(1);
331
553
  }
332
554
  const shouldDelete = forceDelete || await confirm(`Delete profile "${match.label}"?`);
333
555
  if (shouldDelete) {
334
- fs.unlinkSync(path.join(PROFILES_DIR, match.value));
335
- console.log(`\x1B[32m\u2713\x1B[0m Deleted: ${match.label}`);
556
+ const filePath = path3.join(PROFILES_DIR, match.value);
557
+ if (fs2.existsSync(filePath)) {
558
+ fs2.unlinkSync(filePath);
559
+ console.log(`\x1B[32m\u2713\x1B[0m Deleted: ${match.label}`);
560
+ } else {
561
+ console.log(`\x1B[31mProfile file not found: ${match.value}\x1B[0m`);
562
+ }
336
563
  } else {
337
564
  console.log("\x1B[33mCancelled\x1B[0m");
338
565
  }
@@ -341,11 +568,21 @@ if (cmd === "delete") {
341
568
  if (cmd === "edit") {
342
569
  const profiles = loadProfiles();
343
570
  const target = args[1];
344
- const idx = parseInt(target) - 1;
345
- const match = profiles[idx] || profiles.find((p) => p.label.toLowerCase() === target?.toLowerCase());
571
+ if (!target) {
572
+ console.log("\x1B[31mUsage: cm edit <profile>\x1B[0m");
573
+ console.log(" profile: Profile name or number");
574
+ process.exit(1);
575
+ }
576
+ const idx = safeParseInt(target, -1);
577
+ const match = idx > 0 && idx <= profiles.length ? profiles[idx - 1] : profiles.find((p) => p.label.toLowerCase() === target?.toLowerCase());
346
578
  if (match) {
347
579
  const editor = process.env.EDITOR || "nano";
348
- spawnSync(editor, [path.join(PROFILES_DIR, match.value)], { stdio: "inherit" });
580
+ const filePath = path3.join(PROFILES_DIR, match.value);
581
+ if (fs2.existsSync(filePath)) {
582
+ spawnSync(editor, [filePath], { stdio: "inherit" });
583
+ } else {
584
+ console.log(`\x1B[31mProfile file not found: ${match.value}\x1B[0m`);
585
+ }
349
586
  } else {
350
587
  console.log(`\x1B[31mProfile not found: ${target}\x1B[0m`);
351
588
  }
@@ -361,87 +598,29 @@ if (cmd === "copy") {
361
598
  console.log(" new-name: Name for the copied profile");
362
599
  process.exit(1);
363
600
  }
364
- const idx = parseInt(target) - 1;
365
- const match = profiles[idx] || profiles.find((p) => p.label.toLowerCase() === target?.toLowerCase());
601
+ const idx = safeParseInt(target, -1);
602
+ const match = idx > 0 && idx <= profiles.length ? profiles[idx - 1] : profiles.find((p) => p.label.toLowerCase() === target?.toLowerCase());
366
603
  if (!match) {
367
604
  console.log(`\x1B[31mProfile not found: ${target}\x1B[0m`);
368
605
  process.exit(1);
369
606
  }
370
- const profile = JSON.parse(fs.readFileSync(path.join(PROFILES_DIR, match.value), "utf8"));
607
+ const sourcePath = path3.join(PROFILES_DIR, match.value);
608
+ const profile = JSON.parse(fs2.readFileSync(sourcePath, "utf8"));
371
609
  profile.name = newName;
372
- const newFilename = newName.toLowerCase().replace(/\s+/g, "-") + ".json";
373
- if (fs.existsSync(path.join(PROFILES_DIR, newFilename))) {
610
+ const newFilename = sanitizeProfileName(newName) + ".json";
611
+ const destPath = path3.join(PROFILES_DIR, newFilename);
612
+ if (fs2.existsSync(destPath)) {
374
613
  const shouldOverwrite = await confirm(`Profile "${newName}" already exists. Overwrite?`);
375
614
  if (!shouldOverwrite) {
376
615
  console.log("\x1B[33mCancelled\x1B[0m");
377
616
  process.exit(0);
378
617
  }
379
618
  }
380
- fs.writeFileSync(path.join(PROFILES_DIR, newFilename), JSON.stringify(profile, null, 2));
619
+ fs2.writeFileSync(destPath, JSON.stringify(profile, null, 2));
381
620
  console.log(`\x1B[32m\u2713\x1B[0m Copied "${match.label}" to "${newName}"`);
382
621
  process.exit(0);
383
622
  }
384
- const MCP_PAGE_SIZE = 50;
385
- const searchMcpServers = async (query, offset = 0) => {
386
- const controller = new AbortController();
387
- const timeout = setTimeout(() => controller.abort(), 1e4);
388
- try {
389
- const res = await fetch(`${MCP_REGISTRY_URL}?limit=200`, { signal: controller.signal });
390
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
391
- const data = await res.json();
392
- const seen = /* @__PURE__ */ new Set();
393
- const filtered = data.servers.filter((s) => {
394
- if (seen.has(s.server.name)) return false;
395
- seen.add(s.server.name);
396
- const isLatest = s._meta?.["io.modelcontextprotocol.registry/official"]?.isLatest !== false;
397
- const matchesQuery = !query || s.server.name.toLowerCase().includes(query.toLowerCase()) || s.server.description?.toLowerCase().includes(query.toLowerCase());
398
- return isLatest && matchesQuery;
399
- });
400
- return {
401
- servers: filtered.slice(offset, offset + MCP_PAGE_SIZE),
402
- total: filtered.length,
403
- hasMore: offset + MCP_PAGE_SIZE < filtered.length,
404
- offset
405
- };
406
- } catch (error) {
407
- logError("searchMcpServers", error);
408
- return { servers: [], total: 0, hasMore: false, offset: 0 };
409
- } finally {
410
- clearTimeout(timeout);
411
- }
412
- };
413
- const addMcpToProfile = (server, profileFile) => {
414
- const profilePath = path.join(PROFILES_DIR, profileFile);
415
- const profile = JSON.parse(fs.readFileSync(profilePath, "utf8"));
416
- if (!profile.mcpServers) profile.mcpServers = {};
417
- const s = server.server;
418
- const name = s.name.split("/").pop();
419
- if (s.remotes?.[0]) {
420
- const remote = s.remotes[0];
421
- profile.mcpServers[name] = {
422
- type: remote.type === "streamable-http" ? "http" : remote.type,
423
- url: remote.url
424
- };
425
- } else if (s.packages?.[0]) {
426
- const pkg = s.packages[0];
427
- if (pkg.registryType === "npm") {
428
- profile.mcpServers[name] = {
429
- type: "stdio",
430
- command: "npx",
431
- args: ["-y", pkg.identifier]
432
- };
433
- } else if (pkg.registryType === "pypi") {
434
- profile.mcpServers[name] = {
435
- type: "stdio",
436
- command: "uvx",
437
- args: [pkg.identifier]
438
- };
439
- }
440
- }
441
- fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2));
442
- return name;
443
- };
444
- const McpSearch = () => {
623
+ var McpSearch = () => {
445
624
  const { exit } = useApp();
446
625
  const [step, setStep] = useState(args[1] ? "loading" : "search");
447
626
  const [query, setQuery] = useState(args[1] || "");
@@ -519,9 +698,14 @@ const McpSearch = () => {
519
698
  {
520
699
  items: profileItems,
521
700
  onSelect: (item) => {
522
- const name = addMcpToProfile(selectedServer, item.value);
523
- console.log(`
701
+ try {
702
+ const name = addMcpToProfile(selectedServer, item.value);
703
+ console.log(`
524
704
  \x1B[32m\u2713\x1B[0m Added ${name} to ${item.label}`);
705
+ } catch (error) {
706
+ console.log(`
707
+ \x1B[31m\u2717\x1B[0m ${error.message}`);
708
+ }
525
709
  exit();
526
710
  }
527
711
  }
@@ -529,72 +713,11 @@ const McpSearch = () => {
529
713
  }
530
714
  return null;
531
715
  };
532
- const SKILL_SOURCES = [
533
- { url: "https://api.github.com/repos/anthropics/skills/contents/skills", base: "https://github.com/anthropics/skills/tree/main/skills" },
534
- { url: "https://api.github.com/repos/Prat011/awesome-llm-skills/contents/skills", base: "https://github.com/Prat011/awesome-llm-skills/tree/main/skills" },
535
- { url: "https://api.github.com/repos/skillcreatorai/Ai-Agent-Skills/contents/skills", base: "https://github.com/skillcreatorai/Ai-Agent-Skills/tree/main/skills" }
536
- ];
537
- const fetchSkills = async () => {
538
- const seen = /* @__PURE__ */ new Set();
539
- const skills = [];
540
- const promises = SKILL_SOURCES.map(async (source) => {
541
- const controller = new AbortController();
542
- const timeout = setTimeout(() => controller.abort(), 1e4);
543
- try {
544
- const res = await fetch(source.url, {
545
- signal: controller.signal,
546
- headers: { "Accept": "application/vnd.github.v3+json" }
547
- });
548
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
549
- const data = await res.json();
550
- if (Array.isArray(data)) {
551
- for (const s of data.filter((s2) => s2.type === "dir")) {
552
- if (!seen.has(s.name)) {
553
- seen.add(s.name);
554
- skills.push({
555
- label: s.name,
556
- value: `${source.base}/${s.name}`,
557
- key: s.name
558
- });
559
- }
560
- }
561
- }
562
- } catch (error) {
563
- logError(`fetchSkills(${source.url})`, error);
564
- } finally {
565
- clearTimeout(timeout);
566
- }
567
- });
568
- await Promise.all(promises);
569
- return skills.sort((a, b) => a.label.localeCompare(b.label));
570
- };
571
- const SKILLS_DIR = path.join(os.homedir(), ".claude", "skills");
572
- const addSkillToClaudeJson = (skillName, skillUrl) => {
573
- try {
574
- if (!fs.existsSync(SKILLS_DIR)) fs.mkdirSync(SKILLS_DIR, { recursive: true });
575
- const skillPath = path.join(SKILLS_DIR, skillName);
576
- if (fs.existsSync(skillPath)) {
577
- return { success: false, message: "Skill already installed" };
578
- }
579
- const match = skillUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\/tree\/([^\/]+)\/(.+)/);
580
- if (!match) return { success: false, message: "Invalid skill URL" };
581
- const [, owner, repo, branch, skillSubPath] = match;
582
- const tempDir = `/tmp/skill-clone-${Date.now()}`;
583
- execSync(`git clone --depth 1 --filter=blob:none --sparse "https://github.com/${owner}/${repo}.git" "${tempDir}" 2>/dev/null`, { timeout: 3e4 });
584
- execSync(`cd "${tempDir}" && git sparse-checkout set "${skillSubPath}" 2>/dev/null`, { timeout: 1e4 });
585
- execSync(`mv "${tempDir}/${skillSubPath}" "${skillPath}"`, { timeout: 5e3 });
586
- execSync(`rm -rf "${tempDir}"`, { timeout: 5e3 });
587
- return { success: true };
588
- } catch (e) {
589
- return { success: false, message: "Failed to download skill" };
590
- }
591
- };
592
- const SkillsBrowser = () => {
716
+ var SkillsBrowser = () => {
593
717
  const { exit } = useApp();
594
718
  const [allSkills, setAllSkills] = useState([]);
595
719
  const [loading, setLoading] = useState(true);
596
720
  const [offset, setOffset] = useState(0);
597
- const SKILLS_PAGE_SIZE = 50;
598
721
  useEffect(() => {
599
722
  const loadSkills = async () => {
600
723
  const s = await fetchSkills();
@@ -662,8 +785,8 @@ if (cmd === "skills") {
662
785
  process.exit(1);
663
786
  }
664
787
  const installed = getInstalledSkills();
665
- const idx = parseInt(target) - 1;
666
- const match = installed[idx] || installed.find((s) => s.toLowerCase() === target?.toLowerCase());
788
+ const idx = safeParseInt(target, -1);
789
+ const match = idx > 0 && idx <= installed.length ? installed[idx - 1] : installed.find((s) => s.toLowerCase() === target?.toLowerCase());
667
790
  if (!match) {
668
791
  console.log(`\x1B[31mSkill not found: ${target}\x1B[0m`);
669
792
  console.log('Run "cm skills list" to see installed skills');
@@ -699,14 +822,18 @@ if (cmd === "skills") {
699
822
  console.log(" profile: Profile name or number");
700
823
  process.exit(1);
701
824
  }
702
- const idx = parseInt(targetProfile) - 1;
703
- const profileMatch = profiles[idx] || profiles.find((p) => p.label.toLowerCase() === targetProfile?.toLowerCase());
825
+ const idx = safeParseInt(targetProfile, -1);
826
+ const profileMatch = idx > 0 && idx <= profiles.length ? profiles[idx - 1] : profiles.find((p) => p.label.toLowerCase() === targetProfile?.toLowerCase());
704
827
  if (!profileMatch) {
705
828
  console.log(`\x1B[31mProfile not found: ${targetProfile}\x1B[0m`);
706
829
  process.exit(1);
707
830
  }
708
- const profilePath = path.join(PROFILES_DIR, profileMatch.value);
709
- const profile = JSON.parse(fs.readFileSync(profilePath, "utf8"));
831
+ const profilePath = path3.join(PROFILES_DIR, profileMatch.value);
832
+ if (!fs2.existsSync(profilePath)) {
833
+ console.log(`\x1B[31mProfile file not found: ${profileMatch.value}\x1B[0m`);
834
+ process.exit(1);
835
+ }
836
+ const profile = JSON.parse(fs2.readFileSync(profilePath, "utf8"));
710
837
  const mcpServers = profile.mcpServers || {};
711
838
  if (Object.keys(mcpServers).length === 0) {
712
839
  console.log(`\x1B[33mNo MCP servers configured in "${profileMatch.label}"\x1B[0m`);
@@ -721,7 +848,7 @@ if (cmd === "skills") {
721
848
  if (shouldRemove) {
722
849
  delete mcpServers[serverName];
723
850
  profile.mcpServers = mcpServers;
724
- fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2));
851
+ fs2.writeFileSync(profilePath, JSON.stringify(profile, null, 2));
725
852
  console.log(`\x1B[32m\u2713\x1B[0m Removed "${serverName}" from "${profileMatch.label}"`);
726
853
  } else {
727
854
  console.log("\x1B[33mCancelled\x1B[0m");
@@ -739,43 +866,23 @@ if (cmd === "skills") {
739
866
  const [model, setModel] = useState("");
740
867
  const [group, setGroup] = useState("");
741
868
  const [validationErrors, setValidationErrors] = useState([]);
742
- const providers = [
743
- { label: "Anthropic (Direct)", value: "anthropic", url: "", needsKey: true },
744
- { label: "Amazon Bedrock", value: "bedrock", url: "", needsKey: false },
745
- { label: "Z.AI", value: "zai", url: "https://api.z.ai/api/anthropic", needsKey: true },
746
- { label: "MiniMax", value: "minimax", url: "https://api.minimax.io/anthropic", needsKey: true },
747
- { label: "Custom", value: "custom", url: "", needsKey: true }
748
- ];
749
869
  const handleSave = () => {
750
- const prov = providers.find((p) => p.value === provider);
751
- const profile = {
752
- name,
753
- group: group || void 0,
754
- env: {
755
- ...apiKey && { ANTHROPIC_AUTH_TOKEN: apiKey },
756
- ...model && { ANTHROPIC_MODEL: model },
757
- ...prov?.url && { ANTHROPIC_BASE_URL: prov.url },
758
- API_TIMEOUT_MS: "3000000"
759
- },
760
- model: "opus",
761
- alwaysThinkingEnabled: true,
762
- defaultMode: "bypassPermissions"
763
- };
870
+ const profile = buildProfileData(name, provider, apiKey, model, group, PROVIDERS);
764
871
  const validation = validateProfile(profile);
765
872
  if (!validation.valid) {
766
873
  setStep("error");
767
874
  setValidationErrors(validation.errors);
768
875
  return;
769
876
  }
770
- const filename = name.toLowerCase().replace(/\s+/g, "-") + ".json";
771
- fs.writeFileSync(path.join(PROFILES_DIR, filename), JSON.stringify(profile, null, 2));
877
+ const filename = sanitizeProfileName(name) + ".json";
878
+ fs2.writeFileSync(path3.join(PROFILES_DIR, filename), JSON.stringify(profile, null, 2));
772
879
  console.log(`
773
880
  \x1B[32m\u2713\x1B[0m Created: ${name}`);
774
881
  exit();
775
882
  };
776
883
  const handleProviderSelect = (item) => {
777
884
  setProvider(item.value);
778
- const prov = providers.find((p) => p.value === item.value);
885
+ const prov = PROVIDERS.find((p) => p.value === item.value);
779
886
  setStep(prov.needsKey ? "apikey" : "model");
780
887
  };
781
888
  useInput((input, key) => {
@@ -784,7 +891,7 @@ if (cmd === "skills") {
784
891
  setValidationErrors([]);
785
892
  }
786
893
  });
787
- 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 })), step === "error" && /* @__PURE__ */ React.createElement(Box, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React.createElement(Text, { color: "red" }, "Validation errors:"), validationErrors.map((err, i) => /* @__PURE__ */ React.createElement(Text, { key: i, color: "yellow" }, " \u2022 ", err)), /* @__PURE__ */ React.createElement(Text, { marginTop: 1 }, "Press any key to go back and fix...")));
894
+ 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 })), step === "error" && /* @__PURE__ */ React.createElement(Box, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React.createElement(Text, { color: "red" }, "Validation errors:"), validationErrors.map((err, i) => /* @__PURE__ */ React.createElement(Text, { key: i, color: "yellow" }, " \u2022 ", err)), /* @__PURE__ */ React.createElement(Text, { marginTop: 1 }, "Press any key to go back and fix...")));
788
895
  };
789
896
  render(/* @__PURE__ */ React.createElement(NewProfileWizard, null));
790
897
  } else {
@@ -818,9 +925,9 @@ if (cmd === "skills") {
818
925
  { label: "/skills", description: "Browse and install skills", action: () => render(/* @__PURE__ */ React.createElement(SkillsBrowser, null)) },
819
926
  { label: "/mcp", description: "Search and add MCP servers", action: () => render(/* @__PURE__ */ React.createElement(McpSearch, null)) },
820
927
  { label: "/new", description: "Create new profile", action: () => setStep("newProfile") },
821
- { label: "/list", description: "List all profiles", action: () => execSync("cm list", { stdio: "inherit" }) },
822
- { label: "/status", description: "Show current settings", action: () => execSync("cm status", { stdio: "inherit" }) },
823
- { label: "/config", description: "Edit Claude settings", action: () => execSync("cm config", { stdio: "inherit" }) },
928
+ { label: "/list", description: "List all profiles", action: () => execSync2("cm list", { stdio: "inherit" }) },
929
+ { label: "/status", description: "Show current settings", action: () => execSync2("cm status", { stdio: "inherit" }) },
930
+ { label: "/config", description: "Edit Claude settings", action: () => execSync2("cm config", { stdio: "inherit" }) },
824
931
  { label: "/help", description: "Show keyboard shortcuts", action: () => setShowHelp(true) },
825
932
  { label: "/quit", description: "Exit cm", action: () => process.exit(0) }
826
933
  ];
@@ -828,8 +935,7 @@ if (cmd === "skills") {
828
935
  if (!filter) return profiles;
829
936
  const fuse = new Fuse(profiles, {
830
937
  keys: ["label", "group"],
831
- threshold: 0.3,
832
- // Lower = more strict matching
938
+ threshold: FUSE_THRESHOLD,
833
939
  ignoreLocation: true,
834
940
  includeScore: true
835
941
  });
@@ -840,7 +946,7 @@ if (cmd === "skills") {
840
946
  const search = commandInput.toLowerCase().replace(/^\//, "");
841
947
  const fuse = new Fuse(commands, {
842
948
  keys: ["label", "description"],
843
- threshold: 0.3,
949
+ threshold: FUSE_THRESHOLD,
844
950
  ignoreLocation: true
845
951
  });
846
952
  return fuse.search(search).map((r) => r.item);
@@ -848,7 +954,7 @@ if (cmd === "skills") {
848
954
  useEffect(() => {
849
955
  setTimeout(() => setStep("select"), 1500);
850
956
  if (!skipUpdate) {
851
- checkForUpdate().then(setUpdateInfo);
957
+ checkForUpdate(skipUpdate).then(setUpdateInfo);
852
958
  }
853
959
  }, []);
854
960
  useInput((input, key) => {
@@ -880,24 +986,24 @@ if (cmd === "skills") {
880
986
  return;
881
987
  }
882
988
  if (step === "select") {
883
- const num = parseInt(input);
989
+ const num = safeParseInt(input, -1);
884
990
  if (num >= 1 && num <= 9 && num <= filteredProfiles.length) {
885
991
  const profile = filteredProfiles[num - 1];
886
992
  applyProfile(profile.value);
887
993
  console.log(`
888
994
  \x1B[32m\u2713\x1B[0m Applied: ${profile.label}
889
995
  `);
890
- launchClaude();
996
+ launchClaude(dangerMode);
891
997
  }
892
998
  if (input === "u" && updateInfo?.needsUpdate) {
893
999
  console.log("\n\x1B[33mUpdating Claude...\x1B[0m\n");
894
1000
  try {
895
1001
  if (process.platform === "darwin") {
896
- execSync("brew upgrade claude-code", { stdio: "inherit" });
1002
+ execSync2("brew upgrade claude-code", { stdio: "inherit" });
897
1003
  } else {
898
- execSync("npm update -g @anthropic-ai/claude-code", { stdio: "inherit" });
1004
+ execSync2("npm update -g @anthropic-ai/claude-code", { stdio: "inherit" });
899
1005
  }
900
- console.log("\n\x1B[32m\u2713 Updated!\x1B[0m\n");
1006
+ console.log("\x1B[32m\u2713 Updated!\x1B[0m\n");
901
1007
  setUpdateInfo({ ...updateInfo, needsUpdate: false });
902
1008
  } catch (error) {
903
1009
  console.log("\x1B[31m\u2717 Update failed\x1B[0m\n");
@@ -923,17 +1029,9 @@ if (cmd === "skills") {
923
1029
  }
924
1030
  if (input === "c") {
925
1031
  const editor = process.env.EDITOR || "nano";
926
- const configPath = SETTINGS_PATH;
927
- if (!fs.existsSync(configPath)) {
928
- fs.writeFileSync(configPath, JSON.stringify({
929
- env: {},
930
- model: "opus",
931
- alwaysThinkingEnabled: true,
932
- defaultMode: "bypassPermissions"
933
- }, null, 2));
934
- }
1032
+ createDefaultSettings();
935
1033
  console.clear();
936
- spawnSync(editor, [configPath], { stdio: "inherit" });
1034
+ spawnSync(editor, [SETTINGS_PATH], { stdio: "inherit" });
937
1035
  console.log("\n\x1B[36mConfig edited. Press Enter to continue...\x1B[0m");
938
1036
  }
939
1037
  }
@@ -970,9 +1068,9 @@ if (cmd === "skills") {
970
1068
  console.log(`
971
1069
  \x1B[32m\u2713\x1B[0m Applied: ${item.label.replace(/^\d+\.\s*/, "")}
972
1070
  `);
973
- launchClaude();
1071
+ launchClaude(dangerMode);
974
1072
  };
975
- return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, LOGO), /* @__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 select, / commands, ? help, c config", updateInfo?.needsUpdate ? ", u update" : "", ")")), /* @__PURE__ */ React.createElement(
1073
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, LOGO), /* @__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" }, "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 select, / commands, ? help, c config", updateInfo?.needsUpdate ? ", u update" : "", ")")), /* @__PURE__ */ React.createElement(
976
1074
  SelectInput,
977
1075
  {
978
1076
  items: groupedItems,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-manager",
3
- "version": "1.5.3",
3
+ "version": "1.5.4",
4
4
  "description": "Terminal app for managing Claude Code settings, profiles, MCP servers, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  "claude-manager": "./dist/cli.js"
9
9
  },
10
10
  "scripts": {
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",
11
+ "build": "esbuild src/cli.js --platform=node --format=esm --loader:.js=jsx --bundle --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
12
  "prepublishOnly": "npm run build"
13
13
  },
14
14
  "files": [