claude-manager 1.5.3 → 1.5.5

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 +29 -9
  2. package/dist/cli.js +438 -331
  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
@@ -129,13 +144,15 @@ Profiles are stored in `~/.claude/profiles/*.json`
129
144
 
130
145
  Pre-configured in `cm new`:
131
146
 
132
- | Provider | Base URL |
133
- |----------|----------|
134
- | Anthropic (Direct) | Default |
135
- | Amazon Bedrock | Default |
136
- | Z.AI | `https://api.z.ai/api/anthropic` |
137
- | MiniMax | `https://api.minimax.io/anthropic` |
138
- | Custom | Your URL |
147
+ | Provider | Base URL | Notes |
148
+ |----------|----------|-------|
149
+ | Anthropic (Direct) | Default | Standard `sk-ant-` keys |
150
+ | Amazon Bedrock | Default | No API key needed |
151
+ | Z.AI | `https://api.z.ai/api/anthropic` | Standard `sk-ant-` keys |
152
+ | MiniMax | `https://api.minimax.io/anthropic` | Supports both `sk-ant-` and `sk-cp-` (coding plan) keys |
153
+ | Custom | Your URL | Depends on provider |
154
+
155
+ **MiniMax Coding Plan Keys**: If you have a MiniMax coding plan subscription, get your `sk-cp-` key from the [Account/Coding Plan](https://platform.minimax.io/user-center/payment/coding-plan) page. Regular platform keys (`sk-ant-`) are available from the [API Keys](https://platform.minimax.io/user-center/basic-information/interface-key) page.
139
156
 
140
157
  ## Per-Project Profiles
141
158
 
@@ -171,6 +188,9 @@ cm edit 1
171
188
 
172
189
  # Check what's installed
173
190
  cm status
191
+
192
+ # Update cm via npm
193
+ npm update -g claude-manager
174
194
  ```
175
195
 
176
196
  ## 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.5";
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,19 +156,28 @@ 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");
135
168
  }
136
169
  if (profile.env?.ANTHROPIC_AUTH_TOKEN) {
137
170
  const key = profile.env.ANTHROPIC_AUTH_TOKEN;
138
- if (!key.startsWith("sk-ant-")) {
139
- errors.push('API key should start with "sk-ant-"');
171
+ const baseUrl = profile.env?.ANTHROPIC_BASE_URL || "";
172
+ const isMiniMax = baseUrl.includes("minimax.io") || baseUrl.includes("minimaxi.com");
173
+ if (isMiniMax) {
174
+ if (!key.startsWith("sk-ant-") && !key.startsWith("sk-cp-")) {
175
+ errors.push('MiniMax API key should start with "sk-ant-" or "sk-cp-" (for coding plans)');
176
+ }
177
+ } else {
178
+ if (!key.startsWith("sk-ant-")) {
179
+ errors.push('API key should start with "sk-ant-"');
180
+ }
140
181
  }
141
182
  if (key.length < 20) {
142
183
  errors.push("API key appears too short");
@@ -148,6 +189,7 @@ const validateProfile = (profile) => {
148
189
  /^claude-\d+(\.\d+)?(-\d+)?$/,
149
190
  /^glm-/,
150
191
  /^minimax-/,
192
+ /^MiniMax-M\d+(\.\d+)?$/,
151
193
  /^anthropic\.claude-/
152
194
  ];
153
195
  if (!validPatterns.some((p) => p.test(model))) {
@@ -163,28 +205,41 @@ const validateProfile = (profile) => {
163
205
  }
164
206
  return { valid: errors.length === 0, errors };
165
207
  };
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
- });
208
+ var getInstalledSkills = () => {
209
+ if (!fs.existsSync(SKILLS_DIR)) return [];
210
+ try {
211
+ return fs.readdirSync(SKILLS_DIR).filter((f) => {
212
+ const p = path2.join(SKILLS_DIR, f);
213
+ try {
214
+ return fs.statSync(p).isDirectory() && !f.startsWith(".");
215
+ } catch {
216
+ return false;
217
+ }
218
+ });
219
+ } catch (error) {
220
+ logError("getInstalledSkills", error);
221
+ return [];
222
+ }
173
223
  };
174
- const removeSkill = (skillName) => {
175
- const skillPath = path.join(os.homedir(), ".claude", "skills", skillName);
224
+ var removeSkill = (skillName) => {
225
+ const skillPath = path2.join(SKILLS_DIR, skillName);
176
226
  if (!fs.existsSync(skillPath)) {
177
227
  return { success: false, message: "Skill not found" };
178
228
  }
179
- fs.rmSync(skillPath, { recursive: true, force: true });
180
- return { success: true };
229
+ try {
230
+ fs.rmSync(skillPath, { recursive: true, force: true });
231
+ return { success: true };
232
+ } catch (error) {
233
+ logError("removeSkill", error);
234
+ return { success: false, message: "Failed to remove skill" };
235
+ }
181
236
  };
182
- const checkForUpdate = async () => {
183
- if (skipUpdate) return { needsUpdate: false };
237
+ var checkForUpdate = async (skipUpdate2) => {
238
+ if (skipUpdate2) return { needsUpdate: false };
239
+ const { exec } = await import("child_process");
240
+ const { promisify } = await import("util");
241
+ const execAsync = promisify(exec);
184
242
  try {
185
- const { exec } = await import("child_process");
186
- const { promisify } = await import("util");
187
- const execAsync = promisify(exec);
188
243
  const versionResult = await execAsync("claude --version 2>/dev/null").catch(() => ({ stdout: "" }));
189
244
  const current = versionResult.stdout.match(/(\d+\.\d+\.\d+)/)?.[1];
190
245
  if (!current) return { needsUpdate: false };
@@ -197,7 +252,7 @@ const checkForUpdate = async () => {
197
252
  const npmListResult = await execAsync("npm list -g @anthropic-ai/claude-code 2>/dev/null").catch(() => ({ stdout: "" }));
198
253
  if (npmListResult.stdout.includes("@anthropic-ai/claude-code")) {
199
254
  try {
200
- const npmOutdated = await execAsync("npm outdated -g @anthropic-ai/claude-code --json 2>/dev/null || true", { timeout: 5e3 });
255
+ const npmOutdated = await execAsync("npm outdated -g @anthropic-ai/claude-code --json 2>/dev/null || true", { timeout: NPM_OUTDATED_TIMEOUT });
201
256
  needsUpdate = npmOutdated.stdout.length > 0;
202
257
  } catch {
203
258
  needsUpdate = true;
@@ -210,35 +265,222 @@ const checkForUpdate = async () => {
210
265
  return { needsUpdate: false };
211
266
  }
212
267
  };
213
- const launchClaude = () => {
268
+ var launchClaude = (dangerMode2) => {
214
269
  try {
215
- const claudeArgs = dangerMode ? "--dangerously-skip-permissions" : "";
270
+ const claudeArgs = dangerMode2 ? "--dangerously-skip-permissions" : "";
216
271
  execSync(`claude ${claudeArgs}`, { stdio: "inherit" });
217
272
  } catch (e) {
218
273
  process.exit(e.status || 1);
219
274
  }
220
275
  process.exit(0);
221
276
  };
277
+ var searchMcpServers = async (query, offset = 0) => {
278
+ const controller = new AbortController();
279
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
280
+ try {
281
+ const res = await fetch(`${MCP_REGISTRY_URL}?limit=200`, { signal: controller.signal });
282
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
283
+ const data = await res.json();
284
+ const seen = /* @__PURE__ */ new Set();
285
+ const filtered = data.servers.filter((s) => {
286
+ if (seen.has(s.server.name)) return false;
287
+ seen.add(s.server.name);
288
+ const isLatest = s._meta?.["io.modelcontextprotocol.registry/official"]?.isLatest !== false;
289
+ const matchesQuery = !query || s.server.name.toLowerCase().includes(query.toLowerCase()) || s.server.description?.toLowerCase().includes(query.toLowerCase());
290
+ return isLatest && matchesQuery;
291
+ });
292
+ const MCP_PAGE_SIZE2 = 50;
293
+ return {
294
+ servers: filtered.slice(offset, offset + MCP_PAGE_SIZE2),
295
+ total: filtered.length,
296
+ hasMore: offset + MCP_PAGE_SIZE2 < filtered.length,
297
+ offset
298
+ };
299
+ } catch (error) {
300
+ logError("searchMcpServers", error);
301
+ return { servers: [], total: 0, hasMore: false, offset: 0 };
302
+ } finally {
303
+ clearTimeout(timeout);
304
+ }
305
+ };
306
+ var addMcpToProfile = (server, profileFile) => {
307
+ const sanitizedFile = sanitizeFilePath(profileFile, PROFILES_DIR);
308
+ if (!sanitizedFile) {
309
+ throw new Error("Invalid profile file");
310
+ }
311
+ const profilePath = path2.join(PROFILES_DIR, sanitizedFile);
312
+ const profile = JSON.parse(fs.readFileSync(profilePath, "utf8"));
313
+ if (!profile.mcpServers) profile.mcpServers = {};
314
+ const s = server.server;
315
+ const name = s.name.split("/").pop();
316
+ if (s.remotes?.[0]) {
317
+ const remote = s.remotes[0];
318
+ profile.mcpServers[name] = {
319
+ type: remote.type === "streamable-http" ? "http" : remote.type,
320
+ url: remote.url
321
+ };
322
+ } else if (s.packages?.[0]) {
323
+ const pkg = s.packages[0];
324
+ if (pkg.registryType === "npm") {
325
+ profile.mcpServers[name] = {
326
+ type: "stdio",
327
+ command: "npx",
328
+ args: ["-y", pkg.identifier]
329
+ };
330
+ } else if (pkg.registryType === "pypi") {
331
+ profile.mcpServers[name] = {
332
+ type: "stdio",
333
+ command: "uvx",
334
+ args: [pkg.identifier]
335
+ };
336
+ }
337
+ }
338
+ fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2));
339
+ return name;
340
+ };
341
+ var fetchSkills = async () => {
342
+ const seen = /* @__PURE__ */ new Set();
343
+ const skills = [];
344
+ const promises = SKILL_SOURCES.map(async (source) => {
345
+ const controller = new AbortController();
346
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
347
+ try {
348
+ const res = await fetch(source.url, {
349
+ signal: controller.signal,
350
+ headers: { "Accept": "application/vnd.github.v3+json" }
351
+ });
352
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
353
+ const data = await res.json();
354
+ if (Array.isArray(data)) {
355
+ for (const s of data.filter((s2) => s2.type === "dir")) {
356
+ if (!seen.has(s.name)) {
357
+ seen.add(s.name);
358
+ skills.push({
359
+ label: s.name,
360
+ value: `${source.base}/${s.name}`,
361
+ key: s.name
362
+ });
363
+ }
364
+ }
365
+ }
366
+ } catch (error) {
367
+ logError(`fetchSkills(${source.url})`, error);
368
+ } finally {
369
+ clearTimeout(timeout);
370
+ }
371
+ });
372
+ await Promise.all(promises);
373
+ return skills.sort((a, b) => a.label.localeCompare(b.label));
374
+ };
375
+ var addSkillToClaudeJson = (skillName, skillUrl) => {
376
+ try {
377
+ if (!fs.existsSync(SKILLS_DIR)) fs.mkdirSync(SKILLS_DIR, { recursive: true });
378
+ const skillPath = path2.join(SKILLS_DIR, skillName);
379
+ if (fs.existsSync(skillPath)) {
380
+ return { success: false, message: "Skill already installed" };
381
+ }
382
+ const match = skillUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\/tree\/([^\/]+)\/(.+)/);
383
+ if (!match) return { success: false, message: "Invalid skill URL" };
384
+ const [, owner, repo, branch, skillSubPath] = match;
385
+ const tempDir = `/tmp/skill-clone-${Date.now()}`;
386
+ const sanitizedTempDir = sanitizeFilePath(`skill-clone-${Date.now()}`, "/tmp");
387
+ const finalTempDir = path2.join("/tmp", sanitizedTempDir || "skill-clone");
388
+ execSync(`git clone --depth 1 --filter=blob:none --sparse "https://github.com/${owner}/${repo}.git" "${finalTempDir}" 2>/dev/null`, { timeout: GIT_CLONE_TIMEOUT });
389
+ execSync(`cd "${finalTempDir}" && git sparse-checkout set "${skillSubPath}" 2>/dev/null`, { timeout: GIT_SPARSE_TIMEOUT });
390
+ const sourcePath = path2.join(finalTempDir, skillSubPath);
391
+ if (fs.existsSync(sourcePath)) {
392
+ execSync(`mv "${sourcePath}" "${skillPath}"`, { timeout: GIT_MOVE_TIMEOUT });
393
+ }
394
+ execSync(`rm -rf "${finalTempDir}"`, { timeout: GIT_CLEANUP_TIMEOUT });
395
+ return { success: true };
396
+ } catch (e) {
397
+ logError("addSkillToClaudeJson", e);
398
+ return { success: false, message: "Failed to download skill" };
399
+ }
400
+ };
401
+ var createDefaultSettings = () => {
402
+ if (!fs.existsSync(SETTINGS_PATH)) {
403
+ fs.writeFileSync(SETTINGS_PATH, JSON.stringify(DEFAULT_SETTINGS, null, 2));
404
+ }
405
+ };
406
+ var buildProfileData = (name, provider, apiKey, model, group, providers) => {
407
+ const prov = providers.find((p) => p.value === provider);
408
+ return {
409
+ name,
410
+ group: group || void 0,
411
+ env: {
412
+ ...apiKey && { ANTHROPIC_AUTH_TOKEN: apiKey },
413
+ ...model && { ANTHROPIC_MODEL: model },
414
+ ...prov?.url && { ANTHROPIC_BASE_URL: prov.url },
415
+ API_TIMEOUT_MS
416
+ },
417
+ model: "opus",
418
+ alwaysThinkingEnabled: true,
419
+ defaultMode: "bypassPermissions"
420
+ };
421
+ };
422
+
423
+ // src/cli.js
424
+ ensureProfilesDir();
425
+ var args = process.argv.slice(2);
426
+ var cmd = args[0];
427
+ if (args.includes("-v") || args.includes("--version")) {
428
+ console.log(`cm v${VERSION}`);
429
+ process.exit(0);
430
+ }
431
+ if (args.includes("-h") || args.includes("--help")) {
432
+ console.log(`cm v${VERSION} - Claude Settings Manager
433
+
434
+ Usage: cm [command] [options]
435
+
436
+ Commands:
437
+ (none) Select profile interactively
438
+ new Create a new profile
439
+ edit <n> Edit profile (by name or number)
440
+ copy <n> <new> Copy/duplicate a profile
441
+ delete <n> Delete profile (by name or number)
442
+ status Show current settings
443
+ list List all profiles
444
+ config Open Claude settings.json in editor
445
+ mcp [query] Search and add MCP servers
446
+ mcp remove Remove MCP server from profile
447
+ skills Browse and add Anthropic skills
448
+ skills list List installed skills
449
+ skills remove Remove an installed skill
450
+
451
+ Options:
452
+ --last, -l Use last profile without menu
453
+ --skip-update Skip update check
454
+ --yolo Run claude with --dangerously-skip-permissions
455
+ --force, -f Skip confirmation prompts (e.g., for delete)
456
+ -v, --version Show version
457
+ -h, --help Show help`);
458
+ process.exit(0);
459
+ }
460
+ var skipUpdate = args.includes("--skip-update");
461
+ var useLast = args.includes("--last") || args.includes("-l");
462
+ var dangerMode = args.includes("--dangerously-skip-permissions") || args.includes("--yolo");
222
463
  if (useLast) {
223
464
  const last = getLastProfile();
224
- if (last && fs.existsSync(path.join(PROFILES_DIR, last))) {
465
+ const lastPath = last ? path3.join(PROFILES_DIR, last) : null;
466
+ if (last && lastPath && fs2.existsSync(lastPath)) {
225
467
  const name = applyProfile(last);
226
468
  console.log(`\x1B[32m\u2713\x1B[0m Applied: ${name}
227
469
  `);
228
- launchClaude();
470
+ launchClaude(dangerMode);
229
471
  } else {
230
472
  console.log("\x1B[31mNo last profile found\x1B[0m");
231
473
  process.exit(1);
232
474
  }
233
475
  }
234
- const projectProfile = checkProjectProfile();
476
+ var projectProfile = checkProjectProfile();
235
477
  if (projectProfile && !cmd) {
236
478
  const profiles = loadProfiles();
237
479
  const match = profiles.find((p) => p.label === projectProfile || p.value === projectProfile + ".json");
238
480
  if (match) {
239
481
  console.log(`\x1B[36mUsing project profile: ${match.label}\x1B[0m`);
240
482
  applyProfile(match.value);
241
- launchClaude();
483
+ launchClaude(dangerMode);
242
484
  }
243
485
  }
244
486
  if (cmd === "status") {
@@ -260,36 +502,29 @@ Profile MCP Servers (${Object.keys(mcpServers).length}):`);
260
502
  } else {
261
503
  console.log("No profile active");
262
504
  }
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(`
505
+ const installedSkills = getInstalledSkills();
506
+ if (installedSkills.length > 0) {
507
+ console.log(`
272
508
  Installed Skills (${installedSkills.length}):`);
273
- installedSkills.forEach((s) => console.log(` - ${s}`));
274
- }
275
- }
276
- } catch {
509
+ installedSkills.forEach((s) => console.log(` - ${s}`));
277
510
  }
278
511
  try {
279
- const claudeJson = JSON.parse(fs.readFileSync(CLAUDE_JSON_PATH, "utf8"));
512
+ const claudeJson = JSON.parse(fs2.readFileSync(CLAUDE_JSON_PATH, "utf8"));
280
513
  const globalMcp = claudeJson.mcpServers || {};
281
514
  if (Object.keys(globalMcp).length > 0) {
282
515
  console.log(`
283
516
  Global MCP Servers (${Object.keys(globalMcp).length}):`);
284
517
  Object.keys(globalMcp).forEach((s) => console.log(` - ${s}`));
285
518
  }
286
- } catch {
519
+ } catch (error) {
520
+ logError("status-mcp", error);
287
521
  }
288
522
  try {
289
- const ver = execSync("claude --version 2>/dev/null", { encoding: "utf8" }).trim();
523
+ const ver = execSync2("claude --version 2>/dev/null", { encoding: "utf8" }).trim();
290
524
  console.log(`
291
525
  Claude: ${ver}`);
292
- } catch {
526
+ } catch (error) {
527
+ logError("status-version", error);
293
528
  }
294
529
  process.exit(0);
295
530
  }
@@ -305,34 +540,35 @@ if (cmd === "list") {
305
540
  }
306
541
  if (cmd === "config") {
307
542
  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" });
543
+ createDefaultSettings();
544
+ console.log(`Opening ${SETTINGS_PATH} in ${editor}...`);
545
+ spawnSync(editor, [SETTINGS_PATH], { stdio: "inherit" });
320
546
  process.exit(0);
321
547
  }
322
548
  if (cmd === "delete") {
323
549
  const forceDelete = args.includes("--force") || args.includes("-f");
324
550
  const profiles = loadProfiles();
325
551
  const target = args[1];
326
- const idx = parseInt(target) - 1;
327
- const match = profiles[idx] || profiles.find((p) => p.label.toLowerCase() === target?.toLowerCase());
552
+ if (!target) {
553
+ console.log("\x1B[31mUsage: cm delete <profile>\x1B[0m");
554
+ console.log(" profile: Profile name or number");
555
+ process.exit(1);
556
+ }
557
+ const idx = safeParseInt(target, -1);
558
+ const match = idx > 0 && idx <= profiles.length ? profiles[idx - 1] : profiles.find((p) => p.label.toLowerCase() === target?.toLowerCase());
328
559
  if (!match) {
329
560
  console.log(`\x1B[31mProfile not found: ${target}\x1B[0m`);
330
561
  process.exit(1);
331
562
  }
332
563
  const shouldDelete = forceDelete || await confirm(`Delete profile "${match.label}"?`);
333
564
  if (shouldDelete) {
334
- fs.unlinkSync(path.join(PROFILES_DIR, match.value));
335
- console.log(`\x1B[32m\u2713\x1B[0m Deleted: ${match.label}`);
565
+ const filePath = path3.join(PROFILES_DIR, match.value);
566
+ if (fs2.existsSync(filePath)) {
567
+ fs2.unlinkSync(filePath);
568
+ console.log(`\x1B[32m\u2713\x1B[0m Deleted: ${match.label}`);
569
+ } else {
570
+ console.log(`\x1B[31mProfile file not found: ${match.value}\x1B[0m`);
571
+ }
336
572
  } else {
337
573
  console.log("\x1B[33mCancelled\x1B[0m");
338
574
  }
@@ -341,11 +577,21 @@ if (cmd === "delete") {
341
577
  if (cmd === "edit") {
342
578
  const profiles = loadProfiles();
343
579
  const target = args[1];
344
- const idx = parseInt(target) - 1;
345
- const match = profiles[idx] || profiles.find((p) => p.label.toLowerCase() === target?.toLowerCase());
580
+ if (!target) {
581
+ console.log("\x1B[31mUsage: cm edit <profile>\x1B[0m");
582
+ console.log(" profile: Profile name or number");
583
+ process.exit(1);
584
+ }
585
+ const idx = safeParseInt(target, -1);
586
+ const match = idx > 0 && idx <= profiles.length ? profiles[idx - 1] : profiles.find((p) => p.label.toLowerCase() === target?.toLowerCase());
346
587
  if (match) {
347
588
  const editor = process.env.EDITOR || "nano";
348
- spawnSync(editor, [path.join(PROFILES_DIR, match.value)], { stdio: "inherit" });
589
+ const filePath = path3.join(PROFILES_DIR, match.value);
590
+ if (fs2.existsSync(filePath)) {
591
+ spawnSync(editor, [filePath], { stdio: "inherit" });
592
+ } else {
593
+ console.log(`\x1B[31mProfile file not found: ${match.value}\x1B[0m`);
594
+ }
349
595
  } else {
350
596
  console.log(`\x1B[31mProfile not found: ${target}\x1B[0m`);
351
597
  }
@@ -361,87 +607,29 @@ if (cmd === "copy") {
361
607
  console.log(" new-name: Name for the copied profile");
362
608
  process.exit(1);
363
609
  }
364
- const idx = parseInt(target) - 1;
365
- const match = profiles[idx] || profiles.find((p) => p.label.toLowerCase() === target?.toLowerCase());
610
+ const idx = safeParseInt(target, -1);
611
+ const match = idx > 0 && idx <= profiles.length ? profiles[idx - 1] : profiles.find((p) => p.label.toLowerCase() === target?.toLowerCase());
366
612
  if (!match) {
367
613
  console.log(`\x1B[31mProfile not found: ${target}\x1B[0m`);
368
614
  process.exit(1);
369
615
  }
370
- const profile = JSON.parse(fs.readFileSync(path.join(PROFILES_DIR, match.value), "utf8"));
616
+ const sourcePath = path3.join(PROFILES_DIR, match.value);
617
+ const profile = JSON.parse(fs2.readFileSync(sourcePath, "utf8"));
371
618
  profile.name = newName;
372
- const newFilename = newName.toLowerCase().replace(/\s+/g, "-") + ".json";
373
- if (fs.existsSync(path.join(PROFILES_DIR, newFilename))) {
619
+ const newFilename = sanitizeProfileName(newName) + ".json";
620
+ const destPath = path3.join(PROFILES_DIR, newFilename);
621
+ if (fs2.existsSync(destPath)) {
374
622
  const shouldOverwrite = await confirm(`Profile "${newName}" already exists. Overwrite?`);
375
623
  if (!shouldOverwrite) {
376
624
  console.log("\x1B[33mCancelled\x1B[0m");
377
625
  process.exit(0);
378
626
  }
379
627
  }
380
- fs.writeFileSync(path.join(PROFILES_DIR, newFilename), JSON.stringify(profile, null, 2));
628
+ fs2.writeFileSync(destPath, JSON.stringify(profile, null, 2));
381
629
  console.log(`\x1B[32m\u2713\x1B[0m Copied "${match.label}" to "${newName}"`);
382
630
  process.exit(0);
383
631
  }
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 = () => {
632
+ var McpSearch = () => {
445
633
  const { exit } = useApp();
446
634
  const [step, setStep] = useState(args[1] ? "loading" : "search");
447
635
  const [query, setQuery] = useState(args[1] || "");
@@ -519,9 +707,14 @@ const McpSearch = () => {
519
707
  {
520
708
  items: profileItems,
521
709
  onSelect: (item) => {
522
- const name = addMcpToProfile(selectedServer, item.value);
523
- console.log(`
710
+ try {
711
+ const name = addMcpToProfile(selectedServer, item.value);
712
+ console.log(`
524
713
  \x1B[32m\u2713\x1B[0m Added ${name} to ${item.label}`);
714
+ } catch (error) {
715
+ console.log(`
716
+ \x1B[31m\u2717\x1B[0m ${error.message}`);
717
+ }
525
718
  exit();
526
719
  }
527
720
  }
@@ -529,72 +722,11 @@ const McpSearch = () => {
529
722
  }
530
723
  return null;
531
724
  };
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 = () => {
725
+ var SkillsBrowser = () => {
593
726
  const { exit } = useApp();
594
727
  const [allSkills, setAllSkills] = useState([]);
595
728
  const [loading, setLoading] = useState(true);
596
729
  const [offset, setOffset] = useState(0);
597
- const SKILLS_PAGE_SIZE = 50;
598
730
  useEffect(() => {
599
731
  const loadSkills = async () => {
600
732
  const s = await fetchSkills();
@@ -662,8 +794,8 @@ if (cmd === "skills") {
662
794
  process.exit(1);
663
795
  }
664
796
  const installed = getInstalledSkills();
665
- const idx = parseInt(target) - 1;
666
- const match = installed[idx] || installed.find((s) => s.toLowerCase() === target?.toLowerCase());
797
+ const idx = safeParseInt(target, -1);
798
+ const match = idx > 0 && idx <= installed.length ? installed[idx - 1] : installed.find((s) => s.toLowerCase() === target?.toLowerCase());
667
799
  if (!match) {
668
800
  console.log(`\x1B[31mSkill not found: ${target}\x1B[0m`);
669
801
  console.log('Run "cm skills list" to see installed skills');
@@ -699,14 +831,18 @@ if (cmd === "skills") {
699
831
  console.log(" profile: Profile name or number");
700
832
  process.exit(1);
701
833
  }
702
- const idx = parseInt(targetProfile) - 1;
703
- const profileMatch = profiles[idx] || profiles.find((p) => p.label.toLowerCase() === targetProfile?.toLowerCase());
834
+ const idx = safeParseInt(targetProfile, -1);
835
+ const profileMatch = idx > 0 && idx <= profiles.length ? profiles[idx - 1] : profiles.find((p) => p.label.toLowerCase() === targetProfile?.toLowerCase());
704
836
  if (!profileMatch) {
705
837
  console.log(`\x1B[31mProfile not found: ${targetProfile}\x1B[0m`);
706
838
  process.exit(1);
707
839
  }
708
- const profilePath = path.join(PROFILES_DIR, profileMatch.value);
709
- const profile = JSON.parse(fs.readFileSync(profilePath, "utf8"));
840
+ const profilePath = path3.join(PROFILES_DIR, profileMatch.value);
841
+ if (!fs2.existsSync(profilePath)) {
842
+ console.log(`\x1B[31mProfile file not found: ${profileMatch.value}\x1B[0m`);
843
+ process.exit(1);
844
+ }
845
+ const profile = JSON.parse(fs2.readFileSync(profilePath, "utf8"));
710
846
  const mcpServers = profile.mcpServers || {};
711
847
  if (Object.keys(mcpServers).length === 0) {
712
848
  console.log(`\x1B[33mNo MCP servers configured in "${profileMatch.label}"\x1B[0m`);
@@ -721,7 +857,7 @@ if (cmd === "skills") {
721
857
  if (shouldRemove) {
722
858
  delete mcpServers[serverName];
723
859
  profile.mcpServers = mcpServers;
724
- fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2));
860
+ fs2.writeFileSync(profilePath, JSON.stringify(profile, null, 2));
725
861
  console.log(`\x1B[32m\u2713\x1B[0m Removed "${serverName}" from "${profileMatch.label}"`);
726
862
  } else {
727
863
  console.log("\x1B[33mCancelled\x1B[0m");
@@ -739,43 +875,23 @@ if (cmd === "skills") {
739
875
  const [model, setModel] = useState("");
740
876
  const [group, setGroup] = useState("");
741
877
  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
878
  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
- };
879
+ const profile = buildProfileData(name, provider, apiKey, model, group, PROVIDERS);
764
880
  const validation = validateProfile(profile);
765
881
  if (!validation.valid) {
766
882
  setStep("error");
767
883
  setValidationErrors(validation.errors);
768
884
  return;
769
885
  }
770
- const filename = name.toLowerCase().replace(/\s+/g, "-") + ".json";
771
- fs.writeFileSync(path.join(PROFILES_DIR, filename), JSON.stringify(profile, null, 2));
886
+ const filename = sanitizeProfileName(name) + ".json";
887
+ fs2.writeFileSync(path3.join(PROFILES_DIR, filename), JSON.stringify(profile, null, 2));
772
888
  console.log(`
773
889
  \x1B[32m\u2713\x1B[0m Created: ${name}`);
774
890
  exit();
775
891
  };
776
892
  const handleProviderSelect = (item) => {
777
893
  setProvider(item.value);
778
- const prov = providers.find((p) => p.value === item.value);
894
+ const prov = PROVIDERS.find((p) => p.value === item.value);
779
895
  setStep(prov.needsKey ? "apikey" : "model");
780
896
  };
781
897
  useInput((input, key) => {
@@ -784,7 +900,7 @@ if (cmd === "skills") {
784
900
  setValidationErrors([]);
785
901
  }
786
902
  });
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...")));
903
+ 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
904
  };
789
905
  render(/* @__PURE__ */ React.createElement(NewProfileWizard, null));
790
906
  } else {
@@ -818,9 +934,9 @@ if (cmd === "skills") {
818
934
  { label: "/skills", description: "Browse and install skills", action: () => render(/* @__PURE__ */ React.createElement(SkillsBrowser, null)) },
819
935
  { label: "/mcp", description: "Search and add MCP servers", action: () => render(/* @__PURE__ */ React.createElement(McpSearch, null)) },
820
936
  { 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" }) },
937
+ { label: "/list", description: "List all profiles", action: () => execSync2("cm list", { stdio: "inherit" }) },
938
+ { label: "/status", description: "Show current settings", action: () => execSync2("cm status", { stdio: "inherit" }) },
939
+ { label: "/config", description: "Edit Claude settings", action: () => execSync2("cm config", { stdio: "inherit" }) },
824
940
  { label: "/help", description: "Show keyboard shortcuts", action: () => setShowHelp(true) },
825
941
  { label: "/quit", description: "Exit cm", action: () => process.exit(0) }
826
942
  ];
@@ -828,8 +944,7 @@ if (cmd === "skills") {
828
944
  if (!filter) return profiles;
829
945
  const fuse = new Fuse(profiles, {
830
946
  keys: ["label", "group"],
831
- threshold: 0.3,
832
- // Lower = more strict matching
947
+ threshold: FUSE_THRESHOLD,
833
948
  ignoreLocation: true,
834
949
  includeScore: true
835
950
  });
@@ -840,7 +955,7 @@ if (cmd === "skills") {
840
955
  const search = commandInput.toLowerCase().replace(/^\//, "");
841
956
  const fuse = new Fuse(commands, {
842
957
  keys: ["label", "description"],
843
- threshold: 0.3,
958
+ threshold: FUSE_THRESHOLD,
844
959
  ignoreLocation: true
845
960
  });
846
961
  return fuse.search(search).map((r) => r.item);
@@ -848,7 +963,7 @@ if (cmd === "skills") {
848
963
  useEffect(() => {
849
964
  setTimeout(() => setStep("select"), 1500);
850
965
  if (!skipUpdate) {
851
- checkForUpdate().then(setUpdateInfo);
966
+ checkForUpdate(skipUpdate).then(setUpdateInfo);
852
967
  }
853
968
  }, []);
854
969
  useInput((input, key) => {
@@ -880,24 +995,24 @@ if (cmd === "skills") {
880
995
  return;
881
996
  }
882
997
  if (step === "select") {
883
- const num = parseInt(input);
998
+ const num = safeParseInt(input, -1);
884
999
  if (num >= 1 && num <= 9 && num <= filteredProfiles.length) {
885
1000
  const profile = filteredProfiles[num - 1];
886
1001
  applyProfile(profile.value);
887
1002
  console.log(`
888
1003
  \x1B[32m\u2713\x1B[0m Applied: ${profile.label}
889
1004
  `);
890
- launchClaude();
1005
+ launchClaude(dangerMode);
891
1006
  }
892
1007
  if (input === "u" && updateInfo?.needsUpdate) {
893
1008
  console.log("\n\x1B[33mUpdating Claude...\x1B[0m\n");
894
1009
  try {
895
1010
  if (process.platform === "darwin") {
896
- execSync("brew upgrade claude-code", { stdio: "inherit" });
1011
+ execSync2("brew upgrade claude-code", { stdio: "inherit" });
897
1012
  } else {
898
- execSync("npm update -g @anthropic-ai/claude-code", { stdio: "inherit" });
1013
+ execSync2("npm update -g @anthropic-ai/claude-code", { stdio: "inherit" });
899
1014
  }
900
- console.log("\n\x1B[32m\u2713 Updated!\x1B[0m\n");
1015
+ console.log("\x1B[32m\u2713 Updated!\x1B[0m\n");
901
1016
  setUpdateInfo({ ...updateInfo, needsUpdate: false });
902
1017
  } catch (error) {
903
1018
  console.log("\x1B[31m\u2717 Update failed\x1B[0m\n");
@@ -923,17 +1038,9 @@ if (cmd === "skills") {
923
1038
  }
924
1039
  if (input === "c") {
925
1040
  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
- }
1041
+ createDefaultSettings();
935
1042
  console.clear();
936
- spawnSync(editor, [configPath], { stdio: "inherit" });
1043
+ spawnSync(editor, [SETTINGS_PATH], { stdio: "inherit" });
937
1044
  console.log("\n\x1B[36mConfig edited. Press Enter to continue...\x1B[0m");
938
1045
  }
939
1046
  }
@@ -970,9 +1077,9 @@ if (cmd === "skills") {
970
1077
  console.log(`
971
1078
  \x1B[32m\u2713\x1B[0m Applied: ${item.label.replace(/^\d+\.\s*/, "")}
972
1079
  `);
973
- launchClaude();
1080
+ launchClaude(dangerMode);
974
1081
  };
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(
1082
+ 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
1083
  SelectInput,
977
1084
  {
978
1085
  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.5",
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": [