claude-manager 1.5.0 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +985 -0
- package/package.json +12 -4
- package/install.sh +0 -104
- package/src/cli.js +0 -770
- package/src/config.js +0 -29
package/dist/cli.js
ADDED
|
@@ -0,0 +1,985 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import React, { useState, useEffect, useMemo } from "react";
|
|
3
|
+
import { render, Box, Text, useInput, useApp } from "ink";
|
|
4
|
+
import SelectInput from "ink-select-input";
|
|
5
|
+
import TextInput from "ink-text-input";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import os from "os";
|
|
9
|
+
import { execSync, spawnSync } from "child_process";
|
|
10
|
+
import { createInterface } from "readline";
|
|
11
|
+
import Fuse from "fuse.js";
|
|
12
|
+
const VERSION = "1.5.3";
|
|
13
|
+
const LOGO = `\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
14
|
+
\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
|
+
\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
|
+
\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
|
+
\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
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D`;
|
|
19
|
+
const MCP_REGISTRY_URL = "https://registry.modelcontextprotocol.io/v0/servers";
|
|
20
|
+
const PROFILES_DIR = path.join(os.homedir(), ".claude", "profiles");
|
|
21
|
+
const SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
|
|
22
|
+
const CLAUDE_JSON_PATH = path.join(os.homedir(), ".claude.json");
|
|
23
|
+
const LAST_PROFILE_PATH = path.join(os.homedir(), ".claude", ".last-profile");
|
|
24
|
+
const args = process.argv.slice(2);
|
|
25
|
+
const cmd = args[0];
|
|
26
|
+
if (!fs.existsSync(PROFILES_DIR)) fs.mkdirSync(PROFILES_DIR, { recursive: true });
|
|
27
|
+
if (args.includes("-v") || args.includes("--version")) {
|
|
28
|
+
console.log(`cm v${VERSION}`);
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
32
|
+
console.log(`cm v${VERSION} - Claude Settings Manager
|
|
33
|
+
|
|
34
|
+
Usage: cm [command] [options]
|
|
35
|
+
|
|
36
|
+
Commands:
|
|
37
|
+
(none) Select profile interactively
|
|
38
|
+
new Create a new profile
|
|
39
|
+
edit <n> Edit profile (by name or number)
|
|
40
|
+
copy <n> <new> Copy/duplicate a profile
|
|
41
|
+
delete <n> Delete profile (by name or number)
|
|
42
|
+
status Show current settings
|
|
43
|
+
list List all profiles
|
|
44
|
+
config Open Claude settings.json in editor
|
|
45
|
+
mcp [query] Search and add MCP servers
|
|
46
|
+
mcp remove Remove MCP server from profile
|
|
47
|
+
skills Browse and add Anthropic skills
|
|
48
|
+
skills list List installed skills
|
|
49
|
+
skills remove Remove an installed skill
|
|
50
|
+
|
|
51
|
+
Options:
|
|
52
|
+
--last, -l Use last profile without menu
|
|
53
|
+
--skip-update Skip update check
|
|
54
|
+
--yolo Run claude with --dangerously-skip-permissions
|
|
55
|
+
--force, -f Skip confirmation prompts (e.g., for delete)
|
|
56
|
+
-v, --version Show version
|
|
57
|
+
-h, --help Show help`);
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
const skipUpdate = args.includes("--skip-update");
|
|
61
|
+
const useLast = args.includes("--last") || args.includes("-l");
|
|
62
|
+
const dangerMode = args.includes("--dangerously-skip-permissions") || args.includes("--yolo");
|
|
63
|
+
const loadProfiles = () => {
|
|
64
|
+
const profiles = [];
|
|
65
|
+
if (fs.existsSync(PROFILES_DIR)) {
|
|
66
|
+
for (const file of fs.readdirSync(PROFILES_DIR).sort()) {
|
|
67
|
+
if (file.endsWith(".json")) {
|
|
68
|
+
try {
|
|
69
|
+
const content = JSON.parse(fs.readFileSync(path.join(PROFILES_DIR, file), "utf8"));
|
|
70
|
+
profiles.push({
|
|
71
|
+
label: content.name || file.replace(".json", ""),
|
|
72
|
+
value: file,
|
|
73
|
+
key: file,
|
|
74
|
+
group: content.group || null,
|
|
75
|
+
data: content
|
|
76
|
+
});
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return profiles;
|
|
83
|
+
};
|
|
84
|
+
const applyProfile = (filename) => {
|
|
85
|
+
const profilePath = path.join(PROFILES_DIR, filename);
|
|
86
|
+
const profile = JSON.parse(fs.readFileSync(profilePath, "utf8"));
|
|
87
|
+
const { name, group, mcpServers, ...settings } = profile;
|
|
88
|
+
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
89
|
+
if (mcpServers !== void 0) {
|
|
90
|
+
try {
|
|
91
|
+
const claudeJson = fs.existsSync(CLAUDE_JSON_PATH) ? JSON.parse(fs.readFileSync(CLAUDE_JSON_PATH, "utf8")) : {};
|
|
92
|
+
claudeJson.mcpServers = mcpServers;
|
|
93
|
+
fs.writeFileSync(CLAUDE_JSON_PATH, JSON.stringify(claudeJson, null, 2));
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
fs.writeFileSync(LAST_PROFILE_PATH, filename);
|
|
98
|
+
return name || filename;
|
|
99
|
+
};
|
|
100
|
+
const getLastProfile = () => {
|
|
101
|
+
try {
|
|
102
|
+
return fs.readFileSync(LAST_PROFILE_PATH, "utf8").trim();
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const checkProjectProfile = () => {
|
|
108
|
+
const localProfile = path.join(process.cwd(), ".claude-profile");
|
|
109
|
+
if (fs.existsSync(localProfile)) {
|
|
110
|
+
return fs.readFileSync(localProfile, "utf8").trim();
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
};
|
|
114
|
+
const logError = (context, error) => {
|
|
115
|
+
if (process.env.DEBUG || process.env.CM_DEBUG) {
|
|
116
|
+
console.error(`[${context}]`, error?.message || error);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const confirm = async (message) => {
|
|
120
|
+
const rl = createInterface({
|
|
121
|
+
input: process.stdin,
|
|
122
|
+
output: process.stdout
|
|
123
|
+
});
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
rl.question(`${message} (y/N): `, (answer) => {
|
|
126
|
+
rl.close();
|
|
127
|
+
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
const validateProfile = (profile) => {
|
|
132
|
+
const errors = [];
|
|
133
|
+
if (!profile.name || profile.name.trim().length === 0) {
|
|
134
|
+
errors.push("Profile name is required");
|
|
135
|
+
}
|
|
136
|
+
if (profile.env?.ANTHROPIC_AUTH_TOKEN) {
|
|
137
|
+
const key = profile.env.ANTHROPIC_AUTH_TOKEN;
|
|
138
|
+
if (!key.startsWith("sk-ant-")) {
|
|
139
|
+
errors.push('API key should start with "sk-ant-"');
|
|
140
|
+
}
|
|
141
|
+
if (key.length < 20) {
|
|
142
|
+
errors.push("API key appears too short");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (profile.env?.ANTHROPIC_MODEL) {
|
|
146
|
+
const model = profile.env.ANTHROPIC_MODEL;
|
|
147
|
+
const validPatterns = [
|
|
148
|
+
/^claude-\d+(\.\d+)?(-\d+)?$/,
|
|
149
|
+
/^glm-/,
|
|
150
|
+
/^minimax-/,
|
|
151
|
+
/^anthropic\.claude-/
|
|
152
|
+
];
|
|
153
|
+
if (!validPatterns.some((p) => p.test(model))) {
|
|
154
|
+
errors.push(`Model format looks invalid: ${model}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (profile.env?.ANTHROPIC_BASE_URL) {
|
|
158
|
+
try {
|
|
159
|
+
new URL(profile.env.ANTHROPIC_BASE_URL);
|
|
160
|
+
} catch {
|
|
161
|
+
errors.push("Base URL is not a valid URL");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { valid: errors.length === 0, errors };
|
|
165
|
+
};
|
|
166
|
+
const getInstalledSkills = () => {
|
|
167
|
+
const skillsDir = path.join(os.homedir(), ".claude", "skills");
|
|
168
|
+
if (!fs.existsSync(skillsDir)) return [];
|
|
169
|
+
return fs.readdirSync(skillsDir).filter((f) => {
|
|
170
|
+
const p = path.join(skillsDir, f);
|
|
171
|
+
return fs.statSync(p).isDirectory() && !f.startsWith(".");
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
const removeSkill = (skillName) => {
|
|
175
|
+
const skillPath = path.join(os.homedir(), ".claude", "skills", skillName);
|
|
176
|
+
if (!fs.existsSync(skillPath)) {
|
|
177
|
+
return { success: false, message: "Skill not found" };
|
|
178
|
+
}
|
|
179
|
+
fs.rmSync(skillPath, { recursive: true, force: true });
|
|
180
|
+
return { success: true };
|
|
181
|
+
};
|
|
182
|
+
const checkForUpdate = async () => {
|
|
183
|
+
if (skipUpdate) return { needsUpdate: false };
|
|
184
|
+
try {
|
|
185
|
+
const { exec } = await import("child_process");
|
|
186
|
+
const { promisify } = await import("util");
|
|
187
|
+
const execAsync = promisify(exec);
|
|
188
|
+
const versionResult = await execAsync("claude --version 2>/dev/null").catch(() => ({ stdout: "" }));
|
|
189
|
+
const current = versionResult.stdout.match(/(\d+\.\d+\.\d+)/)?.[1];
|
|
190
|
+
if (!current) return { needsUpdate: false };
|
|
191
|
+
let needsUpdate = false;
|
|
192
|
+
if (process.platform === "darwin") {
|
|
193
|
+
const outdatedResult = await execAsync("brew outdated claude-code 2>&1 || true").catch(() => ({ stdout: "" }));
|
|
194
|
+
needsUpdate = outdatedResult.stdout.includes("claude-code");
|
|
195
|
+
}
|
|
196
|
+
if (!needsUpdate) {
|
|
197
|
+
const npmListResult = await execAsync("npm list -g @anthropic-ai/claude-code 2>/dev/null").catch(() => ({ stdout: "" }));
|
|
198
|
+
if (npmListResult.stdout.includes("@anthropic-ai/claude-code")) {
|
|
199
|
+
try {
|
|
200
|
+
const npmOutdated = await execAsync("npm outdated -g @anthropic-ai/claude-code --json 2>/dev/null || true", { timeout: 5e3 });
|
|
201
|
+
needsUpdate = npmOutdated.stdout.length > 0;
|
|
202
|
+
} catch {
|
|
203
|
+
needsUpdate = true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { current, needsUpdate };
|
|
208
|
+
} catch (error) {
|
|
209
|
+
logError("checkForUpdate", error);
|
|
210
|
+
return { needsUpdate: false };
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const launchClaude = () => {
|
|
214
|
+
try {
|
|
215
|
+
const claudeArgs = dangerMode ? "--dangerously-skip-permissions" : "";
|
|
216
|
+
execSync(`claude ${claudeArgs}`, { stdio: "inherit" });
|
|
217
|
+
} catch (e) {
|
|
218
|
+
process.exit(e.status || 1);
|
|
219
|
+
}
|
|
220
|
+
process.exit(0);
|
|
221
|
+
};
|
|
222
|
+
if (useLast) {
|
|
223
|
+
const last = getLastProfile();
|
|
224
|
+
if (last && fs.existsSync(path.join(PROFILES_DIR, last))) {
|
|
225
|
+
const name = applyProfile(last);
|
|
226
|
+
console.log(`\x1B[32m\u2713\x1B[0m Applied: ${name}
|
|
227
|
+
`);
|
|
228
|
+
launchClaude();
|
|
229
|
+
} else {
|
|
230
|
+
console.log("\x1B[31mNo last profile found\x1B[0m");
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const projectProfile = checkProjectProfile();
|
|
235
|
+
if (projectProfile && !cmd) {
|
|
236
|
+
const profiles = loadProfiles();
|
|
237
|
+
const match = profiles.find((p) => p.label === projectProfile || p.value === projectProfile + ".json");
|
|
238
|
+
if (match) {
|
|
239
|
+
console.log(`\x1B[36mUsing project profile: ${match.label}\x1B[0m`);
|
|
240
|
+
applyProfile(match.value);
|
|
241
|
+
launchClaude();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (cmd === "status") {
|
|
245
|
+
const last = getLastProfile();
|
|
246
|
+
const profiles = loadProfiles();
|
|
247
|
+
const current = profiles.find((p) => p.value === last);
|
|
248
|
+
console.log(`\x1B[1m\x1B[36mClaude Settings Manager v${VERSION}\x1B[0m`);
|
|
249
|
+
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`);
|
|
250
|
+
if (current) {
|
|
251
|
+
console.log(`Current profile: \x1B[32m${current.label}\x1B[0m`);
|
|
252
|
+
console.log(`Model: ${current.data.env?.ANTHROPIC_MODEL || "default"}`);
|
|
253
|
+
console.log(`Provider: ${current.data.env?.ANTHROPIC_BASE_URL || "Anthropic Direct"}`);
|
|
254
|
+
const mcpServers = current.data.mcpServers || {};
|
|
255
|
+
if (Object.keys(mcpServers).length > 0) {
|
|
256
|
+
console.log(`
|
|
257
|
+
Profile MCP Servers (${Object.keys(mcpServers).length}):`);
|
|
258
|
+
Object.keys(mcpServers).forEach((s) => console.log(` - ${s}`));
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
console.log("No profile active");
|
|
262
|
+
}
|
|
263
|
+
const skillsDir = path.join(os.homedir(), ".claude", "skills");
|
|
264
|
+
try {
|
|
265
|
+
if (fs.existsSync(skillsDir)) {
|
|
266
|
+
const installedSkills = fs.readdirSync(skillsDir).filter((f) => {
|
|
267
|
+
const p = path.join(skillsDir, f);
|
|
268
|
+
return fs.statSync(p).isDirectory() && !f.startsWith(".");
|
|
269
|
+
});
|
|
270
|
+
if (installedSkills.length > 0) {
|
|
271
|
+
console.log(`
|
|
272
|
+
Installed Skills (${installedSkills.length}):`);
|
|
273
|
+
installedSkills.forEach((s) => console.log(` - ${s}`));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const claudeJson = JSON.parse(fs.readFileSync(CLAUDE_JSON_PATH, "utf8"));
|
|
280
|
+
const globalMcp = claudeJson.mcpServers || {};
|
|
281
|
+
if (Object.keys(globalMcp).length > 0) {
|
|
282
|
+
console.log(`
|
|
283
|
+
Global MCP Servers (${Object.keys(globalMcp).length}):`);
|
|
284
|
+
Object.keys(globalMcp).forEach((s) => console.log(` - ${s}`));
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const ver = execSync("claude --version 2>/dev/null", { encoding: "utf8" }).trim();
|
|
290
|
+
console.log(`
|
|
291
|
+
Claude: ${ver}`);
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
process.exit(0);
|
|
295
|
+
}
|
|
296
|
+
if (cmd === "list") {
|
|
297
|
+
const profiles = loadProfiles();
|
|
298
|
+
console.log(`\x1B[1m\x1B[36mProfiles\x1B[0m (${profiles.length})`);
|
|
299
|
+
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`);
|
|
300
|
+
profiles.forEach((p, i) => {
|
|
301
|
+
const group = p.group ? `\x1B[33m[${p.group}]\x1B[0m ` : "";
|
|
302
|
+
console.log(`${i + 1}. ${group}${p.label}`);
|
|
303
|
+
});
|
|
304
|
+
process.exit(0);
|
|
305
|
+
}
|
|
306
|
+
if (cmd === "config") {
|
|
307
|
+
const editor = process.env.EDITOR || "nano";
|
|
308
|
+
const configPath = SETTINGS_PATH;
|
|
309
|
+
if (!fs.existsSync(configPath)) {
|
|
310
|
+
console.log(`\x1B[33mSettings file not found. Creating default settings...\x1B[0m`);
|
|
311
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
312
|
+
env: {},
|
|
313
|
+
model: "opus",
|
|
314
|
+
alwaysThinkingEnabled: true,
|
|
315
|
+
defaultMode: "bypassPermissions"
|
|
316
|
+
}, null, 2));
|
|
317
|
+
}
|
|
318
|
+
console.log(`Opening ${configPath} in ${editor}...`);
|
|
319
|
+
spawnSync(editor, [configPath], { stdio: "inherit" });
|
|
320
|
+
process.exit(0);
|
|
321
|
+
}
|
|
322
|
+
if (cmd === "delete") {
|
|
323
|
+
const forceDelete = args.includes("--force") || args.includes("-f");
|
|
324
|
+
const profiles = loadProfiles();
|
|
325
|
+
const target = args[1];
|
|
326
|
+
const idx = parseInt(target) - 1;
|
|
327
|
+
const match = profiles[idx] || profiles.find((p) => p.label.toLowerCase() === target?.toLowerCase());
|
|
328
|
+
if (!match) {
|
|
329
|
+
console.log(`\x1B[31mProfile not found: ${target}\x1B[0m`);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
const shouldDelete = forceDelete || await confirm(`Delete profile "${match.label}"?`);
|
|
333
|
+
if (shouldDelete) {
|
|
334
|
+
fs.unlinkSync(path.join(PROFILES_DIR, match.value));
|
|
335
|
+
console.log(`\x1B[32m\u2713\x1B[0m Deleted: ${match.label}`);
|
|
336
|
+
} else {
|
|
337
|
+
console.log("\x1B[33mCancelled\x1B[0m");
|
|
338
|
+
}
|
|
339
|
+
process.exit(0);
|
|
340
|
+
}
|
|
341
|
+
if (cmd === "edit") {
|
|
342
|
+
const profiles = loadProfiles();
|
|
343
|
+
const target = args[1];
|
|
344
|
+
const idx = parseInt(target) - 1;
|
|
345
|
+
const match = profiles[idx] || profiles.find((p) => p.label.toLowerCase() === target?.toLowerCase());
|
|
346
|
+
if (match) {
|
|
347
|
+
const editor = process.env.EDITOR || "nano";
|
|
348
|
+
spawnSync(editor, [path.join(PROFILES_DIR, match.value)], { stdio: "inherit" });
|
|
349
|
+
} else {
|
|
350
|
+
console.log(`\x1B[31mProfile not found: ${target}\x1B[0m`);
|
|
351
|
+
}
|
|
352
|
+
process.exit(0);
|
|
353
|
+
}
|
|
354
|
+
if (cmd === "copy") {
|
|
355
|
+
const profiles = loadProfiles();
|
|
356
|
+
const target = args[1];
|
|
357
|
+
const newName = args[2];
|
|
358
|
+
if (!newName) {
|
|
359
|
+
console.log("\x1B[31mUsage: cm copy <source> <new-name>\x1B[0m");
|
|
360
|
+
console.log(" source: Profile name or number");
|
|
361
|
+
console.log(" new-name: Name for the copied profile");
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
const idx = parseInt(target) - 1;
|
|
365
|
+
const match = profiles[idx] || profiles.find((p) => p.label.toLowerCase() === target?.toLowerCase());
|
|
366
|
+
if (!match) {
|
|
367
|
+
console.log(`\x1B[31mProfile not found: ${target}\x1B[0m`);
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
const profile = JSON.parse(fs.readFileSync(path.join(PROFILES_DIR, match.value), "utf8"));
|
|
371
|
+
profile.name = newName;
|
|
372
|
+
const newFilename = newName.toLowerCase().replace(/\s+/g, "-") + ".json";
|
|
373
|
+
if (fs.existsSync(path.join(PROFILES_DIR, newFilename))) {
|
|
374
|
+
const shouldOverwrite = await confirm(`Profile "${newName}" already exists. Overwrite?`);
|
|
375
|
+
if (!shouldOverwrite) {
|
|
376
|
+
console.log("\x1B[33mCancelled\x1B[0m");
|
|
377
|
+
process.exit(0);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
fs.writeFileSync(path.join(PROFILES_DIR, newFilename), JSON.stringify(profile, null, 2));
|
|
381
|
+
console.log(`\x1B[32m\u2713\x1B[0m Copied "${match.label}" to "${newName}"`);
|
|
382
|
+
process.exit(0);
|
|
383
|
+
}
|
|
384
|
+
const MCP_PAGE_SIZE = 50;
|
|
385
|
+
const searchMcpServers = async (query, offset = 0) => {
|
|
386
|
+
const controller = new AbortController();
|
|
387
|
+
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
388
|
+
try {
|
|
389
|
+
const res = await fetch(`${MCP_REGISTRY_URL}?limit=200`, { signal: controller.signal });
|
|
390
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
391
|
+
const data = await res.json();
|
|
392
|
+
const seen = /* @__PURE__ */ new Set();
|
|
393
|
+
const filtered = data.servers.filter((s) => {
|
|
394
|
+
if (seen.has(s.server.name)) return false;
|
|
395
|
+
seen.add(s.server.name);
|
|
396
|
+
const isLatest = s._meta?.["io.modelcontextprotocol.registry/official"]?.isLatest !== false;
|
|
397
|
+
const matchesQuery = !query || s.server.name.toLowerCase().includes(query.toLowerCase()) || s.server.description?.toLowerCase().includes(query.toLowerCase());
|
|
398
|
+
return isLatest && matchesQuery;
|
|
399
|
+
});
|
|
400
|
+
return {
|
|
401
|
+
servers: filtered.slice(offset, offset + MCP_PAGE_SIZE),
|
|
402
|
+
total: filtered.length,
|
|
403
|
+
hasMore: offset + MCP_PAGE_SIZE < filtered.length,
|
|
404
|
+
offset
|
|
405
|
+
};
|
|
406
|
+
} catch (error) {
|
|
407
|
+
logError("searchMcpServers", error);
|
|
408
|
+
return { servers: [], total: 0, hasMore: false, offset: 0 };
|
|
409
|
+
} finally {
|
|
410
|
+
clearTimeout(timeout);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
const addMcpToProfile = (server, profileFile) => {
|
|
414
|
+
const profilePath = path.join(PROFILES_DIR, profileFile);
|
|
415
|
+
const profile = JSON.parse(fs.readFileSync(profilePath, "utf8"));
|
|
416
|
+
if (!profile.mcpServers) profile.mcpServers = {};
|
|
417
|
+
const s = server.server;
|
|
418
|
+
const name = s.name.split("/").pop();
|
|
419
|
+
if (s.remotes?.[0]) {
|
|
420
|
+
const remote = s.remotes[0];
|
|
421
|
+
profile.mcpServers[name] = {
|
|
422
|
+
type: remote.type === "streamable-http" ? "http" : remote.type,
|
|
423
|
+
url: remote.url
|
|
424
|
+
};
|
|
425
|
+
} else if (s.packages?.[0]) {
|
|
426
|
+
const pkg = s.packages[0];
|
|
427
|
+
if (pkg.registryType === "npm") {
|
|
428
|
+
profile.mcpServers[name] = {
|
|
429
|
+
type: "stdio",
|
|
430
|
+
command: "npx",
|
|
431
|
+
args: ["-y", pkg.identifier]
|
|
432
|
+
};
|
|
433
|
+
} else if (pkg.registryType === "pypi") {
|
|
434
|
+
profile.mcpServers[name] = {
|
|
435
|
+
type: "stdio",
|
|
436
|
+
command: "uvx",
|
|
437
|
+
args: [pkg.identifier]
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2));
|
|
442
|
+
return name;
|
|
443
|
+
};
|
|
444
|
+
const McpSearch = () => {
|
|
445
|
+
const { exit } = useApp();
|
|
446
|
+
const [step, setStep] = useState(args[1] ? "loading" : "search");
|
|
447
|
+
const [query, setQuery] = useState(args[1] || "");
|
|
448
|
+
const [searchResults, setSearchResults] = useState({ servers: [], total: 0, hasMore: false, offset: 0 });
|
|
449
|
+
const [selectedServer, setSelectedServer] = useState(null);
|
|
450
|
+
const profiles = loadProfiles();
|
|
451
|
+
useEffect(() => {
|
|
452
|
+
const loadInitialResults = async () => {
|
|
453
|
+
if (args[1] && step === "loading") {
|
|
454
|
+
const results = await searchMcpServers(args[1]);
|
|
455
|
+
setSearchResults(results);
|
|
456
|
+
setStep("results");
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
loadInitialResults();
|
|
460
|
+
}, []);
|
|
461
|
+
const doSearch = async () => {
|
|
462
|
+
setStep("loading");
|
|
463
|
+
const results = await searchMcpServers(query, 0);
|
|
464
|
+
setSearchResults(results);
|
|
465
|
+
setStep("results");
|
|
466
|
+
};
|
|
467
|
+
const nextPage = async () => {
|
|
468
|
+
const results = await searchMcpServers(query, searchResults.offset + MCP_PAGE_SIZE);
|
|
469
|
+
setSearchResults(results);
|
|
470
|
+
};
|
|
471
|
+
const prevPage = async () => {
|
|
472
|
+
const results = await searchMcpServers(query, Math.max(0, searchResults.offset - MCP_PAGE_SIZE));
|
|
473
|
+
setSearchResults(results);
|
|
474
|
+
};
|
|
475
|
+
const serverItems = searchResults.servers.map((s) => ({
|
|
476
|
+
label: `${s.server.name} - ${s.server.description?.slice(0, 50) || ""}`,
|
|
477
|
+
value: s,
|
|
478
|
+
key: s.server.name + s.server.version
|
|
479
|
+
}));
|
|
480
|
+
const profileItems = profiles.map((p) => ({ label: p.label, value: p.value, key: p.key }));
|
|
481
|
+
useInput((input, key) => {
|
|
482
|
+
if (step === "results") {
|
|
483
|
+
if (key.return && !selectedServer) return;
|
|
484
|
+
if ((input === "n" || key.rightArrow) && searchResults.hasMore) {
|
|
485
|
+
nextPage();
|
|
486
|
+
}
|
|
487
|
+
if ((input === "p" || key.leftArrow) && searchResults.offset > 0) {
|
|
488
|
+
prevPage();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
if (step === "search") {
|
|
493
|
+
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 })));
|
|
494
|
+
}
|
|
495
|
+
if (step === "loading") {
|
|
496
|
+
return /* @__PURE__ */ React.createElement(Box, { padding: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Searching MCP registry..."));
|
|
497
|
+
}
|
|
498
|
+
if (step === "results") {
|
|
499
|
+
if (searchResults.servers.length === 0) {
|
|
500
|
+
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, 'No servers found for "', query, '"'));
|
|
501
|
+
}
|
|
502
|
+
const start = searchResults.offset + 1;
|
|
503
|
+
const end = Math.min(searchResults.offset + MCP_PAGE_SIZE, searchResults.total);
|
|
504
|
+
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(
|
|
505
|
+
SelectInput,
|
|
506
|
+
{
|
|
507
|
+
items: serverItems,
|
|
508
|
+
onSelect: (item) => {
|
|
509
|
+
setSelectedServer(item.value);
|
|
510
|
+
setStep("profile");
|
|
511
|
+
},
|
|
512
|
+
limit: 10
|
|
513
|
+
}
|
|
514
|
+
)));
|
|
515
|
+
}
|
|
516
|
+
if (step === "profile") {
|
|
517
|
+
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, "Add to Profile"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"), /* @__PURE__ */ React.createElement(Text, null, "Server: ", selectedServer.server.name), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Select profile:"), /* @__PURE__ */ React.createElement(
|
|
518
|
+
SelectInput,
|
|
519
|
+
{
|
|
520
|
+
items: profileItems,
|
|
521
|
+
onSelect: (item) => {
|
|
522
|
+
const name = addMcpToProfile(selectedServer, item.value);
|
|
523
|
+
console.log(`
|
|
524
|
+
\x1B[32m\u2713\x1B[0m Added ${name} to ${item.label}`);
|
|
525
|
+
exit();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
)));
|
|
529
|
+
}
|
|
530
|
+
return null;
|
|
531
|
+
};
|
|
532
|
+
const SKILL_SOURCES = [
|
|
533
|
+
{ url: "https://api.github.com/repos/anthropics/skills/contents/skills", base: "https://github.com/anthropics/skills/tree/main/skills" },
|
|
534
|
+
{ url: "https://api.github.com/repos/Prat011/awesome-llm-skills/contents/skills", base: "https://github.com/Prat011/awesome-llm-skills/tree/main/skills" },
|
|
535
|
+
{ url: "https://api.github.com/repos/skillcreatorai/Ai-Agent-Skills/contents/skills", base: "https://github.com/skillcreatorai/Ai-Agent-Skills/tree/main/skills" }
|
|
536
|
+
];
|
|
537
|
+
const fetchSkills = async () => {
|
|
538
|
+
const seen = /* @__PURE__ */ new Set();
|
|
539
|
+
const skills = [];
|
|
540
|
+
const promises = SKILL_SOURCES.map(async (source) => {
|
|
541
|
+
const controller = new AbortController();
|
|
542
|
+
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
543
|
+
try {
|
|
544
|
+
const res = await fetch(source.url, {
|
|
545
|
+
signal: controller.signal,
|
|
546
|
+
headers: { "Accept": "application/vnd.github.v3+json" }
|
|
547
|
+
});
|
|
548
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
549
|
+
const data = await res.json();
|
|
550
|
+
if (Array.isArray(data)) {
|
|
551
|
+
for (const s of data.filter((s2) => s2.type === "dir")) {
|
|
552
|
+
if (!seen.has(s.name)) {
|
|
553
|
+
seen.add(s.name);
|
|
554
|
+
skills.push({
|
|
555
|
+
label: s.name,
|
|
556
|
+
value: `${source.base}/${s.name}`,
|
|
557
|
+
key: s.name
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
} catch (error) {
|
|
563
|
+
logError(`fetchSkills(${source.url})`, error);
|
|
564
|
+
} finally {
|
|
565
|
+
clearTimeout(timeout);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
await Promise.all(promises);
|
|
569
|
+
return skills.sort((a, b) => a.label.localeCompare(b.label));
|
|
570
|
+
};
|
|
571
|
+
const SKILLS_DIR = path.join(os.homedir(), ".claude", "skills");
|
|
572
|
+
const addSkillToClaudeJson = (skillName, skillUrl) => {
|
|
573
|
+
try {
|
|
574
|
+
if (!fs.existsSync(SKILLS_DIR)) fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
575
|
+
const skillPath = path.join(SKILLS_DIR, skillName);
|
|
576
|
+
if (fs.existsSync(skillPath)) {
|
|
577
|
+
return { success: false, message: "Skill already installed" };
|
|
578
|
+
}
|
|
579
|
+
const match = skillUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\/tree\/([^\/]+)\/(.+)/);
|
|
580
|
+
if (!match) return { success: false, message: "Invalid skill URL" };
|
|
581
|
+
const [, owner, repo, branch, skillSubPath] = match;
|
|
582
|
+
const tempDir = `/tmp/skill-clone-${Date.now()}`;
|
|
583
|
+
execSync(`git clone --depth 1 --filter=blob:none --sparse "https://github.com/${owner}/${repo}.git" "${tempDir}" 2>/dev/null`, { timeout: 3e4 });
|
|
584
|
+
execSync(`cd "${tempDir}" && git sparse-checkout set "${skillSubPath}" 2>/dev/null`, { timeout: 1e4 });
|
|
585
|
+
execSync(`mv "${tempDir}/${skillSubPath}" "${skillPath}"`, { timeout: 5e3 });
|
|
586
|
+
execSync(`rm -rf "${tempDir}"`, { timeout: 5e3 });
|
|
587
|
+
return { success: true };
|
|
588
|
+
} catch (e) {
|
|
589
|
+
return { success: false, message: "Failed to download skill" };
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
const SkillsBrowser = () => {
|
|
593
|
+
const { exit } = useApp();
|
|
594
|
+
const [allSkills, setAllSkills] = useState([]);
|
|
595
|
+
const [loading, setLoading] = useState(true);
|
|
596
|
+
const [offset, setOffset] = useState(0);
|
|
597
|
+
const SKILLS_PAGE_SIZE = 50;
|
|
598
|
+
useEffect(() => {
|
|
599
|
+
const loadSkills = async () => {
|
|
600
|
+
const s = await fetchSkills();
|
|
601
|
+
setAllSkills(s);
|
|
602
|
+
setLoading(false);
|
|
603
|
+
};
|
|
604
|
+
loadSkills();
|
|
605
|
+
}, []);
|
|
606
|
+
const paginatedSkills = allSkills.slice(offset, offset + SKILLS_PAGE_SIZE);
|
|
607
|
+
const hasMore = offset + SKILLS_PAGE_SIZE < allSkills.length;
|
|
608
|
+
useInput((input, key) => {
|
|
609
|
+
if (!loading) {
|
|
610
|
+
if ((input === "n" || key.rightArrow) && hasMore) {
|
|
611
|
+
setOffset(offset + SKILLS_PAGE_SIZE);
|
|
612
|
+
}
|
|
613
|
+
if ((input === "p" || key.leftArrow) && offset > 0) {
|
|
614
|
+
setOffset(Math.max(0, offset - SKILLS_PAGE_SIZE));
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
if (loading) {
|
|
619
|
+
return /* @__PURE__ */ React.createElement(Box, { padding: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Loading skills..."));
|
|
620
|
+
}
|
|
621
|
+
if (allSkills.length === 0) {
|
|
622
|
+
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "Could not fetch skills"));
|
|
623
|
+
}
|
|
624
|
+
const start = offset + 1;
|
|
625
|
+
const end = Math.min(offset + SKILLS_PAGE_SIZE, allSkills.length);
|
|
626
|
+
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(
|
|
627
|
+
SelectInput,
|
|
628
|
+
{
|
|
629
|
+
items: paginatedSkills,
|
|
630
|
+
onSelect: (item) => {
|
|
631
|
+
const result = addSkillToClaudeJson(item.label, item.value);
|
|
632
|
+
if (result.success) {
|
|
633
|
+
console.log(`
|
|
634
|
+
\x1B[32m\u2713\x1B[0m Installed skill: ${item.label}`);
|
|
635
|
+
console.log(`\x1B[36mLocation: ~/.claude/skills/${item.label}/\x1B[0m`);
|
|
636
|
+
} else {
|
|
637
|
+
console.log(`
|
|
638
|
+
\x1B[31m\u2717\x1B[0m ${result.message || "Failed to install skill"}`);
|
|
639
|
+
}
|
|
640
|
+
exit();
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
)));
|
|
644
|
+
};
|
|
645
|
+
if (cmd === "skills") {
|
|
646
|
+
const subCommand = args[1];
|
|
647
|
+
if (subCommand === "list") {
|
|
648
|
+
const installed = getInstalledSkills();
|
|
649
|
+
console.log(`\x1B[1m\x1B[36mInstalled Skills\x1B[0m (${installed.length})`);
|
|
650
|
+
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");
|
|
651
|
+
if (installed.length === 0) {
|
|
652
|
+
console.log("No skills installed");
|
|
653
|
+
} else {
|
|
654
|
+
installed.forEach((s, i) => console.log(`${i + 1}. ${s}`));
|
|
655
|
+
}
|
|
656
|
+
process.exit(0);
|
|
657
|
+
}
|
|
658
|
+
if (subCommand === "remove") {
|
|
659
|
+
const target = args[2];
|
|
660
|
+
if (!target) {
|
|
661
|
+
console.log("\x1B[31mUsage: cm skills remove <skill-name>\x1B[0m");
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
664
|
+
const installed = getInstalledSkills();
|
|
665
|
+
const idx = parseInt(target) - 1;
|
|
666
|
+
const match = installed[idx] || installed.find((s) => s.toLowerCase() === target?.toLowerCase());
|
|
667
|
+
if (!match) {
|
|
668
|
+
console.log(`\x1B[31mSkill not found: ${target}\x1B[0m`);
|
|
669
|
+
console.log('Run "cm skills list" to see installed skills');
|
|
670
|
+
process.exit(1);
|
|
671
|
+
}
|
|
672
|
+
const shouldRemove = await confirm(`Remove skill "${match}"?`);
|
|
673
|
+
if (shouldRemove) {
|
|
674
|
+
const result = removeSkill(match);
|
|
675
|
+
if (result.success) {
|
|
676
|
+
console.log(`\x1B[32m\u2713\x1B[0m Removed skill: ${match}`);
|
|
677
|
+
} else {
|
|
678
|
+
console.log(`\x1B[31m\u2717\x1B[0m ${result.message}`);
|
|
679
|
+
}
|
|
680
|
+
} else {
|
|
681
|
+
console.log("\x1B[33mCancelled\x1B[0m");
|
|
682
|
+
}
|
|
683
|
+
process.exit(0);
|
|
684
|
+
}
|
|
685
|
+
render(/* @__PURE__ */ React.createElement(SkillsBrowser, null));
|
|
686
|
+
} else if (cmd === "mcp") {
|
|
687
|
+
const subCommand = args[1];
|
|
688
|
+
if (subCommand === "remove") {
|
|
689
|
+
const profiles = loadProfiles();
|
|
690
|
+
if (profiles.length === 0) {
|
|
691
|
+
console.log("\x1B[31mNo profiles found\x1B[0m");
|
|
692
|
+
process.exit(1);
|
|
693
|
+
}
|
|
694
|
+
const serverName = args[2];
|
|
695
|
+
const targetProfile = args[3];
|
|
696
|
+
if (!targetProfile) {
|
|
697
|
+
console.log("\x1B[31mUsage: cm mcp remove <server-name> <profile>\x1B[0m");
|
|
698
|
+
console.log(" server-name: MCP server name to remove");
|
|
699
|
+
console.log(" profile: Profile name or number");
|
|
700
|
+
process.exit(1);
|
|
701
|
+
}
|
|
702
|
+
const idx = parseInt(targetProfile) - 1;
|
|
703
|
+
const profileMatch = profiles[idx] || profiles.find((p) => p.label.toLowerCase() === targetProfile?.toLowerCase());
|
|
704
|
+
if (!profileMatch) {
|
|
705
|
+
console.log(`\x1B[31mProfile not found: ${targetProfile}\x1B[0m`);
|
|
706
|
+
process.exit(1);
|
|
707
|
+
}
|
|
708
|
+
const profilePath = path.join(PROFILES_DIR, profileMatch.value);
|
|
709
|
+
const profile = JSON.parse(fs.readFileSync(profilePath, "utf8"));
|
|
710
|
+
const mcpServers = profile.mcpServers || {};
|
|
711
|
+
if (Object.keys(mcpServers).length === 0) {
|
|
712
|
+
console.log(`\x1B[33mNo MCP servers configured in "${profileMatch.label}"\x1B[0m`);
|
|
713
|
+
process.exit(0);
|
|
714
|
+
}
|
|
715
|
+
if (!mcpServers[serverName]) {
|
|
716
|
+
console.log(`\x1B[31mMCP server not found: ${serverName}\x1B[0m`);
|
|
717
|
+
console.log(`Available servers: ${Object.keys(mcpServers).join(", ")}`);
|
|
718
|
+
process.exit(1);
|
|
719
|
+
}
|
|
720
|
+
const shouldRemove = await confirm(`Remove "${serverName}" from "${profileMatch.label}"?`);
|
|
721
|
+
if (shouldRemove) {
|
|
722
|
+
delete mcpServers[serverName];
|
|
723
|
+
profile.mcpServers = mcpServers;
|
|
724
|
+
fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2));
|
|
725
|
+
console.log(`\x1B[32m\u2713\x1B[0m Removed "${serverName}" from "${profileMatch.label}"`);
|
|
726
|
+
} else {
|
|
727
|
+
console.log("\x1B[33mCancelled\x1B[0m");
|
|
728
|
+
}
|
|
729
|
+
process.exit(0);
|
|
730
|
+
}
|
|
731
|
+
render(/* @__PURE__ */ React.createElement(McpSearch, null));
|
|
732
|
+
} else if (cmd === "new") {
|
|
733
|
+
const NewProfileWizard = () => {
|
|
734
|
+
const { exit } = useApp();
|
|
735
|
+
const [step, setStep] = useState("name");
|
|
736
|
+
const [name, setName] = useState("");
|
|
737
|
+
const [provider, setProvider] = useState("");
|
|
738
|
+
const [apiKey, setApiKey] = useState("");
|
|
739
|
+
const [model, setModel] = useState("");
|
|
740
|
+
const [group, setGroup] = useState("");
|
|
741
|
+
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
|
+
const handleSave = () => {
|
|
750
|
+
const prov = providers.find((p) => p.value === provider);
|
|
751
|
+
const profile = {
|
|
752
|
+
name,
|
|
753
|
+
group: group || void 0,
|
|
754
|
+
env: {
|
|
755
|
+
...apiKey && { ANTHROPIC_AUTH_TOKEN: apiKey },
|
|
756
|
+
...model && { ANTHROPIC_MODEL: model },
|
|
757
|
+
...prov?.url && { ANTHROPIC_BASE_URL: prov.url },
|
|
758
|
+
API_TIMEOUT_MS: "3000000"
|
|
759
|
+
},
|
|
760
|
+
model: "opus",
|
|
761
|
+
alwaysThinkingEnabled: true,
|
|
762
|
+
defaultMode: "bypassPermissions"
|
|
763
|
+
};
|
|
764
|
+
const validation = validateProfile(profile);
|
|
765
|
+
if (!validation.valid) {
|
|
766
|
+
setStep("error");
|
|
767
|
+
setValidationErrors(validation.errors);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
const filename = name.toLowerCase().replace(/\s+/g, "-") + ".json";
|
|
771
|
+
fs.writeFileSync(path.join(PROFILES_DIR, filename), JSON.stringify(profile, null, 2));
|
|
772
|
+
console.log(`
|
|
773
|
+
\x1B[32m\u2713\x1B[0m Created: ${name}`);
|
|
774
|
+
exit();
|
|
775
|
+
};
|
|
776
|
+
const handleProviderSelect = (item) => {
|
|
777
|
+
setProvider(item.value);
|
|
778
|
+
const prov = providers.find((p) => p.value === item.value);
|
|
779
|
+
setStep(prov.needsKey ? "apikey" : "model");
|
|
780
|
+
};
|
|
781
|
+
useInput((input, key) => {
|
|
782
|
+
if (step === "error") {
|
|
783
|
+
setStep("group");
|
|
784
|
+
setValidationErrors([]);
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, "New Profile"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"), step === "name" && /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Name: "), /* @__PURE__ */ React.createElement(TextInput, { value: name, onChange: setName, onSubmit: () => setStep("provider") })), step === "provider" && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Provider:"), /* @__PURE__ */ React.createElement(SelectInput, { items: providers, onSelect: handleProviderSelect })), step === "apikey" && /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "API Key: "), /* @__PURE__ */ React.createElement(TextInput, { value: apiKey, onChange: setApiKey, onSubmit: () => setStep("model"), mask: "*" })), step === "model" && /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Model ID (optional): "), /* @__PURE__ */ React.createElement(TextInput, { value: model, onChange: setModel, onSubmit: () => setStep("group") })), step === "group" && /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Group (optional): "), /* @__PURE__ */ React.createElement(TextInput, { value: group, onChange: setGroup, onSubmit: handleSave })), step === "error" && /* @__PURE__ */ React.createElement(Box, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React.createElement(Text, { color: "red" }, "Validation errors:"), validationErrors.map((err, i) => /* @__PURE__ */ React.createElement(Text, { key: i, color: "yellow" }, " \u2022 ", err)), /* @__PURE__ */ React.createElement(Text, { marginTop: 1 }, "Press any key to go back and fix...")));
|
|
788
|
+
};
|
|
789
|
+
render(/* @__PURE__ */ React.createElement(NewProfileWizard, null));
|
|
790
|
+
} else {
|
|
791
|
+
const LoadingScreen = ({ message = "Loading..." }) => {
|
|
792
|
+
const [dots, setDots] = useState("");
|
|
793
|
+
const [colorIdx, setColorIdx] = useState(0);
|
|
794
|
+
const colors = ["cyan", "blue", "magenta", "red", "yellow", "green"];
|
|
795
|
+
useEffect(() => {
|
|
796
|
+
const dotsInterval = setInterval(() => {
|
|
797
|
+
setDots((d) => d.length >= 3 ? "" : d + ".");
|
|
798
|
+
}, 500);
|
|
799
|
+
const colorInterval = setInterval(() => {
|
|
800
|
+
setColorIdx((i) => (i + 1) % colors.length);
|
|
801
|
+
}, 200);
|
|
802
|
+
return () => {
|
|
803
|
+
clearInterval(dotsInterval);
|
|
804
|
+
clearInterval(colorInterval);
|
|
805
|
+
};
|
|
806
|
+
}, []);
|
|
807
|
+
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));
|
|
808
|
+
};
|
|
809
|
+
const App = () => {
|
|
810
|
+
const [step, setStep] = useState("loading");
|
|
811
|
+
const [updateInfo, setUpdateInfo] = useState(null);
|
|
812
|
+
const [filter, setFilter] = useState("");
|
|
813
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
814
|
+
const [showCommandPalette, setShowCommandPalette] = useState(false);
|
|
815
|
+
const [commandInput, setCommandInput] = useState("");
|
|
816
|
+
const profiles = loadProfiles();
|
|
817
|
+
const commands = [
|
|
818
|
+
{ label: "/skills", description: "Browse and install skills", action: () => render(/* @__PURE__ */ React.createElement(SkillsBrowser, null)) },
|
|
819
|
+
{ label: "/mcp", description: "Search and add MCP servers", action: () => render(/* @__PURE__ */ React.createElement(McpSearch, null)) },
|
|
820
|
+
{ label: "/new", description: "Create new profile", action: () => setStep("newProfile") },
|
|
821
|
+
{ label: "/list", description: "List all profiles", action: () => execSync("cm list", { stdio: "inherit" }) },
|
|
822
|
+
{ label: "/status", description: "Show current settings", action: () => execSync("cm status", { stdio: "inherit" }) },
|
|
823
|
+
{ label: "/config", description: "Edit Claude settings", action: () => execSync("cm config", { stdio: "inherit" }) },
|
|
824
|
+
{ label: "/help", description: "Show keyboard shortcuts", action: () => setShowHelp(true) },
|
|
825
|
+
{ label: "/quit", description: "Exit cm", action: () => process.exit(0) }
|
|
826
|
+
];
|
|
827
|
+
const filteredProfiles = useMemo(() => {
|
|
828
|
+
if (!filter) return profiles;
|
|
829
|
+
const fuse = new Fuse(profiles, {
|
|
830
|
+
keys: ["label", "group"],
|
|
831
|
+
threshold: 0.3,
|
|
832
|
+
// Lower = more strict matching
|
|
833
|
+
ignoreLocation: true,
|
|
834
|
+
includeScore: true
|
|
835
|
+
});
|
|
836
|
+
return fuse.search(filter).map((r) => r.item);
|
|
837
|
+
}, [profiles, filter]);
|
|
838
|
+
const filteredCommands = useMemo(() => {
|
|
839
|
+
if (!commandInput) return commands;
|
|
840
|
+
const search = commandInput.toLowerCase().replace(/^\//, "");
|
|
841
|
+
const fuse = new Fuse(commands, {
|
|
842
|
+
keys: ["label", "description"],
|
|
843
|
+
threshold: 0.3,
|
|
844
|
+
ignoreLocation: true
|
|
845
|
+
});
|
|
846
|
+
return fuse.search(search).map((r) => r.item);
|
|
847
|
+
}, [commands, commandInput]);
|
|
848
|
+
useEffect(() => {
|
|
849
|
+
setTimeout(() => setStep("select"), 1500);
|
|
850
|
+
if (!skipUpdate) {
|
|
851
|
+
checkForUpdate().then(setUpdateInfo);
|
|
852
|
+
}
|
|
853
|
+
}, []);
|
|
854
|
+
useInput((input, key) => {
|
|
855
|
+
if (showCommandPalette) {
|
|
856
|
+
if (key.escape) {
|
|
857
|
+
setShowCommandPalette(false);
|
|
858
|
+
setCommandInput("");
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (key.return) {
|
|
862
|
+
const matchedCommand = commandInput.startsWith("/") ? commands.find((c) => c.label === commandInput) : filteredCommands[0];
|
|
863
|
+
if (matchedCommand) {
|
|
864
|
+
setShowCommandPalette(false);
|
|
865
|
+
setCommandInput("");
|
|
866
|
+
matchedCommand.action();
|
|
867
|
+
}
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
if (key.backspace || key.delete) {
|
|
871
|
+
setCommandInput((c) => c.slice(0, -1));
|
|
872
|
+
if (commandInput.length <= 1) {
|
|
873
|
+
setShowCommandPalette(false);
|
|
874
|
+
}
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (input && !key.ctrl && !key.meta) {
|
|
878
|
+
setCommandInput((c) => c + input);
|
|
879
|
+
}
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (step === "select") {
|
|
883
|
+
const num = parseInt(input);
|
|
884
|
+
if (num >= 1 && num <= 9 && num <= filteredProfiles.length) {
|
|
885
|
+
const profile = filteredProfiles[num - 1];
|
|
886
|
+
applyProfile(profile.value);
|
|
887
|
+
console.log(`
|
|
888
|
+
\x1B[32m\u2713\x1B[0m Applied: ${profile.label}
|
|
889
|
+
`);
|
|
890
|
+
launchClaude();
|
|
891
|
+
}
|
|
892
|
+
if (input === "u" && updateInfo?.needsUpdate) {
|
|
893
|
+
console.log("\n\x1B[33mUpdating Claude...\x1B[0m\n");
|
|
894
|
+
try {
|
|
895
|
+
if (process.platform === "darwin") {
|
|
896
|
+
execSync("brew upgrade claude-code", { stdio: "inherit" });
|
|
897
|
+
} else {
|
|
898
|
+
execSync("npm update -g @anthropic-ai/claude-code", { stdio: "inherit" });
|
|
899
|
+
}
|
|
900
|
+
console.log("\n\x1B[32m\u2713 Updated!\x1B[0m\n");
|
|
901
|
+
setUpdateInfo({ ...updateInfo, needsUpdate: false });
|
|
902
|
+
} catch (error) {
|
|
903
|
+
console.log("\x1B[31m\u2717 Update failed\x1B[0m\n");
|
|
904
|
+
logError("update", error);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
if (input === "/" && !showHelp) {
|
|
908
|
+
setShowCommandPalette(true);
|
|
909
|
+
setCommandInput("/");
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (input.match(/^[a-zA-Z]$/) && input !== "u" && input !== "c" && input !== "?" && input !== "/") {
|
|
913
|
+
setFilter((f) => f + input);
|
|
914
|
+
}
|
|
915
|
+
if (key.backspace || key.delete) {
|
|
916
|
+
setFilter((f) => f.slice(0, -1));
|
|
917
|
+
}
|
|
918
|
+
if (key.escape) {
|
|
919
|
+
setFilter("");
|
|
920
|
+
}
|
|
921
|
+
if (input === "?") {
|
|
922
|
+
setShowHelp(true);
|
|
923
|
+
}
|
|
924
|
+
if (input === "c") {
|
|
925
|
+
const editor = process.env.EDITOR || "nano";
|
|
926
|
+
const configPath = SETTINGS_PATH;
|
|
927
|
+
if (!fs.existsSync(configPath)) {
|
|
928
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
929
|
+
env: {},
|
|
930
|
+
model: "opus",
|
|
931
|
+
alwaysThinkingEnabled: true,
|
|
932
|
+
defaultMode: "bypassPermissions"
|
|
933
|
+
}, null, 2));
|
|
934
|
+
}
|
|
935
|
+
console.clear();
|
|
936
|
+
spawnSync(editor, [configPath], { stdio: "inherit" });
|
|
937
|
+
console.log("\n\x1B[36mConfig edited. Press Enter to continue...\x1B[0m");
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
if (showHelp && (input === "q" || input === "?" || key.escape || key.return)) {
|
|
941
|
+
setShowHelp(false);
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
const groupedItems = [];
|
|
945
|
+
const groups = [...new Set(filteredProfiles.map((p) => p.group).filter(Boolean))];
|
|
946
|
+
if (groups.length > 0) {
|
|
947
|
+
groups.forEach((g) => {
|
|
948
|
+
groupedItems.push({ label: `\u2500\u2500 ${g} \u2500\u2500`, value: `group-${g}`, key: `group-${g}`, disabled: true });
|
|
949
|
+
filteredProfiles.filter((p) => p.group === g).forEach((p, i) => {
|
|
950
|
+
groupedItems.push({ ...p, label: `${i + 1}. ${p.label}` });
|
|
951
|
+
});
|
|
952
|
+
});
|
|
953
|
+
const ungrouped = filteredProfiles.filter((p) => !p.group);
|
|
954
|
+
if (ungrouped.length > 0) {
|
|
955
|
+
groupedItems.push({ label: "\u2500\u2500 Other \u2500\u2500", value: "group-other", key: "group-other", disabled: true });
|
|
956
|
+
ungrouped.forEach((p, i) => groupedItems.push({ ...p, label: `${i + 1}. ${p.label}` }));
|
|
957
|
+
}
|
|
958
|
+
} else {
|
|
959
|
+
filteredProfiles.forEach((p, i) => groupedItems.push({ ...p, label: `${i + 1}. ${p.label}` }));
|
|
960
|
+
}
|
|
961
|
+
if (step === "loading") {
|
|
962
|
+
return /* @__PURE__ */ React.createElement(LoadingScreen, { message: "Initializing Claude Manager" });
|
|
963
|
+
}
|
|
964
|
+
if (profiles.length === 0) {
|
|
965
|
+
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, "CLAUDE MANAGER"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"), /* @__PURE__ */ React.createElement(Text, { color: "yellow", marginTop: 1 }, "No profiles found!"), /* @__PURE__ */ React.createElement(Text, null, "Run: cm new"));
|
|
966
|
+
}
|
|
967
|
+
const handleSelect = (item) => {
|
|
968
|
+
if (item.disabled) return;
|
|
969
|
+
applyProfile(item.value);
|
|
970
|
+
console.log(`
|
|
971
|
+
\x1B[32m\u2713\x1B[0m Applied: ${item.label.replace(/^\d+\.\s*/, "")}
|
|
972
|
+
`);
|
|
973
|
+
launchClaude();
|
|
974
|
+
};
|
|
975
|
+
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, LOGO), /* @__PURE__ */ React.createElement(Text, { bold: true, color: "magenta" }, "MANAGER v", VERSION), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"), updateInfo?.current && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Claude v", updateInfo.current), updateInfo?.needsUpdate && /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "\u26A0 Update available! Press 'u' to upgrade"), filter && /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "Filter: ", filter), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Select Profile: ", /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "(1-9 select, / commands, ? help, c config", updateInfo?.needsUpdate ? ", u update" : "", ")")), /* @__PURE__ */ React.createElement(
|
|
976
|
+
SelectInput,
|
|
977
|
+
{
|
|
978
|
+
items: groupedItems,
|
|
979
|
+
onSelect: handleSelect,
|
|
980
|
+
itemComponent: ({ isSelected, label, disabled }) => /* @__PURE__ */ React.createElement(Text, { color: disabled ? "gray" : isSelected ? "cyan" : "white", dimColor: disabled }, disabled ? label : (isSelected ? "\u276F " : " ") + label)
|
|
981
|
+
}
|
|
982
|
+
)), 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")));
|
|
983
|
+
};
|
|
984
|
+
render(/* @__PURE__ */ React.createElement(App, null));
|
|
985
|
+
}
|