claude-manager 1.5.1 → 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 +470 -75
- package/package.json +5 -2
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import React, { useState, useEffect } from "react";
|
|
2
|
+
import React, { useState, useEffect, useMemo } from "react";
|
|
3
3
|
import { render, Box, Text, useInput, useApp } from "ink";
|
|
4
4
|
import SelectInput from "ink-select-input";
|
|
5
5
|
import TextInput from "ink-text-input";
|
|
@@ -7,12 +7,20 @@ import fs from "fs";
|
|
|
7
7
|
import path from "path";
|
|
8
8
|
import os from "os";
|
|
9
9
|
import { execSync, spawnSync } from "child_process";
|
|
10
|
-
|
|
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";
|
|
11
20
|
const PROFILES_DIR = path.join(os.homedir(), ".claude", "profiles");
|
|
12
21
|
const SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
|
|
13
22
|
const CLAUDE_JSON_PATH = path.join(os.homedir(), ".claude.json");
|
|
14
23
|
const LAST_PROFILE_PATH = path.join(os.homedir(), ".claude", ".last-profile");
|
|
15
|
-
const MCP_REGISTRY_URL = "https://registry.modelcontextprotocol.io/v0/servers";
|
|
16
24
|
const args = process.argv.slice(2);
|
|
17
25
|
const cmd = args[0];
|
|
18
26
|
if (!fs.existsSync(PROFILES_DIR)) fs.mkdirSync(PROFILES_DIR, { recursive: true });
|
|
@@ -26,19 +34,25 @@ if (args.includes("-h") || args.includes("--help")) {
|
|
|
26
34
|
Usage: cm [command] [options]
|
|
27
35
|
|
|
28
36
|
Commands:
|
|
29
|
-
(none)
|
|
30
|
-
new
|
|
31
|
-
edit <n>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
37
50
|
|
|
38
51
|
Options:
|
|
39
52
|
--last, -l Use last profile without menu
|
|
40
53
|
--skip-update Skip update check
|
|
41
54
|
--yolo Run claude with --dangerously-skip-permissions
|
|
55
|
+
--force, -f Skip confirmation prompts (e.g., for delete)
|
|
42
56
|
-v, --version Show version
|
|
43
57
|
-h, --help Show help`);
|
|
44
58
|
process.exit(0);
|
|
@@ -97,13 +111,102 @@ const checkProjectProfile = () => {
|
|
|
97
111
|
}
|
|
98
112
|
return null;
|
|
99
113
|
};
|
|
100
|
-
const
|
|
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 () => {
|
|
101
183
|
if (skipUpdate) return { needsUpdate: false };
|
|
102
184
|
try {
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
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);
|
|
107
210
|
return { needsUpdate: false };
|
|
108
211
|
}
|
|
109
212
|
};
|
|
@@ -200,16 +303,38 @@ if (cmd === "list") {
|
|
|
200
303
|
});
|
|
201
304
|
process.exit(0);
|
|
202
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
|
+
}
|
|
203
322
|
if (cmd === "delete") {
|
|
323
|
+
const forceDelete = args.includes("--force") || args.includes("-f");
|
|
204
324
|
const profiles = loadProfiles();
|
|
205
325
|
const target = args[1];
|
|
206
326
|
const idx = parseInt(target) - 1;
|
|
207
327
|
const match = profiles[idx] || profiles.find((p) => p.label.toLowerCase() === target?.toLowerCase());
|
|
208
|
-
if (match) {
|
|
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) {
|
|
209
334
|
fs.unlinkSync(path.join(PROFILES_DIR, match.value));
|
|
210
335
|
console.log(`\x1B[32m\u2713\x1B[0m Deleted: ${match.label}`);
|
|
211
336
|
} else {
|
|
212
|
-
console.log(
|
|
337
|
+
console.log("\x1B[33mCancelled\x1B[0m");
|
|
213
338
|
}
|
|
214
339
|
process.exit(0);
|
|
215
340
|
}
|
|
@@ -226,20 +351,63 @@ if (cmd === "edit") {
|
|
|
226
351
|
}
|
|
227
352
|
process.exit(0);
|
|
228
353
|
}
|
|
229
|
-
|
|
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);
|
|
230
388
|
try {
|
|
231
|
-
const res =
|
|
232
|
-
|
|
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();
|
|
233
392
|
const seen = /* @__PURE__ */ new Set();
|
|
234
|
-
|
|
393
|
+
const filtered = data.servers.filter((s) => {
|
|
235
394
|
if (seen.has(s.server.name)) return false;
|
|
236
395
|
seen.add(s.server.name);
|
|
237
396
|
const isLatest = s._meta?.["io.modelcontextprotocol.registry/official"]?.isLatest !== false;
|
|
238
397
|
const matchesQuery = !query || s.server.name.toLowerCase().includes(query.toLowerCase()) || s.server.description?.toLowerCase().includes(query.toLowerCase());
|
|
239
398
|
return isLatest && matchesQuery;
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
|
|
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);
|
|
243
411
|
}
|
|
244
412
|
};
|
|
245
413
|
const addMcpToProfile = (server, profileFile) => {
|
|
@@ -277,27 +445,50 @@ const McpSearch = () => {
|
|
|
277
445
|
const { exit } = useApp();
|
|
278
446
|
const [step, setStep] = useState(args[1] ? "loading" : "search");
|
|
279
447
|
const [query, setQuery] = useState(args[1] || "");
|
|
280
|
-
const [
|
|
448
|
+
const [searchResults, setSearchResults] = useState({ servers: [], total: 0, hasMore: false, offset: 0 });
|
|
281
449
|
const [selectedServer, setSelectedServer] = useState(null);
|
|
282
450
|
const profiles = loadProfiles();
|
|
283
451
|
useEffect(() => {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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();
|
|
289
460
|
}, []);
|
|
290
|
-
const doSearch = () => {
|
|
291
|
-
|
|
292
|
-
|
|
461
|
+
const doSearch = async () => {
|
|
462
|
+
setStep("loading");
|
|
463
|
+
const results = await searchMcpServers(query, 0);
|
|
464
|
+
setSearchResults(results);
|
|
293
465
|
setStep("results");
|
|
294
466
|
};
|
|
295
|
-
const
|
|
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) => ({
|
|
296
476
|
label: `${s.server.name} - ${s.server.description?.slice(0, 50) || ""}`,
|
|
297
477
|
value: s,
|
|
298
478
|
key: s.server.name + s.server.version
|
|
299
479
|
}));
|
|
300
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
|
+
});
|
|
301
492
|
if (step === "search") {
|
|
302
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 })));
|
|
303
494
|
}
|
|
@@ -305,10 +496,12 @@ const McpSearch = () => {
|
|
|
305
496
|
return /* @__PURE__ */ React.createElement(Box, { padding: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Searching MCP registry..."));
|
|
306
497
|
}
|
|
307
498
|
if (step === "results") {
|
|
308
|
-
if (servers.length === 0) {
|
|
499
|
+
if (searchResults.servers.length === 0) {
|
|
309
500
|
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, 'No servers found for "', query, '"'));
|
|
310
501
|
}
|
|
311
|
-
|
|
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(
|
|
312
505
|
SelectInput,
|
|
313
506
|
{
|
|
314
507
|
items: serverItems,
|
|
@@ -341,13 +534,19 @@ const SKILL_SOURCES = [
|
|
|
341
534
|
{ url: "https://api.github.com/repos/Prat011/awesome-llm-skills/contents/skills", base: "https://github.com/Prat011/awesome-llm-skills/tree/main/skills" },
|
|
342
535
|
{ url: "https://api.github.com/repos/skillcreatorai/Ai-Agent-Skills/contents/skills", base: "https://github.com/skillcreatorai/Ai-Agent-Skills/tree/main/skills" }
|
|
343
536
|
];
|
|
344
|
-
const fetchSkills = () => {
|
|
537
|
+
const fetchSkills = async () => {
|
|
345
538
|
const seen = /* @__PURE__ */ new Set();
|
|
346
539
|
const skills = [];
|
|
347
|
-
|
|
540
|
+
const promises = SKILL_SOURCES.map(async (source) => {
|
|
541
|
+
const controller = new AbortController();
|
|
542
|
+
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
348
543
|
try {
|
|
349
|
-
const res =
|
|
350
|
-
|
|
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();
|
|
351
550
|
if (Array.isArray(data)) {
|
|
352
551
|
for (const s of data.filter((s2) => s2.type === "dir")) {
|
|
353
552
|
if (!seen.has(s.name)) {
|
|
@@ -360,9 +559,13 @@ const fetchSkills = () => {
|
|
|
360
559
|
}
|
|
361
560
|
}
|
|
362
561
|
}
|
|
363
|
-
} catch {
|
|
562
|
+
} catch (error) {
|
|
563
|
+
logError(`fetchSkills(${source.url})`, error);
|
|
564
|
+
} finally {
|
|
565
|
+
clearTimeout(timeout);
|
|
364
566
|
}
|
|
365
|
-
}
|
|
567
|
+
});
|
|
568
|
+
await Promise.all(promises);
|
|
366
569
|
return skills.sort((a, b) => a.label.localeCompare(b.label));
|
|
367
570
|
};
|
|
368
571
|
const SKILLS_DIR = path.join(os.homedir(), ".claude", "skills");
|
|
@@ -388,23 +591,42 @@ const addSkillToClaudeJson = (skillName, skillUrl) => {
|
|
|
388
591
|
};
|
|
389
592
|
const SkillsBrowser = () => {
|
|
390
593
|
const { exit } = useApp();
|
|
391
|
-
const [
|
|
594
|
+
const [allSkills, setAllSkills] = useState([]);
|
|
392
595
|
const [loading, setLoading] = useState(true);
|
|
596
|
+
const [offset, setOffset] = useState(0);
|
|
597
|
+
const SKILLS_PAGE_SIZE = 50;
|
|
393
598
|
useEffect(() => {
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
599
|
+
const loadSkills = async () => {
|
|
600
|
+
const s = await fetchSkills();
|
|
601
|
+
setAllSkills(s);
|
|
602
|
+
setLoading(false);
|
|
603
|
+
};
|
|
604
|
+
loadSkills();
|
|
397
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
|
+
});
|
|
398
618
|
if (loading) {
|
|
399
619
|
return /* @__PURE__ */ React.createElement(Box, { padding: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Loading skills..."));
|
|
400
620
|
}
|
|
401
|
-
if (
|
|
621
|
+
if (allSkills.length === 0) {
|
|
402
622
|
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "Could not fetch skills"));
|
|
403
623
|
}
|
|
404
|
-
|
|
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(
|
|
405
627
|
SelectInput,
|
|
406
628
|
{
|
|
407
|
-
items:
|
|
629
|
+
items: paginatedSkills,
|
|
408
630
|
onSelect: (item) => {
|
|
409
631
|
const result = addSkillToClaudeJson(item.label, item.value);
|
|
410
632
|
if (result.success) {
|
|
@@ -421,8 +643,91 @@ const SkillsBrowser = () => {
|
|
|
421
643
|
)));
|
|
422
644
|
};
|
|
423
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
|
+
}
|
|
424
685
|
render(/* @__PURE__ */ React.createElement(SkillsBrowser, null));
|
|
425
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
|
+
}
|
|
426
731
|
render(/* @__PURE__ */ React.createElement(McpSearch, null));
|
|
427
732
|
} else if (cmd === "new") {
|
|
428
733
|
const NewProfileWizard = () => {
|
|
@@ -433,6 +738,7 @@ if (cmd === "skills") {
|
|
|
433
738
|
const [apiKey, setApiKey] = useState("");
|
|
434
739
|
const [model, setModel] = useState("");
|
|
435
740
|
const [group, setGroup] = useState("");
|
|
741
|
+
const [validationErrors, setValidationErrors] = useState([]);
|
|
436
742
|
const providers = [
|
|
437
743
|
{ label: "Anthropic (Direct)", value: "anthropic", url: "", needsKey: true },
|
|
438
744
|
{ label: "Amazon Bedrock", value: "bedrock", url: "", needsKey: false },
|
|
@@ -455,6 +761,12 @@ if (cmd === "skills") {
|
|
|
455
761
|
alwaysThinkingEnabled: true,
|
|
456
762
|
defaultMode: "bypassPermissions"
|
|
457
763
|
};
|
|
764
|
+
const validation = validateProfile(profile);
|
|
765
|
+
if (!validation.valid) {
|
|
766
|
+
setStep("error");
|
|
767
|
+
setValidationErrors(validation.errors);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
458
770
|
const filename = name.toLowerCase().replace(/\s+/g, "-") + ".json";
|
|
459
771
|
fs.writeFileSync(path.join(PROFILES_DIR, filename), JSON.stringify(profile, null, 2));
|
|
460
772
|
console.log(`
|
|
@@ -466,7 +778,13 @@ if (cmd === "skills") {
|
|
|
466
778
|
const prov = providers.find((p) => p.value === item.value);
|
|
467
779
|
setStep(prov.needsKey ? "apikey" : "model");
|
|
468
780
|
};
|
|
469
|
-
|
|
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...")));
|
|
470
788
|
};
|
|
471
789
|
render(/* @__PURE__ */ React.createElement(NewProfileWizard, null));
|
|
472
790
|
} else {
|
|
@@ -486,28 +804,81 @@ if (cmd === "skills") {
|
|
|
486
804
|
clearInterval(colorInterval);
|
|
487
805
|
};
|
|
488
806
|
}, []);
|
|
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));
|
|
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));
|
|
495
808
|
};
|
|
496
809
|
const App = () => {
|
|
497
|
-
const [step, setStep] = useState("
|
|
810
|
+
const [step, setStep] = useState("loading");
|
|
498
811
|
const [updateInfo, setUpdateInfo] = useState(null);
|
|
499
812
|
const [filter, setFilter] = useState("");
|
|
813
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
814
|
+
const [showCommandPalette, setShowCommandPalette] = useState(false);
|
|
815
|
+
const [commandInput, setCommandInput] = useState("");
|
|
500
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]);
|
|
501
848
|
useEffect(() => {
|
|
502
849
|
setTimeout(() => setStep("select"), 1500);
|
|
503
850
|
if (!skipUpdate) {
|
|
504
|
-
|
|
505
|
-
const info = checkForUpdate();
|
|
506
|
-
setUpdateInfo(info);
|
|
507
|
-
});
|
|
851
|
+
checkForUpdate().then(setUpdateInfo);
|
|
508
852
|
}
|
|
509
853
|
}, []);
|
|
510
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
|
+
}
|
|
511
882
|
if (step === "select") {
|
|
512
883
|
const num = parseInt(input);
|
|
513
884
|
if (num >= 1 && num <= 9 && num <= filteredProfiles.length) {
|
|
@@ -521,13 +892,24 @@ if (cmd === "skills") {
|
|
|
521
892
|
if (input === "u" && updateInfo?.needsUpdate) {
|
|
522
893
|
console.log("\n\x1B[33mUpdating Claude...\x1B[0m\n");
|
|
523
894
|
try {
|
|
524
|
-
|
|
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
|
+
}
|
|
525
900
|
console.log("\n\x1B[32m\u2713 Updated!\x1B[0m\n");
|
|
526
901
|
setUpdateInfo({ ...updateInfo, needsUpdate: false });
|
|
527
|
-
} catch {
|
|
902
|
+
} catch (error) {
|
|
903
|
+
console.log("\x1B[31m\u2717 Update failed\x1B[0m\n");
|
|
904
|
+
logError("update", error);
|
|
528
905
|
}
|
|
529
906
|
}
|
|
530
|
-
if (input
|
|
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 !== "/") {
|
|
531
913
|
setFilter((f) => f + input);
|
|
532
914
|
}
|
|
533
915
|
if (key.backspace || key.delete) {
|
|
@@ -536,11 +918,29 @@ if (cmd === "skills") {
|
|
|
536
918
|
if (key.escape) {
|
|
537
919
|
setFilter("");
|
|
538
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);
|
|
539
942
|
}
|
|
540
943
|
});
|
|
541
|
-
const filteredProfiles = profiles.filter(
|
|
542
|
-
(p) => !filter || p.label.toLowerCase().includes(filter.toLowerCase())
|
|
543
|
-
);
|
|
544
944
|
const groupedItems = [];
|
|
545
945
|
const groups = [...new Set(filteredProfiles.map((p) => p.group).filter(Boolean))];
|
|
546
946
|
if (groups.length > 0) {
|
|
@@ -572,19 +972,14 @@ if (cmd === "skills") {
|
|
|
572
972
|
`);
|
|
573
973
|
launchClaude();
|
|
574
974
|
};
|
|
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(
|
|
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(
|
|
581
976
|
SelectInput,
|
|
582
977
|
{
|
|
583
978
|
items: groupedItems,
|
|
584
979
|
onSelect: handleSelect,
|
|
585
980
|
itemComponent: ({ isSelected, label, disabled }) => /* @__PURE__ */ React.createElement(Text, { color: disabled ? "gray" : isSelected ? "cyan" : "white", dimColor: disabled }, disabled ? label : (isSelected ? "\u276F " : " ") + label)
|
|
586
981
|
}
|
|
587
|
-
)));
|
|
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")));
|
|
588
983
|
};
|
|
589
984
|
render(/* @__PURE__ */ React.createElement(App, null));
|
|
590
985
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-manager",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.3",
|
|
4
4
|
"description": "Terminal app for managing Claude Code settings, profiles, MCP servers, and skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"build": "esbuild src/cli.js --platform=node --format=esm --loader:.js=jsx --outfile=dist/cli.js --packages=external && echo '#!/usr/bin/env node' | cat - dist/cli.js > dist/tmp && mv dist/tmp dist/cli.js && chmod +x dist/cli.js",
|
|
12
12
|
"prepublishOnly": "npm run build"
|
|
13
13
|
},
|
|
14
|
-
"files": [
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
15
17
|
"keywords": [
|
|
16
18
|
"claude",
|
|
17
19
|
"claude-code",
|
|
@@ -32,6 +34,7 @@
|
|
|
32
34
|
"node": ">=18"
|
|
33
35
|
},
|
|
34
36
|
"dependencies": {
|
|
37
|
+
"fuse.js": "^7.1.0",
|
|
35
38
|
"ink": "^5.1.0",
|
|
36
39
|
"ink-select-input": "^6.0.0",
|
|
37
40
|
"ink-text-input": "^6.0.0",
|