skill-search 0.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/src/config.js ADDED
@@ -0,0 +1,220 @@
1
+ // src/config.js
2
+ // SkillsMP API Configuration
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+
8
+ // Data Directory
9
+ const DATA_DIR = path.join(os.homedir(), '.skill-search');
10
+ const CONFIG_FILE = path.join(DATA_DIR, 'config.json');
11
+
12
+ // Default Configuration
13
+ const DEFAULT_CONFIG = {
14
+ api: {
15
+ baseUrl: 'https://skillsmp.com/api/v1',
16
+ // API Key should be configured by user
17
+ apiKey: process.env.SKILLSMP_API_KEY || ''
18
+ },
19
+ sync: {
20
+ defaultLimit: 20,
21
+ sortBy: 'recent',
22
+ marketplaceOnly: false
23
+ },
24
+ customPaths: [], // Custom paths to scan for skills
25
+ cache: {
26
+ ttl: 24 * 60 * 60 * 1000, // 24 hours
27
+ indexTtl: 7 * 24 * 60 * 60 * 1000 // 7 days
28
+ }
29
+ };
30
+
31
+ /**
32
+ * Ensure data directory exists
33
+ */
34
+ function ensureDataDir() {
35
+ if (!fs.existsSync(DATA_DIR)) {
36
+ fs.mkdirSync(DATA_DIR, { recursive: true });
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Get user configuration
42
+ */
43
+ function getUserConfig() {
44
+ ensureDataDir();
45
+
46
+ if (fs.existsSync(CONFIG_FILE)) {
47
+ try {
48
+ const content = fs.readFileSync(CONFIG_FILE, 'utf8');
49
+ return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
50
+ } catch (err) {
51
+ return DEFAULT_CONFIG;
52
+ }
53
+ }
54
+ return DEFAULT_CONFIG;
55
+ }
56
+
57
+ /**
58
+ * Set user configuration
59
+ */
60
+ function setUserConfig(config) {
61
+ ensureDataDir();
62
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
63
+ }
64
+
65
+ /**
66
+ * Get API Base URL
67
+ */
68
+ function getApiBaseUrl() {
69
+ const config = getUserConfig();
70
+ return config.api.baseUrl;
71
+ }
72
+
73
+ /**
74
+ * Get API Key
75
+ */
76
+ function getApiKey() {
77
+ const config = getUserConfig();
78
+ return config.api.apiKey || process.env.SKILLSMP_API_KEY || '';
79
+ }
80
+
81
+ /**
82
+ * Set API Key
83
+ */
84
+ function setApiKey(apiKey) {
85
+ const config = getUserConfig();
86
+ config.api.apiKey = apiKey;
87
+ setUserConfig(config);
88
+ }
89
+
90
+ /**
91
+ * Get data directory path
92
+ */
93
+ function getDataDir() {
94
+ ensureDataDir();
95
+ return DATA_DIR;
96
+ }
97
+
98
+ // Default primary directory name (without leading dot)
99
+ const DEFAULT_PRIMARY_DIR = 'skill-search';
100
+
101
+ // Available primary directory options
102
+ const AVAILABLE_PRIMARY_DIRS = [
103
+ { key: 'skill-search', name: '.skill-search', desc: 'Default Skill Search directory' },
104
+ { key: 'claude', name: '.claude', desc: 'Claude AI directory' },
105
+ { key: 'codex', name: '.codex', desc: 'OpenAI Codex directory' },
106
+ { key: 'gemini', name: '.gemini', desc: 'Google Gemini directory' },
107
+ { key: 'copilot', name: '.copilot', desc: 'GitHub Copilot directory' }
108
+ ];
109
+
110
+ /**
111
+ * Get primary directory name (the key, not the full path)
112
+ */
113
+ function getPrimaryDirName() {
114
+ const config = getUserConfig();
115
+ return config.primaryDir || DEFAULT_PRIMARY_DIR;
116
+ }
117
+
118
+ /**
119
+ * Get primary directory full path
120
+ */
121
+ function getPrimaryDirPath() {
122
+ const dirName = getPrimaryDirName();
123
+ return path.join(os.homedir(), '.' + dirName);
124
+ }
125
+
126
+ /**
127
+ * Set primary directory
128
+ */
129
+ function setPrimaryDir(dirKey) {
130
+ const config = getUserConfig();
131
+ config.primaryDir = dirKey;
132
+ setUserConfig(config);
133
+ }
134
+
135
+ /**
136
+ * Get various file paths (uses primary directory)
137
+ */
138
+ function getPaths() {
139
+ const dataDir = getPrimaryDirPath();
140
+ // Ensure data dir exists
141
+ if (!fs.existsSync(dataDir)) {
142
+ fs.mkdirSync(dataDir, { recursive: true });
143
+ }
144
+ return {
145
+ dataDir,
146
+ indexFile: path.join(dataDir, 'index.json'),
147
+ metaFile: path.join(dataDir, 'meta.json'),
148
+ configFile: CONFIG_FILE, // Config file stays in .skill-search
149
+ docsDir: path.join(dataDir, 'docs'),
150
+ skillsDir: path.join(dataDir, 'skills')
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Get available primary directories
156
+ */
157
+ function getAvailablePrimaryDirs() {
158
+ return AVAILABLE_PRIMARY_DIRS;
159
+ }
160
+
161
+ /**
162
+ * Get custom paths
163
+ */
164
+ function getCustomPaths() {
165
+ const config = getUserConfig();
166
+ return config.customPaths || [];
167
+ }
168
+
169
+ /**
170
+ * Add a custom path
171
+ */
172
+ function addCustomPath(customPath) {
173
+ const config = getUserConfig();
174
+ if (!config.customPaths) {
175
+ config.customPaths = [];
176
+ }
177
+ // Avoid duplicates
178
+ if (!config.customPaths.includes(customPath)) {
179
+ config.customPaths.push(customPath);
180
+ setUserConfig(config);
181
+ return true;
182
+ }
183
+ return false;
184
+ }
185
+
186
+ /**
187
+ * Remove a custom path
188
+ */
189
+ function removeCustomPath(customPath) {
190
+ const config = getUserConfig();
191
+ if (!config.customPaths) return false;
192
+ const idx = config.customPaths.indexOf(customPath);
193
+ if (idx !== -1) {
194
+ config.customPaths.splice(idx, 1);
195
+ setUserConfig(config);
196
+ return true;
197
+ }
198
+ return false;
199
+ }
200
+
201
+ module.exports = {
202
+ DEFAULT_CONFIG,
203
+ DEFAULT_PRIMARY_DIR,
204
+ AVAILABLE_PRIMARY_DIRS,
205
+ ensureDataDir,
206
+ getUserConfig,
207
+ setUserConfig,
208
+ getApiBaseUrl,
209
+ getApiKey,
210
+ setApiKey,
211
+ getDataDir,
212
+ getPaths,
213
+ getPrimaryDirName,
214
+ getPrimaryDirPath,
215
+ setPrimaryDir,
216
+ getAvailablePrimaryDirs,
217
+ getCustomPaths,
218
+ addCustomPath,
219
+ removeCustomPath
220
+ };
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "updated": "2024-01-01T00:00:00Z",
4
+ "skills": [],
5
+ "categories": []
6
+ }
@@ -0,0 +1,23 @@
1
+ const inquirer = require('inquirer');
2
+ const chalk = require('chalk');
3
+
4
+ async function selectSkill(matches) {
5
+ if (!matches || matches.length === 0) return null;
6
+
7
+ const { selected } = await inquirer.prompt([{
8
+ type: 'list',
9
+ name: 'selected',
10
+ message: 'Multiple matches found, please select:',
11
+ choices: matches.map(m => ({
12
+ name: `${chalk.bold(m.id)} - ${m.name || m.description || ''}`,
13
+ value: m
14
+ })),
15
+ loop: false
16
+ }]);
17
+
18
+ return selected;
19
+ }
20
+
21
+ module.exports = {
22
+ selectSkill
23
+ };
@@ -0,0 +1,204 @@
1
+ // src/localCrawler.js
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const { promisify } = require('util');
6
+ const config = require('./config');
7
+
8
+ const readdir = promisify(fs.readdir);
9
+ const stat = promisify(fs.stat);
10
+
11
+ /**
12
+ * Crawl local directories for skills
13
+ * Looks for patterns: ~/{folder}/{*skill*}/{skill}
14
+ * Matches any subfolder containing 'skill' in its name (case-insensitive):
15
+ * - skills, skill, global_skills, etc.
16
+ */
17
+ async function crawlLocalSkills() {
18
+ const homeDir = os.homedir();
19
+ const results = [];
20
+
21
+ let homeChildren;
22
+ try {
23
+ homeChildren = await readdir(homeDir);
24
+ } catch (e) {
25
+ return [];
26
+ }
27
+
28
+ // Scan all folders in home directory for subdirectories containing 'skill' in their name
29
+ // This matches: skills, skill, global_skills, etc. (case-insensitive)
30
+
31
+ for (const child of homeChildren) {
32
+ if (child === '.skill-search') continue; // Skip own data directory (handled by store)
33
+ const childPath = path.join(homeDir, child);
34
+
35
+ // Check if this is a directory
36
+ let childStats;
37
+ try {
38
+ childStats = await stat(childPath);
39
+ } catch (e) {
40
+ continue;
41
+ }
42
+ if (!childStats.isDirectory()) continue;
43
+
44
+ // List all subdirectories in this folder
45
+ let subfolders;
46
+ try {
47
+ subfolders = await readdir(childPath);
48
+ } catch (e) {
49
+ continue;
50
+ }
51
+
52
+ // Find all subdirectories containing 'skill' in their name (case-insensitive)
53
+ for (const subfolder of subfolders) {
54
+ // Check if subfolder name contains 'skill' (case-insensitive)
55
+ if (!subfolder.toLowerCase().includes('skill')) continue;
56
+
57
+ const skillsPath = path.join(childPath, subfolder);
58
+
59
+ // Check if this is a directory
60
+ let skillsStats;
61
+ try {
62
+ skillsStats = await stat(skillsPath);
63
+ } catch (e) {
64
+ continue;
65
+ }
66
+ if (!skillsStats.isDirectory()) continue;
67
+
68
+ // Found a skill-containing directory, list its content
69
+ const providerName = child; // e.g. .claude
70
+
71
+ let skillFolders;
72
+ try {
73
+ skillFolders = await readdir(skillsPath);
74
+ } catch (e) {
75
+ continue;
76
+ }
77
+
78
+ for (const skillFolder of skillFolders) {
79
+ // Ignore system files like .DS_Store
80
+ if (skillFolder.startsWith('.')) continue;
81
+
82
+ const skillPath = path.join(skillsPath, skillFolder);
83
+ try {
84
+ const skillStats = await stat(skillPath);
85
+ if (skillStats.isDirectory()) {
86
+ results.push({
87
+ id: skillFolder,
88
+ name: skillFolder,
89
+ provider: providerName, // "xxx" level 1
90
+ path: skillPath,
91
+ updatedAt: skillStats.mtime,
92
+ source: 'local_scan',
93
+ skillsFolder: subfolder // Track which skill folder this came from
94
+ });
95
+ }
96
+ } catch (e) { }
97
+ }
98
+ }
99
+ }
100
+
101
+
102
+ // Scan custom paths from config
103
+ const customPaths = config.getCustomPaths();
104
+ for (const customPath of customPaths) {
105
+ // Resolve ~ to home dir if needed
106
+ const resolvedPath = customPath.replace(/^~/, homeDir);
107
+
108
+ if (!fs.existsSync(resolvedPath)) continue;
109
+
110
+ let stats;
111
+ try {
112
+ stats = await stat(resolvedPath);
113
+ } catch (e) { continue; }
114
+
115
+ if (!stats.isDirectory()) continue;
116
+
117
+ const providerName = path.basename(resolvedPath);
118
+
119
+ let skillFolders;
120
+ try {
121
+ skillFolders = await readdir(resolvedPath);
122
+ } catch (e) { continue; }
123
+
124
+ for (const skillFolder of skillFolders) {
125
+ // Ignore system files like .DS_Store
126
+ if (skillFolder.startsWith('.')) continue;
127
+
128
+ const skillPath = path.join(resolvedPath, skillFolder);
129
+ try {
130
+ const skillStats = await stat(skillPath);
131
+ if (skillStats.isDirectory()) {
132
+ results.push({
133
+ id: skillFolder,
134
+ name: skillFolder,
135
+ provider: providerName,
136
+ path: skillPath,
137
+ updatedAt: skillStats.mtime,
138
+ source: 'custom_path',
139
+ skillsFolder: providerName // Use provider name as skills folder name
140
+ });
141
+ }
142
+ } catch (e) { }
143
+ }
144
+ }
145
+
146
+ return results;
147
+ }
148
+
149
+ /**
150
+ * Scan home directory for any folders containing subdirectories with 'skill' in their name
151
+ * Returns list of directory names (e.g. ['.claude', '.codex', '.skill-search'])
152
+ */
153
+ async function scanForSkillDirectories() {
154
+ const homeDir = os.homedir();
155
+ const results = [];
156
+
157
+ let homeChildren;
158
+ try {
159
+ homeChildren = await readdir(homeDir);
160
+ } catch (e) {
161
+ return [];
162
+ }
163
+
164
+ for (const child of homeChildren) {
165
+ const childPath = path.join(homeDir, child);
166
+
167
+ // Check if this is a directory first
168
+ let childStats;
169
+ try {
170
+ childStats = await stat(childPath);
171
+ } catch (e) {
172
+ continue;
173
+ }
174
+ if (!childStats.isDirectory()) continue;
175
+
176
+ // List subdirectories to find any containing 'skill' in their name
177
+ let subfolders;
178
+ try {
179
+ subfolders = await readdir(childPath);
180
+ } catch (e) {
181
+ continue;
182
+ }
183
+
184
+ // Check if any subfolder contains 'skill' in its name
185
+ for (const subfolder of subfolders) {
186
+ if (subfolder.toLowerCase().includes('skill')) {
187
+ const subfolderPath = path.join(childPath, subfolder);
188
+ try {
189
+ const subfolderStats = await stat(subfolderPath);
190
+ if (subfolderStats.isDirectory()) {
191
+ results.push(child);
192
+ break; // Found at least one skill folder, add this parent and move on
193
+ }
194
+ } catch (e) {
195
+ continue;
196
+ }
197
+ }
198
+ }
199
+ }
200
+
201
+ return results;
202
+ }
203
+
204
+ module.exports = { crawlLocalSkills, scanForSkillDirectories };
package/src/matcher.js ADDED
@@ -0,0 +1,170 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const termStyles = require('./utils'); // Assuming needed or just remove if unrelated
4
+ const Fuse = require('fuse.js');
5
+ const store = require('./store');
6
+ const config = require('./config');
7
+ const { crawlLocalSkills } = require('./localCrawler');
8
+
9
+ let fuseInstance = null;
10
+ let skillsData = null;
11
+
12
+ /**
13
+ * Scan all known skills directories (all available primary directories)
14
+ * This captures skills synced to any of the supported directories
15
+ */
16
+ async function crawlStoreSkills() {
17
+ const results = [];
18
+ const availableDirs = config.getAvailablePrimaryDirs();
19
+ const os = require('os');
20
+
21
+ for (const dir of availableDirs) {
22
+ const dirPath = path.join(os.homedir(), dir.name, 'skills');
23
+
24
+ if (!fs.existsSync(dirPath)) {
25
+ continue;
26
+ }
27
+
28
+ try {
29
+ const folders = fs.readdirSync(dirPath);
30
+ for (const folder of folders) {
31
+ if (folder.startsWith('.')) continue; // Skip hidden files
32
+
33
+ const skillPath = path.join(dirPath, folder);
34
+ try {
35
+ const stats = fs.statSync(skillPath);
36
+ if (stats.isDirectory()) {
37
+ results.push({
38
+ id: folder,
39
+ name: folder,
40
+ provider: dir.name, // Use the actual directory name (.skill-search, .claude, etc.)
41
+ path: skillPath,
42
+ updatedAt: stats.mtime,
43
+ source: 'store_scan'
44
+ });
45
+ }
46
+ } catch (e) { }
47
+ }
48
+ } catch (e) { }
49
+ }
50
+
51
+ return results;
52
+ }
53
+
54
+ /**
55
+ * Initialize Matcher
56
+ */
57
+ async function initMatcher() {
58
+ if (skillsData) return;
59
+
60
+ // 1. Scan .skill-search/skills directory directly (highest priority)
61
+ // This captures skills synced via "Sync Local Skills" command
62
+ const storeSkills = await crawlStoreSkills();
63
+
64
+ // 2. Crawl other local skills directories (e.g., ~/.claude/skills, ~/.codex/skills)
65
+ const localSkills = await crawlLocalSkills();
66
+
67
+ // 3. Create a set of paths already in storeSkills to avoid duplicates
68
+ const storePathSet = new Set(storeSkills.map(s => s.path));
69
+
70
+ // 4. Filter out localSkills that are already in storeSkills (by path)
71
+ const filteredLocalSkills = localSkills.filter(s => !storePathSet.has(s.path));
72
+
73
+ // Merge: store skills first (already synced), then other local skills
74
+ skillsData = [...storeSkills, ...filteredLocalSkills];
75
+
76
+ if (skillsData.length === 0) {
77
+ // If no data, maybe not synced yet
78
+ return;
79
+ }
80
+
81
+ fuseInstance = new Fuse(skillsData, {
82
+ keys: [
83
+ { name: 'id', weight: 2 },
84
+ { name: 'name', weight: 1.5 },
85
+ { name: 'keywords', weight: 1 },
86
+ { name: 'description', weight: 0.5 },
87
+ { name: 'provider', weight: 0.8 } // Added provider to keys
88
+ ],
89
+ threshold: 0.4,
90
+ includeScore: true
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Match Skill
96
+ */
97
+ async function matchSkill(input) {
98
+ await initMatcher();
99
+
100
+ if (!skillsData || skillsData.length === 0) {
101
+ return { type: 'empty' };
102
+ }
103
+
104
+ const normalized = input.toLowerCase().trim();
105
+
106
+ // 1. Exact ID Match
107
+ const exactMatch = skillsData.find(s =>
108
+ s.id.toLowerCase() === normalized
109
+ );
110
+ if (exactMatch) {
111
+ return { type: 'exact', skill: exactMatch };
112
+ }
113
+
114
+ // 2. Exact Keyword Match
115
+ const keywordMatch = skillsData.find(s =>
116
+ s.keywords && s.keywords.some(k => k.toLowerCase() === normalized)
117
+ );
118
+ if (keywordMatch) {
119
+ return { type: 'keyword', skill: keywordMatch };
120
+ }
121
+
122
+ // 3. Fuzzy Search
123
+ const fuzzyResults = fuseInstance.search(normalized);
124
+
125
+ if (fuzzyResults.length === 0) {
126
+ return { type: 'none' };
127
+ }
128
+
129
+ // Single high confidence result
130
+ if (fuzzyResults[0].score < 0.2) {
131
+ return { type: 'fuzzy', skill: fuzzyResults[0].item };
132
+ }
133
+
134
+ // Multiple candidates
135
+ if (fuzzyResults.length > 1) {
136
+ return {
137
+ type: 'multiple',
138
+ matches: fuzzyResults.slice(0, 5).map(r => r.item)
139
+ };
140
+ }
141
+
142
+ return { type: 'fuzzy', skill: fuzzyResults[0].item };
143
+ }
144
+
145
+ /**
146
+ * Search Skills
147
+ */
148
+ async function searchSkills(query) {
149
+ await initMatcher();
150
+ if (!fuseInstance) return [];
151
+
152
+ return fuseInstance.search(query).slice(0, 10).map(r => ({
153
+ ...r.item,
154
+ score: r.score
155
+ }));
156
+ }
157
+
158
+ /**
159
+ * List All Skills
160
+ */
161
+ async function listSkills(force = false) {
162
+ if (force) {
163
+ skillsData = null;
164
+ fuseInstance = null;
165
+ }
166
+ await initMatcher();
167
+ return skillsData || [];
168
+ }
169
+
170
+ module.exports = { matchSkill, searchSkills, listSkills };