@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 +200 -12
- package/dist/core/logger.js +1 -1
- package/dist/http/http-client.js +1 -2
- package/dist/installer/install-service.d.ts +24 -4
- package/dist/installer/install-service.js +104 -27
- package/dist/wiki/index.d.ts +3 -0
- package/dist/wiki/index.js +9 -0
- package/dist/wiki/skill-selector.d.ts +13 -0
- package/dist/wiki/skill-selector.js +104 -0
- package/dist/wiki/wiki-parser.d.ts +26 -0
- package/dist/wiki/wiki-parser.js +105 -0
- package/dist/wiki/wiki-searcher.d.ts +33 -0
- package/dist/wiki/wiki-searcher.js +98 -0
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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 (
|
|
163
|
+
if (singleResult.success) {
|
|
91
164
|
console.log(`✓ Skill installed successfully!`);
|
|
92
|
-
console.log(` Name: ${
|
|
93
|
-
console.log(` Path: ${
|
|
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: ${
|
|
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
|
|
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(
|
|
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
|
-
|
|
379
|
-
console.log(
|
|
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();
|
package/dist/core/logger.js
CHANGED
package/dist/http/http-client.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
46
|
+
private findAllSkillDirectories;
|
|
26
47
|
/**
|
|
27
|
-
*
|
|
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
|
|
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
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
*
|
|
120
|
-
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
210
|
+
results.push(dir);
|
|
211
|
+
// Don't recurse into a valid skill directory
|
|
212
|
+
return;
|
|
131
213
|
}
|
|
132
214
|
}
|
|
133
|
-
//
|
|
215
|
+
// Recurse into subdirectories
|
|
134
216
|
for (const entry of entries) {
|
|
135
|
-
if (
|
|
136
|
-
|
|
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,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(/ /g, ' ')
|
|
88
|
+
.replace(/&/g, '&')
|
|
89
|
+
.replace(/</g, '<')
|
|
90
|
+
.replace(/>/g, '>')
|
|
91
|
+
.replace(/"/g, '"')
|
|
92
|
+
.replace(/'/g, "'")
|
|
93
|
+
.replace(/'/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