@sstar/skill-install 1.0.0 → 1.0.1

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/dist/cli.js CHANGED
@@ -6,9 +6,25 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const commander_1 = require("commander");
8
8
  const readline_1 = __importDefault(require("readline"));
9
+ const os_1 = __importDefault(require("os"));
9
10
  const install_service_1 = require("./installer/install-service");
10
11
  const skills_manager_1 = require("./skills/skills-manager");
11
12
  const logger_1 = require("./core/logger");
13
+ const wiki_1 = require("./wiki");
14
+ // Color codes for terminal output
15
+ const colors = {
16
+ reset: '\x1b[0m',
17
+ bright: '\x1b[1m',
18
+ dim: '\x1b[2m',
19
+ red: '\x1b[31m',
20
+ green: '\x1b[32m',
21
+ yellow: '\x1b[33m',
22
+ blue: '\x1b[34m',
23
+ magenta: '\x1b[35m',
24
+ cyan: '\x1b[36m',
25
+ white: '\x1b[37m',
26
+ gray: '\x1b[90m'
27
+ };
12
28
  const program = new commander_1.Command();
13
29
  const skillsManager = new skills_manager_1.SkillsManager();
14
30
  program
@@ -20,10 +36,11 @@ program
20
36
  .option('-v, --verbose', 'Enable verbose logging');
21
37
  // Install command
22
38
  program
23
- .command('install <source>')
24
- .description('Install a skill from URL or local archive file')
39
+ .command('install [source]')
40
+ .description('Install a skill from URL or local archive file (no source = search Wiki)')
25
41
  .option('-u, --username <username>', 'Wiki username (for wiki URLs)')
26
42
  .option('-p, --password <password>', 'Wiki password (not recommended, use interactive input instead)')
43
+ .option('-w, --wiki-url <url>', 'Wiki base URL (for search)', 'https://sswiki.sigmastar.com.tw:8090')
27
44
  .option('--allow-self-signed', 'Allow self-signed SSL certificates (default: true)', true)
28
45
  .option('-f, --force', 'Reinstall if skill already exists')
29
46
  .option('-g, --global', 'Install to global directory')
@@ -58,9 +75,23 @@ program
58
75
  console.log(`AI Tool: ${aiTool}`);
59
76
  console.log(`Installing to: ${skillsDir}`);
60
77
  console.log('');
61
- // Get username/password for wiki URLs
62
- let username = options.username || process.env.WIKI_USERNAME || '';
78
+ // Get username/password for wiki URLs or search
79
+ // Auto-detect username from system if not provided
80
+ let username = options.username || process.env.WIKI_USERNAME || os_1.default.userInfo().username;
63
81
  let password = options.password || process.env.WIKI_PASSWORD || '';
82
+ // If no source provided, trigger search flow
83
+ if (!source) {
84
+ // Only require password, username is auto-detected
85
+ if (!password) {
86
+ password = await promptPassword();
87
+ }
88
+ const selectedUrl = await searchAndSelectSkill(username, password, options.wikiUrl, options.allowSelfSigned);
89
+ if (!selectedUrl) {
90
+ console.log('No skill selected.');
91
+ process.exit(0);
92
+ }
93
+ source = selectedUrl;
94
+ }
64
95
  // Check if source appears to be a wiki URL
65
96
  const needsAuth = source.includes('/download/attachments/') ||
66
97
  source.includes('/wiki/download/') ||
@@ -86,16 +117,58 @@ program
86
117
  allowSelfSigned: options.allowSelfSigned,
87
118
  force: options.force
88
119
  });
120
+ // Handle multi-skill package
121
+ if (result.isMultiSkill && result.skills) {
122
+ const selectedSkills = await selectSkillsFromPackage(result.skills);
123
+ if (selectedSkills.length === 0) {
124
+ console.log('No skills selected.');
125
+ process.exit(0);
126
+ }
127
+ // Install selected skills
128
+ const installResults = await installer.installSelectedSkills(selectedSkills, '', // extractDir is already handled in install()
129
+ {
130
+ source,
131
+ skillsDir,
132
+ username,
133
+ password,
134
+ allowSelfSigned: options.allowSelfSigned,
135
+ force: options.force
136
+ });
137
+ // Display results
138
+ console.log('');
139
+ let successCount = 0;
140
+ let failCount = 0;
141
+ for (const res of installResults) {
142
+ if (res.success) {
143
+ console.log(`✓ Skill "${res.skillName}" installed successfully!`);
144
+ console.log(` Path: ${res.skillPath}`);
145
+ successCount++;
146
+ }
147
+ else {
148
+ console.error(`✗ Failed to install skill: ${res.error}`);
149
+ failCount++;
150
+ }
151
+ }
152
+ console.log('');
153
+ console.log(`Installation complete: ${successCount} succeeded, ${failCount} failed.`);
154
+ process.exit(failCount > 0 ? 1 : 0);
155
+ }
156
+ // Handle single skill result
157
+ const singleResult = result.singleSkillResult;
158
+ if (!singleResult) {
159
+ console.error('✗ Installation failed: No result returned');
160
+ process.exit(1);
161
+ }
89
162
  console.log('');
