claudeup 3.7.2 → 3.9.0

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 (52) hide show
  1. package/package.json +1 -1
  2. package/src/data/settings-catalog.js +612 -0
  3. package/src/data/settings-catalog.ts +689 -0
  4. package/src/data/skill-repos.js +86 -0
  5. package/src/data/skill-repos.ts +97 -0
  6. package/src/services/plugin-manager.js +2 -0
  7. package/src/services/plugin-manager.ts +3 -0
  8. package/src/services/profiles.js +161 -0
  9. package/src/services/profiles.ts +225 -0
  10. package/src/services/settings-manager.js +108 -0
  11. package/src/services/settings-manager.ts +140 -0
  12. package/src/services/skills-manager.js +239 -0
  13. package/src/services/skills-manager.ts +328 -0
  14. package/src/services/skillsmp-client.js +67 -0
  15. package/src/services/skillsmp-client.ts +89 -0
  16. package/src/types/index.ts +101 -1
  17. package/src/ui/App.js +23 -18
  18. package/src/ui/App.tsx +27 -23
  19. package/src/ui/components/TabBar.js +9 -8
  20. package/src/ui/components/TabBar.tsx +15 -19
  21. package/src/ui/components/layout/ScreenLayout.js +8 -14
  22. package/src/ui/components/layout/ScreenLayout.tsx +51 -58
  23. package/src/ui/components/modals/ModalContainer.js +43 -11
  24. package/src/ui/components/modals/ModalContainer.tsx +44 -12
  25. package/src/ui/components/modals/SelectModal.js +4 -18
  26. package/src/ui/components/modals/SelectModal.tsx +10 -21
  27. package/src/ui/screens/CliToolsScreen.js +2 -2
  28. package/src/ui/screens/CliToolsScreen.tsx +8 -8
  29. package/src/ui/screens/EnvVarsScreen.js +248 -116
  30. package/src/ui/screens/EnvVarsScreen.tsx +419 -184
  31. package/src/ui/screens/McpRegistryScreen.tsx +18 -6
  32. package/src/ui/screens/McpScreen.js +1 -1
  33. package/src/ui/screens/McpScreen.tsx +15 -5
  34. package/src/ui/screens/ModelSelectorScreen.js +3 -5
  35. package/src/ui/screens/ModelSelectorScreen.tsx +12 -16
  36. package/src/ui/screens/PluginsScreen.js +154 -66
  37. package/src/ui/screens/PluginsScreen.tsx +280 -97
  38. package/src/ui/screens/ProfilesScreen.js +255 -0
  39. package/src/ui/screens/ProfilesScreen.tsx +487 -0
  40. package/src/ui/screens/SkillsScreen.js +325 -0
  41. package/src/ui/screens/SkillsScreen.tsx +574 -0
  42. package/src/ui/screens/StatusLineScreen.js +2 -2
  43. package/src/ui/screens/StatusLineScreen.tsx +10 -12
  44. package/src/ui/screens/index.js +3 -2
  45. package/src/ui/screens/index.ts +3 -2
  46. package/src/ui/state/AppContext.js +2 -1
  47. package/src/ui/state/AppContext.tsx +2 -0
  48. package/src/ui/state/reducer.js +151 -19
  49. package/src/ui/state/reducer.ts +167 -19
  50. package/src/ui/state/types.ts +58 -14
  51. package/src/utils/clipboard.js +56 -0
  52. package/src/utils/clipboard.ts +58 -0
