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.
Files changed (2) hide show
  1. package/dist/cli.js +470 -75
  2. 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
- const VERSION = "1.5.1";
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) Select profile interactively
30
- new Create a new profile
31
- edit <n> Edit profile (by name or number)
32
- delete <n> Delete profile (by name or number)
33
- status Show current settings
34
- list List all profiles
35
- mcp [query] Search and add MCP servers
36
- skills Browse and add Anthropic skills
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 checkForUpdate = () => {
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 current = execSync("claude --version 2>/dev/null", { encoding: "utf8" }).match(/(\d+\.\d+\.\d+)/)?.[1];
104
- const output = execSync("brew outdated claude-code 2>&1 || true", { encoding: "utf8" }).trim();
105
- return { current, needsUpdate: output.includes("claude-code") };
106
- } catch {
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(`\x1B[31mProfile not found: ${target}\x1B[0m`);
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
- const searchMcpServers = (query) => {
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 = execSync(`curl -s "${MCP_REGISTRY_URL}?limit=100"`, { encoding: "utf8", timeout: 1e4 });
232
- const data = JSON.parse(res);
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
- return data.servers.filter((s) => {
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
- }).slice(0, 15);
241
- } catch {
242
- return [];
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 [servers, setServers] = useState([]);
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
- if (args[1] && step === "loading") {
285
- const results = searchMcpServers(args[1]);
286
- setServers(results);
287
- setStep("results");
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
- const results = searchMcpServers(query);
292
- setServers(results);
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 serverItems = servers.map((s) => ({
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
- 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 }, "Found ", servers.length, " servers"), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React.createElement(
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
- for (const source of SKILL_SOURCES) {
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 = execSync(`curl -s "${source.url}"`, { encoding: "utf8", timeout: 1e4 });
350
- const data = JSON.parse(res);
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 [skills, setSkills] = useState([]);
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 s = fetchSkills();
395
- setSkills(s);
396
- setLoading(false);
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 (skills.length === 0) {
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
- 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 }, "Found ", skills.length, " skills"), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React.createElement(
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: skills,
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
- 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 })));
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] }, `\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
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("select");
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
- Promise.resolve().then(() => {
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
- execSync("brew upgrade claude-code", { stdio: "inherit" });
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.match(/^[a-zA-Z]$/) && input !== "u") {
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" }, `\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
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.1",
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": ["dist"],
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",