huhaa-myskills 0.2.5 → 0.2.7

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.
@@ -325,7 +325,12 @@ async function cmdSync() {
325
325
  }
326
326
 
327
327
  async function syncToEditors(skills, editors) {
328
- const supported = ['cursor', 'vscode', 'windsurf', 'zed', 'neovim', 'helix', 'sublime', 'vim', 'emacs'];
328
+ const supported = [
329
+ 'cursor', 'vscode', 'vscode-insiders', 'windsurf', 'zed', 'helix',
330
+ 'neovim', 'vim', 'emacs', 'sublime', 'sublime4', 'textmate',
331
+ 'bbedit', 'atom', 'kate', 'gedit', 'jetbrains', 'openclaw',
332
+ 'herems', 'trae', 'trae-cn', 'codex', 'claude'
333
+ ];
329
334
  const validEditors = editors.filter(e => supported.includes(e));
330
335
 
331
336
  if (validEditors.length === 0) {
@@ -368,19 +373,35 @@ async function interactiveSync(skills) {
368
373
 
369
374
  const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
370
375
 
376
+ const allEditors = [
377
+ 'cursor', 'vscode', 'vscode-insiders', 'windsurf', 'zed', 'helix',
378
+ 'neovim', 'vim', 'emacs', 'sublime', 'sublime4', 'textmate',
379
+ 'bbedit', 'atom', 'kate', 'gedit', 'jetbrains', 'openclaw',
380
+ 'herems', 'trae', 'trae-cn', 'codex', 'claude'
381
+ ];
382
+
371
383
  console.log(`\n[sync] found ${skills.length} skills`);
372
384
  console.log('\nSupported editors:');
373
- console.log(' [1] cursor [2] vscode [3] windsurf');
374
- console.log(' [4] zed [5] neovim [6] helix');
375
- console.log(' [7] sublime [8] vim [9] emacs');
376
- console.log(' [0] all editors');
377
- console.log(' [q] quit\n');
378
-
379
- const editorMap = {
380
- '1': 'cursor', '2': 'vscode', '3': 'windsurf',
381
- '4': 'zed', '5': 'neovim', '6': 'helix',
382
- '7': 'sublime', '8': 'vim', '9': 'emacs'
383
- };
385
+
386
+ // Display editors in 3 columns
387
+ for (let i = 0; i < allEditors.length; i += 3) {
388
+ const line = [];
389
+ for (let j = 0; j < 3; j++) {
390
+ const idx = i + j + 1;
391
+ const editor = allEditors[i + j];
392
+ if (editor) {
393
+ line.push(`[${idx.toString().padEnd(2)}] ${editor.padEnd(16)}`);
394
+ }
395
+ }
396
+ console.log(' ' + line.join(' '));
397
+ }
398
+
399
+ console.log(' [0] all editors [q] quit\n');
400
+
401
+ const editorMap = {};
402
+ allEditors.forEach((editor, i) => {
403
+ editorMap[(i + 1).toString()] = editor;
404
+ });
384
405
 
385
406
  const input = await question('Select editors (0/1/2/... or q): ');
386
407
  rl.close();
@@ -392,7 +413,7 @@ async function interactiveSync(skills) {
392
413
 
393
414
  let editors = [];
394
415
  if (input === '0') {
395
- editors = Object.values(editorMap);
416
+ editors = allEditors;
396
417
  } else {
397
418
  const selected = input.split(/[,\s]+/).filter(s => s);
398
419
  editors = selected.map(s => editorMap[s]).filter(Boolean);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huhaa-myskills",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "private": false,
5
5
  "description": "Local skill / plugin / MCP aggregation hub. Browse all your AI agent skills at http://localhost:11520.",
6
6
  "license": "MIT",
@@ -36,7 +36,7 @@
36
36
  "init": "node bin/huhaa-myskills.mjs init",
37
37
  "purge": "node bin/huhaa-myskills.mjs purge",
38
38
  "dev": "node bin/huhaa-myskills.mjs dev",
39
- "sync": "bash scripts/sync-skills.sh",
39
+ "sync": "node bin/huhaa-myskills.mjs sync",
40
40
  "build:web": "node scripts/build-web.mjs",
41
41
  "test": "node --test packages/scanner/test/*.test.mjs packages/server/test/*.test.mjs",
42
42
  "verify": "node scripts/verify.mjs",
@@ -102,7 +102,7 @@ function parseSkillFile({ abs, root, source, limits }) {
102
102
  });
103
103
  }
104
104
 
105
- const { data: fm, body, parseError } = parseFrontmatter(text);
105
+ const { data: fm, body, i18n, parseError } = parseFrontmatter(text);
106
106
 
107
107
  const name = (fm.name || dirName).toString().trim();
108
108
  const title = (fm.title || fm.name || dirName).toString().trim();
@@ -140,6 +140,16 @@ function parseSkillFile({ abs, root, source, limits }) {
140
140
  updatedAt: new Date(mtime).toISOString(),
141
141
  product,
142
142
  };
143
+
144
+ // Include i18n translations if present
145
+ if (i18n) {
146
+ item.i18n = {
147
+ ...i18n,
148
+ translatedAt: new Date().toISOString(),
149
+ translationModel: 'frontmatter',
150
+ };
151
+ }
152
+
143
153
  item.brand = inferBrand({
144
154
  name: item.name,
145
155
  description: item.description,
@@ -132,6 +132,18 @@ export async function scan() {
132
132
 
133
133
  const out = dedupeSemantic(all);
134
134
 
135
+ // Optional: translate skills to Chinese via LLM
136
+ if (process.env.HUHAA_TRANSLATE === '1') {
137
+ console.log('[scan] translating skills to Chinese...');
138
+ try {
139
+ const { translateSkill } = await import('../../../packages/server/src/translator.mjs');
140
+ const translated = await Promise.all(out.map(s => translateSkill(s, 'zh')));
141
+ return translated;
142
+ } catch (e) {
143
+ console.warn('[scan] translation failed:', e.message);
144
+ }
145
+ }
146
+
135
147
  if (process.env.HUHAA_DEBUG) {
136
148
  console.error('[scan] stats:', JSON.stringify(stats, null, 2));
137
149
  }
@@ -82,7 +82,7 @@ const FM_RE = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/;
82
82
 
83
83
  /**
84
84
  * Parse YAML frontmatter at the very top of a markdown file.
85
- * Returns { data, body, parseError? }. `data` is always an object (possibly
85
+ * Returns { data, body, i18n?, parseError? }. `data` is always an object (possibly
86
86
  * empty). Body never has the frontmatter block.
87
87
  */
88
88
  export function parseFrontmatter(text) {
@@ -96,9 +96,17 @@ export function parseFrontmatter(text) {
96
96
  const [, yamlBlock, body] = m;
97
97
  try {
98
98
  const data = YAML.parse(yamlBlock) || {};
99
+
100
+ // Extract i18n fields if present
101
+ const i18n = {};
102
+ for (const key of ['name_zh', 'description_zh', 'category_zh']) {
103
+ if (data[key]) i18n[key] = data[key];
104
+ }
105
+
99
106
  return {
100
107
  data: typeof data === 'object' && !Array.isArray(data) ? data : {},
101
108
  body: body || '',
109
+ ...(Object.keys(i18n).length > 0 && { i18n }),
102
110
  };
103
111
  } catch (e) {
104
112
  return { data: {}, body: body || text, parseError: `frontmatter: ${e.message}` };
@@ -232,6 +232,24 @@ export async function startServer({ port = 11520 } = {}) {
232
232
  }
233
233
  });
234
234
 
235
+ app.post('/api/translate', async (req, reply) => {
236
+ const { id } = req.body || {};
237
+ const item = irById.get(id);
238
+ if (!item) {
239
+ reply.code(404);
240
+ return { error: 'not found' };
241
+ }
242
+
243
+ const { translateSkill } = await import('./translator.mjs');
244
+ try {
245
+ const translated = await translateSkill(item, 'zh');
246
+ return translated;
247
+ } catch (e) {
248
+ reply.code(500);
249
+ return { error: e.message };
250
+ }
251
+ });
252
+
235
253
  app.get('/assets/*', async (req, reply) => {
236
254
  if (!hasSpa) {
237
255
  reply.code(404);
@@ -0,0 +1,71 @@
1
+ export async function translateSkill(skill, targetLang = 'zh') {
2
+ if (targetLang !== 'zh' || skill.i18n?.name_zh) return skill;
3
+
4
+ const apiKey = process.env.ANTHROPIC_API_KEY;
5
+ if (!apiKey) {
6
+ console.warn('[translator] ANTHROPIC_API_KEY not set, skipping translation');
7
+ return skill;
8
+ }
9
+
10
+ const prompt = `Translate these skill metadata to Chinese (简体中文). Return JSON only, no markdown.
11
+
12
+ Current metadata:
13
+ - name: ${skill.name}
14
+ - description: ${skill.description || '(none)'}
15
+ - category: ${skill.category || '(none)'}
16
+
17
+ Return JSON format (translate only the values):
18
+ {
19
+ "name_zh": "中文名称",
20
+ "description_zh": "中文描述",
21
+ "category_zh": "中文分类"
22
+ }
23
+
24
+ Guidelines:
25
+ - Keep technical terms like "API", "MCP", "LLM" in English when appropriate
26
+ - Use common Chinese equivalents for concepts (e.g., "skill" → "技能", "rule" → "规则")
27
+ - Keep descriptions concise (under 100 chars)
28
+ - Preserve the technical meaning
29
+ - Return valid JSON only`;
30
+
31
+ try {
32
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
33
+ method: 'POST',
34
+ headers: {
35
+ 'Content-Type': 'application/json',
36
+ 'x-api-key': apiKey,
37
+ 'anthropic-version': '2023-06-01',
38
+ },
39
+ body: JSON.stringify({
40
+ model: 'claude-3-5-sonnet-20241022',
41
+ max_tokens: 500,
42
+ messages: [{
43
+ role: 'user',
44
+ content: prompt,
45
+ }],
46
+ }),
47
+ });
48
+
49
+ if (!response.ok) {
50
+ const error = await response.text();
51
+ throw new Error(`Claude API: ${response.status} ${error}`);
52
+ }
53
+
54
+ const data = await response.json();
55
+ const translated = JSON.parse(data.content[0].text);
56
+
57
+ return {
58
+ ...skill,
59
+ i18n: {
60
+ name_zh: translated.name_zh,
61
+ description_zh: translated.description_zh,
62
+ category_zh: translated.category_zh,
63
+ translatedAt: new Date().toISOString(),
64
+ translationModel: 'claude-3.5-sonnet',
65
+ },
66
+ };
67
+ } catch (e) {
68
+ console.error(`[translator] error: ${e.message}`);
69
+ return skill;
70
+ }
71
+ }