claude-code-templates 1.26.4 ā 1.28.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.
- package/bin/create-claude-config.js +1 -0
- package/components/sandbox/docker/Dockerfile +38 -0
- package/components/sandbox/docker/README.md +453 -0
- package/components/sandbox/docker/docker-launcher.js +184 -0
- package/components/sandbox/docker/execute.js +251 -0
- package/components/sandbox/docker/package.json +26 -0
- package/package.json +2 -1
- package/src/index.js +294 -24
- package/src/skill-dashboard-web/index.html +326 -0
- package/src/skill-dashboard-web/script.js +445 -0
- package/src/skill-dashboard-web/styles.css +3469 -0
- package/src/skill-dashboard.js +441 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const express = require('express');
|
|
5
|
+
const open = require('open');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const yaml = require('js-yaml');
|
|
8
|
+
|
|
9
|
+
class SkillDashboard {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.options = options;
|
|
12
|
+
this.app = express();
|
|
13
|
+
this.port = 3337;
|
|
14
|
+
this.httpServer = null;
|
|
15
|
+
this.homeDir = os.homedir();
|
|
16
|
+
this.claudeDir = path.join(this.homeDir, '.claude');
|
|
17
|
+
this.personalSkillsDir = path.join(this.claudeDir, 'skills');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async initialize() {
|
|
21
|
+
// Load skills data
|
|
22
|
+
await this.loadSkillsData();
|
|
23
|
+
this.setupWebServer();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async loadSkillsData() {
|
|
27
|
+
try {
|
|
28
|
+
// Load personal skills
|
|
29
|
+
this.personalSkills = await this.loadSkillsFromDirectory(this.personalSkillsDir, 'Personal');
|
|
30
|
+
|
|
31
|
+
// Load project skills (if in a project directory)
|
|
32
|
+
const projectSkillsDir = path.join(process.cwd(), '.claude', 'skills');
|
|
33
|
+
this.projectSkills = await this.loadSkillsFromDirectory(projectSkillsDir, 'Project');
|
|
34
|
+
|
|
35
|
+
// Combine all skills
|
|
36
|
+
this.skills = [...this.personalSkills, ...this.projectSkills];
|
|
37
|
+
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(chalk.red('Error loading skills data:'), error.message);
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async loadSkillsFromDirectory(skillsDir, source) {
|
|
45
|
+
const skills = [];
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
if (!(await fs.pathExists(skillsDir))) {
|
|
49
|
+
if (this.options.verbose) {
|
|
50
|
+
console.log(chalk.yellow(`Skills directory not found: ${skillsDir}`));
|
|
51
|
+
}
|
|
52
|
+
return skills;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const skillDirs = await fs.readdir(skillsDir);
|
|
56
|
+
|
|
57
|
+
for (const skillDir of skillDirs) {
|
|
58
|
+
const skillPath = path.join(skillsDir, skillDir);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const stat = await fs.stat(skillPath);
|
|
62
|
+
if (!stat.isDirectory()) continue;
|
|
63
|
+
|
|
64
|
+
// Look for SKILL.md
|
|
65
|
+
const skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
66
|
+
|
|
67
|
+
if (await fs.pathExists(skillMdPath)) {
|
|
68
|
+
const skillData = await this.parseSkill(skillMdPath, skillPath, skillDir, source);
|
|
69
|
+
if (skillData) {
|
|
70
|
+
skills.push(skillData);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.warn(chalk.yellow(`Warning: Error loading skill ${skillDir}`), error.message);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return skills;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.warn(chalk.yellow(`Warning: Error loading skills from ${skillsDir}`), error.message);
|
|
81
|
+
return skills;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async parseSkill(skillMdPath, skillPath, skillDirName, source) {
|
|
86
|
+
try {
|
|
87
|
+
const content = await fs.readFile(skillMdPath, 'utf8');
|
|
88
|
+
|
|
89
|
+
// Parse YAML frontmatter
|
|
90
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
91
|
+
let frontmatter = {};
|
|
92
|
+
let markdownContent = content;
|
|
93
|
+
|
|
94
|
+
if (frontmatterMatch) {
|
|
95
|
+
try {
|
|
96
|
+
frontmatter = yaml.load(frontmatterMatch[1]);
|
|
97
|
+
markdownContent = content.substring(frontmatterMatch[0].length).trim();
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.warn(chalk.yellow(`Warning: Could not parse YAML frontmatter for ${skillDirName}`));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Get file stats
|
|
104
|
+
const stats = await fs.stat(skillMdPath);
|
|
105
|
+
|
|
106
|
+
// Scan for supporting files
|
|
107
|
+
const supportingFiles = await this.scanSupportingFiles(skillPath);
|
|
108
|
+
|
|
109
|
+
// Categorize files by loading strategy
|
|
110
|
+
const categorizedFiles = this.categorizeFiles(supportingFiles, markdownContent);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
name: frontmatter.name || skillDirName,
|
|
114
|
+
description: frontmatter.description || 'No description available',
|
|
115
|
+
allowedTools: frontmatter['allowed-tools'] || frontmatter.allowedTools || null,
|
|
116
|
+
source,
|
|
117
|
+
path: skillPath,
|
|
118
|
+
mainFile: 'SKILL.md',
|
|
119
|
+
mainFilePath: skillMdPath,
|
|
120
|
+
mainFileSize: this.formatFileSize(stats.size),
|
|
121
|
+
lastModified: stats.mtime,
|
|
122
|
+
fileCount: supportingFiles.length + 1, // +1 for SKILL.md
|
|
123
|
+
supportingFiles: categorizedFiles,
|
|
124
|
+
rawContent: content,
|
|
125
|
+
markdownContent
|
|
126
|
+
};
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.warn(chalk.yellow(`Warning: Error parsing skill ${skillDirName}`), error.message);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async scanSupportingFiles(skillPath) {
|
|
134
|
+
const files = [];
|
|
135
|
+
const self = this; // Preserve 'this' context
|
|
136
|
+
|
|
137
|
+
async function scanDirectory(dir, relativePath = '') {
|
|
138
|
+
const entries = await fs.readdir(dir);
|
|
139
|
+
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
const fullPath = path.join(dir, entry);
|
|
142
|
+
const relPath = relativePath ? path.join(relativePath, entry) : entry;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const stat = await fs.stat(fullPath);
|
|
146
|
+
|
|
147
|
+
if (stat.isDirectory()) {
|
|
148
|
+
await scanDirectory(fullPath, relPath);
|
|
149
|
+
} else if (entry !== 'SKILL.md') {
|
|
150
|
+
files.push({
|
|
151
|
+
name: entry,
|
|
152
|
+
path: fullPath,
|
|
153
|
+
relativePath: relPath,
|
|
154
|
+
size: stat.size,
|
|
155
|
+
isDirectory: false,
|
|
156
|
+
type: self.getFileType(entry) // Use self instead of this
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
// Skip files we can't read
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
await scanDirectory(skillPath);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.warn(chalk.yellow(`Warning: Error scanning skill directory ${skillPath}`), error.message);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return files;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
categorizeFiles(files, markdownContent) {
|
|
175
|
+
const categorized = {
|
|
176
|
+
alwaysLoaded: ['SKILL.md'],
|
|
177
|
+
onDemand: [],
|
|
178
|
+
progressive: []
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Parse referenced files from markdown content
|
|
182
|
+
const referencedFiles = this.extractReferencedFiles(markdownContent);
|
|
183
|
+
|
|
184
|
+
for (const file of files) {
|
|
185
|
+
const ext = path.extname(file.name).toLowerCase();
|
|
186
|
+
|
|
187
|
+
// Check if file is referenced in SKILL.md
|
|
188
|
+
const isReferenced = referencedFiles.some(ref =>
|
|
189
|
+
file.relativePath.includes(ref) || ref.includes(file.name)
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Categorize based on file type and references
|
|
193
|
+
if (isReferenced && ext === '.md') {
|
|
194
|
+
categorized.onDemand.push(file);
|
|
195
|
+
} else if (ext === '.md') {
|
|
196
|
+
categorized.onDemand.push(file);
|
|
197
|
+
} else if (file.relativePath.startsWith('scripts/') ||
|
|
198
|
+
file.relativePath.startsWith('templates/') ||
|
|
199
|
+
ext === '.py' || ext === '.js' || ext === '.sh') {
|
|
200
|
+
categorized.progressive.push(file);
|
|
201
|
+
} else {
|
|
202
|
+
categorized.onDemand.push(file);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return categorized;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
extractReferencedFiles(markdownContent) {
|
|
210
|
+
const references = [];
|
|
211
|
+
|
|
212
|
+
// Match markdown links: [text](file.md)
|
|
213
|
+
const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
214
|
+
let match;
|
|
215
|
+
|
|
216
|
+
while ((match = linkPattern.exec(markdownContent)) !== null) {
|
|
217
|
+
const href = match[2];
|
|
218
|
+
// Only include relative file references (not URLs)
|
|
219
|
+
if (!href.startsWith('http://') && !href.startsWith('https://') && !href.startsWith('#')) {
|
|
220
|
+
references.push(href);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return references;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
getFileType(filename) {
|
|
228
|
+
const ext = path.extname(filename).toLowerCase();
|
|
229
|
+
const typeMap = {
|
|
230
|
+
'.md': 'markdown',
|
|
231
|
+
'.py': 'python',
|
|
232
|
+
'.js': 'javascript',
|
|
233
|
+
'.ts': 'typescript',
|
|
234
|
+
'.sh': 'shell',
|
|
235
|
+
'.json': 'json',
|
|
236
|
+
'.yaml': 'yaml',
|
|
237
|
+
'.yml': 'yaml',
|
|
238
|
+
'.txt': 'text',
|
|
239
|
+
'.html': 'html',
|
|
240
|
+
'.css': 'css'
|
|
241
|
+
};
|
|
242
|
+
return typeMap[ext] || 'unknown';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
formatFileSize(bytes) {
|
|
246
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
247
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
248
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
setupWebServer() {
|
|
252
|
+
// Add CORS middleware
|
|
253
|
+
this.app.use((req, res, next) => {
|
|
254
|
+
res.header('Access-Control-Allow-Origin', '*');
|
|
255
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
256
|
+
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
|
257
|
+
|
|
258
|
+
if (req.method === 'OPTIONS') {
|
|
259
|
+
res.sendStatus(200);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
next();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// JSON middleware
|
|
267
|
+
this.app.use(express.json());
|
|
268
|
+
|
|
269
|
+
// Serve static files
|
|
270
|
+
this.app.use(express.static(path.join(__dirname, 'skill-dashboard-web')));
|
|
271
|
+
|
|
272
|
+
// API endpoints - reload data on each request
|
|
273
|
+
this.app.get('/api/skills', async (req, res) => {
|
|
274
|
+
try {
|
|
275
|
+
await this.loadSkillsData();
|
|
276
|
+
res.json({
|
|
277
|
+
skills: this.skills || [],
|
|
278
|
+
count: (this.skills || []).length,
|
|
279
|
+
timestamp: new Date().toISOString()
|
|
280
|
+
});
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.error('Error loading skills:', error);
|
|
283
|
+
res.status(500).json({ error: 'Failed to load skills' });
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
this.app.get('/api/skills/:name', async (req, res) => {
|
|
288
|
+
try {
|
|
289
|
+
await this.loadSkillsData();
|
|
290
|
+
const skillName = req.params.name;
|
|
291
|
+
const skill = this.skills.find(s =>
|
|
292
|
+
s.name === skillName ||
|
|
293
|
+
s.name.toLowerCase().replace(/\s+/g, '-') === skillName.toLowerCase()
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
if (!skill) {
|
|
297
|
+
return res.status(404).json({ error: 'Skill not found' });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
res.json({
|
|
301
|
+
skill,
|
|
302
|
+
timestamp: new Date().toISOString()
|
|
303
|
+
});
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.error('Error loading skill:', error);
|
|
306
|
+
res.status(500).json({ error: 'Failed to load skill' });
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
this.app.get('/api/skills/:name/file/*', async (req, res) => {
|
|
311
|
+
try {
|
|
312
|
+
const skillName = req.params.name;
|
|
313
|
+
const filePath = req.params[0]; // Capture the wildcard path
|
|
314
|
+
|
|
315
|
+
await this.loadSkillsData();
|
|
316
|
+
const skill = this.skills.find(s =>
|
|
317
|
+
s.name === skillName ||
|
|
318
|
+
s.name.toLowerCase().replace(/\s+/g, '-') === skillName.toLowerCase()
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
if (!skill) {
|
|
322
|
+
return res.status(404).json({ error: 'Skill not found' });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const fullPath = path.join(skill.path, filePath);
|
|
326
|
+
|
|
327
|
+
// Security check: ensure the file is within the skill directory
|
|
328
|
+
const normalizedPath = path.normalize(fullPath);
|
|
329
|
+
if (!normalizedPath.startsWith(skill.path)) {
|
|
330
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!(await fs.pathExists(fullPath))) {
|
|
334
|
+
return res.status(404).json({ error: 'File not found' });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const content = await fs.readFile(fullPath, 'utf8');
|
|
338
|
+
const stats = await fs.stat(fullPath);
|
|
339
|
+
|
|
340
|
+
res.json({
|
|
341
|
+
content,
|
|
342
|
+
path: filePath,
|
|
343
|
+
size: this.formatFileSize(stats.size),
|
|
344
|
+
lastModified: stats.mtime,
|
|
345
|
+
timestamp: new Date().toISOString()
|
|
346
|
+
});
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.error('Error loading file:', error);
|
|
349
|
+
res.status(500).json({ error: 'Failed to load file' });
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
this.app.get('/api/summary', async (req, res) => {
|
|
354
|
+
try {
|
|
355
|
+
await this.loadSkillsData();
|
|
356
|
+
const personalCount = this.skills.filter(s => s.source === 'Personal').length;
|
|
357
|
+
const projectCount = this.skills.filter(s => s.source === 'Project').length;
|
|
358
|
+
const pluginCount = this.skills.filter(s => s.source === 'Plugin').length;
|
|
359
|
+
|
|
360
|
+
res.json({
|
|
361
|
+
total: this.skills.length,
|
|
362
|
+
personal: personalCount,
|
|
363
|
+
project: projectCount,
|
|
364
|
+
plugin: pluginCount,
|
|
365
|
+
timestamp: new Date().toISOString()
|
|
366
|
+
});
|
|
367
|
+
} catch (error) {
|
|
368
|
+
console.error('Error loading summary:', error);
|
|
369
|
+
res.status(500).json({ error: 'Failed to load summary' });
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Main route
|
|
374
|
+
this.app.get('/', (req, res) => {
|
|
375
|
+
res.sendFile(path.join(__dirname, 'skill-dashboard-web', 'index.html'));
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async startServer() {
|
|
380
|
+
return new Promise((resolve) => {
|
|
381
|
+
this.httpServer = this.app.listen(this.port, async () => {
|
|
382
|
+
console.log(chalk.green(`šÆ Skills dashboard started at http://localhost:${this.port}`));
|
|
383
|
+
resolve();
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async openBrowser() {
|
|
389
|
+
const url = `http://localhost:${this.port}`;
|
|
390
|
+
console.log(chalk.blue('š Opening browser to Skills Dashboard...'));
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
await open(url);
|
|
394
|
+
} catch (error) {
|
|
395
|
+
console.log(chalk.yellow('Could not open browser automatically. Please visit:'));
|
|
396
|
+
console.log(chalk.cyan(url));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
stop() {
|
|
401
|
+
if (this.httpServer) {
|
|
402
|
+
this.httpServer.close();
|
|
403
|
+
}
|
|
404
|
+
console.log(chalk.yellow('Skills dashboard stopped'));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function runSkillDashboard(options = {}) {
|
|
409
|
+
console.log(chalk.blue('šÆ Starting Claude Code Skills Dashboard...'));
|
|
410
|
+
|
|
411
|
+
const dashboard = new SkillDashboard(options);
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
await dashboard.initialize();
|
|
415
|
+
await dashboard.startServer();
|
|
416
|
+
await dashboard.openBrowser();
|
|
417
|
+
|
|
418
|
+
console.log(chalk.green('ā
Skills dashboard is running!'));
|
|
419
|
+
console.log(chalk.cyan(`š Access at: http://localhost:${dashboard.port}`));
|
|
420
|
+
console.log(chalk.gray('Press Ctrl+C to stop the server'));
|
|
421
|
+
|
|
422
|
+
// Handle graceful shutdown
|
|
423
|
+
process.on('SIGINT', () => {
|
|
424
|
+
console.log(chalk.yellow('\nš Shutting down skills dashboard...'));
|
|
425
|
+
dashboard.stop();
|
|
426
|
+
process.exit(0);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Keep the process running
|
|
430
|
+
await new Promise(() => {});
|
|
431
|
+
|
|
432
|
+
} catch (error) {
|
|
433
|
+
console.error(chalk.red('ā Failed to start skills dashboard:'), error.message);
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
module.exports = {
|
|
439
|
+
runSkillDashboard,
|
|
440
|
+
SkillDashboard
|
|
441
|
+
};
|