@haolin-ai/skillman 1.0.1 → 1.0.2

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/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # Skillman
2
+
3
+ A CLI tool to install AI agent skills across multiple platforms.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @haolin-ai/skillman
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Interactive Mode (Recommended)
14
+
15
+ Run without arguments to start the interactive installation wizard:
16
+
17
+ ```bash
18
+ skillman
19
+ ```
20
+
21
+ This will guide you through:
22
+ 1. Scanning available skills in the current directory
23
+ 2. Selecting a skill to install
24
+ 3. Choosing the target agent (Claude Code, OpenClaw, Qoder, etc.)
25
+ 4. Selecting installation scope (global or workspace)
26
+ 5. Confirming the installation
27
+
28
+ ### Install from URL
29
+
30
+ Install skills directly from GitHub or any git repository:
31
+
32
+ ```bash
33
+ # GitHub shorthand
34
+ skillman install vercel-labs/agent-skills
35
+
36
+ # Full GitHub URL
37
+ skillman install https://github.com/vercel-labs/agent-skills
38
+
39
+ # With subdirectory
40
+ skillman install https://github.com/vercel-labs/agent-skills/tree/main/skills/web-design
41
+
42
+ # GitLab or other git URLs
43
+ skillman install https://gitlab.com/org/repo
44
+ skillman install git@github.com:org/repo.git
45
+ ```
46
+
47
+ ### Commands
48
+
49
+ ```bash
50
+ # List all available agents
51
+ skillman agents
52
+
53
+ # Preview installation without making changes
54
+ skillman --dry-run
55
+
56
+ # Show help
57
+ skillman --help
58
+
59
+ # Show version
60
+ skillman --version
61
+ ```
62
+
63
+ ## Features
64
+
65
+ - **Multi-Agent Support**: Works with Claude Code, OpenClaw, Qoder, Codex, Cursor, and more
66
+ - **Workspace History**: Remembers previously used workspace paths for quick selection
67
+ - **Bilingual Support**: Automatically switches between English and Chinese based on system language
68
+ - **Dry-Run Mode**: Preview installations before applying changes
69
+ - **Smart Path Resolution**: Automatically resolves relative paths to absolute paths
70
+
71
+ ## Configuration
72
+
73
+ Agent configurations are stored in `src/agents.yaml`. Each agent has:
74
+ - `globalSkillsDir`: Global skills directory (usually in home directory)
75
+ - `skillsDir`: Relative path for workspace-specific skills
76
+
77
+ ## Workspace History
78
+
79
+ When installing to a workspace scope, Skillman remembers your paths in:
80
+ ```
81
+ ~/.config/skillman/history.json
82
+ ```
83
+
84
+ History is organized by agent, so each agent has its own list of recently used workspaces.
85
+
86
+ ## Development
87
+
88
+ ```bash
89
+ # Clone the repository
90
+ git clone <repo-url>
91
+ cd skillman
92
+
93
+ # Install dependencies
94
+ pnpm install
95
+
96
+ # Run locally
97
+ node bin/skillman.mjs
98
+ ```
99
+
100
+ ## License
101
+
102
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haolin-ai/skillman",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "A CLI tool to install AI agent skills across multiple platforms",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -5,12 +5,13 @@
5
5
  import fs from 'fs/promises';
6
6
  import path from 'path';
7
7
  import { fileURLToPath } from 'url';
8
- import { select, confirm, input, Separator } from '@inquirer/prompts';
8
+ import { select, confirm, input, Separator, checkbox } from '@inquirer/prompts';
9
9
  import { scanSkills } from './scanner.js';
10
10
  import { installSkill } from './installer.js';
11
11
  import { loadAgents } from './config.js';
12
12
  import { t } from './i18n.js';
13
- import { loadHistory, addWorkspace } from './history.js';
13
+ import { loadHistory, addWorkspace, saveLastUsed } from './history.js';
14
+ import { downloadSkill, parseUrl, cleanupDownloads } from './downloader.js';
14
15
 
15
16
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
17
  const VERSION = '1.0.0';