90
- if (result.success) {
163
+ if (singleResult.success) {
91
164
  console.log(`✓ Skill installed successfully!`);
92
- console.log(` Name: ${result.skillName}`);
93
- console.log(` Path: ${result.skillPath}`);
165
+ console.log(` Name: ${singleResult.skillName}`);
166
+ console.log(` Path: ${singleResult.skillPath}`);
94
167
  process.exit(0);
95
168
  }
96
169
  else {
97
170
  console.error(`✗ Installation failed!`);
98
- console.error(` Error: ${result.error || 'Unknown error'}`);
171
+ console.error(` Error: ${singleResult.error || 'Unknown error'}`);
99
172
  process.exit(1);
100
173
  }
101
174
  }
@@ -364,21 +437,94 @@ function promptAiTool() {
364
437
  });
365
438
  });
366
439
  }
367
- function promptSkillSelection(skills) {
440
+ function selectSkillsFromPackage(skills) {
368
441
  return new Promise((resolve) => {
369
442
  const rl = readline_1.default.createInterface({
370
443
  input: process.stdin,
371
444
  output: process.stdout
372
445
  });
373
446
  console.log('');
374
- console.log('Installed skills:');
447
+ console.log(`${colors.cyan}Multi-skill package detected!${colors.reset}`);
448
+ console.log(`${colors.cyan}Found ${skills.length} skill(s) in this package:${colors.reset}`);
375
449
  console.log('');
450
+ // Display all skills with colors and separators
376
451
  for (let i = 0; i < skills.length; i++) {
377
452
  const skill = skills[i];
378
- console.log(` [${i + 1}] ${skill.name}`);
379
- console.log(` ${skill.description}`);
453
+ // Separator line before each skill
454
+ console.log(`${colors.dim}${'─'.repeat(60)}${colors.reset}`);
455
+ // Index and skill name
456
+ console.log(`${colors.bright}${colors.green}[${i + 1}]${colors.reset} ${colors.bright}${colors.yellow}${skill.name}${colors.reset}`);
457
+ // Description
458
+ const lines = skill.description.split('\n');
459
+ for (const line of lines) {
460
+ console.log(`${colors.dim} ${line}${colors.reset}`);
461
+ }
462
+ // Relative path in package
463
+ console.log(`${colors.dim} Package path: ${skill.relativePath}${colors.reset}`);
464
+ }
465
+ // Separator line after last skill
466
+ console.log(`${colors.dim}${'─'.repeat(60)}${colors.reset}`);
467
+ console.log('');
468
+ console.log('Enter the numbers of the skills to install (separated by spaces, e.g., "1 3 5"):');
469
+ console.log('Press Enter to install all skills:');
470
+ rl.question('Your choice: ', (answer) => {
471
+ rl.close();
472
+ const input = answer.trim();
473
+ if (!input) {
474
+ // Install all skills
475
+ console.log(`Installing all ${skills.length} skills...`);
476
+ resolve(skills);
477
+ return;
478
+ }
479
+ // Parse the input and extract valid indices
480
+ const selectedNumbers = input.split(/\s+/).map(s => parseInt(s, 10));
481
+ const selectedSkills = [];
482
+ for (const num of selectedNumbers) {
483
+ if (isNaN(num)) {
484
+ console.log(`Skipping invalid number: ${input}`);
485
+ continue;
486
+ }
487
+ if (num < 1 || num > skills.length) {
488
+ console.log(`Skipping out of range number: ${num}`);
489
+ continue;
490
+ }
491
+ selectedSkills.push(skills[num - 1]);
492
+ }
493
+ if (selectedSkills.length === 0) {
494
+ console.log('No valid skills selected.');
495
+ resolve([]);
496
+ return;
497
+ }
380
498
  console.log('');
499
+ console.log(`Selected skills: ${selectedSkills.map(s => s.name).join(', ')}`);
500
+ resolve(selectedSkills);
501
+ });
502
+ });
503
+ }
504
+ function promptSkillSelection(skills) {
505
+ return new Promise((resolve) => {
506
+ const rl = readline_1.default.createInterface({
507
+ input: process.stdin,
508
+ output: process.stdout
509
+ });
510
+ console.log('');
511
+ console.log(`${colors.cyan}Installed skills:${colors.reset}`);
512
+ console.log('');
513
+ // Display all skills with colors and separators
514
+ for (let i = 0; i < skills.length; i++) {
515
+ const skill = skills[i];
516
+ // Separator line before each skill
517
+ console.log(`${colors.dim}${'─'.repeat(60)}${colors.reset}`);
518
+ // Index and skill name
519
+ console.log(`${colors.bright}${colors.green}[${i + 1}]${colors.reset} ${colors.bright}${colors.yellow}${skill.name}${colors.reset}`);
520
+ // Description
521
+ console.log(`${colors.dim} ${skill.description}${colors.reset}`);
522
+ // Path
523
+ console.log(`${colors.dim} ${skill.path}${colors.reset}`);
381
524
  }
525
+ // Separator line after last skill
526
+ console.log(`${colors.dim}${'─'.repeat(60)}${colors.reset}`);
527
+ console.log('');
382
528
  console.log('Enter the numbers of the skills to uninstall (separated by spaces, e.g., "1 3 5"):');
383
529
  console.log('Press Enter to cancel:');
384
530
  rl.question('Your choice: ', (answer) => {
@@ -456,4 +602,46 @@ async function listSkillsFromBothDirectories(aiTool) {
456
602
  console.log('No skills installed in either location.');
457
603
  }
458
604
  }
605
+ function promptUsername() {
606
+ return new Promise((resolve) => {
607
+ const rl = readline_1.default.createInterface({
608
+ input: process.stdin,
609
+ output: process.stdout
610
+ });
611
+ rl.question('Wiki username: ', (answer) => {
612
+ rl.close();
613
+ resolve(answer.trim());
614
+ });
615
+ });
616
+ }
617
+ async function searchAndSelectSkill(username, password, wikiUrl, allowSelfSigned) {
618
+ const searcher = new wiki_1.WikiSearcher();
619
+ const selector = new wiki_1.SkillSelector();
620
+ console.log(`Username: ${username}`);
621
+ console.log('Searching Wiki for skills...');
622
+ console.log('');
623
+ try {
624
+ // Search for attachments with agent_skill tag
625
+ const items = await searcher.search({
626
+ username,
627
+ password,
628
+ baseUrl: wikiUrl,
629
+ allowSelfSigned
630
+ });
631
+ if (items.length === 0) {
632
+ console.log('No skills found.');
633
+ return null;
634
+ }
635
+ // Parse items (fetch comments)
636
+ const parser = new wiki_1.WikiParser(searcher.httpClientInstance);
637
+ const parsed = await parser.parseSkillItems(items);
638
+ // Display and select
639
+ const selectedUrl = await selector.displayAndSelect(parsed);
640
+ return selectedUrl;
641
+ }
642
+ catch (error) {
643
+ console.error(`Search failed: ${error.message}`);
644
+ return null;
645
+ }
646
+ }
459
647
  program.parse();
@@ -65,4 +65,4 @@ class Logger {
65
65
  }
66
66
  }
67
67
  exports.Logger = Logger;
68
- Logger.globalLevel = 'info';
68
+ Logger.globalLevel = 'error';
@@ -13,10 +13,9 @@ class HttpClient {
13
13
  this.options = options;
14
14
  this.logger = new logger_1.Logger('HttpClient');
15
15
  this.cookieStore = new Map();
16
- // 设置环境变量来忽略证书验证(必须在创建axios实例之前)
16
+ // 配置axios实例
17
17
  this.logger.info(`HTTP Client config: baseUrl=${options.baseUrl}, allowSelfSigned=${options.allowSelfSigned}`);
18
18
  if (options.baseUrl.startsWith('https://') && options.allowSelfSigned) {
19
- process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
20
19
  this.logger.warn('SSL certificate verification disabled for self-signed certificates');
21
20
  }
22
21
  // 配置axios实例
@@ -12,6 +12,17 @@ export interface InstallResult {
12
12
  skillPath?: string;
13
13
  error?: string;
14
14
  }
15
+ export interface SkillInPackage {
16
+ name: string;
17
+ description: string;
18
+ path: string;
19
+ relativePath: string;
20
+ }
21
+ export interface MultiSkillPackageResult {
22
+ isMultiSkill: boolean;
23
+ skills?: SkillInPackage[];
24
+ singleSkillResult?: InstallResult;
25
+ }
15
26
  export declare class InstallService {
16
27
  private readonly logger;
17
28
  private readonly sourceDetector;
@@ -21,13 +32,22 @@ export declare class InstallService {
21
32
  private readonly skillsManager;
22
33
  /**
23
34
  * Install a skill from a source (URL or local file)
35
+ * Returns MultiSkillPackageResult to handle both single and multi-skill packages
36
+ */
37
+ install(options: InstallOptions): Promise<MultiSkillPackageResult>;
38
+ /**
39
+ * Install selected skills from a multi-skill package
40
+ */
41
+ installSelectedSkills(skills: SkillInPackage[], extractDir: string, options: InstallOptions): Promise<InstallResult[]>;
42
+ /**
43
+ * Find all skill directories in the extracted files
44
+ * Searches recursively for all directories containing SKILL.md
24
45
  */
25
- install(options: InstallOptions): Promise<InstallResult>;
46
+ private findAllSkillDirectories;
26
47
  /**
27
- * Find the skill directory in the extracted files
28
- * The archive may contain a single root directory or have SKILL.md at root
48
+ * Recursively find all directories containing valid SKILL.md
29
49
  */
30
- private findSkillDirectory;
50
+ private findSkillDirectoriesRecursive;
31
51
  /**
32
52
  * Move a directory from one location to another
33
53
  * Handles cross-device moves by copying and then deleting
@@ -22,6 +22,7 @@ class InstallService {
22
22
  }
23
23
  /**
24
24
  * Install a skill from a source (URL or local file)
25
+ * Returns MultiSkillPackageResult to handle both single and multi-skill packages
25
26
  */
26
27
  async install(options) {
27
28
  const tempDir = await (0, promises_1.mkdtemp)((0, path_1.join)((0, os_1.tmpdir)(), 'skill-install-'));
@@ -64,11 +65,30 @@ class InstallService {
64
65
  catch (error) {
65
66
  throw new errors_1.WikiError(errors_1.ErrorType.EXTRACTION_FAILED, `Failed to extract archive: ${error.message}`, error);
66
67
  }
67
- // Find the skill directory (may be the root or a subdirectory)
68
- const skillDir = await this.findSkillDirectory(extractDir);
69
- if (!skillDir) {
68
+ // Find all skill directories in the package
69
+ const skillDirs = await this.findAllSkillDirectories(extractDir);
70
+ if (skillDirs.length === 0) {
70
71
  throw new errors_1.WikiError(errors_1.ErrorType.INVALID_SKILL, 'No valid skill found in archive. A valid skill must contain SKILL.md with name and description.');
71
72
  }
73
+ // If multiple skills found, return them for user selection
74
+ if (skillDirs.length > 1) {
75
+ const skills = [];
76
+ for (const skillDir of skillDirs) {
77
+ const metadata = await this.validator.validateOrThrow(skillDir);
78
+ skills.push({
79
+ name: metadata.name,
80
+ description: metadata.description,
81
+ path: skillDir,
82
+ relativePath: (0, path_1.relative)(extractDir, skillDir)
83
+ });
84
+ }
85
+ return {
86
+ isMultiSkill: true,
87
+ skills
88
+ };
89
+ }
90
+ // Single skill - proceed with installation
91
+ const skillDir = skillDirs[0];
72
92
  // Validate the skill
73
93
  const metadata = await this.validator.validateOrThrow(skillDir);
74
94
  // Check if already installed
@@ -92,17 +112,23 @@ class InstallService {
92
112
  await this.moveDirectory(skillDir, targetPath);
93
113
  this.logger.info(`Skill installed successfully: ${metadata.name} -> ${targetPath}`);
94
114
  return {
95
- success: true,
96
- skillName: metadata.name,
97
- skillPath: targetPath
115
+ isMultiSkill: false,
116
+ singleSkillResult: {
117
+ success: true,
118
+ skillName: metadata.name,
119
+ skillPath: targetPath
120
+ }
98
121
  };
99
122
  }
100
123
  catch (error) {
101
124
  const errorMsg = error instanceof Error ? error.message : String(error);
102
125
  this.logger.error(`Installation failed: ${errorMsg}`);
103
126
  return {
104
- success: false,
105
- error: errorMsg
127
+ isMultiSkill: false,
128
+ singleSkillResult: {
129
+ success: false,
130
+ error: errorMsg
131
+ }
106
132
  };
107
133
  }
108
134
  finally {
@@ -116,32 +142,83 @@ class InstallService {
116
142
  }
117
143
  }
118
144
  /**
119
- * Find the skill directory in the extracted files
120
- * The archive may contain a single root directory or have SKILL.md at root
145
+ * Install selected skills from a multi-skill package
146
+ */
147
+ async installSelectedSkills(skills, extractDir, options) {
148
+ const results = [];
149
+ const targetSkillsDir = this.skillsManager.getEffectiveSkillsDir(options.skillsDir);
150
+ await (0, promises_1.mkdir)(targetSkillsDir, { recursive: true });
151
+ for (const skill of skills) {
152
+ try {
153
+ const targetPath = (0, path_1.join)(targetSkillsDir, skill.name);
154
+ // Check if already installed
155
+ if (!options.force && await this.skillsManager.isInstalled(skill.name, options.skillsDir)) {
156
+ results.push({
157
+ success: false,
158
+ error: `Skill "${skill.name}" is already installed. Use --force to reinstall.`
159
+ });
160
+ continue;
161
+ }
162
+ // If the skill already exists and force is true, remove it first
163
+ if (options.force) {
164
+ try {
165
+ await (0, promises_1.rm)(targetPath, { recursive: true, force: true });
166
+ }
167
+ catch {
168
+ // Ignore if directory doesn't exist
169
+ }
170
+ }
171
+ // Move the skill directory to the final location
172
+ await this.moveDirectory(skill.path, targetPath);
173
+ this.logger.info(`Skill installed successfully: ${skill.name} -> ${targetPath}`);
174
+ results.push({
175
+ success: true,
176
+ skillName: skill.name,
177
+ skillPath: targetPath
178
+ });
179
+ }
180
+ catch (error) {
181
+ const errorMsg = error instanceof Error ? error.message : String(error);
182
+ this.logger.error(`Failed to install ${skill.name}: ${errorMsg}`);
183
+ results.push({
184
+ success: false,
185
+ error: errorMsg
186
+ });
187
+ }
188
+ }
189
+ return results;
190
+ }
191
+ /**
192
+ * Find all skill directories in the extracted files
193
+ * Searches recursively for all directories containing SKILL.md
121
194
  */
122
- async findSkillDirectory(extractDir) {
123
- // First, check if SKILL.md exists at the root
124
- const entries = await (0, promises_1.readdir)(extractDir, { withFileTypes: true });
125
- // Check for SKILL.md at root level
126
- const hasSkillMdAtRoot = entries.some(e => e.name === 'SKILL.md');
127
- if (hasSkillMdAtRoot) {
128
- const isValid = await this.validator.validate(extractDir);
195
+ async findAllSkillDirectories(extractDir) {
196
+ const skillDirs = [];
197
+ await this.findSkillDirectoriesRecursive(extractDir, skillDirs);
198
+ return skillDirs;
199
+ }
200
+ /**
201
+ * Recursively find all directories containing valid SKILL.md
202
+ */
203
+ async findSkillDirectoriesRecursive(dir, results) {
204
+ const entries = await (0, promises_1.readdir)(dir, { withFileTypes: true });
205
+ // Check if current directory has SKILL.md
206
+ const hasSkillMd = entries.some(e => e.name === 'SKILL.md');
207
+ if (hasSkillMd) {
208
+ const isValid = await this.validator.validate(dir);
129
209
  if (isValid.valid) {
130
- return extractDir;
210
+ results.push(dir);
211
+ // Don't recurse into a valid skill directory
212
+ return;
131
213
  }
132
214
  }
133
- // Check subdirectories
215
+ // Recurse into subdirectories
134
216
  for (const entry of entries) {
135
- if (!entry.isDirectory()) {
136
- continue;
137
- }
138
- const subDirPath = (0, path_1.join)(extractDir, entry.name);
139
- const isValid = await this.validator.validate(subDirPath);
140
- if (isValid.valid) {
141
- return subDirPath;
217
+ if (entry.isDirectory()) {
218
+ const subDirPath = (0, path_1.join)(dir, entry.name);
219
+ await this.findSkillDirectoriesRecursive(subDirPath, results);
142
220
  }
143
221
  }
144
- return null;
145
222
  }
146
223
  /**
147
224
  * Move a directory from one location to another
@@ -0,0 +1,3 @@
1
+ export { WikiSearcher, WikiSkillItem, SearchOptions } from './wiki-searcher';
2
+ export { WikiParser, ParsedSkillItem } from './wiki-parser';
3
+ export { SkillSelector } from './skill-selector';
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SkillSelector = exports.WikiParser = exports.WikiSearcher = void 0;
4
+ var wiki_searcher_1 = require("./wiki-searcher");
5
+ Object.defineProperty(exports, "WikiSearcher", { enumerable: true, get: function () { return wiki_searcher_1.WikiSearcher; } });
6
+ var wiki_parser_1 = require("./wiki-parser");
7
+ Object.defineProperty(exports, "WikiParser", { enumerable: true, get: function () { return wiki_parser_1.WikiParser; } });
8
+ var skill_selector_1 = require("./skill-selector");
9
+ Object.defineProperty(exports, "SkillSelector", { enumerable: true, get: function () { return skill_selector_1.SkillSelector; } });
@@ -0,0 +1,13 @@
1
+ import { ParsedSkillItem } from './wiki-parser';
2
+ export declare class SkillSelector {
3
+ private readonly logger;
4
+ /**
5
+ * Display skill list and wait for user selection
6
+ * Returns the download URL of selected skill, or null if cancelled
7
+ */
8
+ displayAndSelect(items: ParsedSkillItem[]): Promise<string | null>;
9
+ /**
10
+ * Prompt user to select a skill from the list
11
+ */
12
+ private promptChoice;
13
+ }
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SkillSelector = void 0;
7
+ const readline_1 = __importDefault(require("readline"));
8
+ const logger_1 = require("../core/logger");
9
+ // Color codes for terminal output
10
+ const colors = {
11
+ reset: '\x1b[0m',
12
+ bright: '\x1b[1m',
13
+ dim: '\x1b[2m',
14
+ red: '\x1b[31m',
15
+ green: '\x1b[32m',
16
+ yellow: '\x1b[33m',
17
+ blue: '\x1b[34m',
18
+ magenta: '\x1b[35m',
19
+ cyan: '\x1b[36m',
20
+ white: '\x1b[37m',
21
+ gray: '\x1b[90m'
22
+ };
23
+ class SkillSelector {
24
+ constructor() {
25
+ this.logger = new logger_1.Logger('SkillSelector');
26
+ }
27
+ /**
28
+ * Display skill list and wait for user selection
29
+ * Returns the download URL of selected skill, or null if cancelled
30
+ */
31
+ async displayAndSelect(items) {
32
+ if (items.length === 0) {
33
+ this.logger.info('No skills found to display.');
34
+ return null;
35
+ }
36
+ console.log('');
37
+ console.log(`${colors.cyan}Found ${items.length} skill(s):${colors.reset}`);
38
+ console.log('');
39
+ // Display all skills
40
+ for (let i = 0; i < items.length; i++) {
41
+ const item = items[i];
42
+ // Add separator line before each skill
43
+ console.log(`${colors.dim}${'─'.repeat(60)}${colors.reset}`);
44
+ // Index with color
45
+ console.log(`${colors.bright}${colors.green}[${i + 1}]${colors.reset} ${colors.bright}${colors.yellow}${item.name}${colors.reset} ${colors.dim}${colors.gray}${item.pageUrl}${colors.reset}`);
46
+ // Display comment (may be multiline)
47
+ if (item.description) {
48
+ const lines = item.description.split('\n');
49
+ for (const line of lines) {
50
+ console.log(`${colors.dim} ${line}${colors.reset}`);
51
+ }
52
+ }
53
+ }
54
+ // Add separator line after the last skill
55
+ console.log(`${colors.dim}${'─'.repeat(60)}${colors.reset}`);
56
+ console.log('');
57
+ // Prompt for selection
58
+ const choice = await this.promptChoice(items.length);
59
+ if (choice === null) {
60
+ return null;
61
+ }
62
+ const selected = items[choice];
63
+ this.logger.info(`Selected: ${selected.name}`);
64
+ return selected.downloadUrl;
65
+ }
66
+ /**
67
+ * Prompt user to select a skill from the list
68
+ */
69
+ promptChoice(max) {
70
+ return new Promise((resolve) => {
71
+ const rl = readline_1.default.createInterface({
72
+ input: process.stdin,
73
+ output: process.stdout
74
+ });
75
+ rl.question(`Select skill to install [1-${max}] (0 to cancel): `, (answer) => {
76
+ rl.close();
77
+ const trimmed = answer.trim();
78
+ if (trimmed === '') {
79
+ console.log('No selection made.');
80
+ resolve(null);
81
+ return;
82
+ }
83
+ const num = parseInt(trimmed, 10);
84
+ if (isNaN(num)) {
85
+ console.log('Invalid input. Please enter a number.');
86
+ resolve(null);
87
+ return;
88
+ }
89
+ if (num < 0 || num > max) {
90
+ console.log(`Invalid selection. Please enter a number between 0 and ${max}.`);
91
+ resolve(null);
92
+ return;
93
+ }
94
+ if (num === 0) {
95
+ console.log('Cancelled.');
96
+ resolve(null);
97
+ return;
98
+ }
99
+ resolve(num - 1); // Convert to 0-based index
100
+ });
101
+ });
102
+ }
103
+ }
104
+ exports.SkillSelector = SkillSelector;
@@ -0,0 +1,26 @@
1
+ import { HttpClient } from '../http/http-client';
2
+ import { WikiSkillItem } from './wiki-searcher';
3
+ export interface ParsedSkillItem extends WikiSkillItem {
4
+ description: string;
5
+ }
6
+ export declare class WikiParser {
7
+ private httpClient;
8
+ private readonly logger;
9
+ constructor(httpClient: HttpClient);
10
+ /**
11
+ * Get attachment comment/description from the attachment metadata
12
+ */
13
+ getAttachmentComments(attachmentId: string): Promise<string>;
14
+ /**
15
+ * Get actual replies/comments on the attachment
16
+ */
17
+ private getAttachmentReplies;
18
+ /**
19
+ * Parse all skill items and fetch their comments
20
+ */
21
+ parseSkillItems(items: WikiSkillItem[]): Promise<ParsedSkillItem[]>;
22
+ /**
23
+ * Extract plain text description from HTML comments
24
+ */
25
+ private extractDescription;
26
+ }
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WikiParser = void 0;
4
+ const logger_1 = require("../core/logger");
5
+ class WikiParser {
6
+ constructor(httpClient) {
7
+ this.httpClient = httpClient;
8
+ this.logger = new logger_1.Logger('WikiParser');
9
+ }
10
+ /**
11
+ * Get attachment comment/description from the attachment metadata
12
+ */
13
+ async getAttachmentComments(attachmentId) {
14
+ try {
15
+ this.logger.info(`Fetching attachment details for ID: ${attachmentId}`);
16
+ // Get attachment details with metadata and extensions
17
+ const response = await this.httpClient.get(`/rest/api/content/${attachmentId}?expand=metadata,extensions`);
18
+ this.logger.debug(`Attachment API response status: ${response.status}`);
19
+ // Try to get comment from metadata or extensions
20
+ const comment = response.data?.metadata?.comment ||
21
+ response.data?.extensions?.comment ||
22
+ '';
23
+ if (comment) {
24
+ this.logger.info(`Found comment for attachment ${attachmentId}: ${comment}`);
25
+ return comment;
26
+ }
27
+ this.logger.debug(`No comment found in attachment metadata for: ${attachmentId}`);
28
+ // Fallback: try to get actual comments on the attachment
29
+ return await this.getAttachmentReplies(attachmentId);
30
+ }
31
+ catch (error) {
32
+ this.logger.error(`Failed to fetch attachment details for ${attachmentId}: ${error.message}`);
33
+ return '';
34
+ }
35
+ }
36
+ /**
37
+ * Get actual replies/comments on the attachment
38
+ */
39
+ async getAttachmentReplies(attachmentId) {
40
+ try {
41
+ const expandParams = 'body.view,body.export_view,body.storage';
42
+ const response = await this.httpClient.get(`/rest/api/content/${attachmentId}/child/comment?expand=${expandParams}`);
43
+ if (!response.data?.results?.length) {
44
+ return '';
45
+ }
46
+ const comments = response.data.results
47
+ .map((c) => {
48
+ return c.body?.view?.value ||
49
+ c.body?.export_view?.value ||
50
+ c.body?.storage?.value ||
51
+ '';
52
+ })
53
+ .filter(Boolean)
54
+ .join('\n---\n');
55
+ this.logger.info(`Found ${response.data.results.length} comment(s) for attachment: ${attachmentId}`);
56
+ return comments;
57
+ }
58
+ catch (error) {
59
+ this.logger.debug(`Failed to fetch attachment replies: ${error.message}`);
60
+ return '';
61
+ }
62
+ }
63
+ /**
64
+ * Parse all skill items and fetch their comments
65
+ */
66
+ async parseSkillItems(items) {
67
+ const results = [];
68
+ this.logger.info(`Parsing ${items.length} skill item(s)...`);
69
+ for (const item of items) {
70
+ const comments = await this.getAttachmentComments(item.id);
71
+ results.push({
72
+ ...item,
73
+ description: this.extractDescription(comments)
74
+ });
75
+ }
76
+ return results;
77
+ }
78
+ /**
79
+ * Extract plain text description from HTML comments
80
+ */
81
+ extractDescription(htmlComments) {
82
+ if (!htmlComments || htmlComments.trim() === '') {
83
+ return 'No description available.';
84
+ }
85
+ // Decode HTML entities
86
+ let text = htmlComments
87
+ .replace(/&nbsp;/g, ' ')
88
+ .replace(/&amp;/g, '&')
89
+ .replace(/&lt;/g, '<')
90
+ .replace(/&gt;/g, '>')
91
+ .replace(/&quot;/g, '"')
92
+ .replace(/&#39;/g, "'")
93
+ .replace(/&apos;/g, "'");
94
+ // Remove HTML tags
95
+ text = text.replace(/<[^>]*>/g, '');
96
+ // Clean up whitespace
97
+ text = text.replace(/\s+/g, ' ').trim();
98
+ // Limit to 200 characters
99
+ if (text.length > 200) {
100
+ text = text.substring(0, 197) + '...';
101
+ }
102
+ return text || 'No description available.';
103
+ }
104
+ }
105
+ exports.WikiParser = WikiParser;
@@ -0,0 +1,33 @@
1
+ import { HttpClient } from '../http/http-client';
2
+ export interface WikiSkillItem {
3
+ id: string;
4
+ name: string;
5
+ downloadUrl: string;
6
+ containerId: string;
7
+ pageUrl: string;
8
+ }
9
+ export interface SearchOptions {
10
+ username: string;
11
+ password: string;
12
+ baseUrl?: string;
13
+ allowSelfSigned?: boolean;
14
+ }
15
+ export declare class WikiSearcher {
16
+ private readonly logger;
17
+ private httpClient;
18
+ private authService;
19
+ get httpClientInstance(): HttpClient;
20
+ constructor();
21
+ /**
22
+ * Search for attachments with agent_skill tag
23
+ */
24
+ search(options: SearchOptions): Promise<WikiSkillItem[]>;
25
+ /**
26
+ * Filter search results by file extension
27
+ */
28
+ private filterByExtension;
29
+ /**
30
+ * Build full download URL from API response
31
+ */
32
+ private buildDownloadUrl;
33
+ }
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WikiSearcher = void 0;
4
+ const http_client_1 = require("../http/http-client");
5
+ const auth_service_1 = require("../auth/auth-service");
6
+ const logger_1 = require("../core/logger");
7
+ const errors_1 = require("../core/errors");
8
+ const ALLOWED_EXTENSIONS = ['.tar.gz', '.tgz', '.zip'];
9
+ class WikiSearcher {
10
+ get httpClientInstance() {
11
+ return this.httpClient;
12
+ }
13
+ constructor() {
14
+ this.logger = new logger_1.Logger('WikiSearcher');
15
+ // Will be initialized in search method
16
+ this.httpClient = null;
17
+ this.authService = null;
18
+ }
19
+ /**
20
+ * Search for attachments with agent_skill tag
21
+ */
22
+ async search(options) {
23
+ // Determine base URL from environment or use default
24
+ const baseUrl = options.baseUrl || process.env.WIKI_BASE_URL || '';
25
+ if (!baseUrl) {
26
+ throw new errors_1.WikiError(errors_1.ErrorType.INVALID_INPUT, 'Wiki base URL is required. Set WIKI_BASE_URL environment variable or use --wiki-url option.');
27
+ }
28
+ // Initialize HTTP client and auth service
29
+ this.httpClient = new http_client_1.HttpClient({
30
+ baseUrl,
31
+ allowSelfSigned: options.allowSelfSigned ?? true
32
+ });
33
+ this.authService = new auth_service_1.AuthService(this.httpClient, {
34
+ baseUrl,
35
+ username: options.username,
36
+ password: options.password
37
+ });
38
+ // Authenticate
39
+ this.logger.info('Authenticating to Wiki...');
40
+ await this.authService.ensureAuthenticated();
41
+ this.logger.info('Authentication successful');
42
+ // Search for attachments with agent_skill label
43
+ this.logger.info('Searching for attachments with agent_skill tag...');
44
+ const response = await this.httpClient.get('/rest/api/content/search?cql=type=attachment+and+label=agent_skill&expand=container,version');
45
+ if (!response.data?.results) {
46
+ this.logger.warn('No results found');
47
+ return [];
48
+ }
49
+ // Filter by allowed extensions
50
+ const filtered = this.filterByExtension(response.data.results, ALLOWED_EXTENSIONS);
51
+ this.logger.info(`Found ${filtered.length} skill(s) matching criteria`);
52
+ return filtered;
53
+ }
54
+ /**
55
+ * Filter search results by file extension
56
+ */
57
+ filterByExtension(results, extensions) {
58
+ const filtered = [];
59
+ if (!results) {
60
+ return filtered;
61
+ }
62
+ for (const item of results) {
63
+ const filename = item.title || '';
64
+ // Check if filename ends with any allowed extension
65
+ const hasAllowedExtension = extensions.some(ext => filename.endsWith(ext));
66
+ if (hasAllowedExtension) {
67
+ const baseUrl = this.httpClient.getBaseURL();
68
+ filtered.push({
69
+ id: item.id,
70
+ name: filename,
71
+ downloadUrl: this.buildDownloadUrl(baseUrl, item),
72
+ containerId: item.container?.id || '',
73
+ pageUrl: item._links?.webui ? `${baseUrl}${item._links.webui}` : ''
74
+ });
75
+ }
76
+ }
77
+ return filtered;
78
+ }
79
+ /**
80
+ * Build full download URL from API response
81
+ */
82
+ buildDownloadUrl(baseUrl, item) {
83
+ // Try to use download link from API
84
+ if (item._links?.download) {
85
+ const downloadPath = item._links.download;
86
+ if (downloadPath.startsWith('http')) {
87
+ return downloadPath;
88
+ }
89
+ return `${baseUrl}${downloadPath}`;
90
+ }
91
+ // Fallback: construct URL from attachment info
92
+ if (item.container?.id && item.id) {
93
+ return `${baseUrl}/download/attachments/${item.container.id}/${encodeURIComponent(item.title)}`;
94
+ }
95
+ throw new errors_1.WikiError(errors_1.ErrorType.INVALID_INPUT, `Cannot build download URL for attachment: ${item.title}`);
96
+ }
97
+ }
98
+ exports.WikiSearcher = WikiSearcher;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sstar/skill-install",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Agent Skill installation tool - download, extract, validate, and install skills for Claude Code and Codex",
5
5
  "main": "dist/index.js",
6
6
  "bin": {