@seojin08/skill-finder 0.0.0-stage → 0.3.3

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.
Files changed (82) hide show
  1. package/.env.example +3 -0
  2. package/CHANGELOG.md +44 -0
  3. package/LICENSE +21 -0
  4. package/README.md +86 -2
  5. package/bin/skill-finder.mjs +389 -0
  6. package/package.json +39 -4
  7. package/server/src/analyze/frontmatter.js +130 -0
  8. package/server/src/analyze/quality.js +34 -0
  9. package/server/src/analyze/risk.js +123 -0
  10. package/server/src/app.js +57 -0
  11. package/server/src/bootstrap.js +46 -0
  12. package/server/src/claude/cli.js +89 -0
  13. package/server/src/config.js +82 -0
  14. package/server/src/context.js +1 -0
  15. package/server/src/db/index.js +128 -0
  16. package/server/src/db/schema.js +95 -0
  17. package/server/src/db/store.js +353 -0
  18. package/server/src/discovery/classifyRepo.js +81 -0
  19. package/server/src/discovery/genericRules.js +182 -0
  20. package/server/src/discovery/pluginRules.js +59 -0
  21. package/server/src/discovery/tree.js +79 -0
  22. package/server/src/index.js +55 -0
  23. package/server/src/indexer.js +502 -0
  24. package/server/src/install/installed.js +206 -0
  25. package/server/src/install/plan.js +118 -0
  26. package/server/src/install/run.js +97 -0
  27. package/server/src/install/scan.js +70 -0
  28. package/server/src/install/summary.js +14 -0
  29. package/server/src/install/uninstall.js +59 -0
  30. package/server/src/install/validate.js +24 -0
  31. package/server/src/jobs.js +107 -0
  32. package/server/src/recommend/readme.js +508 -0
  33. package/server/src/release.js +41 -0
  34. package/server/src/routes/admin.js +203 -0
  35. package/server/src/routes/favorites.js +61 -0
  36. package/server/src/routes/health.js +27 -0
  37. package/server/src/routes/install.js +283 -0
  38. package/server/src/routes/presets.js +66 -0
  39. package/server/src/routes/recommend.js +36 -0
  40. package/server/src/routes/search.js +53 -0
  41. package/server/src/routes/skills.js +214 -0
  42. package/server/src/search/activity.js +13 -0
  43. package/server/src/search/category.js +109 -0
  44. package/server/src/search/koreanTerms.js +284 -0
  45. package/server/src/search/pipeline.js +977 -0
  46. package/server/src/search/rank.js +138 -0
  47. package/server/src/search/result.js +129 -0
  48. package/server/src/search/searchLog.js +53 -0
  49. package/server/src/security.js +53 -0
  50. package/server/src/settings.js +56 -0
  51. package/server/src/sources/gitTree.js +101 -0
  52. package/server/src/sources/github.js +503 -0
  53. package/server/src/sources/marketplace.js +78 -0
  54. package/server/src/sources/pluginSource.js +98 -0
  55. package/server/src/sources/popularRepos.js +74 -0
  56. package/server/src/sources/skillsSh.js +74 -0
  57. package/server/src/tasks.js +216 -0
  58. package/server/src/update.js +53 -0
  59. package/server/src/util/folderPicker.js +208 -0
  60. package/server/src/util/http.js +41 -0
  61. package/server/src/util/limiter.js +110 -0
  62. package/server/src/util/log.js +94 -0
  63. package/server/src/util/ndjson.js +27 -0
  64. package/server/src/util/proc.js +80 -0
  65. package/server/src/util/resolveFolder.js +78 -0
  66. package/server/src/util/sanitize.js +45 -0
  67. package/server/src/util/sizedCache.js +74 -0
  68. package/server/src/util/textFiles.js +9 -0
  69. package/server/src/util/tokens.js +21 -0
  70. package/server/src/version.js +12 -0
  71. package/web/dist/LICENSE.txt +21 -0
  72. package/web/dist/THIRD-PARTY-LICENSES.txt +2267 -0
  73. package/web/dist/assets/ArasakaGame-B0V_LZLl.css +1 -0
  74. package/web/dist/assets/ArasakaGame-C1Wkmqik.js +36 -0
  75. package/web/dist/assets/index-BgL1OaU4.css +2 -0
  76. package/web/dist/assets/index-Ec2XBK4-.js +66 -0
  77. package/web/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  78. package/web/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  79. package/web/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  80. package/web/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  81. package/web/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  82. package/web/dist/index.html +15 -0
