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.
- package/README.md +20 -2
- package/dist/cli.js +427 -329
- 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
|
-

|
|
6
6
|

|
|
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
|
-
- [
|
|
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
|
|
7
|
-
import
|
|
8
|
-
import
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
85
|
-
const profilePath =
|
|
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
|
-
|
|
132
|
+
var getLastProfile = () => {
|
|
101
133
|
try {
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
108
|
-
const localProfile =
|
|
140
|
+
var checkProjectProfile = () => {
|
|
141
|
+
const localProfile = path2.join(process.cwd(), ".claude-profile");
|
|
109
142
|
if (fs.existsSync(localProfile)) {
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
+
const normalized = answer.toLowerCase().trim();
|
|
160
|
+
resolve(normalized === "y" || normalized === "yes");
|
|
128
161
|
});
|
|
129
162
|
});
|
|
130
163
|
};
|
|
131
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
175
|
-
const skillPath =
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
183
|
-
if (
|
|
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:
|
|
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
|
-
|
|
259
|
+
var launchClaude = (dangerMode2) => {
|
|
214
260
|
try {
|
|
215
|
-
const claudeArgs =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
} catch {
|
|
500
|
+
installedSkills.forEach((s) => console.log(` - ${s}`));
|
|
277
501
|
}
|
|
278
502
|
try {
|
|
279
|
-
const claudeJson = JSON.parse(
|
|
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 =
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
327
|
-
|
|
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
|
-
|
|
335
|
-
|
|
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
|
-
|
|
345
|
-
|
|
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
|
-
|
|
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 =
|
|
365
|
-
const match = profiles[idx]
|
|
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
|
|
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
|
|
373
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
523
|
-
|
|
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
|
-
|
|
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 =
|
|
666
|
-
const match = installed[idx]
|
|
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 =
|
|
703
|
-
const profileMatch = profiles[idx]
|
|
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 =
|
|
709
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
771
|
-
|
|
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 =
|
|
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:
|
|
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: () =>
|
|
822
|
-
{ label: "/status", description: "Show current settings", action: () =>
|
|
823
|
-
{ label: "/config", description: "Edit Claude settings", action: () =>
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
1002
|
+
execSync2("brew upgrade claude-code", { stdio: "inherit" });
|
|
897
1003
|
} else {
|
|
898
|
-
|
|
1004
|
+
execSync2("npm update -g @anthropic-ai/claude-code", { stdio: "inherit" });
|
|
899
1005
|
}
|
|
900
|
-
console.log("\
|
|
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
|
-
|
|
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, [
|
|
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" }, "
|
|
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
|
+
"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": [
|