@sstar/skill-install 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Plugin and Marketplace type definitions for Claude Code
3
+ */
4
+ /**
5
+ * Supported package types
6
+ */
7
+ export declare enum PackageType {
8
+ SKILL = "skill",
9
+ PLUGIN = "plugin",
10
+ MARKETPLACE = "marketplace",
11
+ UNKNOWN = "unknown"
12
+ }
13
+ /**
14
+ * Plugin metadata from plugin.json
15
+ */
16
+ export interface PluginMetadata {
17
+ name: string;
18
+ version?: string;
19
+ description?: string;
20
+ author?: AuthorInfo;
21
+ homepage?: string;
22
+ repository?: string;
23
+ license?: string;
24
+ keywords?: string[];
25
+ commands?: string | string[];
26
+ agents?: string | string[];
27
+ hooks?: string | object;
28
+ mcpServers?: string | object;
29
+ lspServers?: string | object;
30
+ claudeCode?: {
31
+ minVersion?: string;
32
+ };
33
+ }
34
+ /**
35
+ * Author information
36
+ */
37
+ export interface AuthorInfo {
38
+ name: string;
39
+ email?: string;
40
+ url?: string;
41
+ }
42
+ /**
43
+ * Marketplace metadata from marketplace.json
44
+ */
45
+ export interface MarketplaceMetadata {
46
+ name: string;
47
+ description?: string;
48
+ version?: string;
49
+ owner: {
50
+ name: string;
51
+ email?: string;
52
+ url?: string;
53
+ };
54
+ plugins: PluginDefinition[];
55
+ metadata?: {
56
+ description?: string;
57
+ version?: string;
58
+ pluginRoot?: string;
59
+ };
60
+ }
61
+ /**
62
+ * Plugin definition in marketplace.json
63
+ */
64
+ export interface PluginDefinition {
65
+ name: string;
66
+ source: string | PluginSource;
67
+ description?: string;
68
+ version?: string;
69
+ author?: AuthorInfo;
70
+ homepage?: string;
71
+ repository?: string;
72
+ license?: string;
73
+ keywords?: string[];
74
+ category?: string;
75
+ tags?: string[];
76
+ commands?: string | string[];
77
+ agents?: string | string[];
78
+ hooks?: string | object;
79
+ mcpServers?: string | object;
80
+ lspServers?: string | object;
81
+ strict?: boolean;
82
+ }
83
+ /**
84
+ * Plugin source reference (matches official Claude Code schema)
85
+ */
86
+ export interface PluginSource {
87
+ source: 'url' | 'github' | 'git' | 'npm' | 'file' | 'directory';
88
+ repo?: string;
89
+ url?: string;
90
+ path?: string;
91
+ ref?: string;
92
+ sha?: string;
93
+ }
94
+ /**
95
+ * Detected plugin information
96
+ */
97
+ export interface PluginInfo {
98
+ name: string;
99
+ path: string;
100
+ version?: string;
101
+ source: 'plugin.json' | 'marketplace' | 'scanned' | 'standalone';
102
+ metadata: PluginMetadata | PluginDefinition;
103
+ }
104
+ /**
105
+ * Detected skill information
106
+ */
107
+ export interface SkillInfo {
108
+ name: string;
109
+ path: string;
110
+ description: string;
111
+ }
112
+ /**
113
+ * Package structure after detection
114
+ */
115
+ export interface PackageStructure {
116
+ type: PackageType;
117
+ plugins: PluginInfo[];
118
+ skills: SkillInfo[];
119
+ marketplace?: MarketplaceMetadata | null;
120
+ marketplaceAsPlugin?: PluginMetadata | null;
121
+ hasMarketplace: boolean;
122
+ hasPluginAtRoot: boolean;
123
+ hasPluginInClaudeDir: boolean;
124
+ }
125
+ /**
126
+ * Installation result types
127
+ */
128
+ export interface BaseInstallResult {
129
+ type: 'marketplace' | 'plugin' | 'skill';
130
+ success: boolean;
131
+ path: string;
132
+ }
133
+ export interface MarketplaceInstallResult extends BaseInstallResult {
134
+ type: 'marketplace';
135
+ marketplace: string;
136
+ installedPlugins?: string[];
137
+ pluginAsPluginInstalled?: boolean;
138
+ }
139
+ export interface PluginInstallResult extends BaseInstallResult {
140
+ type: 'plugin';
141
+ plugin: string;
142
+ version?: string;
143
+ marketplace?: string;
144
+ error?: string;
145
+ }
146
+ export interface SkillInstallResult extends BaseInstallResult {
147
+ type: 'skill';
148
+ skill: string;
149
+ description?: string;
150
+ }
151
+ export type InstallResult = MarketplaceInstallResult | PluginInstallResult | SkillInstallResult;
152
+ /**
153
+ * Marketplace info in known_marketplaces.json
154
+ */
155
+ export interface KnownMarketplace {
156
+ source: PluginSource;
157
+ installLocation: string;
158
+ lastUpdated: string;
159
+ }
160
+ /**
161
+ * Installed plugin info in installed_plugins.json
162
+ */
163
+ export interface InstalledPluginEntry {
164
+ name: string;
165
+ scope: 'user' | 'local';
166
+ installPath: string;
167
+ version: string;
168
+ installedAt: string;
169
+ lastUpdated?: string;
170
+ isLocal: boolean;
171
+ projectPath?: string;
172
+ }
173
+ /**
174
+ * known_marketplaces.json structure
175
+ */
176
+ export interface KnownMarketplacesJson {
177
+ [marketplaceName: string]: KnownMarketplace;
178
+ }
179
+ /**
180
+ * installed_plugins.json structure
181
+ */
182
+ export interface InstalledPluginsJson {
183
+ version: 2;
184
+ plugins: {
185
+ [pluginKey: string]: InstalledPluginEntry[];
186
+ };
187
+ }
188
+ /**
189
+ * Plugin list for user selection
190
+ */
191
+ export interface SelectablePlugin {
192
+ name: string;
193
+ version?: string;
194
+ description?: string;
195
+ path: string;
196
+ fromMarketplace?: string;
197
+ }
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ /**
3
+ * Plugin and Marketplace type definitions for Claude Code
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PackageType = void 0;
7
+ /**
8
+ * Supported package types
9
+ */
10
+ var PackageType;
11
+ (function (PackageType) {
12
+ PackageType["SKILL"] = "skill";
13
+ PackageType["PLUGIN"] = "plugin";
14
+ PackageType["MARKETPLACE"] = "marketplace";
15
+ PackageType["UNKNOWN"] = "unknown";
16
+ })(PackageType || (exports.PackageType = PackageType = {}));
@@ -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.1.0",
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": {