@@ -0,0 +1,108 @@
1
+ import { readSettings, writeSettings, readGlobalSettings, writeGlobalSettings, } from "./claude-settings.js";
2
+ /** Read the current value of a setting from the given scope */
3
+ export async function readSettingValue(setting, scope, projectPath) {
4
+ const settings = scope === "user"
5
+ ? await readGlobalSettings()
6
+ : await readSettings(projectPath);
7
+ if (setting.storage.type === "env") {
8
+ const env = settings.env;
9
+ return env?.[setting.storage.key];
10
+ }
11
+ else {
12
+ // Direct settings key (supports dot notation like "permissions.defaultMode")
13
+ const keys = setting.storage.key.split(".");
14
+ let value = settings;
15
+ for (const k of keys) {
16
+ value = value?.[k];
17
+ }
18
+ return value !== undefined && value !== null ? String(value) : undefined;
19
+ }
20
+ }
21
+ /** Write a setting value to the given scope */
22
+ export async function writeSettingValue(setting, value, scope, projectPath) {
23
+ const settings = scope === "user"
24
+ ? await readGlobalSettings()
25
+ : await readSettings(projectPath);
26
+ if (setting.storage.type === "env") {
27
+ // Write to the "env" block in settings.json
28
+ settings.env = settings.env || {};
29
+ if (value === undefined || value === "" || value === setting.defaultValue) {
30
+ delete settings.env[setting.storage.key];
31
+ // Clean up empty env block
32
+ if (Object.keys(settings.env).length === 0) {
33
+ delete settings.env;
34
+ }
35
+ }
36
+ else {
37
+ settings.env[setting.storage.key] = value;
38
+ }
39
+ }
40
+ else {
41
+ // Direct settings key (supports dot notation)
42
+ const keys = setting.storage.key.split(".");
43
+ if (keys.length === 1) {
44
+ if (value === undefined || value === "") {
45
+ delete settings[keys[0]];
46
+ }
47
+ else {
48
+ // Try to preserve type: boolean, number, or string
49
+ settings[keys[0]] = parseSettingValue(value, setting.type);
50
+ }
51
+ }
52
+ else {
53
+ // Nested key like "permissions.defaultMode"
54
+ let obj = settings;
55
+ for (let i = 0; i < keys.length - 1; i++) {
56
+ obj[keys[i]] = obj[keys[i]] || {};
57
+ obj = obj[keys[i]];
58
+ }
59
+ const lastKey = keys[keys.length - 1];
60
+ if (value === undefined || value === "") {
61
+ delete obj[lastKey];
62
+ }
63
+ else {
64
+ obj[lastKey] = parseSettingValue(value, setting.type);
65
+ }
66
+ }
67
+ }
68
+ if (scope === "user") {
69
+ await writeGlobalSettings(settings);
70
+ }
71
+ else {
72
+ await writeSettings(settings, projectPath);
73
+ }
74
+ }
75
+ function parseSettingValue(value, type) {
76
+ if (type === "boolean") {
77
+ return value === "true" || value === "1";
78
+ }
79
+ // Try number
80
+ const num = Number(value);
81
+ if (!Number.isNaN(num) && value.trim() !== "") {
82
+ return value; // Keep as string for env vars
83
+ }
84
+ return value;
85
+ }
86
+ /** Read all setting values for the catalog */
87
+ export async function readAllSettings(catalog, scope, projectPath) {
88
+ const values = new Map();
89
+ for (const setting of catalog) {
90
+ values.set(setting.id, await readSettingValue(setting, scope, projectPath));
91
+ }
92
+ return values;
93
+ }
94
+ /** Read all setting values from both scopes simultaneously */
95
+ export async function readAllSettingsBothScopes(catalog, projectPath) {
96
+ const result = new Map();
97
+ const [userValues, projectValues] = await Promise.all([
98
+ readAllSettings(catalog, "user", projectPath),
99
+ readAllSettings(catalog, "project", projectPath),
100
+ ]);
101
+ for (const setting of catalog) {
102
+ result.set(setting.id, {
103
+ user: userValues.get(setting.id),
104
+ project: projectValues.get(setting.id),
105
+ });
106
+ }
107
+ return result;
108
+ }
@@ -0,0 +1,140 @@
1
+ import {
2
+ readSettings,
3
+ writeSettings,
4
+ readGlobalSettings,
5
+ writeGlobalSettings,
6
+ } from "./claude-settings.js";
7
+ import type { SettingDefinition } from "../data/settings-catalog.js";
8
+
9
+ export type SettingScope = "user" | "project";
10
+
11
+ /** Read the current value of a setting from the given scope */
12
+ export async function readSettingValue(
13
+ setting: SettingDefinition,
14
+ scope: SettingScope,
15
+ projectPath?: string,
16
+ ): Promise<string | undefined> {
17
+ const settings =
18
+ scope === "user"
19
+ ? await readGlobalSettings()
20
+ : await readSettings(projectPath);
21
+
22
+ if (setting.storage.type === "env") {
23
+ const env = (settings as any).env as Record<string, string> | undefined;
24
+ return env?.[setting.storage.key];
25
+ } else {
26
+ // Direct settings key (supports dot notation like "permissions.defaultMode")
27
+ const keys = setting.storage.key.split(".");
28
+ let value: any = settings;
29
+ for (const k of keys) {
30
+ value = value?.[k];
31
+ }
32
+ return value !== undefined && value !== null ? String(value) : undefined;
33
+ }
34
+ }
35
+
36
+ /** Write a setting value to the given scope */
37
+ export async function writeSettingValue(
38
+ setting: SettingDefinition,
39
+ value: string | undefined,
40
+ scope: SettingScope,
41
+ projectPath?: string,
42
+ ): Promise<void> {
43
+ const settings =
44
+ scope === "user"
45
+ ? await readGlobalSettings()
46
+ : await readSettings(projectPath);
47
+
48
+ if (setting.storage.type === "env") {
49
+ // Write to the "env" block in settings.json
50
+ (settings as any).env = (settings as any).env || {};
51
+ if (value === undefined || value === "" || value === setting.defaultValue) {
52
+ delete (settings as any).env[setting.storage.key];
53
+ // Clean up empty env block
54
+ if (Object.keys((settings as any).env).length === 0) {
55
+ delete (settings as any).env;
56
+ }
57
+ } else {
58
+ (settings as any).env[setting.storage.key] = value;
59
+ }
60
+ } else {
61
+ // Direct settings key (supports dot notation)
62
+ const keys = setting.storage.key.split(".");
63
+ if (keys.length === 1) {
64
+ if (value === undefined || value === "") {
65
+ delete (settings as any)[keys[0]];
66
+ } else {
67
+ // Try to preserve type: boolean, number, or string
68
+ (settings as any)[keys[0]] = parseSettingValue(value, setting.type);
69
+ }
70
+ } else {
71
+ // Nested key like "permissions.defaultMode"
72
+ let obj: any = settings;
73
+ for (let i = 0; i < keys.length - 1; i++) {
74
+ obj[keys[i]] = obj[keys[i]] || {};
75
+ obj = obj[keys[i]];
76
+ }
77
+ const lastKey = keys[keys.length - 1];
78
+ if (value === undefined || value === "") {
79
+ delete obj[lastKey];
80
+ } else {
81
+ obj[lastKey] = parseSettingValue(value, setting.type);
82
+ }
83
+ }
84
+ }
85
+
86
+ if (scope === "user") {
87
+ await writeGlobalSettings(settings);
88
+ } else {
89
+ await writeSettings(settings, projectPath);
90
+ }
91
+ }
92
+
93
+ function parseSettingValue(value: string, type: string): any {
94
+ if (type === "boolean") {
95
+ return value === "true" || value === "1";
96
+ }
97
+ // Try number
98
+ const num = Number(value);
99
+ if (!Number.isNaN(num) && value.trim() !== "") {
100
+ return value; // Keep as string for env vars
101
+ }
102
+ return value;
103
+ }
104
+
105
+ /** Read all setting values for the catalog */
106
+ export async function readAllSettings(
107
+ catalog: SettingDefinition[],
108
+ scope: SettingScope,
109
+ projectPath?: string,
110
+ ): Promise<Map<string, string | undefined>> {
111
+ const values = new Map<string, string | undefined>();
112
+ for (const setting of catalog) {
113
+ values.set(setting.id, await readSettingValue(setting, scope, projectPath));
114
+ }
115
+ return values;
116
+ }
117
+
118
+ export interface ScopedSettingValues {
119
+ user: string | undefined;
120
+ project: string | undefined;
121
+ }
122
+
123
+ /** Read all setting values from both scopes simultaneously */
124
+ export async function readAllSettingsBothScopes(
125
+ catalog: SettingDefinition[],
126
+ projectPath?: string,
127
+ ): Promise<Map<string, ScopedSettingValues>> {
128
+ const result = new Map<string, ScopedSettingValues>();
129
+ const [userValues, projectValues] = await Promise.all([
130
+ readAllSettings(catalog, "user", projectPath),
131
+ readAllSettings(catalog, "project", projectPath),
132
+ ]);
133
+ for (const setting of catalog) {
134
+ result.set(setting.id, {
135
+ user: userValues.get(setting.id),
136
+ project: projectValues.get(setting.id),
137
+ });
138
+ }
139
+ return result;
140
+ }
@@ -0,0 +1,239 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ const treeCache = new Map();
5
+ // ─── Path helpers ──────────────────────────────────────────────────────────────
6
+ export function getUserSkillsDir() {
7
+ return path.join(os.homedir(), ".claude", "skills");
8
+ }
9
+ export function getProjectSkillsDir(projectPath) {
10
+ return path.join(projectPath ?? process.cwd(), ".claude", "skills");
11
+ }
12
+ // ─── GitHub Tree API ──────────────────────────────────────────────────────────
13
+ async function fetchGitTree(repo) {
14
+ const cached = treeCache.get(repo);
15
+ const url = `https://api.github.com/repos/${repo}/git/trees/HEAD?recursive=1`;
16
+ const headers = {
17
+ Accept: "application/vnd.github+json",
18
+ "X-GitHub-Api-Version": "2022-11-28",
19
+ };
20
+ if (cached?.etag) {
21
+ headers["If-None-Match"] = cached.etag;
22
+ }
23
+ const token = process.env.GITHUB_TOKEN || process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
24
+ if (token) {
25
+ headers.Authorization = `Bearer ${token}`;
26
+ }
27
+ const response = await fetch(url, {
28
+ headers,
29
+ signal: AbortSignal.timeout(10000),
30
+ });
31
+ if (response.status === 304 && cached) {
32
+ return cached.tree;
33
+ }
34
+ if (response.status === 403 || response.status === 429) {
35
+ const resetHeader = response.headers.get("X-RateLimit-Reset");
36
+ const resetTime = resetHeader
37
+ ? new Date(Number(resetHeader) * 1000).toLocaleTimeString()
38
+ : "unknown";
39
+ throw new Error(`GitHub API rate limit exceeded. Resets at ${resetTime}. Set GITHUB_TOKEN to increase limits.`);
40
+ }
41
+ if (!response.ok) {
42
+ throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
43
+ }
44
+ const etag = response.headers.get("ETag") || "";
45
+ const tree = (await response.json());
46
+ treeCache.set(repo, { etag, tree, fetchedAt: Date.now() });
47
+ return tree;
48
+ }
49
+ // ─── Frontmatter parser ───────────────────────────────────────────────────────
50
+ function parseYamlFrontmatter(content) {
51
+ const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
52
+ if (!frontmatterMatch)
53
+ return {};
54
+ const yaml = frontmatterMatch[1];
55
+ const result = {};
56
+ for (const line of yaml.split("\n")) {
57
+ const colonIdx = line.indexOf(":");
58
+ if (colonIdx === -1)
59
+ continue;
60
+ const key = line.slice(0, colonIdx).trim();
61
+ const rawValue = line.slice(colonIdx + 1).trim();
62
+ if (!key)
63
+ continue;
64
+ // Handle arrays (simple inline: [a, b, c])
65
+ if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
66
+ const items = rawValue
67
+ .slice(1, -1)
68
+ .split(",")
69
+ .map((s) => s.trim().replace(/^["']|["']$/g, ""))
70
+ .filter(Boolean);
71
+ result[key] = items;
72
+ }
73
+ else {
74
+ // Strip quotes
75
+ result[key] = rawValue.replace(/^["']|["']$/g, "");
76
+ }
77
+ }
78
+ return result;
79
+ }
80
+ export async function fetchSkillFrontmatter(skill) {
81
+ const url = `https://raw.githubusercontent.com/${skill.source.repo}/HEAD/${skill.repoPath}`;
82
+ const response = await fetch(url, {
83
+ signal: AbortSignal.timeout(10000),
84
+ });
85
+ if (!response.ok) {
86
+ return {
87
+ name: skill.name,
88
+ description: "(no description)",
89
+ category: "general",
90
+ };
91
+ }
92
+ const content = await response.text();
93
+ const parsed = parseYamlFrontmatter(content);
94
+ return {
95
+ name: parsed.name || skill.name,
96
+ description: parsed.description || "(no description)",
97
+ category: parsed.category,
98
+ author: parsed.author,
99
+ version: parsed.version,
100
+ tags: parsed.tags,
101
+ };
102
+ }
103
+ // ─── Check installation ───────────────────────────────────────────────────────
104
+ export async function getInstalledSkillNames(scope, projectPath) {
105
+ const dir = scope === "user"
106
+ ? getUserSkillsDir()
107
+ : getProjectSkillsDir(projectPath);
108
+ const installed = new Set();
109
+ try {
110
+ if (!(await fs.pathExists(dir)))
111
+ return installed;
112
+ const entries = await fs.readdir(dir);
113
+ for (const entry of entries) {
114
+ const skillMd = path.join(dir, entry, "SKILL.md");
115
+ if (await fs.pathExists(skillMd)) {
116
+ installed.add(entry);
117
+ }
118
+ }
119
+ }
120
+ catch {
121
+ // ignore
122
+ }
123
+ return installed;
124
+ }
125
+ // ─── Fetch available skills ───────────────────────────────────────────────────
126
+ export async function fetchAvailableSkills(repos, projectPath) {
127
+ const userInstalled = await getInstalledSkillNames("user");
128
+ const projectInstalled = await getInstalledSkillNames("project", projectPath);
129
+ const skills = [];
130
+ for (const source of repos) {
131
+ let tree;
132
+ try {
133
+ tree = await fetchGitTree(source.repo);
134
+ }
135
+ catch {
136
+ // Skip this repo on error
137
+ continue;
138
+ }
139
+ // Filter for SKILL.md files under skillsPath
140
+ const prefix = source.skillsPath ? `${source.skillsPath}/` : "";
141
+ for (const item of tree.tree) {
142
+ if (item.type !== "blob")
143
+ continue;
144
+ if (!item.path.endsWith("/SKILL.md"))
145
+ continue;
146
+ if (prefix && !item.path.startsWith(prefix))
147
+ continue;
148
+ // Extract skill name: second-to-last segment of path
149
+ const parts = item.path.split("/");
150
+ if (parts.length < 2)
151
+ continue;
152
+ const skillName = parts[parts.length - 2];
153
+ // Validate name (prevent traversal)
154
+ if (!/^[a-z0-9][a-z0-9-_]*$/i.test(skillName))
155
+ continue;
156
+ const isUserInstalled = userInstalled.has(skillName);
157
+ const isProjInstalled = projectInstalled.has(skillName);
158
+ const installed = isUserInstalled || isProjInstalled;
159
+ const installedScope = isProjInstalled
160
+ ? "project"
161
+ : isUserInstalled
162
+ ? "user"
163
+ : null;
164
+ skills.push({
165
+ id: `${source.repo}/${item.path}`,
166
+ name: skillName,
167
+ source,
168
+ repoPath: item.path,
169
+ gitBlobSha: item.sha,
170
+ frontmatter: null,
171
+ installed,
172
+ installedScope,
173
+ hasUpdate: false,
174
+ });
175
+ }
176
+ }
177
+ // Sort by name within each repo
178
+ skills.sort((a, b) => a.name.localeCompare(b.name));
179
+ return skills;
180
+ }
181
+ // ─── Install / Uninstall ──────────────────────────────────────────────────────
182
+ export async function installSkill(skill, scope, projectPath) {
183
+ const url = `https://raw.githubusercontent.com/${skill.source.repo}/HEAD/${skill.repoPath}`;
184
+ const response = await fetch(url, {
185
+ signal: AbortSignal.timeout(15000),
186
+ });
187
+ if (!response.ok) {
188
+ throw new Error(`Failed to fetch skill: ${response.status} ${response.statusText}`);
189
+ }
190
+ const content = await response.text();
191
+ const installDir = scope === "user"
192
+ ? path.join(getUserSkillsDir(), skill.name)
193
+ : path.join(getProjectSkillsDir(projectPath), skill.name);
194
+ await fs.ensureDir(installDir);
195
+ await fs.writeFile(path.join(installDir, "SKILL.md"), content, "utf8");
196
+ }
197
+ export async function uninstallSkill(skillName, scope, projectPath) {
198
+ const installDir = scope === "user"
199
+ ? path.join(getUserSkillsDir(), skillName)
200
+ : path.join(getProjectSkillsDir(projectPath), skillName);
201
+ const skillMdPath = path.join(installDir, "SKILL.md");
202
+ if (await fs.pathExists(skillMdPath)) {
203
+ await fs.remove(skillMdPath);
204
+ }
205
+ // Try to remove directory if empty
206
+ try {
207
+ await fs.rmdir(installDir);
208
+ }
209
+ catch {
210
+ // Ignore if not empty or doesn't exist
211
+ }
212
+ }
213
+ // ─── Check installed skills from file system ──────────────────────────────────
214
+ export async function getInstalledSkillsFromFs(projectPath) {
215
+ const result = [];
216
+ const userDir = getUserSkillsDir();
217
+ const projDir = getProjectSkillsDir(projectPath);
218
+ const scopedDirs = [
219
+ [userDir, "user"],
220
+ [projDir, "project"],
221
+ ];
222
+ for (const [dir, scope] of scopedDirs) {
223
+ try {
224
+ if (!(await fs.pathExists(dir)))
225
+ continue;
226
+ const entries = await fs.readdir(dir);
227
+ for (const entry of entries) {
228
+ const skillMd = path.join(dir, entry, "SKILL.md");
229
+ if (await fs.pathExists(skillMd)) {
230
+ result.push({ name: entry, scope });
231
+ }
232
+ }
233
+ }
234
+ catch {
235
+ // ignore
236
+ }
237
+ }
238
+ return result;
239
+ }