@seojin08/skill-finder 0.0.0-stage → 0.3.2

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 +39 -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 +70 -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 +180 -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 +975 -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,284 @@
1
+ import { GENERIC_WORDS } from "./rank.js";
2
+ // Korean words → the English words skill descriptions actually use. Rule-based only (no AI, user decision):
3
+ // the search suggests the English query as a button, and the README recommendation maps Korean READMEs with it.
4
+ export const KO_EN = {
5
+ // xlsx and excel first: the words skill names and descriptions use most (spreadsheet narrowed the search)
6
+ 엑셀: ['xlsx', 'excel', 'spreadsheet'],
7
+ 스프레드시트: ['spreadsheet'],
8
+ 표: ['table'],
9
+ 추출: ['extract'],
10
+ 변환: ['convert'],
11
+ 요약: ['summarize'],
12
+ 번역: ['translate'],
13
+ 리액트: ['react'],
14
+ 워드: ['docx', 'word'],
15
+ 파워포인트: ['pptx', 'powerpoint'],
16
+ 피피티: ['pptx', 'slides'],
17
+ 슬라이드: ['slides'],
18
+ 발표: ['presentation'],
19
+ 테스트: ['testing'],
20
+ 단위: ['unit'],
21
+ 브라우저: ['browser'],
22
+ 데이터베이스: ['database', 'sql'],
23
+ 배포: ['deploy', 'deployment'],
24
+ 보안: ['security'],
25
+ 취약점: ['vulnerability'],
26
+ 디자인: ['design'],
27
+ 문서: ['document', 'documentation'],
28
+ 이미지: ['image'],
29
+ 서버: ['server'],
30
+ 깃허브: ['github'],
31
+ 깃: ['git'],
32
+ 커밋: ['commit'],
33
+ 리뷰: ['review'],
34
+ 검토: ['review'],
35
+ 풀리퀘스트: ['pull', 'request'],
36
+ 이슈: ['issue'],
37
+ 자동화: ['automation'],
38
+ 크롤링: ['scraping'],
39
+ 스크래핑: ['scraping'],
40
+ 웹: ['web'],
41
+ 앱: ['app'],
42
+ 모바일: ['mobile'],
43
+ 프론트엔드: ['frontend'],
44
+ 백엔드: ['backend'],
45
+ 성능: ['performance'],
46
+ 최적화: ['optimization'],
47
+ 디버깅: ['debugging'],
48
+ 디버그: ['debug'],
49
+ 로그: ['logging'],
50
+ 리팩토링: ['refactoring'],
51
+ 리팩터링: ['refactoring'],
52
+ 마이그레이션: ['migration'],
53
+ 차트: ['chart'],
54
+ 그래프: ['chart'],
55
+ 시각화: ['visualization'],
56
+ 분석: ['analysis'],
57
+ 데이터: ['data'],
58
+ 이메일: ['email'],
59
+ 메일: ['email'],
60
+ 캘린더: ['calendar'],
61
+ 일정: ['calendar'],
62
+ 영상: ['video'],
63
+ 동영상: ['video'],
64
+ 음성: ['audio'],
65
+ 회의록: ['meeting', 'notes'],
66
+ 릴리스: ['release'],
67
+ 릴리즈: ['release'],
68
+ 노트: ['notes'],
69
+ 마크다운: ['markdown'],
70
+ 파이썬: ['python'],
71
+ 자바스크립트: ['javascript'],
72
+ 타입스크립트: ['typescript'],
73
+ 도커: ['docker'],
74
+ 쿠버네티스: ['kubernetes'],
75
+ 인프라: ['infrastructure'],
76
+ 클라우드: ['cloud'],
77
+ 결제: ['payment'],
78
+ 인증: ['authentication'],
79
+ 로그인: ['login'],
80
+ 검색: ['search'],
81
+ 기획: ['planning'],
82
+ 글쓰기: ['writing'],
83
+ 블로그: ['blog'],
84
+ 피그마: ['figma'],
85
+ 노션: ['notion'],
86
+ 슬랙: ['slack'],
87
+ // second batch (search debate, panel 7): words real Korean queries use that the first list lacked
88
+ 코드: ['code'],
89
+ 메시지: ['message'],
90
+ 점검: ['audit'],
91
+ 감사: ['audit'],
92
+ 웹사이트: ['website'],
93
+ 사이트: ['website'],
94
+ 크롤러: ['scraper'],
95
+ 프롬프트: ['prompt'],
96
+ 에이전트: ['agent'],
97
+ 스킬: ['skill'],
98
+ 버그: ['bug'],
99
+ 수정: ['fix'],
100
+ 오류: ['error'],
101
+ 에러: ['error'],
102
+ 타입: ['type'],
103
+ 로고: ['logo'],
104
+ 알림: ['notification'],
105
+ 회의: ['meeting'],
106
+ 문서화: ['documentation'],
107
+ 생성: ['generate'],
108
+ 한글: ['hwp'],
109
+ 파일: ['file'],
110
+ 플러그인: ['plugin'],
111
+ 터미널: ['terminal'],
112
+ 스크립트: ['script'],
113
+ 템플릿: ['template'],
114
+ 대시보드: ['dashboard'],
115
+ 컴포넌트: ['component'],
116
+ 함수: ['function'],
117
+ 린트: ['lint'],
118
+ 포맷: ['format'],
119
+ 정규식: ['regex'],
120
+ 브랜치: ['branch'],
121
+ 머지: ['merge'],
122
+ 충돌: ['conflict'],
123
+ 변경로그: ['changelog'],
124
+ 보고서: ['report'],
125
+ 리포트: ['report'],
126
+ 이력서: ['resume'],
127
+ 계약서: ['contract'],
128
+ 자막: ['subtitle'],
129
+ 썸네일: ['thumbnail'],
130
+ 아이콘: ['icon'],
131
+ 폰트: ['font'],
132
+ 접근성: ['accessibility'],
133
+ 마케팅: ['marketing'],
134
+ 광고: ['ads'],
135
+ 게임: ['game'],
136
+ 유니티: ['unity'],
137
+ 안드로이드: ['android'],
138
+ 스위프트: ['swift'],
139
+ 코틀린: ['kotlin'],
140
+ 자바: ['java'],
141
+ 러스트: ['rust'],
142
+ 스프링: ['spring'],
143
+ 장고: ['django'],
144
+ 넥스트: ['nextjs'],
145
+ 테일윈드: ['tailwind'],
146
+ 수파베이스: ['supabase'],
147
+ 파이어베이스: ['firebase'],
148
+ 모니터링: ['monitoring'],
149
+ 백업: ['backup'],
150
+ 머신러닝: ['machine', 'learning'],
151
+ 챗봇: ['chatbot'],
152
+ 웹훅: ['webhook'],
153
+ 큐: ['queue'],
154
+ 캐시: ['cache'],
155
+ 쿼리: ['query'],
156
+ 스키마: ['schema'],
157
+ 랜딩페이지: ['landing', 'page'],
158
+ 크롬: ['chrome'],
159
+ 확장: ['extension'],
160
+ 유튜브: ['youtube'],
161
+ 트위터: ['twitter'],
162
+ 인스타그램: ['instagram'],
163
+ 주식: ['stock'],
164
+ 회계: ['accounting'],
165
+ 세금: ['tax'],
166
+ 논문: ['paper'],
167
+ 연구: ['research'],
168
+ 그림: ['image'],
169
+ 사진: ['photo'],
170
+ 음악: ['music'],
171
+ };
172
+ /** Endings a Korean noun takes in a query ("엑셀로", "표를", "정리해줘" → the noun before them). */
173
+ const PARTICLES = ['', '을', '를', '이', '가', '은', '는', '의', '에', '에서', '로', '으로', '와', '과', '도', '만', '까지', '부터', '하기', '하는', '해', '해줘', '하고', '용'];
174
+ /** More endings, for splitting a query word into dictionary words (koreanSuggestion). */
175
+ // (no "화": "한글화" is localization, not 한글 + an ending; "문서화" has its own entry)
176
+ const ENDINGS = [...PARTICLES.slice(1), '에게', '한테', '랑', '이랑', '처럼', '들', '들을', '할', '한', '하자', '해서', '하려면', '시키기'];
177
+ /**
178
+ * Words that say what to do rather than what about ("정리", "만들기"): dropped from the English query rather
179
+ * than blocking it. Those in REQUEST_WORDS are not even worth showing as dropped.
180
+ */
181
+ const EMPTY_WORDS = ['정리', '작성', '만들기', '만들어', '만들어줘', '만드는', '찾기', '찾아줘', '해줘', '해', '하기', '그리기', '좀', '관련', '방법', '내용', '도구', '자동', '자료'];
182
+ const REQUEST_WORDS = new Set(['해줘', '해', '하기', '좀', '만들어', '만들어줘', '찾아줘']);
183
+ /** At most this many dropped words of at most this many characters are listed (a pasted sentence stays short). */
184
+ const DROPPED_MAX = 5;
185
+ const DROPPED_CHARS = 12;
186
+ /**
187
+ * The words the README recommendation maps (the specific ones: "데이터", "웹" or "검색" in a README say little
188
+ * about which skills fit). It also takes any ending ("문서화", "테스트합니다"), as it always has.
189
+ */
190
+ export const README_TERMS = new Set(['엑셀', '스프레드시트', '리액트', '워드', '파워포인트', '슬라이드', '테스트', '브라우저', '데이터베이스', '배포', '보안', '디자인', '문서', '이미지', '서버', '깃허브']);
191
+ /**
192
+ * Longest dictionary word the token starts with, when what follows it is only an ending ("엑셀로" → 엑셀,
193
+ * "표준" is not 표); else null. `readme`: only README_TERMS, and anything may follow.
194
+ */
195
+ export function koreanTerm(token, readme = false) {
196
+ let best = null;
197
+ for (const k of Object.keys(KO_EN)) {
198
+ if (readme ? !README_TERMS.has(k) : !PARTICLES.includes(token.slice(k.length)))
199
+ continue;
200
+ if (token.startsWith(k) && (!best || k.length > best.length))
201
+ best = k;
202
+ }
203
+ return best ? KO_EN[best] : null;
204
+ }
205
+ /** Words longer than this are not split (a pasted sentence without spaces would only waste time). */
206
+ const SPLIT_MAX_CHARS = 40;
207
+ /**
208
+ * Splits a Korean word into dictionary words and empty words, each optionally followed by one ending
209
+ * ("코드리뷰" → 코드 + 리뷰, "엑셀표추출해줘" → 엑셀 + 표 + 추출 + 해줘), in the fewest pieces. Null unless
210
+ * the whole word is covered: "표준" is not 표 + something, "서버리스" is not 서버.
211
+ */
212
+ function splitKorean(word) {
213
+ if (word.length > SPLIT_MAX_CHARS)
214
+ return null;
215
+ // longest keys first: with as many pieces either way, the first found stays, so "문서화" is one word
216
+ // (documentation) rather than 문서 + an ending, whatever order the dictionary lists them in
217
+ const keys = [...Object.keys(KO_EN), ...EMPTY_WORDS].sort((a, b) => b.length - a.length);
218
+ const best = Array(word.length + 1).fill(null);
219
+ best[0] = [];
220
+ for (let i = 0; i < word.length; i++) {
221
+ const here = best[i];
222
+ if (!here)
223
+ continue;
224
+ for (const k of keys) {
225
+ if (!word.startsWith(k, i))
226
+ continue;
227
+ for (const e of ['', ...ENDINGS]) {
228
+ if (e && !word.startsWith(e, i + k.length))
229
+ continue;
230
+ const j = i + k.length + e.length;
231
+ const piece = { key: k, en: EMPTY_WORDS.includes(k) ? null : KO_EN[k] };
232
+ if (!best[j] || here.length + 1 < best[j].length)
233
+ best[j] = [...here, piece];
234
+ }
235
+ }
236
+ }
237
+ return best[word.length];
238
+ }
239
+ /**
240
+ * An English query for a Korean one, word by word: Latin words stay ("pdf에서" → "pdf"), dictionary words are
241
+ * replaced (two synonyms at most, and not "deploy deployment"), other Korean words are dropped and listed.
242
+ * Null unless at least one Korean word was translated. Rule-based only (no AI, user decision).
243
+ */
244
+ export function koreanSuggestion(query) {
245
+ if (!/\p{Script=Hangul}/u.test(query))
246
+ return null;
247
+ const out = [];
248
+ const dropped = [];
249
+ let translated = false;
250
+ for (const token of query.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? []) {
251
+ out.push(...(token.match(/[a-z0-9]+/g) ?? []).filter((w) => w.length >= 2));
252
+ const korean = token.replace(/[a-z0-9]+/g, ''); // "pdf에서" → "에서", an ending: nothing to translate
253
+ if (!korean || ENDINGS.includes(korean))
254
+ continue;
255
+ const pieces = splitKorean(korean);
256
+ if (!pieces) {
257
+ dropped.push(korean);
258
+ continue;
259
+ }
260
+ for (const p of pieces) {
261
+ if (!p.en) {
262
+ if (!REQUEST_WORDS.has(p.key))
263
+ dropped.push(p.key);
264
+ continue;
265
+ }
266
+ // two words at most, the second only if it is not a form of the first: more would lower each result's
267
+ // share of matching words and narrow the all-words search
268
+ const [a, b] = p.en;
269
+ out.push(a);
270
+ if (b && !b.startsWith(a) && !a.startsWith(b))
271
+ out.push(b);
272
+ translated = true;
273
+ }
274
+ }
275
+ const words = [...new Set(out)];
276
+ // "스킬 만들기" → "skill" alone would only lead to the "too broad" notice
277
+ if (!translated || words.length === 0 || words.every((w) => GENERIC_WORDS.has(w)))
278
+ return null;
279
+ return { query: words.join(' '), dropped: [...new Set(dropped)].slice(0, DROPPED_MAX).map((w) => (w.length > DROPPED_CHARS ? `${w.slice(0, DROPPED_CHARS)}…` : w)) };
280
+ }
281
+ /** koreanSuggestion's query alone. */
282
+ export function englishSuggestion(query) {
283
+ return koreanSuggestion(query)?.query ?? null;
284
+ }