@@ -71,6 +72,7 @@ function showHelp() {
71
72
  ${c.cyan}${t('help.usage')}:${c.reset}
72
73
  skillman ${t('help.cmd.interactive')}
73
74
  skillman install <path> ${t('help.cmd.install')}
75
+ skillman i <path> ${t('help.cmd.install')}
74
76
  skillman agents ${t('help.cmd.agents')}
75
77
 
76
78
  ${c.cyan}${t('help.options')}:${c.reset}
@@ -82,6 +84,7 @@ ${c.cyan}${t('help.examples')}:${c.reset}
82
84
  skillman # ${t('help.cmd.interactive')}
83
85
  skillman --dry-run # ${t('help.opt.dry_run')}
84
86
  skillman install ./my-skill # ${t('help.cmd.install')}
87
+ skillman i github.com/owner/repo # ${t('help.cmd.install')}
85
88
  skillman agents # ${t('help.cmd.agents')}
86
89
  `);
87
90
  }
@@ -105,14 +108,32 @@ async function listAgents() {
105
108
  }
106
109
  }
107
110
 
108
- // Interactive install flow
109
- async function interactiveInstall(dryRun) {
111
+ // Install from URL or local path
112
+ async function installFromUrl(url, dryRun) {
110
113
  console.log(`${c.green}${t('app.name')}${c.reset} - ${t('app.description')}${dryRun ? c.yellow + ' [DRY-RUN]' + c.reset : ''}\n`);
111
114
 
112
- // Step 1: Scan skills
115
+ const parsed = parseUrl(url);
116
+ const isRemote = parsed.type !== 'local';
117
+
118
+ // Step 1: Download/resolve path
119
+ let sourcePath;
120
+ if (isRemote) {
121
+ log.step(t('msg.downloading') || 'Downloading...');
122
+ try {
123
+ sourcePath = await downloadSkill(url);
124
+ log.success(t('msg.downloaded') || 'Downloaded');
125
+ } catch (error) {
126
+ log.error(error.message);
127
+ process.exit(1);
128
+ }
129
+ } else {
130
+ sourcePath = url;
131
+ }
132
+
133
+ // Step 2: Scan skills
113
134
  log.step(t('step.scan'));
135
+ const skills = await scanSkills(sourcePath);
114
136
 
115
- const skills = await scanSkills(process.cwd());
116
137
  if (skills.length === 0) {
117
138
  log.error(t('msg.no_skills'));
118
139
  process.exit(1);
@@ -120,25 +141,57 @@ async function interactiveInstall(dryRun) {
120
141
 
121
142
  log.success(t('msg.found_skills', { count: skills.length }));
122
143
 
123
- // Step 2: Select skill
124
- const skillChoices = skills.map(s => ({
125
- name: s.description
126
- ? `${s.name} ${c.gray}(${s.description.slice(0, 40)}${s.description.length > 40 ? '...' : ''})${c.reset}`
127
- : s.name,
128
- value: s
129
- }));
144
+ // Step 3: Select skills (if multiple)
145
+ let selectedSkills;
146
+ if (skills.length === 1) {
147
+ selectedSkills = [skills[0]];
148
+ log.success(`${t('msg.selected')}: ${skills[0].name}`);
149
+ } else {
150
+ const skillChoices = skills.map(s => ({
151
+ name: s.description
152
+ ? `${s.name} ${c.gray}(${s.description.slice(0, 40)}${s.description.length > 40 ? '...' : ''})${c.reset}`
153
+ : s.name,
154
+ value: s
155
+ }));
156
+
157
+ selectedSkills = await checkbox({
158
+ message: t('step.select_skills') + ':',
159
+ choices: skillChoices,
160
+ pageSize: 10,
161
+ loop: false,
162
+ validate: (selected) => {
163
+ if (selected.length === 0) {
164
+ return t('error.no_selection') || 'Please select at least one skill';
165
+ }
166
+ return true;
167
+ }
168
+ });
130
169
 
131
- const selectedSkill = await select({
132
- message: t('step.select_skill') + ':',
133
- choices: skillChoices,
134
- pageSize: 10
135
- });
170
+ log.success(`${t('msg.selected_count', { count: selectedSkills.length })}`);
171
+ }
136
172
 
137
- log.success(`${t('msg.selected')}: ${selectedSkill.name}`);
173
+ // Continue with rest of interactive flow
174
+ await continueInstallMultiple(selectedSkills, dryRun);
175
+
176
+ // Cleanup temp downloads
177
+ if (isRemote) {
178
+ await cleanupDownloads();
179
+ }
180
+ }
138
181
 
139
- // Step 3: Select agent
182
+ // Continue installation after skill selection (multiple skills)
183
+ async function continueInstallMultiple(selectedSkills, dryRun) {
184
+ // Load last used preferences
185
+ const { lastAgent, lastScope } = await loadHistory();
140
186
  const agents = await loadAgents();
141
- const agentChoices = Object.values(agents).map(a => ({
187
+
188
+ // Step 4: Select agent (with default from history)
189
+ const agentList = Object.values(agents);
190
+ const defaultAgentIndex = lastAgent
191
+ ? agentList.findIndex(a => a.name === lastAgent)
192
+ : -1;
193
+
194
+ const agentChoices = agentList.map(a => ({
142
195
  name: a.displayName,
143
196
  value: a
144
197
  }));
@@ -146,31 +199,42 @@ async function interactiveInstall(dryRun) {
146
199
  const agent = await select({
147
200
  message: t('step.select_agent') + ':',
148
201
  choices: agentChoices,
149
- pageSize: 10
202
+ pageSize: 10,
203
+ default: defaultAgentIndex >= 0 ? agentChoices[defaultAgentIndex].value : undefined
150
204
  });
151
205
 
152
206
  log.success(`${t('msg.agent')}: ${agent.displayName}`);
153
207
 
154
- // Step 4: Select scope
208
+ // Step 5: Select scope (with default from history)
209
+ const scopeChoices = [
210
+ { name: `${t('option.global')} ${c.gray}(${agent.globalSkillsDir})${c.reset}`, value: 'global' },
211
+ { name: `${t('option.workspace')} ${c.gray}(${t('option.custom_path')})${c.reset}`, value: 'workspace' }
212
+ ];
213
+
214
+ const defaultScopeIndex = lastScope
215
+ ? scopeChoices.findIndex(s => s.value === lastScope)
216
+ : -1;
217
+
155
218
  const scope = await select({
156
219
  message: t('step.select_scope') + ':',
157
- choices: [
158
- { name: `${t('option.global')} ${c.gray}(${agent.globalSkillsDir})${c.reset}`, value: 'global' },
159
- { name: `${t('option.workspace')} ${c.gray}(${t('option.custom_path')})${c.reset}`, value: 'workspace' }
160
- ]
220
+ choices: scopeChoices,
221
+ default: defaultScopeIndex >= 0 ? scopeChoices[defaultScopeIndex].value : undefined
161
222
  });
162
223
 
163
- // Step 5: If workspace scope, ask for workspace path
224
+ // Save preferences for next time
225
+ await saveLastUsed(agent.name, scope);
226
+
227
+ // Step 6: If workspace scope, ask for workspace path
164
228
  let workspacePath = agent.skillsDir;
165
229
  if (scope === 'workspace') {
166
- const history = await loadHistory(agent.name);
230
+ const { workspaces } = await loadHistory(agent.name);
167
231
 
168
232
  let customPath;
169
233
 
170
- if (history.length > 0) {
234
+ if (workspaces.length > 0) {
171
235
  // Show history choices with separator
172
236
  const historyChoices = [
173
- ...history.map((h, idx) => ({
237
+ ...workspaces.map((h, idx) => ({
174
238
  name: `${idx + 1}. ${h}`,
175
239
  value: h
176
240
  })),
@@ -218,64 +282,116 @@ async function interactiveInstall(dryRun) {
218
282
  log.info(`${t('msg.workspace_dir')}: ${workspacePath}`);
219
283
  }
220
284
 
221
- // Step 6: Calculate target path
222
- const targetDir = scope === 'global'
223
- ? path.join(agent.globalSkillsDir, selectedSkill.name)
224
- : path.join(workspacePath, selectedSkill.name);
225
-
226
- // Dry-run preview
285
+ // Dry-run preview for all skills
227
286
  if (dryRun) {
228
287
  log.step(t('step.preview'));
229
- log.dry(`${t('msg.source')}: ${selectedSkill.path}`);
230
- log.dry(`${t('msg.target')}: ${targetDir}`);
231
288
 
232
- try {
233
- await fs.access(targetDir);
234
- log.dry(t('msg.exists'));
235
- } catch {
236
- log.dry(t('msg.not_exists'));
289
+ for (const skill of selectedSkills) {
290
+ const targetDir = scope === 'global'
291
+ ? path.join(agent.globalSkillsDir, skill.name)
292
+ : path.join(workspacePath, skill.name);
293
+
294
+ log.dry(`\n${skill.name}:`);
295
+ log.dry(` ${t('msg.source')}: ${skill.path}`);
296
+ log.dry(` ${t('msg.target')}: ${targetDir}`);
297
+
298
+ try {
299
+ await fs.access(targetDir);
300
+ log.dry(` ${t('msg.exists')}`);
301
+ } catch {
302
+ log.dry(` ${t('msg.not_exists')}`);
303
+ }
237
304
  }
238
305
 
239
- log.dry(t('msg.copy'));
240
-
241
306
  console.log(`\n${c.yellow}📋 ${t('msg.preview_summary')}${c.reset}\n`);
242
- console.log(` Skill: ${selectedSkill.name}`);
307
+ console.log(` ${t('msg.selected_count', { count: selectedSkills.length })}`);
243
308
  console.log(` ${t('msg.agent')}: ${agent.displayName}`);
244
309
  console.log(` ${t('msg.scope')}: ${scope}`);
245
- console.log(` ${t('msg.location')}: ${targetDir}`);
246
310
  console.log(`\n${c.gray}${t('msg.dry_run_hint')}${c.reset}\n`);
247
311
  return;
248
312
  }
249
313
 
250
- // Step 6: Install
314
+ // Step 8: Install all skills
251
315
  log.step(t('step.install'));
252
316
 
253
- // Check if already exists
254
- try {
255
- await fs.access(targetDir);
256
- log.warn(t('msg.skill_exists'));
257
- const overwrite = await confirm({ message: t('prompt.overwrite') + '?', default: false });
258
- if (!overwrite) {
259
- log.info(t('msg.install_cancelled'));
260
- process.exit(0);
317
+ let installedCount = 0;
318
+ let skippedCount = 0;
319
+
320
+ for (const skill of selectedSkills) {
321
+ const targetDir = scope === 'global'
322
+ ? path.join(agent.globalSkillsDir, skill.name)
323
+ : path.join(workspacePath, skill.name);
324
+
325
+ log.step(`${t('msg.installing') || 'Installing'}: ${skill.name}`);
326
+
327
+ // Check if already exists
328
+ let shouldInstall = true;
329
+ try {
330
+ await fs.access(targetDir);
331
+ log.warn(t('msg.skill_exists'));
332
+ const overwrite = await confirm({ message: t('prompt.overwrite') + '?', default: false });
333
+ if (!overwrite) {
334
+ log.info(t('msg.skipped') || 'Skipped');
335
+ skippedCount++;
336
+ shouldInstall = false;
337
+ }
338
+ } catch {
339
+ // Directory doesn't exist, proceed
340
+ }
341
+
342
+ if (shouldInstall) {
343
+ await installSkill(skill.path, targetDir);
344
+ log.success(`${t('msg.target')}: ${targetDir}`);
345
+ installedCount++;
261
346
  }
262
- } catch {
263
- // Directory doesn't exist, proceed
264
347
  }
265
348
 
266
- // Install
267
- await installSkill(selectedSkill.path, targetDir);
268
- log.success(`${t('msg.target')}: ${targetDir}`);
269
-
270
349
  // Summary
271
350
  console.log(`\n${c.green}✨ ${t('msg.install_complete')}${c.reset}\n`);
272
- console.log(` Skill: ${selectedSkill.name}`);
351
+ console.log(` ${t('msg.installed') || 'Installed'}: ${installedCount}`);
352
+ if (skippedCount > 0) {
353
+ console.log(` ${t('msg.skipped') || 'Skipped'}: ${skippedCount}`);
354
+ }
273
355
  console.log(` ${t('msg.agent')}: ${agent.displayName}`);
274
356
  console.log(` ${t('msg.scope')}: ${scope}`);
275
- console.log(` ${t('msg.location')}: ${targetDir}`);
276
357
  console.log();
277
358
  }
278
359
 
360
+ // Interactive install flow
361
+ async function interactiveInstall(dryRun) {
362
+ console.log(`${c.green}${t('app.name')}${c.reset} - ${t('app.description')}${dryRun ? c.yellow + ' [DRY-RUN]' + c.reset : ''}\n`);
363
+
364
+ // Step 1: Scan skills
365
+ log.step(t('step.scan'));
366
+
367
+ const skills = await scanSkills(process.cwd());
368
+ if (skills.length === 0) {
369
+ log.error(t('msg.no_skills'));
370
+ process.exit(1);
371
+ }
372
+
373
+ log.success(t('msg.found_skills', { count: skills.length }));
374
+
375
+ // Step 2: Select skill
376
+ const skillChoices = skills.map(s => ({
377
+ name: s.description
378
+ ? `${s.name} ${c.gray}(${s.description.slice(0, 40)}${s.description.length > 40 ? '...' : ''})${c.reset}`
379
+ : s.name,
380
+ value: s
381
+ }));
382
+
383
+ const selectedSkill = await select({
384
+ message: t('step.select_skill') + ':',
385
+ choices: skillChoices,
386
+ pageSize: 10
387
+ });
388
+
389
+ log.success(`${t('msg.selected')}: ${selectedSkill.name}`);
390
+
391
+ // Continue with agent selection and installation
392
+ await continueInstall(selectedSkill, dryRun);
393
+ }
394
+
279
395
  // Main CLI function
280
396
  export async function cli() {
281
397
  const args = process.argv.slice(2);
@@ -296,9 +412,10 @@ export async function cli() {
296
412
  return;
297
413
  }
298
414
 
299
- if (options.command === 'install') {
300
- log.error(t('error.not_implemented'));
301
- process.exit(1);
415
+ if (options.command === 'install' || options.command === 'i') {
416
+ const url = options.positional[0] || process.cwd();
417
+ await installFromUrl(url, options.dryRun);
418
+ return;
302
419
  }
303
420
 
304
421
  // Default: interactive install
@@ -0,0 +1,122 @@
1
+ /**
2
+ * URL downloader for remote skill installation
3
+ * Supports GitHub shorthand, full URLs, and git URLs
4
+ */
5
+
6
+ import { execSync } from 'child_process';
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+ import os from 'os';
10
+
11
+ const TEMP_DIR = path.join(os.tmpdir(), 'skillman-downloads');
12
+
13
+ /**
14
+ * Parse URL to determine type and extract info
15
+ * @param {string} url - Input URL or shorthand
16
+ * @returns {Object} { type, url, subPath }
17
+ */
18
+ export function parseUrl(url) {
19
+ // GitHub shorthand: owner/repo or owner/repo/path
20
+ if (/^[\w-]+\/[\w-]+/.test(url) && !url.includes('://') && !url.startsWith('git@')) {
21
+ const parts = url.split('/');
22
+ if (parts.length === 2) {
23
+ return { type: 'github', url: `https://github.com/${url}.git`, subPath: null };
24
+ } else if (parts.length >= 3) {
25
+ // owner/repo/sub/path
26
+ return {
27
+ type: 'github',
28
+ url: `https://github.com/${parts[0]}/${parts[1]}.git`,
29
+ subPath: parts.slice(2).join('/')
30
+ };
31
+ }
32
+ }
33
+
34
+ // Full GitHub URL with tree/main/path
35
+ const treeMatch = url.match(/github\.com\/([^\/]+)\/([^\/]+)\/tree\/[^\/]+\/(.+)/);
36
+ if (treeMatch) {
37
+ return {
38
+ type: 'github',
39
+ url: `https://github.com/${treeMatch[1]}/${treeMatch[2]}.git`,
40
+ subPath: treeMatch[3]
41
+ };
42
+ }
43
+
44
+ // Regular git URLs
45
+ if (url.startsWith('https://') || url.startsWith('git@') || url.startsWith('http://')) {
46
+ // Remove .git suffix if present for consistency
47
+ const cleanUrl = url.replace(/\.git$/, '');
48
+ return { type: 'git', url: cleanUrl + '.git', subPath: null };
49
+ }
50
+
51
+ // Local path
52
+ return { type: 'local', url, subPath: null };
53
+ }
54
+
55
+ /**
56
+ * Download skill from URL
57
+ * @param {string} inputUrl - URL or shorthand
58
+ * @returns {Promise<string>} Path to downloaded skill directory
59
+ */
60
+ export async function downloadSkill(inputUrl) {
61
+ const parsed = parseUrl(inputUrl);
62
+
63
+ if (parsed.type === 'local') {
64
+ return parsed.url;
65
+ }
66
+
67
+ // Create temp directory
68
+ await fs.mkdir(TEMP_DIR, { recursive: true });
69
+
70
+ // Generate unique directory name
71
+ const repoName = path.basename(parsed.url, '.git');
72
+ const timestamp = Date.now();
73
+ const downloadDir = path.join(TEMP_DIR, `${repoName}-${timestamp}`);
74
+
75
+ try {
76
+ // Clone repository
77
+ execSync(`git clone --depth 1 "${parsed.url}" "${downloadDir}"`, {
78
+ stdio: 'pipe',
79
+ timeout: 60000
80
+ });
81
+
82
+ // If subPath specified, return that subdirectory
83
+ if (parsed.subPath) {
84
+ const subDir = path.join(downloadDir, parsed.subPath);
85
+ try {
86
+ await fs.access(subDir);
87
+ return subDir;
88
+ } catch {
89
+ throw new Error(`Subdirectory not found: ${parsed.subPath}`);
90
+ }
91
+ }
92
+
93
+ return downloadDir;
94
+ } catch (error) {
95
+ // Cleanup on failure
96
+ try {
97
+ await fs.rm(downloadDir, { recursive: true, force: true });
98
+ } catch {}
99
+ throw new Error(`Failed to download: ${error.message}`);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Clean up old downloads
105
+ */
106
+ export async function cleanupDownloads() {
107
+ try {
108
+ const entries = await fs.readdir(TEMP_DIR);
109
+ const now = Date.now();
110
+ const oneHour = 60 * 60 * 1000;
111
+
112
+ for (const entry of entries) {
113
+ const entryPath = path.join(TEMP_DIR, entry);
114
+ try {
115
+ const stats = await fs.stat(entryPath);
116
+ if (now - stats.mtime.getTime() > oneHour) {
117
+ await fs.rm(entryPath, { recursive: true, force: true });
118
+ }
119
+ } catch {}
120
+ }
121
+ } catch {}
122
+ }
package/src/history.js CHANGED
@@ -24,9 +24,13 @@ export async function loadHistory(agentName) {
24
24
  await ensureDir();
25
25
  const data = await fs.readFile(HISTORY_FILE, 'utf8');
26
26
  const history = JSON.parse(data);
27
- return history[agentName]?.workspaces || [];
27
+ return {
28
+ workspaces: history[agentName]?.workspaces || [],
29
+ lastAgent: history.lastAgent || null,
30
+ lastScope: history.lastScope || null
31
+ };
28
32
  } catch {
29
- return [];
33
+ return { workspaces: [], lastAgent: null, lastScope: null };
30
34
  }
31
35
  }
32
36
 
@@ -48,16 +52,34 @@ export async function saveHistory(agentName, workspaces) {
48
52
  await fs.writeFile(HISTORY_FILE, JSON.stringify(data, null, 2));
49
53
  }
50
54
 
55
+ export async function saveLastUsed(agentName, scope) {
56
+ await ensureDir();
57
+ let data = {};
58
+ try {
59
+ const existing = await fs.readFile(HISTORY_FILE, 'utf8');
60
+ data = JSON.parse(existing);
61
+ } catch {
62
+ // File doesn't exist or is invalid
63
+ }
64
+
65
+ if (agentName) data.lastAgent = agentName;
66
+ if (scope) data.lastScope = scope;
67
+ data.updatedAt = new Date().toISOString();
68
+
69
+ await fs.writeFile(HISTORY_FILE, JSON.stringify(data, null, 2));
70
+ }
71
+
51
72
  export async function addWorkspace(agentName, workspacePath) {
52
73
  const normalized = path.resolve(workspacePath);
53
- let workspaces = await loadHistory(agentName);
74
+ const { workspaces } = await loadHistory(agentName);
75
+ let newWorkspaces = [...workspaces];
54
76
 
55
77
  // Remove if exists (to move to front)
56
- workspaces = workspaces.filter(w => path.resolve(w) !== normalized);
78
+ newWorkspaces = newWorkspaces.filter(w => path.resolve(w) !== normalized);
57
79
 
58
80
  // Add to front
59
- workspaces.unshift(normalized);
81
+ newWorkspaces.unshift(normalized);
60
82
 
61
- await saveHistory(agentName, workspaces);
62
- return workspaces;
83
+ await saveHistory(agentName, newWorkspaces);
84
+ return newWorkspaces;
63
85
  }
package/src/i18n.js CHANGED
@@ -34,6 +34,7 @@ const translations = {
34
34
  // Steps
35
35
  'step.scan': '扫描可安装的 Skills...',
36
36
  'step.select_skill': '选择要安装的 Skill',
37
+ 'step.select_skills': '选择要安装的 Skills(空格选择,回车确认)',
37
38
  'step.select_agent': '选择目标 Agent',
38
39
  'step.select_scope': '选择安装范围',
39
40
  'step.install': '开始安装...',
@@ -43,6 +44,7 @@ const translations = {
43
44
  'msg.found_skills': '找到 {count} 个 skill',
44
45
  'msg.no_skills': '当前目录未找到任何 skill (需要包含 SKILL.md 的文件夹)',
45
46
  'msg.selected': '选择',
47
+ 'msg.selected_count': '已选择 {count} 个',
46
48
  'msg.agent': 'Agent',
47
49
  'msg.scope': '范围',
48
50
  'msg.location': '位置',
@@ -57,6 +59,11 @@ const translations = {
57
59
  'msg.dry_run_hint': '使用 --dry-run 预览,未执行任何操作',
58
60
  'msg.skill_exists': '该 skill 已存在',
59
61
  'msg.install_cancelled': '安装已取消',
62
+ 'msg.downloading': '正在下载...',
63
+ 'msg.downloaded': '下载完成',
64
+ 'msg.installing': '正在安装',
65
+ 'msg.skipped': '已跳过',
66
+ 'msg.installed': '已安装',
60
67
 
61
68
  // Prompts
62
69
  'prompt.workspace_path': '输入 Workspace 路径',
@@ -72,6 +79,7 @@ const translations = {
72
79
  // Errors
73
80
  'error.empty_path': '路径不能为空',
74
81
  'error.not_implemented': '该命令尚未实现,请使用交互模式',
82
+ 'error.no_selection': '请至少选择一个 skill',
75
83
 
76
84
  // Help
77
85
  'help.usage': '用法',
@@ -94,6 +102,7 @@ const translations = {
94
102
  // Steps
95
103
  'step.scan': 'Scanning available skills...',
96
104
  'step.select_skill': 'Select skill to install',
105
+ 'step.select_skills': 'Select skills to install (space to select, enter to confirm)',
97
106
  'step.select_agent': 'Select target agent',
98
107
  'step.select_scope': 'Select installation scope',
99
108
  'step.install': 'Starting installation...',
@@ -103,6 +112,7 @@ const translations = {
103
112
  'msg.found_skills': 'Found {count} skill(s)',
104
113
  'msg.no_skills': 'No skills found in current directory (folders with SKILL.md required)',
105
114
  'msg.selected': 'Selected',
115
+ 'msg.selected_count': 'Selected {count}',
106
116
  'msg.agent': 'Agent',
107
117
  'msg.scope': 'Scope',
108
118
  'msg.location': 'Location',
@@ -117,6 +127,11 @@ const translations = {
117
127
  'msg.dry_run_hint': 'Running with --dry-run, no changes made',
118
128
  'msg.skill_exists': 'Skill already exists',
119
129
  'msg.install_cancelled': 'Installation cancelled',
130
+ 'msg.downloading': 'Downloading...',
131
+ 'msg.downloaded': 'Downloaded',
132
+ 'msg.installing': 'Installing',
133
+ 'msg.skipped': 'Skipped',
134
+ 'msg.installed': 'Installed',
120
135
 
121
136
  // Prompts
122
137
  'prompt.workspace_path': 'Enter workspace path',
@@ -132,6 +147,7 @@ const translations = {
132
147
  // Errors
133
148
  'error.empty_path': 'Path cannot be empty',
134
149
  'error.not_implemented': 'Command not implemented, use interactive mode',
150
+ 'error.no_selection': 'Please select at least one skill',
135
151
 
136
152
  // Help
137
153
  'help.usage': 'Usage',
package/src/scanner.js CHANGED
@@ -1,17 +1,21 @@
1
1
  /**
2
2
  * Skill Scanner
3
3
  * Scans a directory for installable skills (folders with SKILL.md)
4
+ * Also checks common skill subdirectories like 'skills/'
4
5
  */
5
6
 
6
7
  import fs from 'fs/promises';
7
8
  import path from 'path';
8
9
 
10
+ // Common skill container directories
11
+ const SKILL_CONTAINERS = ['skills', '.agents/skills', '.claude/skills'];
12
+
9
13
  /**
10
- * Scan directory for skills
14
+ * Scan a single directory for skills
11
15
  * @param {string} dir - Directory to scan
12
- * @returns {Promise<Array<{name: string, path: string, description: string}>>}
16
+ * @returns {Promise<Array>} Found skills
13
17
  */
14
- export async function scanSkills(dir) {
18
+ async function scanSingleDir(dir) {
15
19
  const skills = [];
16
20
 
17
21
  try {
@@ -40,9 +44,38 @@ export async function scanSkills(dir) {
40
44
  // No SKILL.md or parse error, skip
41
45
  }
42
46
  }
43
- } catch (err) {
47
+ } catch {
44
48
  // Directory read error
45
49
  }
46
50
 
47
51
  return skills;
48
52
  }
53
+
54
+ /**
55
+ * Scan directory for skills
56
+ * @param {string} dir - Directory to scan
57
+ * @returns {Promise<Array<{name: string, path: string, description: string}>>}
58
+ */
59
+ export async function scanSkills(dir) {
60
+ // First scan root directory
61
+ const skills = await scanSingleDir(dir);
62
+ if (skills.length > 0) {
63
+ return skills;
64
+ }
65
+
66
+ // If no skills found, check common skill container directories
67
+ for (const container of SKILL_CONTAINERS) {
68
+ const containerPath = path.join(dir, container);
69
+ try {
70
+ await fs.access(containerPath);
71
+ const containerSkills = await scanSingleDir(containerPath);
72
+ if (containerSkills.length > 0) {
73
+ return containerSkills;
74
+ }
75
+ } catch {
76
+ // Container doesn't exist, skip
77
+ }
78
+ }
79
+
80
+ return [];
81
+ }