claude-manager 1.5.1 → 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 +768 -275
- package/package.json +6 -3
package/dist/cli.js
CHANGED
|
@@ -1,74 +1,119 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
// src/cli.js
|
|
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
|
|
10
|
-
const VERSION = "1.5.1";
|
|
11
|
-
const PROFILES_DIR = path.join(os.homedir(), ".claude", "profiles");
|
|
12
|
-
const SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
|
|
13
|
-
const CLAUDE_JSON_PATH = path.join(os.homedir(), ".claude.json");
|
|
14
|
-
const LAST_PROFILE_PATH = path.join(os.homedir(), ".claude", ".last-profile");
|
|
15
|
-
const MCP_REGISTRY_URL = "https://registry.modelcontextprotocol.io/v0/servers";
|
|
16
|
-
const args = process.argv.slice(2);
|
|
17
|
-
const cmd = args[0];
|
|
18
|
-
if (!fs.existsSync(PROFILES_DIR)) fs.mkdirSync(PROFILES_DIR, { recursive: true });
|
|
19
|
-
if (args.includes("-v") || args.includes("--version")) {
|
|
20
|
-
console.log(`cm v${VERSION}`);
|
|
21
|
-
process.exit(0);
|
|
22
|
-
}
|
|
23
|
-
if (args.includes("-h") || args.includes("--help")) {
|
|
24
|
-
console.log(`cm v${VERSION} - Claude Settings Manager
|
|
7
|
+
import { spawnSync, execSync as execSync2 } from "child_process";
|
|
8
|
+
import fs2 from "fs";
|
|
9
|
+
import path3 from "path";
|
|
10
|
+
import Fuse from "fuse.js";
|
|
25
11
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`;
|
|
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;
|
|
37
56
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 = () => {
|
|
50
88
|
const profiles = [];
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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);
|
|
66
108
|
}
|
|
67
109
|
}
|
|
68
110
|
return profiles;
|
|
69
111
|
};
|
|
70
|
-
|
|
71
|
-
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
|
+
}
|
|
72
117
|
const profile = JSON.parse(fs.readFileSync(profilePath, "utf8"));
|
|
73
118
|
const { name, group, mcpServers, ...settings } = profile;
|
|
74
119
|
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
@@ -77,65 +122,356 @@ const applyProfile = (filename) => {
|
|
|
77
122
|
const claudeJson = fs.existsSync(CLAUDE_JSON_PATH) ? JSON.parse(fs.readFileSync(CLAUDE_JSON_PATH, "utf8")) : {};
|
|
78
123
|
claudeJson.mcpServers = mcpServers;
|
|
79
124
|
fs.writeFileSync(CLAUDE_JSON_PATH, JSON.stringify(claudeJson, null, 2));
|
|
80
|
-
} catch {
|
|
125
|
+
} catch (error) {
|
|
126
|
+
logError("applyProfile-mcp", error);
|
|
81
127
|
}
|
|
82
128
|
}
|
|
83
129
|
fs.writeFileSync(LAST_PROFILE_PATH, filename);
|
|
84
130
|
return name || filename;
|
|
85
131
|
};
|
|
86
|
-
|
|
132
|
+
var getLastProfile = () => {
|
|
87
133
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
134
|
+
const content = fs.readFileSync(LAST_PROFILE_PATH, "utf8");
|
|
135
|
+
return content.trim() || null;
|
|
136
|
+
} catch (error) {
|
|
90
137
|
return null;
|
|
91
138
|
}
|
|
92
139
|
};
|
|
93
|
-
|
|
94
|
-
const localProfile =
|
|
140
|
+
var checkProjectProfile = () => {
|
|
141
|
+
const localProfile = path2.join(process.cwd(), ".claude-profile");
|
|
95
142
|
if (fs.existsSync(localProfile)) {
|
|
96
|
-
|
|
143
|
+
try {
|
|
144
|
+
return fs.readFileSync(localProfile, "utf8").trim();
|
|
145
|
+
} catch (error) {
|
|
146
|
+
logError("checkProjectProfile", error);
|
|
147
|
+
}
|
|
97
148
|
}
|
|
98
149
|
return null;
|
|
99
150
|
};
|
|
100
|
-
|
|
101
|
-
|
|
151
|
+
var confirm = async (message) => {
|
|
152
|
+
const rl = createInterface({
|
|
153
|
+
input: process.stdin,
|
|
154
|
+
output: process.stdout
|
|
155
|
+
});
|
|
156
|
+
return new Promise((resolve) => {
|
|
157
|
+
rl.question(`${message} (y/N): `, (answer) => {
|
|
158
|
+
rl.close();
|
|
159
|
+
const normalized = answer.toLowerCase().trim();
|
|
160
|
+
resolve(normalized === "y" || normalized === "yes");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
var validateProfile = (profile) => {
|
|
165
|
+
const errors = [];
|
|
166
|
+
if (!profile.name || profile.name.trim().length === 0) {
|
|
167
|
+
errors.push("Profile name is required");
|
|
168
|
+
}
|
|
169
|
+
if (profile.env?.ANTHROPIC_AUTH_TOKEN) {
|
|
170
|
+
const key = profile.env.ANTHROPIC_AUTH_TOKEN;
|
|
171
|
+
if (!key.startsWith("sk-ant-")) {
|
|
172
|
+
errors.push('API key should start with "sk-ant-"');
|
|
173
|
+
}
|
|
174
|
+
if (key.length < 20) {
|
|
175
|
+
errors.push("API key appears too short");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (profile.env?.ANTHROPIC_MODEL) {
|
|
179
|
+
const model = profile.env.ANTHROPIC_MODEL;
|
|
180
|
+
const validPatterns = [
|
|
181
|
+
/^claude-\d+(\.\d+)?(-\d+)?$/,
|
|
182
|
+
/^glm-/,
|
|
183
|
+
/^minimax-/,
|
|
184
|
+
/^anthropic\.claude-/
|
|
185
|
+
];
|
|
186
|
+
if (!validPatterns.some((p) => p.test(model))) {
|
|
187
|
+
errors.push(`Model format looks invalid: ${model}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (profile.env?.ANTHROPIC_BASE_URL) {
|
|
191
|
+
try {
|
|
192
|
+
new URL(profile.env.ANTHROPIC_BASE_URL);
|
|
193
|
+
} catch {
|
|
194
|
+
errors.push("Base URL is not a valid URL");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { valid: errors.length === 0, errors };
|
|
198
|
+
};
|
|
199
|
+
var getInstalledSkills = () => {
|
|
200
|
+
if (!fs.existsSync(SKILLS_DIR)) return [];
|
|
102
201
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
}
|
|
214
|
+
};
|
|
215
|
+
var removeSkill = (skillName) => {
|
|
216
|
+
const skillPath = path2.join(SKILLS_DIR, skillName);
|
|
217
|
+
if (!fs.existsSync(skillPath)) {
|
|
218
|
+
return { success: false, message: "Skill not found" };
|
|
219
|
+
}
|
|
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
|
+
}
|
|
227
|
+
};
|
|
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);
|
|
233
|
+
try {
|
|
234
|
+
const versionResult = await execAsync("claude --version 2>/dev/null").catch(() => ({ stdout: "" }));
|
|
235
|
+
const current = versionResult.stdout.match(/(\d+\.\d+\.\d+)/)?.[1];
|
|
236
|
+
if (!current) return { needsUpdate: false };
|
|
237
|
+
let needsUpdate = false;
|
|
238
|
+
if (process.platform === "darwin") {
|
|
239
|
+
const outdatedResult = await execAsync("brew outdated claude-code 2>&1 || true").catch(() => ({ stdout: "" }));
|
|
240
|
+
needsUpdate = outdatedResult.stdout.includes("claude-code");
|
|
241
|
+
}
|
|
242
|
+
if (!needsUpdate) {
|
|
243
|
+
const npmListResult = await execAsync("npm list -g @anthropic-ai/claude-code 2>/dev/null").catch(() => ({ stdout: "" }));
|
|
244
|
+
if (npmListResult.stdout.includes("@anthropic-ai/claude-code")) {
|
|
245
|
+
try {
|
|
246
|
+
const npmOutdated = await execAsync("npm outdated -g @anthropic-ai/claude-code --json 2>/dev/null || true", { timeout: NPM_OUTDATED_TIMEOUT });
|
|
247
|
+
needsUpdate = npmOutdated.stdout.length > 0;
|
|
248
|
+
} catch {
|
|
249
|
+
needsUpdate = true;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return { current, needsUpdate };
|
|
254
|
+
} catch (error) {
|
|
255
|
+
logError("checkForUpdate", error);
|
|
107
256
|
return { needsUpdate: false };
|
|
108
257
|
}
|
|
109
258
|
};
|
|
110
|
-
|
|
259
|
+
var launchClaude = (dangerMode2) => {
|
|
111
260
|
try {
|
|
112
|
-
const claudeArgs =
|
|
261
|
+
const claudeArgs = dangerMode2 ? "--dangerously-skip-permissions" : "";
|
|
113
262
|
execSync(`claude ${claudeArgs}`, { stdio: "inherit" });
|
|
114
263
|
} catch (e) {
|
|
115
264
|
process.exit(e.status || 1);
|
|
116
265
|
}
|
|
117
266
|
process.exit(0);
|
|
118
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");
|
|
119
454
|
if (useLast) {
|
|
120
455
|
const last = getLastProfile();
|
|
121
|
-
|
|
456
|
+
const lastPath = last ? path3.join(PROFILES_DIR, last) : null;
|
|
457
|
+
if (last && lastPath && fs2.existsSync(lastPath)) {
|
|
122
458
|
const name = applyProfile(last);
|
|
123
459
|
console.log(`\x1B[32m\u2713\x1B[0m Applied: ${name}
|
|
124
460
|
`);
|
|
125
|
-
launchClaude();
|
|
461
|
+
launchClaude(dangerMode);
|
|
126
462
|
} else {
|
|
127
463
|
console.log("\x1B[31mNo last profile found\x1B[0m");
|
|
128
464
|
process.exit(1);
|
|
129
465
|
}
|
|
130
466
|
}
|
|
131
|
-
|
|
467
|
+
var projectProfile = checkProjectProfile();
|
|
132
468
|
if (projectProfile && !cmd) {
|
|
133
469
|
const profiles = loadProfiles();
|
|
134
470
|
const match = profiles.find((p) => p.label === projectProfile || p.value === projectProfile + ".json");
|
|
135
471
|
if (match) {
|
|
136
472
|
console.log(`\x1B[36mUsing project profile: ${match.label}\x1B[0m`);
|
|
137
473
|
applyProfile(match.value);
|
|
138
|
-
launchClaude();
|
|
474
|
+
launchClaude(dangerMode);
|
|
139
475
|
}
|
|
140
476
|
}
|
|
141
477
|
if (cmd === "status") {
|
|
@@ -157,36 +493,29 @@ Profile MCP Servers (${Object.keys(mcpServers).length}):`);
|
|
|
157
493
|
} else {
|
|
158
494
|
console.log("No profile active");
|
|
159
495
|
}
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const installedSkills = fs.readdirSync(skillsDir).filter((f) => {
|
|
164
|
-
const p = path.join(skillsDir, f);
|
|
165
|
-
return fs.statSync(p).isDirectory() && !f.startsWith(".");
|
|
166
|
-
});
|
|
167
|
-
if (installedSkills.length > 0) {
|
|
168
|
-
console.log(`
|
|
496
|
+
const installedSkills = getInstalledSkills();
|
|
497
|
+
if (installedSkills.length > 0) {
|
|
498
|
+
console.log(`
|
|
169
499
|
Installed Skills (${installedSkills.length}):`);
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
} catch {
|
|
500
|
+
installedSkills.forEach((s) => console.log(` - ${s}`));
|
|
174
501
|
}
|
|
175
502
|
try {
|
|
176
|
-
const claudeJson = JSON.parse(
|
|
503
|
+
const claudeJson = JSON.parse(fs2.readFileSync(CLAUDE_JSON_PATH, "utf8"));
|
|
177
504
|
const globalMcp = claudeJson.mcpServers || {};
|
|
178
505
|
if (Object.keys(globalMcp).length > 0) {
|
|
179
506
|
console.log(`
|
|
180
507
|
Global MCP Servers (${Object.keys(globalMcp).length}):`);
|
|
181
508
|
Object.keys(globalMcp).forEach((s) => console.log(` - ${s}`));
|
|
182
509
|
}
|
|
183
|
-
} catch {
|
|
510
|
+
} catch (error) {
|
|
511
|
+
logError("status-mcp", error);
|
|
184
512
|
}
|
|
185
513
|
try {
|
|
186
|
-
const ver =
|
|
514
|
+
const ver = execSync2("claude --version 2>/dev/null", { encoding: "utf8" }).trim();
|
|
187
515
|
console.log(`
|
|
188
516
|
Claude: ${ver}`);
|
|
189
|
-
} catch {
|
|
517
|
+
} catch (error) {
|
|
518
|
+
logError("status-version", error);
|
|
190
519
|
}
|
|
191
520
|
process.exit(0);
|
|
192
521
|
}
|
|
@@ -200,104 +529,145 @@ if (cmd === "list") {
|
|
|
200
529
|
});
|
|
201
530
|
process.exit(0);
|
|
202
531
|
}
|
|
532
|
+
if (cmd === "config") {
|
|
533
|
+
const editor = process.env.EDITOR || "nano";
|
|
534
|
+
createDefaultSettings();
|
|
535
|
+
console.log(`Opening ${SETTINGS_PATH} in ${editor}...`);
|
|
536
|
+
spawnSync(editor, [SETTINGS_PATH], { stdio: "inherit" });
|
|
537
|
+
process.exit(0);
|
|
538
|
+
}
|
|
203
539
|
if (cmd === "delete") {
|
|
540
|
+
const forceDelete = args.includes("--force") || args.includes("-f");
|
|
204
541
|
const profiles = loadProfiles();
|
|
205
542
|
const target = args[1];
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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());
|
|
550
|
+
if (!match) {
|
|
212
551
|
console.log(`\x1B[31mProfile not found: ${target}\x1B[0m`);
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
const shouldDelete = forceDelete || await confirm(`Delete profile "${match.label}"?`);
|
|
555
|
+
if (shouldDelete) {
|
|
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
|
+
}
|
|
563
|
+
} else {
|
|
564
|
+
console.log("\x1B[33mCancelled\x1B[0m");
|
|
213
565
|
}
|
|
214
566
|
process.exit(0);
|
|
215
567
|
}
|
|
216
568
|
if (cmd === "edit") {
|
|
217
569
|
const profiles = loadProfiles();
|
|
218
570
|
const target = args[1];
|
|
219
|
-
|
|
220
|
-
|
|
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());
|
|
221
578
|
if (match) {
|
|
222
579
|
const editor = process.env.EDITOR || "nano";
|
|
223
|
-
|
|
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
|
+
}
|
|
224
586
|
} else {
|
|
225
587
|
console.log(`\x1B[31mProfile not found: ${target}\x1B[0m`);
|
|
226
588
|
}
|
|
227
589
|
process.exit(0);
|
|
228
590
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const matchesQuery = !query || s.server.name.toLowerCase().includes(query.toLowerCase()) || s.server.description?.toLowerCase().includes(query.toLowerCase());
|
|
239
|
-
return isLatest && matchesQuery;
|
|
240
|
-
}).slice(0, 15);
|
|
241
|
-
} catch {
|
|
242
|
-
return [];
|
|
591
|
+
if (cmd === "copy") {
|
|
592
|
+
const profiles = loadProfiles();
|
|
593
|
+
const target = args[1];
|
|
594
|
+
const newName = args[2];
|
|
595
|
+
if (!newName) {
|
|
596
|
+
console.log("\x1B[31mUsage: cm copy <source> <new-name>\x1B[0m");
|
|
597
|
+
console.log(" source: Profile name or number");
|
|
598
|
+
console.log(" new-name: Name for the copied profile");
|
|
599
|
+
process.exit(1);
|
|
243
600
|
}
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
profile.mcpServers[name] = {
|
|
261
|
-
type: "stdio",
|
|
262
|
-
command: "npx",
|
|
263
|
-
args: ["-y", pkg.identifier]
|
|
264
|
-
};
|
|
265
|
-
} else if (pkg.registryType === "pypi") {
|
|
266
|
-
profile.mcpServers[name] = {
|
|
267
|
-
type: "stdio",
|
|
268
|
-
command: "uvx",
|
|
269
|
-
args: [pkg.identifier]
|
|
270
|
-
};
|
|
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());
|
|
603
|
+
if (!match) {
|
|
604
|
+
console.log(`\x1B[31mProfile not found: ${target}\x1B[0m`);
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
const sourcePath = path3.join(PROFILES_DIR, match.value);
|
|
608
|
+
const profile = JSON.parse(fs2.readFileSync(sourcePath, "utf8"));
|
|
609
|
+
profile.name = newName;
|
|
610
|
+
const newFilename = sanitizeProfileName(newName) + ".json";
|
|
611
|
+
const destPath = path3.join(PROFILES_DIR, newFilename);
|
|
612
|
+
if (fs2.existsSync(destPath)) {
|
|
613
|
+
const shouldOverwrite = await confirm(`Profile "${newName}" already exists. Overwrite?`);
|
|
614
|
+
if (!shouldOverwrite) {
|
|
615
|
+
console.log("\x1B[33mCancelled\x1B[0m");
|
|
616
|
+
process.exit(0);
|
|
271
617
|
}
|
|
272
618
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
619
|
+
fs2.writeFileSync(destPath, JSON.stringify(profile, null, 2));
|
|
620
|
+
console.log(`\x1B[32m\u2713\x1B[0m Copied "${match.label}" to "${newName}"`);
|
|
621
|
+
process.exit(0);
|
|
622
|
+
}
|
|
623
|
+
var McpSearch = () => {
|
|
277
624
|
const { exit } = useApp();
|
|
278
625
|
const [step, setStep] = useState(args[1] ? "loading" : "search");
|
|
279
626
|
const [query, setQuery] = useState(args[1] || "");
|
|
280
|
-
const [
|
|
627
|
+
const [searchResults, setSearchResults] = useState({ servers: [], total: 0, hasMore: false, offset: 0 });
|
|
281
628
|
const [selectedServer, setSelectedServer] = useState(null);
|
|
282
629
|
const profiles = loadProfiles();
|
|
283
630
|
useEffect(() => {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
631
|
+
const loadInitialResults = async () => {
|
|
632
|
+
if (args[1] && step === "loading") {
|
|
633
|
+
const results = await searchMcpServers(args[1]);
|
|
634
|
+
setSearchResults(results);
|
|
635
|
+
setStep("results");
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
loadInitialResults();
|
|
289
639
|
}, []);
|
|
290
|
-
const doSearch = () => {
|
|
291
|
-
|
|
292
|
-
|
|
640
|
+
const doSearch = async () => {
|
|
641
|
+
setStep("loading");
|
|
642
|
+
const results = await searchMcpServers(query, 0);
|
|
643
|
+
setSearchResults(results);
|
|
293
644
|
setStep("results");
|
|
294
645
|
};
|
|
295
|
-
const
|
|
646
|
+
const nextPage = async () => {
|
|
647
|
+
const results = await searchMcpServers(query, searchResults.offset + MCP_PAGE_SIZE);
|
|
648
|
+
setSearchResults(results);
|
|
649
|
+
};
|
|
650
|
+
const prevPage = async () => {
|
|
651
|
+
const results = await searchMcpServers(query, Math.max(0, searchResults.offset - MCP_PAGE_SIZE));
|
|
652
|
+
setSearchResults(results);
|
|
653
|
+
};
|
|
654
|
+
const serverItems = searchResults.servers.map((s) => ({
|
|
296
655
|
label: `${s.server.name} - ${s.server.description?.slice(0, 50) || ""}`,
|
|
297
656
|
value: s,
|
|
298
657
|
key: s.server.name + s.server.version
|
|
299
658
|
}));
|
|
300
659
|
const profileItems = profiles.map((p) => ({ label: p.label, value: p.value, key: p.key }));
|
|
660
|
+
useInput((input, key) => {
|
|
661
|
+
if (step === "results") {
|
|
662
|
+
if (key.return && !selectedServer) return;
|
|
663
|
+
if ((input === "n" || key.rightArrow) && searchResults.hasMore) {
|
|
664
|
+
nextPage();
|
|
665
|
+
}
|
|
666
|
+
if ((input === "p" || key.leftArrow) && searchResults.offset > 0) {
|
|
667
|
+
prevPage();
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
});
|
|
301
671
|
if (step === "search") {
|
|
302
672
|
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, "MCP Server Search"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Search: "), /* @__PURE__ */ React.createElement(TextInput, { value: query, onChange: setQuery, onSubmit: doSearch })));
|
|
303
673
|
}
|
|
@@ -305,10 +675,12 @@ const McpSearch = () => {
|
|
|
305
675
|
return /* @__PURE__ */ React.createElement(Box, { padding: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Searching MCP registry..."));
|
|
306
676
|
}
|
|
307
677
|
if (step === "results") {
|
|
308
|
-
if (servers.length === 0) {
|
|
678
|
+
if (searchResults.servers.length === 0) {
|
|
309
679
|
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, 'No servers found for "', query, '"'));
|
|
310
680
|
}
|
|
311
|
-
|
|
681
|
+
const start = searchResults.offset + 1;
|
|
682
|
+
const end = Math.min(searchResults.offset + MCP_PAGE_SIZE, searchResults.total);
|
|
683
|
+
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, "MCP Servers"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Showing ", start, "-", end, " of ", searchResults.total, " results"), /* @__PURE__ */ React.createElement(Text, { dimColor: true, color: "gray" }, "Navigation: n/\u2192 next page, p/\u2190 prev page"), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React.createElement(
|
|
312
684
|
SelectInput,
|
|
313
685
|
{
|
|
314
686
|
items: serverItems,
|
|
@@ -326,9 +698,14 @@ const McpSearch = () => {
|
|
|
326
698
|
{
|
|
327
699
|
items: profileItems,
|
|
328
700
|
onSelect: (item) => {
|
|
329
|
-
|
|
330
|
-
|
|
701
|
+
try {
|
|
702
|
+
const name = addMcpToProfile(selectedServer, item.value);
|
|
703
|
+
console.log(`
|
|
331
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
|
+
}
|
|
332
709
|
exit();
|
|
333
710
|
}
|
|
334
711
|
}
|
|
@@ -336,75 +713,43 @@ const McpSearch = () => {
|
|
|
336
713
|
}
|
|
337
714
|
return null;
|
|
338
715
|
};
|
|
339
|
-
|
|
340
|
-
{ url: "https://api.github.com/repos/anthropics/skills/contents/skills", base: "https://github.com/anthropics/skills/tree/main/skills" },
|
|
341
|
-
{ url: "https://api.github.com/repos/Prat011/awesome-llm-skills/contents/skills", base: "https://github.com/Prat011/awesome-llm-skills/tree/main/skills" },
|
|
342
|
-
{ url: "https://api.github.com/repos/skillcreatorai/Ai-Agent-Skills/contents/skills", base: "https://github.com/skillcreatorai/Ai-Agent-Skills/tree/main/skills" }
|
|
343
|
-
];
|
|
344
|
-
const fetchSkills = () => {
|
|
345
|
-
const seen = /* @__PURE__ */ new Set();
|
|
346
|
-
const skills = [];
|
|
347
|
-
for (const source of SKILL_SOURCES) {
|
|
348
|
-
try {
|
|
349
|
-
const res = execSync(`curl -s "${source.url}"`, { encoding: "utf8", timeout: 1e4 });
|
|
350
|
-
const data = JSON.parse(res);
|
|
351
|
-
if (Array.isArray(data)) {
|
|
352
|
-
for (const s of data.filter((s2) => s2.type === "dir")) {
|
|
353
|
-
if (!seen.has(s.name)) {
|
|
354
|
-
seen.add(s.name);
|
|
355
|
-
skills.push({
|
|
356
|
-
label: s.name,
|
|
357
|
-
value: `${source.base}/${s.name}`,
|
|
358
|
-
key: s.name
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
} catch {
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
return skills.sort((a, b) => a.label.localeCompare(b.label));
|
|
367
|
-
};
|
|
368
|
-
const SKILLS_DIR = path.join(os.homedir(), ".claude", "skills");
|
|
369
|
-
const addSkillToClaudeJson = (skillName, skillUrl) => {
|
|
370
|
-
try {
|
|
371
|
-
if (!fs.existsSync(SKILLS_DIR)) fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
372
|
-
const skillPath = path.join(SKILLS_DIR, skillName);
|
|
373
|
-
if (fs.existsSync(skillPath)) {
|
|
374
|
-
return { success: false, message: "Skill already installed" };
|
|
375
|
-
}
|
|
376
|
-
const match = skillUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\/tree\/([^\/]+)\/(.+)/);
|
|
377
|
-
if (!match) return { success: false, message: "Invalid skill URL" };
|
|
378
|
-
const [, owner, repo, branch, skillSubPath] = match;
|
|
379
|
-
const tempDir = `/tmp/skill-clone-${Date.now()}`;
|
|
380
|
-
execSync(`git clone --depth 1 --filter=blob:none --sparse "https://github.com/${owner}/${repo}.git" "${tempDir}" 2>/dev/null`, { timeout: 3e4 });
|
|
381
|
-
execSync(`cd "${tempDir}" && git sparse-checkout set "${skillSubPath}" 2>/dev/null`, { timeout: 1e4 });
|
|
382
|
-
execSync(`mv "${tempDir}/${skillSubPath}" "${skillPath}"`, { timeout: 5e3 });
|
|
383
|
-
execSync(`rm -rf "${tempDir}"`, { timeout: 5e3 });
|
|
384
|
-
return { success: true };
|
|
385
|
-
} catch (e) {
|
|
386
|
-
return { success: false, message: "Failed to download skill" };
|
|
387
|
-
}
|
|
388
|
-
};
|
|
389
|
-
const SkillsBrowser = () => {
|
|
716
|
+
var SkillsBrowser = () => {
|
|
390
717
|
const { exit } = useApp();
|
|
391
|
-
const [
|
|
718
|
+
const [allSkills, setAllSkills] = useState([]);
|
|
392
719
|
const [loading, setLoading] = useState(true);
|
|
720
|
+
const [offset, setOffset] = useState(0);
|
|
393
721
|
useEffect(() => {
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
722
|
+
const loadSkills = async () => {
|
|
723
|
+
const s = await fetchSkills();
|
|
724
|
+
setAllSkills(s);
|
|
725
|
+
setLoading(false);
|
|
726
|
+
};
|
|
727
|
+
loadSkills();
|
|
397
728
|
}, []);
|
|
729
|
+
const paginatedSkills = allSkills.slice(offset, offset + SKILLS_PAGE_SIZE);
|
|
730
|
+
const hasMore = offset + SKILLS_PAGE_SIZE < allSkills.length;
|
|
731
|
+
useInput((input, key) => {
|
|
732
|
+
if (!loading) {
|
|
733
|
+
if ((input === "n" || key.rightArrow) && hasMore) {
|
|
734
|
+
setOffset(offset + SKILLS_PAGE_SIZE);
|
|
735
|
+
}
|
|
736
|
+
if ((input === "p" || key.leftArrow) && offset > 0) {
|
|
737
|
+
setOffset(Math.max(0, offset - SKILLS_PAGE_SIZE));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
});
|
|
398
741
|
if (loading) {
|
|
399
742
|
return /* @__PURE__ */ React.createElement(Box, { padding: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Loading skills..."));
|
|
400
743
|
}
|
|
401
|
-
if (
|
|
744
|
+
if (allSkills.length === 0) {
|
|
402
745
|
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "Could not fetch skills"));
|
|
403
746
|
}
|
|
404
|
-
|
|
747
|
+
const start = offset + 1;
|
|
748
|
+
const end = Math.min(offset + SKILLS_PAGE_SIZE, allSkills.length);
|
|
749
|
+
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, "Anthropic Skills"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Showing ", start, "-", end, " of ", allSkills.length, " skills"), /* @__PURE__ */ React.createElement(Text, { dimColor: true, color: "gray" }, "Navigation: n/\u2192 next page, p/\u2190 prev page"), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React.createElement(
|
|
405
750
|
SelectInput,
|
|
406
751
|
{
|
|
407
|
-
items:
|
|
752
|
+
items: paginatedSkills,
|
|
408
753
|
onSelect: (item) => {
|
|
409
754
|
const result = addSkillToClaudeJson(item.label, item.value);
|
|
410
755
|
if (result.success) {
|
|
@@ -421,8 +766,95 @@ const SkillsBrowser = () => {
|
|
|
421
766
|
)));
|
|
422
767
|
};
|
|
423
768
|
if (cmd === "skills") {
|
|
769
|
+
const subCommand = args[1];
|
|
770
|
+
if (subCommand === "list") {
|
|
771
|
+
const installed = getInstalledSkills();
|
|
772
|
+
console.log(`\x1B[1m\x1B[36mInstalled Skills\x1B[0m (${installed.length})`);
|
|
773
|
+
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
774
|
+
if (installed.length === 0) {
|
|
775
|
+
console.log("No skills installed");
|
|
776
|
+
} else {
|
|
777
|
+
installed.forEach((s, i) => console.log(`${i + 1}. ${s}`));
|
|
778
|
+
}
|
|
779
|
+
process.exit(0);
|
|
780
|
+
}
|
|
781
|
+
if (subCommand === "remove") {
|
|
782
|
+
const target = args[2];
|
|
783
|
+
if (!target) {
|
|
784
|
+
console.log("\x1B[31mUsage: cm skills remove <skill-name>\x1B[0m");
|
|
785
|
+
process.exit(1);
|
|
786
|
+
}
|
|
787
|
+
const installed = getInstalledSkills();
|
|
788
|
+
const idx = safeParseInt(target, -1);
|
|
789
|
+
const match = idx > 0 && idx <= installed.length ? installed[idx - 1] : installed.find((s) => s.toLowerCase() === target?.toLowerCase());
|
|
790
|
+
if (!match) {
|
|
791
|
+
console.log(`\x1B[31mSkill not found: ${target}\x1B[0m`);
|
|
792
|
+
console.log('Run "cm skills list" to see installed skills');
|
|
793
|
+
process.exit(1);
|
|
794
|
+
}
|
|
795
|
+
const shouldRemove = await confirm(`Remove skill "${match}"?`);
|
|
796
|
+
if (shouldRemove) {
|
|
797
|
+
const result = removeSkill(match);
|
|
798
|
+
if (result.success) {
|
|
799
|
+
console.log(`\x1B[32m\u2713\x1B[0m Removed skill: ${match}`);
|
|
800
|
+
} else {
|
|
801
|
+
console.log(`\x1B[31m\u2717\x1B[0m ${result.message}`);
|
|
802
|
+
}
|
|
803
|
+
} else {
|
|
804
|
+
console.log("\x1B[33mCancelled\x1B[0m");
|
|
805
|
+
}
|
|
806
|
+
process.exit(0);
|
|
807
|
+
}
|
|
424
808
|
render(/* @__PURE__ */ React.createElement(SkillsBrowser, null));
|
|
425
809
|
} else if (cmd === "mcp") {
|
|
810
|
+
const subCommand = args[1];
|
|
811
|
+
if (subCommand === "remove") {
|
|
812
|
+
const profiles = loadProfiles();
|
|
813
|
+
if (profiles.length === 0) {
|
|
814
|
+
console.log("\x1B[31mNo profiles found\x1B[0m");
|
|
815
|
+
process.exit(1);
|
|
816
|
+
}
|
|
817
|
+
const serverName = args[2];
|
|
818
|
+
const targetProfile = args[3];
|
|
819
|
+
if (!targetProfile) {
|
|
820
|
+
console.log("\x1B[31mUsage: cm mcp remove <server-name> <profile>\x1B[0m");
|
|
821
|
+
console.log(" server-name: MCP server name to remove");
|
|
822
|
+
console.log(" profile: Profile name or number");
|
|
823
|
+
process.exit(1);
|
|
824
|
+
}
|
|
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());
|
|
827
|
+
if (!profileMatch) {
|
|
828
|
+
console.log(`\x1B[31mProfile not found: ${targetProfile}\x1B[0m`);
|
|
829
|
+
process.exit(1);
|
|
830
|
+
}
|
|
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"));
|
|
837
|
+
const mcpServers = profile.mcpServers || {};
|
|
838
|
+
if (Object.keys(mcpServers).length === 0) {
|
|
839
|
+
console.log(`\x1B[33mNo MCP servers configured in "${profileMatch.label}"\x1B[0m`);
|
|
840
|
+
process.exit(0);
|
|
841
|
+
}
|
|
842
|
+
if (!mcpServers[serverName]) {
|
|
843
|
+
console.log(`\x1B[31mMCP server not found: ${serverName}\x1B[0m`);
|
|
844
|
+
console.log(`Available servers: ${Object.keys(mcpServers).join(", ")}`);
|
|
845
|
+
process.exit(1);
|
|
846
|
+
}
|
|
847
|
+
const shouldRemove = await confirm(`Remove "${serverName}" from "${profileMatch.label}"?`);
|
|
848
|
+
if (shouldRemove) {
|
|
849
|
+
delete mcpServers[serverName];
|
|
850
|
+
profile.mcpServers = mcpServers;
|
|
851
|
+
fs2.writeFileSync(profilePath, JSON.stringify(profile, null, 2));
|
|
852
|
+
console.log(`\x1B[32m\u2713\x1B[0m Removed "${serverName}" from "${profileMatch.label}"`);
|
|
853
|
+
} else {
|
|
854
|
+
console.log("\x1B[33mCancelled\x1B[0m");
|
|
855
|
+
}
|
|
856
|
+
process.exit(0);
|
|
857
|
+
}
|
|
426
858
|
render(/* @__PURE__ */ React.createElement(McpSearch, null));
|
|
427
859
|
} else if (cmd === "new") {
|
|
428
860
|
const NewProfileWizard = () => {
|
|
@@ -433,40 +865,33 @@ if (cmd === "skills") {
|
|
|
433
865
|
const [apiKey, setApiKey] = useState("");
|
|
434
866
|
const [model, setModel] = useState("");
|
|
435
867
|
const [group, setGroup] = useState("");
|
|
436
|
-
const
|
|
437
|
-
{ label: "Anthropic (Direct)", value: "anthropic", url: "", needsKey: true },
|
|
438
|
-
{ label: "Amazon Bedrock", value: "bedrock", url: "", needsKey: false },
|
|
439
|
-
{ label: "Z.AI", value: "zai", url: "https://api.z.ai/api/anthropic", needsKey: true },
|
|
440
|
-
{ label: "MiniMax", value: "minimax", url: "https://api.minimax.io/anthropic", needsKey: true },
|
|
441
|
-
{ label: "Custom", value: "custom", url: "", needsKey: true }
|
|
442
|
-
];
|
|
868
|
+
const [validationErrors, setValidationErrors] = useState([]);
|
|
443
869
|
const handleSave = () => {
|
|
444
|
-
const
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
},
|
|
454
|
-
model: "opus",
|
|
455
|
-
alwaysThinkingEnabled: true,
|
|
456
|
-
defaultMode: "bypassPermissions"
|
|
457
|
-
};
|
|
458
|
-
const filename = name.toLowerCase().replace(/\s+/g, "-") + ".json";
|
|
459
|
-
fs.writeFileSync(path.join(PROFILES_DIR, filename), JSON.stringify(profile, null, 2));
|
|
870
|
+
const profile = buildProfileData(name, provider, apiKey, model, group, PROVIDERS);
|
|
871
|
+
const validation = validateProfile(profile);
|
|
872
|
+
if (!validation.valid) {
|
|
873
|
+
setStep("error");
|
|
874
|
+
setValidationErrors(validation.errors);
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
const filename = sanitizeProfileName(name) + ".json";
|
|
878
|
+
fs2.writeFileSync(path3.join(PROFILES_DIR, filename), JSON.stringify(profile, null, 2));
|
|
460
879
|
console.log(`
|
|
461
880
|
\x1B[32m\u2713\x1B[0m Created: ${name}`);
|
|
462
881
|
exit();
|
|
463
882
|
};
|
|
464
883
|
const handleProviderSelect = (item) => {
|
|
465
884
|
setProvider(item.value);
|
|
466
|
-
const prov =
|
|
885
|
+
const prov = PROVIDERS.find((p) => p.value === item.value);
|
|
467
886
|
setStep(prov.needsKey ? "apikey" : "model");
|
|
468
887
|
};
|
|
469
|
-
|
|
888
|
+
useInput((input, key) => {
|
|
889
|
+
if (step === "error") {
|
|
890
|
+
setStep("group");
|
|
891
|
+
setValidationErrors([]);
|
|
892
|
+
}
|
|
893
|
+
});
|
|
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...")));
|
|
470
895
|
};
|
|
471
896
|
render(/* @__PURE__ */ React.createElement(NewProfileWizard, null));
|
|
472
897
|
} else {
|
|
@@ -486,48 +911,111 @@ if (cmd === "skills") {
|
|
|
486
911
|
clearInterval(colorInterval);
|
|
487
912
|
};
|
|
488
913
|
}, []);
|
|
489
|
-
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: colors[colorIdx] },
|
|
490
|
-
\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D
|
|
491
|
-
\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557
|
|
492
|
-
\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D
|
|
493
|
-
\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
494
|
-
\u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D`), /* @__PURE__ */ React.createElement(Text, { bold: true, color: colors[(colorIdx + 3) % colors.length] }, "MANAGER v", VERSION), /* @__PURE__ */ React.createElement(Text, { color: "yellow", marginTop: 1 }, message, dots));
|
|
914
|
+
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: colors[colorIdx] }, LOGO), /* @__PURE__ */ React.createElement(Text, { bold: true, color: colors[(colorIdx + 3) % colors.length] }, "MANAGER v", VERSION), /* @__PURE__ */ React.createElement(Text, { color: "yellow", marginTop: 1 }, message, dots));
|
|
495
915
|
};
|
|
496
916
|
const App = () => {
|
|
497
|
-
const [step, setStep] = useState("
|
|
917
|
+
const [step, setStep] = useState("loading");
|
|
498
918
|
const [updateInfo, setUpdateInfo] = useState(null);
|
|
499
919
|
const [filter, setFilter] = useState("");
|
|
920
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
921
|
+
const [showCommandPalette, setShowCommandPalette] = useState(false);
|
|
922
|
+
const [commandInput, setCommandInput] = useState("");
|
|
500
923
|
const profiles = loadProfiles();
|
|
924
|
+
const commands = [
|
|
925
|
+
{ label: "/skills", description: "Browse and install skills", action: () => render(/* @__PURE__ */ React.createElement(SkillsBrowser, null)) },
|
|
926
|
+
{ label: "/mcp", description: "Search and add MCP servers", action: () => render(/* @__PURE__ */ React.createElement(McpSearch, null)) },
|
|
927
|
+
{ label: "/new", description: "Create new profile", action: () => setStep("newProfile") },
|
|
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" }) },
|
|
931
|
+
{ label: "/help", description: "Show keyboard shortcuts", action: () => setShowHelp(true) },
|
|
932
|
+
{ label: "/quit", description: "Exit cm", action: () => process.exit(0) }
|
|
933
|
+
];
|
|
934
|
+
const filteredProfiles = useMemo(() => {
|
|
935
|
+
if (!filter) return profiles;
|
|
936
|
+
const fuse = new Fuse(profiles, {
|
|
937
|
+
keys: ["label", "group"],
|
|
938
|
+
threshold: FUSE_THRESHOLD,
|
|
939
|
+
ignoreLocation: true,
|
|
940
|
+
includeScore: true
|
|
941
|
+
});
|
|
942
|
+
return fuse.search(filter).map((r) => r.item);
|
|
943
|
+
}, [profiles, filter]);
|
|
944
|
+
const filteredCommands = useMemo(() => {
|
|
945
|
+
if (!commandInput) return commands;
|
|
946
|
+
const search = commandInput.toLowerCase().replace(/^\//, "");
|
|
947
|
+
const fuse = new Fuse(commands, {
|
|
948
|
+
keys: ["label", "description"],
|
|
949
|
+
threshold: FUSE_THRESHOLD,
|
|
950
|
+
ignoreLocation: true
|
|
951
|
+
});
|
|
952
|
+
return fuse.search(search).map((r) => r.item);
|
|
953
|
+
}, [commands, commandInput]);
|
|
501
954
|
useEffect(() => {
|
|
502
955
|
setTimeout(() => setStep("select"), 1500);
|
|
503
956
|
if (!skipUpdate) {
|
|
504
|
-
|
|
505
|
-
const info = checkForUpdate();
|
|
506
|
-
setUpdateInfo(info);
|
|
507
|
-
});
|
|
957
|
+
checkForUpdate(skipUpdate).then(setUpdateInfo);
|
|
508
958
|
}
|
|
509
959
|
}, []);
|
|
510
960
|
useInput((input, key) => {
|
|
961
|
+
if (showCommandPalette) {
|
|
962
|
+
if (key.escape) {
|
|
963
|
+
setShowCommandPalette(false);
|
|
964
|
+
setCommandInput("");
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (key.return) {
|
|
968
|
+
const matchedCommand = commandInput.startsWith("/") ? commands.find((c) => c.label === commandInput) : filteredCommands[0];
|
|
969
|
+
if (matchedCommand) {
|
|
970
|
+
setShowCommandPalette(false);
|
|
971
|
+
setCommandInput("");
|
|
972
|
+
matchedCommand.action();
|
|
973
|
+
}
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
if (key.backspace || key.delete) {
|
|
977
|
+
setCommandInput((c) => c.slice(0, -1));
|
|
978
|
+
if (commandInput.length <= 1) {
|
|
979
|
+
setShowCommandPalette(false);
|
|
980
|
+
}
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (input && !key.ctrl && !key.meta) {
|
|
984
|
+
setCommandInput((c) => c + input);
|
|
985
|
+
}
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
511
988
|
if (step === "select") {
|
|
512
|
-
const num =
|
|
989
|
+
const num = safeParseInt(input, -1);
|
|
513
990
|
if (num >= 1 && num <= 9 && num <= filteredProfiles.length) {
|
|
514
991
|
const profile = filteredProfiles[num - 1];
|
|
515
992
|
applyProfile(profile.value);
|
|
516
993
|
console.log(`
|
|
517
994
|
\x1B[32m\u2713\x1B[0m Applied: ${profile.label}
|
|
518
995
|
`);
|
|
519
|
-
launchClaude();
|
|
996
|
+
launchClaude(dangerMode);
|
|
520
997
|
}
|
|
521
998
|
if (input === "u" && updateInfo?.needsUpdate) {
|
|
522
999
|
console.log("\n\x1B[33mUpdating Claude...\x1B[0m\n");
|
|
523
1000
|
try {
|
|
524
|
-
|
|
525
|
-
|
|
1001
|
+
if (process.platform === "darwin") {
|
|
1002
|
+
execSync2("brew upgrade claude-code", { stdio: "inherit" });
|
|
1003
|
+
} else {
|
|
1004
|
+
execSync2("npm update -g @anthropic-ai/claude-code", { stdio: "inherit" });
|
|
1005
|
+
}
|
|
1006
|
+
console.log("\x1B[32m\u2713 Updated!\x1B[0m\n");
|
|
526
1007
|
setUpdateInfo({ ...updateInfo, needsUpdate: false });
|
|
527
|
-
} catch {
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
console.log("\x1B[31m\u2717 Update failed\x1B[0m\n");
|
|
1010
|
+
logError("update", error);
|
|
528
1011
|
}
|
|
529
1012
|
}
|
|
530
|
-
if (input
|
|
1013
|
+
if (input === "/" && !showHelp) {
|
|
1014
|
+
setShowCommandPalette(true);
|
|
1015
|
+
setCommandInput("/");
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
if (input.match(/^[a-zA-Z]$/) && input !== "u" && input !== "c" && input !== "?" && input !== "/") {
|
|
531
1019
|
setFilter((f) => f + input);
|
|
532
1020
|
}
|
|
533
1021
|
if (key.backspace || key.delete) {
|
|
@@ -536,11 +1024,21 @@ if (cmd === "skills") {
|
|
|
536
1024
|
if (key.escape) {
|
|
537
1025
|
setFilter("");
|
|
538
1026
|
}
|
|
1027
|
+
if (input === "?") {
|
|
1028
|
+
setShowHelp(true);
|
|
1029
|
+
}
|
|
1030
|
+
if (input === "c") {
|
|
1031
|
+
const editor = process.env.EDITOR || "nano";
|
|
1032
|
+
createDefaultSettings();
|
|
1033
|
+
console.clear();
|
|
1034
|
+
spawnSync(editor, [SETTINGS_PATH], { stdio: "inherit" });
|
|
1035
|
+
console.log("\n\x1B[36mConfig edited. Press Enter to continue...\x1B[0m");
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
if (showHelp && (input === "q" || input === "?" || key.escape || key.return)) {
|
|
1039
|
+
setShowHelp(false);
|
|
539
1040
|
}
|
|
540
1041
|
});
|
|
541
|
-
const filteredProfiles = profiles.filter(
|
|
542
|
-
(p) => !filter || p.label.toLowerCase().includes(filter.toLowerCase())
|
|
543
|
-
);
|
|
544
1042
|
const groupedItems = [];
|
|
545
1043
|
const groups = [...new Set(filteredProfiles.map((p) => p.group).filter(Boolean))];
|
|
546
1044
|
if (groups.length > 0) {
|
|
@@ -570,21 +1068,16 @@ if (cmd === "skills") {
|
|
|
570
1068
|
console.log(`
|
|
571
1069
|
\x1B[32m\u2713\x1B[0m Applied: ${item.label.replace(/^\d+\.\s*/, "")}
|
|
572
1070
|
`);
|
|
573
|
-
launchClaude();
|
|
1071
|
+
launchClaude(dangerMode);
|
|
574
1072
|
};
|
|
575
|
-
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" },
|
|
576
|
-
\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D
|
|
577
|
-
\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557
|
|
578
|
-
\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D
|
|
579
|
-
\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
580
|
-
\u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D`), /* @__PURE__ */ React.createElement(Text, { bold: true, color: "magenta" }, "MANAGER v", VERSION), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"), updateInfo?.current && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Claude v", updateInfo.current), updateInfo?.needsUpdate && /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "\u26A0 Update available! Press 'u' to upgrade"), filter && /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "Filter: ", filter), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Select Profile: ", /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "(1-9 quick select, type to filter", updateInfo?.needsUpdate ? ", u to update" : "", ")")), /* @__PURE__ */ React.createElement(
|
|
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(
|
|
581
1074
|
SelectInput,
|
|
582
1075
|
{
|
|
583
1076
|
items: groupedItems,
|
|
584
1077
|
onSelect: handleSelect,
|
|
585
1078
|
itemComponent: ({ isSelected, label, disabled }) => /* @__PURE__ */ React.createElement(Text, { color: disabled ? "gray" : isSelected ? "cyan" : "white", dimColor: disabled }, disabled ? label : (isSelected ? "\u276F " : " ") + label)
|
|
586
1079
|
}
|
|
587
|
-
)));
|
|
1080
|
+
)), showCommandPalette && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1, marginTop: 1, borderStyle: "double", borderColor: "magenta" }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "magenta" }, "Command Palette"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "cyan" }, ">"), /* @__PURE__ */ React.createElement(Text, { color: "white" }, commandInput)), /* @__PURE__ */ React.createElement(Text, { dimColor: true, marginTop: 1 }, "Available commands:"), filteredCommands.map((cmd2, i) => /* @__PURE__ */ React.createElement(Text, { key: cmd2.label }, /* @__PURE__ */ React.createElement(Text, { color: "cyan" }, cmd2.label), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " - "), /* @__PURE__ */ React.createElement(Text, { color: "gray" }, cmd2.description))), /* @__PURE__ */ React.createElement(Text, { dimColor: true, marginTop: 1 }, "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Enter to execute \u2022 Esc to close")), showHelp && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1, marginTop: 1, borderStyle: "single", borderColor: "cyan" }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, "Keyboard Shortcuts"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"), /* @__PURE__ */ React.createElement(Text, { bold: true, color: "magenta" }, "Navigation"), /* @__PURE__ */ React.createElement(Text, null, " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "1-9"), " Quick select profile"), /* @__PURE__ */ React.createElement(Text, null, " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "\u2191/\u2193"), " Navigate list"), /* @__PURE__ */ React.createElement(Text, null, " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "Enter"), " Select profile"), /* @__PURE__ */ React.createElement(Text, { bold: true, color: "magenta", marginTop: 1 }, "Search"), /* @__PURE__ */ React.createElement(Text, null, " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "a-z"), " Fuzzy filter profiles"), /* @__PURE__ */ React.createElement(Text, null, " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "Backspace"), " Delete filter character"), /* @__PURE__ */ React.createElement(Text, null, " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "Escape"), " Clear filter"), updateInfo?.needsUpdate && /* @__PURE__ */ React.createElement(Text, { bold: true, color: "magenta", marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "u"), " Update Claude"), /* @__PURE__ */ React.createElement(Text, { bold: true, color: "magenta", marginTop: 1 }, "Help"), /* @__PURE__ */ React.createElement(Text, null, " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "?"), " Toggle this help"), /* @__PURE__ */ React.createElement(Text, null, " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "q"), " Close help"), /* @__PURE__ */ React.createElement(Text, { bold: true, color: "magenta", marginTop: 1 }, "CLI Commands"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " cm new Create new profile"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " cm config Edit Claude settings"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " cm status Show current settings"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " cm --help Show all commands")));
|
|
588
1081
|
};
|
|
589
1082
|
render(/* @__PURE__ */ React.createElement(App, null));
|
|
590
1083
|
}
|