sumulige-claude 1.0.7 → 1.0.9

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.
@@ -0,0 +1,486 @@
1
+ /**
2
+ * Marketplace Commands
3
+ *
4
+ * Commands for managing the skill marketplace and external skill sources.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { execSync } = require('child_process');
10
+
11
+ const ROOT_DIR = path.join(__dirname, '..');
12
+ const SOURCES_FILE = path.join(ROOT_DIR, 'sources.yaml');
13
+ const MARKETPLACE_FILE = path.join(ROOT_DIR, '.claude-plugin', 'marketplace.json');
14
+ const CATEGORIES_FILE = path.join(ROOT_DIR, 'config', 'skill-categories.json');
15
+ const TEMPLATE_SKILLS_DIR = path.join(ROOT_DIR, 'template', '.claude', 'skills');
16
+
17
+ // ============================================================================
18
+ // Logging Utilities
19
+ // ============================================================================
20
+
21
+ const COLORS = {
22
+ reset: '\x1b[0m',
23
+ green: '\x1b[32m',
24
+ blue: '\x1b[34m',
25
+ yellow: '\x1b[33m',
26
+ red: '\x1b[31m',
27
+ cyan: '\x1b[36m',
28
+ gray: '\x1b[90m',
29
+ magenta: '\x1b[35m'
30
+ };
31
+
32
+ function log(message, color = 'reset') {
33
+ console.log(`${COLORS[color]}${message}${COLORS.reset}`);
34
+ }
35
+
36
+ // ============================================================================
37
+ // YAML Parser (Simple)
38
+ // ============================================================================
39
+
40
+ function parseSimpleYaml(content) {
41
+ const result = { skills: [] };
42
+ let currentSection = null;
43
+ let currentSkill = null;
44
+ let currentKey = null;
45
+
46
+ content.split('\n').forEach(line => {
47
+ const trimmed = line.trim();
48
+ const indent = line.search(/\S/);
49
+
50
+ // Skip empty lines and comments
51
+ if (!trimmed || trimmed.startsWith('#')) {
52
+ return;
53
+ }
54
+
55
+ // Version
56
+ if (trimmed.startsWith('version:')) {
57
+ result.version = parseInt(trimmed.split(':')[1].trim());
58
+ return;
59
+ }
60
+
61
+ // Skills array starts
62
+ if (trimmed === 'skills:') {
63
+ currentSection = 'skills';
64
+ return;
65
+ }
66
+
67
+ // New skill entry (starts with -)
68
+ if (trimmed.startsWith('- name:')) {
69
+ if (currentSkill) {
70
+ result.skills.push(currentSkill);
71
+ }
72
+ currentSkill = { name: trimmed.split(':')[1].trim() };
73
+ return;
74
+ }
75
+
76
+ // Skill properties
77
+ if (currentSection === 'skills' && currentSkill) {
78
+ // Determine nesting level by indent
79
+ const isTopLevel = indent === 2;
80
+
81
+ if (isTopLevel) {
82
+ const match = trimmed.match(/^([\w-]+):\s*(.*)$/);
83
+ if (match) {
84
+ currentKey = match[1];
85
+ let value = match[2].trim();
86
+
87
+ // Handle arrays
88
+ if (value === '[]') {
89
+ value = [];
90
+ } else if (value === 'true') {
91
+ value = true;
92
+ } else if (value === 'false') {
93
+ value = false;
94
+ } else if (value.startsWith('"') || value.startsWith("'")) {
95
+ value = value.slice(1, -1);
96
+ }
97
+
98
+ currentSkill[currentKey] = value;
99
+
100
+ // Initialize nested objects
101
+ if (['source', 'target', 'author', 'sync'].includes(currentKey)) {
102
+ currentSkill[currentKey] = {};
103
+ }
104
+ }
105
+ } else if (currentKey) {
106
+ // Nested property
107
+ const match = trimmed.match(/^([\w-]+):\s*(.*)$/);
108
+ if (match) {
109
+ let value = match[2].trim();
110
+ if (value === 'true') value = true;
111
+ if (value === 'false') value = false;
112
+ if (value === '[]') value = [];
113
+ currentSkill[currentKey][match[1]] = value;
114
+ }
115
+ }
116
+ }
117
+ });
118
+
119
+ // Push last skill
120
+ if (currentSkill) {
121
+ result.skills.push(currentSkill);
122
+ }
123
+
124
+ return result;
125
+ }
126
+
127
+ // ============================================================================
128
+ // Marketplace Commands
129
+ // ============================================================================
130
+
131
+ const marketplaceCommands = {
132
+ // -------------------------------------------------------------------------
133
+ 'marketplace:list': () => {
134
+ log('📋 SMC Skill Marketplace', 'blue');
135
+ log('=====================================', 'gray');
136
+ log('');
137
+
138
+ // Load marketplace registry
139
+ let registry = { plugins: [], metadata: { skill_count: 0, categories: {} } };
140
+ if (fs.existsSync(MARKETPLACE_FILE)) {
141
+ try {
142
+ registry = JSON.parse(fs.readFileSync(MARKETPLACE_FILE, 'utf-8'));
143
+ } catch (e) {
144
+ log('Warning: Failed to parse marketplace.json', 'yellow');
145
+ }
146
+ }
147
+
148
+ // Load sources.yaml
149
+ let sources = { skills: [] };
150
+ if (fs.existsSync(SOURCES_FILE)) {
151
+ try {
152
+ const content = fs.readFileSync(SOURCES_FILE, 'utf-8');
153
+ sources = parseSimpleYaml(content);
154
+ } catch (e) {
155
+ log('Warning: Failed to parse sources.yaml', 'yellow');
156
+ }
157
+ }
158
+
159
+ // Display by category
160
+ const categories = registry.metadata.categories || {
161
+ tools: { name: 'CLI 工具', icon: '🔧' },
162
+ workflow: { name: '工作流编排', icon: '🎼' },
163
+ development: { name: '开发辅助', icon: '💻' },
164
+ productivity: { name: '效率工具', icon: '⚡' },
165
+ automation: { name: '自动化', icon: '🤖' },
166
+ data: { name: '数据处理', icon: '📊' },
167
+ documentation: { name: '文档', icon: '📚' }
168
+ };
169
+
170
+ // Group skills by category
171
+ const byCategory = {};
172
+ for (const [key, cat] of Object.entries(categories)) {
173
+ byCategory[key] = [];
174
+ }
175
+
176
+ // Add from registry
177
+ for (const plugin of registry.plugins) {
178
+ if (plugin.skill_list) {
179
+ for (const skill of plugin.skill_list) {
180
+ if (byCategory[skill.category]) {
181
+ byCategory[skill.category].push({
182
+ name: skill.name,
183
+ description: skill.description,
184
+ external: skill.external
185
+ });
186
+ }
187
+ }
188
+ }
189
+ }
190
+
191
+ // Add from sources.yaml
192
+ for (const skill of sources.skills) {
193
+ const cat = skill.target?.category || 'tools';
194
+ if (!byCategory[cat]) {
195
+ byCategory[cat] = [];
196
+ }
197
+ if (!byCategory[cat].some(s => s.name === skill.name)) {
198
+ byCategory[cat].push({
199
+ name: skill.name,
200
+ description: skill.description,
201
+ native: skill.native
202
+ });
203
+ }
204
+ }
205
+
206
+ // Display
207
+ for (const [key, skills] of Object.entries(byCategory)) {
208
+ if (skills.length === 0) continue;
209
+
210
+ const cat = categories[key] || { name: key, icon: '📁' };
211
+ log(`${cat.icon} ${cat.name}`, 'cyan');
212
+ log('', 'gray');
213
+
214
+ for (const skill of skills) {
215
+ const badge = skill.native ? ' [native]' : skill.external ? ' [external]' : '';
216
+ log(` ${skill.name}${badge}`, 'green');
217
+ if (skill.description) {
218
+ log(` ${skill.description}`, 'gray');
219
+ }
220
+ }
221
+ log('', 'gray');
222
+ }
223
+
224
+ log('=====================================', 'gray');
225
+ log(`Total: ${registry.metadata.skill_count || sources.skills.length} skills`, 'gray');
226
+ log('', 'gray');
227
+ log('Commands:', 'gray');
228
+ log(' smc marketplace:install <name> Install a skill', 'gray');
229
+ log(' smc marketplace:sync Sync external skills', 'gray');
230
+ log(' smc marketplace:add <repo> Add external source', 'gray');
231
+ },
232
+
233
+ // -------------------------------------------------------------------------
234
+ 'marketplace:install': (skillName) => {
235
+ if (!skillName) {
236
+ log('Usage: smc marketplace:install <skill-name>', 'yellow');
237
+ log('', 'gray');
238
+ log('Examples:', 'gray');
239
+ log(' smc marketplace:install dev-browser', 'gray');
240
+ log(' smc marketplace:install gastown', 'gray');
241
+ return;
242
+ }
243
+
244
+ log(`📦 Installing skill: ${skillName}`, 'blue');
245
+ log('', 'gray');
246
+
247
+ // Load sources.yaml to find the skill
248
+ let sources = { skills: [] };
249
+ if (fs.existsSync(SOURCES_FILE)) {
250
+ try {
251
+ const content = fs.readFileSync(SOURCES_FILE, 'utf-8');
252
+ sources = parseSimpleYaml(content);
253
+ } catch (e) {
254
+ log('Warning: Failed to parse sources.yaml', 'yellow');
255
+ }
256
+ }
257
+
258
+ const skill = sources.skills.find(s => s.name === skillName);
259
+ if (!skill) {
260
+ log(`Skill "${skillName}" not found in sources.yaml`, 'yellow');
261
+ log('', 'gray');
262
+ log('Available skills:', 'gray');
263
+ log(' smc marketplace:list', 'gray');
264
+ return;
265
+ }
266
+
267
+ // Native skills are already in the repo
268
+ if (skill.native) {
269
+ log(`✅ Skill "${skillName}" is a native skill, already available at:`, 'green');
270
+ log(` ${skill.target.path}`, 'gray');
271
+ return;
272
+ }
273
+
274
+ // External skills - sync them
275
+ log(`Syncing from external source...`, 'gray');
276
+
277
+ try {
278
+ execSync('npm run sync', { stdio: 'inherit' });
279
+ log(``, 'gray');
280
+ log(`✅ Skill "${skillName}" installed`, 'green');
281
+ } catch (e) {
282
+ log(`❌ Failed to install skill`, 'red');
283
+ }
284
+ },
285
+
286
+ // -------------------------------------------------------------------------
287
+ 'marketplace:sync': () => {
288
+ log('🔄 Syncing external skills...', 'blue');
289
+ log('', 'gray');
290
+
291
+ try {
292
+ execSync('npm run sync:all', { stdio: 'inherit' });
293
+ log('', 'gray');
294
+ log('✅ Sync complete', 'green');
295
+ } catch (e) {
296
+ log('❌ Sync failed', 'red');
297
+ }
298
+ },
299
+
300
+ // -------------------------------------------------------------------------
301
+ 'marketplace:add': (repo) => {
302
+ if (!repo) {
303
+ log('Usage: smc marketplace:add <owner/repo>', 'yellow');
304
+ log('', 'gray');
305
+ log('This command adds an external skill repository to sources.yaml', 'gray');
306
+ log('', 'gray');
307
+ log('Example:', 'gray');
308
+ log(' smc marketplace:add SawyerHood/dev-browser', 'gray');
309
+ return;
310
+ }
311
+
312
+ // Parse repo
313
+ const match = repo.match(/^([^/]+)\/(.+)$/);
314
+ if (!match) {
315
+ log('Invalid repo format. Use: owner/repo', 'red');
316
+ return;
317
+ }
318
+
319
+ const [, owner, name] = match;
320
+ const skillName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
321
+
322
+ log(`📝 Adding skill source: ${repo}`, 'blue');
323
+ log('', 'gray');
324
+
325
+ // Check sources.yaml exists
326
+ if (!fs.existsSync(SOURCES_FILE)) {
327
+ log(`sources.yaml not found. Creating...`, 'yellow');
328
+ fs.writeFileSync(SOURCES_FILE, `version: 1\nskills:\n`);
329
+ }
330
+
331
+ // Read existing sources
332
+ const content = fs.readFileSync(SOURCES_FILE, 'utf-8');
333
+ const lines = content.split('\n');
334
+
335
+ // Check if skill already exists
336
+ if (lines.some(l => l.includes(`name: ${skillName}`))) {
337
+ log(`Skill "${skillName}" already exists in sources.yaml`, 'yellow');
338
+ return;
339
+ }
340
+
341
+ // Find where to insert (after existing skills or before # Comments)
342
+ let insertIndex = lines.length;
343
+ for (let i = 0; i < lines.length; i++) {
344
+ if (lines[i].trim().startsWith('#') && lines[i].includes('Schema')) {
345
+ insertIndex = i;
346
+ break;
347
+ }
348
+ }
349
+
350
+ // Build new skill entry
351
+ const newEntry = `
352
+ # ─────────────────────────────────────────────────────────────────────────────
353
+ # ${skillName} - External skill from ${repo}
354
+ # ─────────────────────────────────────────────────────────────────────────────
355
+ - name: ${skillName}
356
+ description: "TODO: Add description for ${skillName}"
357
+ source:
358
+ repo: ${repo}
359
+ path: skills/${skillName}
360
+ ref: main
361
+ target:
362
+ category: tools
363
+ path: template/.claude/skills/tools/${skillName}
364
+ author:
365
+ name: ${owner}
366
+ github: ${owner}
367
+ license: MIT
368
+ homepage: https://github.com/${repo}
369
+ verified: false
370
+ `;
371
+
372
+ // Insert new entry
373
+ lines.splice(insertIndex, 0, newEntry.trim());
374
+
375
+ // Write back
376
+ fs.writeFileSync(SOURCES_FILE, lines.join('\n'));
377
+
378
+ log(`✅ Added "${skillName}" to sources.yaml`, 'green');
379
+ log('', 'gray');
380
+ log('Next steps:', 'gray');
381
+ log(` 1. Edit sources.yaml to update description and category`, 'gray');
382
+ log(` 2. Run: smc marketplace:sync`, 'gray');
383
+ },
384
+
385
+ // -------------------------------------------------------------------------
386
+ 'marketplace:remove': (skillName) => {
387
+ if (!skillName) {
388
+ log('Usage: smc marketplace:remove <skill-name>', 'yellow');
389
+ return;
390
+ }
391
+
392
+ log(`🗑️ Removing skill: ${skillName}`, 'blue');
393
+ log('', 'gray');
394
+
395
+ // Read sources.yaml
396
+ if (!fs.existsSync(SOURCES_FILE)) {
397
+ log('sources.yaml not found', 'yellow');
398
+ return;
399
+ }
400
+
401
+ const content = fs.readFileSync(SOURCES_FILE, 'utf-8');
402
+ const lines = content.split('\n');
403
+
404
+ // Find and remove the skill entry
405
+ let inSkillEntry = false;
406
+ let removed = false;
407
+ const newLines = [];
408
+
409
+ for (let i = 0; i < lines.length; i++) {
410
+ const line = lines[i];
411
+
412
+ // Check if this is the skill to remove
413
+ if (line.includes(`name: ${skillName}`) && line.trim().startsWith('-')) {
414
+ inSkillEntry = true;
415
+ removed = true;
416
+ continue;
417
+ }
418
+
419
+ // Skip lines while in the skill entry
420
+ if (inSkillEntry) {
421
+ // End of skill entry
422
+ if (line.trim().startsWith('- name:') || line.trim().startsWith('#')) {
423
+ inSkillEntry = false;
424
+ newLines.push(line);
425
+ }
426
+ continue;
427
+ }
428
+
429
+ newLines.push(line);
430
+ }
431
+
432
+ if (!removed) {
433
+ log(`Skill "${skillName}" not found in sources.yaml`, 'yellow');
434
+ return;
435
+ }
436
+
437
+ // Write back
438
+ fs.writeFileSync(SOURCES_FILE, newLines.join('\n'));
439
+
440
+ log(`✅ Removed "${skillName}" from sources.yaml`, 'green');
441
+ log('', 'gray');
442
+ log('Note: The skill files remain in the template directory.', 'yellow');
443
+ log(' To remove them, delete:', 'yellow');
444
+ log(` template/.claude/skills/*/${skillName}`, 'gray');
445
+ },
446
+
447
+ // -------------------------------------------------------------------------
448
+ 'marketplace:status': () => {
449
+ log('📊 Marketplace Status', 'blue');
450
+ log('=====================================', 'gray');
451
+ log('');
452
+
453
+ // Check marketplace.json
454
+ if (fs.existsSync(MARKETPLACE_FILE)) {
455
+ const registry = JSON.parse(fs.readFileSync(MARKETPLACE_FILE, 'utf-8'));
456
+ log(`Registry: ${MARKETPLACE_FILE}`, 'green');
457
+ log(`Version: ${registry.metadata.version}`, 'gray');
458
+ log(`Skills: ${registry.metadata.skill_count}`, 'gray');
459
+ log(`Updated: ${registry.metadata.generated_at}`, 'gray');
460
+ } else {
461
+ log(`Registry: Not found (run: smc marketplace:sync)`, 'yellow');
462
+ }
463
+
464
+ log('');
465
+
466
+ // Check sources.yaml
467
+ if (fs.existsSync(SOURCES_FILE)) {
468
+ const content = fs.readFileSync(SOURCES_FILE, 'utf-8');
469
+ const sources = parseSimpleYaml(content);
470
+ log(`Sources: ${SOURCES_FILE}`, 'green');
471
+ log(`External sources: ${sources.skills.filter(s => !s.native).length}`, 'gray');
472
+ log(`Native skills: ${sources.skills.filter(s => s.native).length}`, 'gray');
473
+ } else {
474
+ log(`Sources: Not found`, 'yellow');
475
+ }
476
+
477
+ log('');
478
+ log('=====================================', 'gray');
479
+ }
480
+ };
481
+
482
+ // ============================================================================
483
+ // Exports
484
+ // ============================================================================
485
+
486
+ exports.marketplaceCommands = marketplaceCommands;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sumulige-claude",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "The Best Agent Harness for Claude Code",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -9,7 +9,10 @@
9
9
  },
10
10
  "scripts": {
11
11
  "test": "echo \"Error: no test specified\" && exit 1",
12
- "postinstall": "node cli.js init"
12
+ "postinstall": "node cli.js init",
13
+ "sync": "node scripts/sync-external.mjs",
14
+ "update-registry": "node scripts/update-registry.mjs",
15
+ "sync:all": "npm run sync && npm run update-registry"
13
16
  },
14
17
  "keywords": [
15
18
  "claude",
@@ -17,7 +20,9 @@
17
20
  "agent",
18
21
  "ai",
19
22
  "skills",
20
- "mcp"
23
+ "mcp",
24
+ "marketplace",
25
+ "plugin"
21
26
  ],
22
27
  "author": "sumulige",
23
28
  "license": "MIT",
@@ -30,6 +35,7 @@
30
35
  "node": ">=16.0.0"
31
36
  },
32
37
  "devDependencies": {
33
- "prettier": "^3.7.4"
38
+ "prettier": "^3.7.4",
39
+ "yaml": "^2.8.2"
34
40
  }
35
41
  }