@@ -0,0 +1,182 @@
1
+ import { basename, joinPath, skillMdPath } from "./tree.js";
2
+ // Mirrors skills CLI 1.7.6 src/skills.ts (discoverSkills) so that what we list is what `skills add --skill` can install.
3
+ // Unchanged since 1.7.4 except two things not mirrored: extra search dirs a repo can list in skills.sh.json (`paths`),
4
+ // and skipping a skill whose name or description is empty once control characters are removed.
5
+ export const SKIP_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__'];
6
+ export const AGENT_PROJECT_SKILL_DIRS = [
7
+ '.agents/skills',
8
+ '.claude/skills',
9
+ '.cline/skills',
10
+ '.codebuddy/skills',
11
+ '.codex/skills',
12
+ '.commandcode/skills',
13
+ '.continue/skills',
14
+ '.factory/skills',
15
+ '.github/skills',
16
+ '.goose/skills',
17
+ '.grok/skills',
18
+ '.iflow/skills',
19
+ '.junie/skills',
20
+ '.kilo/skills',
21
+ '.kilocode/skills',
22
+ '.kimchi/skills',
23
+ '.kiro/skills',
24
+ '.minimax/skills',
25
+ '.mux/skills',
26
+ '.neovate/skills',
27
+ '.opencode/skills',
28
+ '.openhands/skills',
29
+ '.pi/skills',
30
+ '.posit/assistant/skills',
31
+ '.qoder/skills',
32
+ '.roo/skills',
33
+ '.trae/skills',
34
+ '.windsurf/skills',
35
+ '.zcode/skills',
36
+ '.zencoder/skills',
37
+ ];
38
+ /**
39
+ * Candidate skill folders in priority order: root children (depth 1), then `skills/`, `skills/.curated`,
40
+ * `skills/.experimental`, `skills/.system` and agent project folders (depth 3). Does not descend below a
41
+ * folder that has SKILL.md. The root SKILL.md case is handled by the caller.
42
+ */
43
+ export function priorityCandidateDirs(tree, base = '') {
44
+ const priority = [
45
+ base,
46
+ joinPath(base, 'skills'),
47
+ joinPath(base, 'skills/.curated'),
48
+ joinPath(base, 'skills/.experimental'),
49
+ joinPath(base, 'skills/.system'),
50
+ ...AGENT_PROJECT_SKILL_DIRS.map((d) => joinPath(base, d)),
51
+ ];
52
+ const deep = new Set(priority.slice(1));
53
+ const seen = new Set();
54
+ const out = [];
55
+ const walk = (dir, maxDepth, depth) => {
56
+ if (!tree.hasDir(dir))
57
+ return;
58
+ for (const child of tree.childDirs(dir)) {
59
+ if (tree.hasFile(skillMdPath(child))) {
60
+ if (!seen.has(child)) {
61
+ seen.add(child);
62
+ out.push(child);
63
+ }
64
+ continue;
65
+ }
66
+ if (depth >= maxDepth || SKIP_DIRS.includes(basename(child)))
67
+ continue;
68
+ walk(child, maxDepth, depth + 1);
69
+ }
70
+ };
71
+ for (const dir of priority)
72
+ walk(dir, deep.has(dir) ? 3 : 1, 1);
73
+ return out;
74
+ }
75
+ /** Fallback full recursion (max depth 5 from base) used when the priority scan found no valid skill. */
76
+ export function recursiveCandidateDirs(tree, base = '', maxDepth = 5) {
77
+ const out = [];
78
+ const visit = (dir, depth) => {
79
+ if (depth > maxDepth)
80
+ return;
81
+ if (tree.hasFile(skillMdPath(dir)))
82
+ out.push(dir);
83
+ for (const child of tree.childDirs(dir)) {
84
+ if (SKIP_DIRS.includes(basename(child)))
85
+ continue;
86
+ visit(child, depth + 1);
87
+ }
88
+ };
89
+ visit(base, 0);
90
+ return out;
91
+ }
92
+ /**
93
+ * Generic repository mode. A SKILL.md needs a string name and description (as in the CLI);
94
+ * same-name skills keep only the first one.
95
+ */
96
+ export async function discoverGenericSkills(tree, read, validate, opts = {}) {
97
+ const maxFetch = opts.maxFetch ?? 300;
98
+ if (tree.hasFile('SKILL.md')) {
99
+ const raw = await read('SKILL.md');
100
+ const v = raw != null ? validate(raw) : null;
101
+ if (raw != null && v)
102
+ return [{ dirPath: '', name: v.name, raw }];
103
+ }
104
+ const collect = async (dirs) => {
105
+ const limited = dirs.slice(0, maxFetch);
106
+ const raws = await Promise.all(limited.map((d) => read(skillMdPath(d))));
107
+ const seen = new Set();
108
+ const out = [];
109
+ limited.forEach((dirPath, i) => {
110
+ const raw = raws[i];
111
+ const v = raw != null ? validate(raw) : null;
112
+ if (raw == null || !v || seen.has(v.name))
113
+ return;
114
+ seen.add(v.name);
115
+ out.push({ dirPath, name: v.name, raw });
116
+ });
117
+ return out;
118
+ };
119
+ const found = await collect(priorityCandidateDirs(tree));
120
+ if (found.length > 0)
121
+ return found;
122
+ return collect(recursiveCandidateDirs(tree).filter((d) => d !== ''));
123
+ }
124
+ /**
125
+ * Same decision as {@link discoverGenericSkills} for one folder at a time, so looking up a single skill
126
+ * in a repo with hundreds of them reads a handful of SKILL.md files instead of all of them:
127
+ * a valid root SKILL.md hides everything else; a priority folder counts if its own SKILL.md is valid;
128
+ * a deeper folder counts only when no priority folder holds a valid skill.
129
+ */
130
+ export function genericAcceptor(tree, read, validate) {
131
+ const priority = priorityCandidateDirs(tree);
132
+ const prioritySet = new Set(priority);
133
+ const recursive = recursiveCandidateDirs(tree).filter((d) => d !== '');
134
+ const recursiveSet = new Set(recursive);
135
+ const hasRoot = tree.hasFile('SKILL.md');
136
+ const validRaw = async (dir) => {
137
+ const raw = await read(skillMdPath(dir));
138
+ return raw != null && validate(raw) ? raw : null;
139
+ };
140
+ let rootValid = null;
141
+ let anyPriorityValid = null;
142
+ const priorityHasValid = async () => {
143
+ // Usually the first folder is valid, so check a few at a time and stop early.
144
+ for (let i = 0; i < priority.length; i += 4) {
145
+ const raws = await Promise.all(priority.slice(i, i + 4).map(validRaw));
146
+ if (raws.some(Boolean))
147
+ return true;
148
+ }
149
+ return false;
150
+ };
151
+ // Same-name skills keep only the first one in scan order. Checking every earlier folder would mean
152
+ // reading them all; copies almost always sit in a folder named after the skill, so check those.
153
+ const firstWithName = async (tier, dir, raw) => {
154
+ if (raw == null)
155
+ return null;
156
+ const name = validate(raw).name;
157
+ for (const other of tier.slice(0, tier.indexOf(dir))) {
158
+ if (basename(other) !== name)
159
+ continue;
160
+ const earlier = await validRaw(other);
161
+ if (earlier != null && validate(earlier).name === name)
162
+ return null;
163
+ }
164
+ return raw;
165
+ };
166
+ return {
167
+ candidates: [...(hasRoot ? [''] : []), ...priority, ...recursive.filter((d) => !prioritySet.has(d))],
168
+ async accepts(dir) {
169
+ if (hasRoot) {
170
+ rootValid ??= validRaw('').then(Boolean);
171
+ if (await rootValid)
172
+ return dir === '' ? read('SKILL.md') : null;
173
+ }
174
+ if (prioritySet.has(dir))
175
+ return firstWithName(priority, dir, await validRaw(dir));
176
+ if (!recursiveSet.has(dir))
177
+ return null;
178
+ anyPriorityValid ??= priorityHasValid();
179
+ return (await anyPriorityValid) ? null : firstWithName(recursive, dir, await validRaw(dir));
180
+ },
181
+ };
182
+ }
@@ -0,0 +1,59 @@
1
+ import { joinPath, resolveRel, skillMdPath } from "./tree.js";
2
+ const COMPONENT_FIELDS = ['commands', 'agents', 'skills', 'hooks', 'mcpServers', 'lspServers', 'outputStyles', 'monitors'];
3
+ export function hasComponentFields(obj) {
4
+ return !!obj && COMPONENT_FIELDS.some((k) => obj[k] != null);
5
+ }
6
+ const toPathList = (v) => typeof v === 'string' ? [v] : Array.isArray(v) ? v.filter((x) => typeof x === 'string') : [];
7
+ /**
8
+ * Skill folders of one plugin (Claude Code plugin manifest rules):
9
+ * - default `skills/<name>/SKILL.md`
10
+ * - plugin.json `skills` and marketplace entry `skills` add to the default scan; each path is either a folder
11
+ * holding SKILL.md or a folder of `<name>/SKILL.md` bundles
12
+ * - a root SKILL.md is a single-skill plugin when there is no `skills/` folder and no `skills` key
13
+ * - an entry whose source is the marketplace root and lists `skills` loads only that list
14
+ */
15
+ export function discoverPluginSkills(tree, root, manifest, entry, opts = {}) {
16
+ const dirs = new Set();
17
+ const addPath = (rel) => {
18
+ const abs = resolveRel(root, rel);
19
+ if (abs == null)
20
+ return;
21
+ if (tree.hasFile(skillMdPath(abs))) {
22
+ dirs.add(abs);
23
+ return;
24
+ }
25
+ for (const child of tree.childDirs(abs))
26
+ if (tree.hasFile(skillMdPath(child)))
27
+ dirs.add(child);
28
+ };
29
+ const entrySkills = toPathList(entry?.skills);
30
+ if (opts.entryAtMarketplaceRoot && entrySkills.length > 0) {
31
+ for (const p of entrySkills)
32
+ addPath(p);
33
+ }
34
+ else {
35
+ const defaultDir = joinPath(root, 'skills');
36
+ for (const child of tree.childDirs(defaultDir))
37
+ if (tree.hasFile(skillMdPath(child)))
38
+ dirs.add(child);
39
+ for (const p of toPathList(manifest?.skills))
40
+ addPath(p);
41
+ for (const p of entrySkills)
42
+ addPath(p);
43
+ const noSkillsKey = manifest?.skills == null && entry?.skills == null;
44
+ if (tree.hasFile(skillMdPath(root)) && !tree.hasDir(defaultDir) && noSkillsKey)
45
+ dirs.add(root);
46
+ }
47
+ const experimental = (manifest?.experimental ?? null);
48
+ const signals = {
49
+ hooks: tree.hasFile(joinPath(root, 'hooks/hooks.json')) || manifest?.hooks != null || entry?.hooks != null,
50
+ mcp: tree.hasFile(joinPath(root, '.mcp.json')) || manifest?.mcpServers != null || entry?.mcpServers != null,
51
+ bin: tree.hasDir(joinPath(root, 'bin')) && tree.dirHasAnyFile(joinPath(root, 'bin')),
52
+ monitors: tree.hasFile(joinPath(root, 'monitors/monitors.json')) ||
53
+ experimental?.monitors != null ||
54
+ manifest?.monitors != null ||
55
+ entry?.monitors != null,
56
+ };
57
+ const strictConflict = entry?.strict === false && hasComponentFields(manifest) && hasComponentFields(entry);
58
+ return { root, skillDirs: [...dirs].sort(), signals, strictConflict };
59
+ }
@@ -0,0 +1,79 @@
1
+ /** Read-only view over a repository's blob paths with directory helpers. */
2
+ export class TreeIndex {
3
+ files;
4
+ children = new Map();
5
+ constructor(entries) {
6
+ this.files = new Map();
7
+ this.children.set('', new Set());
8
+ for (const e of entries) {
9
+ const path = e.path.replace(/^\/+/, '');
10
+ this.files.set(path, { size: e.size ?? null, sha: e.sha ?? null });
11
+ const parts = path.split('/');
12
+ let dir = '';
13
+ for (let i = 0; i < parts.length - 1; i++) {
14
+ const child = dir ? `${dir}/${parts[i]}` : parts[i];
15
+ let set = this.children.get(dir);
16
+ if (!set)
17
+ this.children.set(dir, (set = new Set()));
18
+ set.add(parts[i]);
19
+ if (!this.children.has(child))
20
+ this.children.set(child, new Set());
21
+ dir = child;
22
+ }
23
+ }
24
+ }
25
+ hasFile(path) {
26
+ return this.files.has(path);
27
+ }
28
+ hasDir(path) {
29
+ return path === '' || this.children.has(path);
30
+ }
31
+ /** Immediate child directory paths, sorted. */
32
+ childDirs(dir) {
33
+ const names = [...(this.children.get(dir) ?? [])].sort();
34
+ return names.map((n) => (dir ? `${dir}/${n}` : n));
35
+ }
36
+ /** Files under a directory, as paths relative to it. */
37
+ filesUnder(dir) {
38
+ const prefix = dir ? `${dir}/` : '';
39
+ const out = [];
40
+ for (const [path, info] of this.files)
41
+ if (path.startsWith(prefix))
42
+ out.push({ path: path.slice(prefix.length), ...info });
43
+ return out.sort((a, b) => a.path.localeCompare(b.path));
44
+ }
45
+ dirHasAnyFile(dir) {
46
+ const prefix = dir ? `${dir}/` : '';
47
+ for (const path of this.files.keys())
48
+ if (path.startsWith(prefix))
49
+ return true;
50
+ return false;
51
+ }
52
+ }
53
+ export const joinPath = (...parts) => parts.filter((p) => p !== '' && p !== '.').join('/');
54
+ export const skillMdPath = (dir) => joinPath(dir, 'SKILL.md');
55
+ export const basename = (p) => p.split('/').pop() ?? p;
56
+ /**
57
+ * Normalizes a manifest-relative path ("./skills/x", ".", "skills/x/") against a base directory.
58
+ * Returns null when the path escapes the base or is absolute.
59
+ */
60
+ export function resolveRel(base, rel) {
61
+ if (typeof rel !== 'string')
62
+ return null;
63
+ if (/^([a-zA-Z]:|\/|\\)/.test(rel))
64
+ return null;
65
+ const out = base ? base.split('/') : [];
66
+ const depthFloor = out.length;
67
+ for (const seg of rel.replace(/\\/g, '/').split('/')) {
68
+ if (seg === '' || seg === '.')
69
+ continue;
70
+ if (seg === '..') {
71
+ if (out.length <= depthFloor)
72
+ return null;
73
+ out.pop();
74
+ continue;
75
+ }
76
+ out.push(seg);
77
+ }
78
+ return out.join('/');
79
+ }
@@ -0,0 +1,55 @@
1
+ import { config, ensureDataDirs } from "./config.js";
2
+ import { createApp } from "./app.js";
3
+ import { openContext } from "./bootstrap.js";
4
+ import { DataTooNewError } from "./db/index.js";
5
+ import { startUpdateChecks } from "./update.js";
6
+ import { startLogFile } from "./util/log.js";
7
+ import { pruneSearchLog, recheckRiskRules, refreshRepoMeta, relevelRisk, REPO_META_EVERY_MS, startSync } from "./tasks.js";
8
+ const HOST = '127.0.0.1';
9
+ const DAY_MS = 24 * 60 * 60 * 1000;
10
+ ensureDataDirs();
11
+ startLogFile(config.dataDir, [config.githubToken]);
12
+ // A background task that fails (a sync, a refresh) is logged, not fatal: Node would otherwise end the server.
13
+ process.on('unhandledRejection', (reason) => console.error('[백그라운드 오류]', reason));
14
+ process.on('uncaughtException', (err) => {
15
+ console.error('[치명적 오류]', err);
16
+ process.exit(1);
17
+ });
18
+ const ctx = (() => {
19
+ try {
20
+ return openContext(config);
21
+ }
22
+ catch (err) {
23
+ if (!(err instanceof DataTooNewError))
24
+ throw err;
25
+ console.error(err.message);
26
+ process.exit(1);
27
+ }
28
+ })();
29
+ const app = createApp(ctx);
30
+ // Express' listen(port) binds 0.0.0.0; always pass the loopback host explicitly.
31
+ const server = app.listen(config.port, HOST, () => {
32
+ console.log(`Skill Finder: http://${HOST}:${config.port}${config.dev ? ' (dev)' : ''}`);
33
+ console.log(`GitHub 토큰: ${config.githubToken ? '있음' : '없음 (GitHub 코드 검색 생략, 트리는 git으로 조회)'}`);
34
+ startSync(ctx);
35
+ setInterval(() => startSync(ctx), DAY_MS).unref();
36
+ pruneSearchLog(ctx);
37
+ setInterval(() => pruneSearchLog(ctx), DAY_MS).unref();
38
+ relevelRisk(ctx);
39
+ startUpdateChecks();
40
+ void recheckRiskRules(ctx);
41
+ setInterval(() => void recheckRiskRules(ctx), DAY_MS).unref();
42
+ void refreshRepoMeta(ctx);
43
+ setInterval(() => void refreshRepoMeta(ctx), REPO_META_EVERY_MS).unref();
44
+ });
45
+ server.on('error', (err) => {
46
+ console.error(err);
47
+ process.exit(1);
48
+ });
49
+ for (const sig of ['SIGINT', 'SIGTERM']) {
50
+ process.on(sig, () => {
51
+ server.close();
52
+ ctx.store.db.close();
53
+ process.exit(0);
54
+ });
55
+ }