@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.
- package/.env.example +3 -0
- package/CHANGELOG.md +39 -0
- package/LICENSE +21 -0
- package/README.md +86 -2
- package/bin/skill-finder.mjs +389 -0
- package/package.json +39 -4
- package/server/src/analyze/frontmatter.js +130 -0
- package/server/src/analyze/quality.js +34 -0
- package/server/src/analyze/risk.js +123 -0
- package/server/src/app.js +57 -0
- package/server/src/bootstrap.js +46 -0
- package/server/src/claude/cli.js +89 -0
- package/server/src/config.js +70 -0
- package/server/src/context.js +1 -0
- package/server/src/db/index.js +128 -0
- package/server/src/db/schema.js +95 -0
- package/server/src/db/store.js +353 -0
- package/server/src/discovery/classifyRepo.js +81 -0
- package/server/src/discovery/genericRules.js +180 -0
- package/server/src/discovery/pluginRules.js +59 -0
- package/server/src/discovery/tree.js +79 -0
- package/server/src/index.js +55 -0
- package/server/src/indexer.js +502 -0
- package/server/src/install/installed.js +206 -0
- package/server/src/install/plan.js +118 -0
- package/server/src/install/run.js +97 -0
- package/server/src/install/scan.js +70 -0
- package/server/src/install/summary.js +14 -0
- package/server/src/install/uninstall.js +59 -0
- package/server/src/install/validate.js +24 -0
- package/server/src/jobs.js +107 -0
- package/server/src/recommend/readme.js +508 -0
- package/server/src/release.js +41 -0
- package/server/src/routes/admin.js +203 -0
- package/server/src/routes/favorites.js +61 -0
- package/server/src/routes/health.js +27 -0
- package/server/src/routes/install.js +283 -0
- package/server/src/routes/presets.js +66 -0
- package/server/src/routes/recommend.js +36 -0
- package/server/src/routes/search.js +53 -0
- package/server/src/routes/skills.js +214 -0
- package/server/src/search/activity.js +13 -0
- package/server/src/search/category.js +109 -0
- package/server/src/search/koreanTerms.js +284 -0
- package/server/src/search/pipeline.js +975 -0
- package/server/src/search/rank.js +138 -0
- package/server/src/search/result.js +129 -0
- package/server/src/search/searchLog.js +53 -0
- package/server/src/security.js +53 -0
- package/server/src/settings.js +56 -0
- package/server/src/sources/gitTree.js +101 -0
- package/server/src/sources/github.js +503 -0
- package/server/src/sources/marketplace.js +78 -0
- package/server/src/sources/pluginSource.js +98 -0
- package/server/src/sources/popularRepos.js +74 -0
- package/server/src/sources/skillsSh.js +74 -0
- package/server/src/tasks.js +216 -0
- package/server/src/update.js +53 -0
- package/server/src/util/folderPicker.js +208 -0
- package/server/src/util/http.js +41 -0
- package/server/src/util/limiter.js +110 -0
- package/server/src/util/log.js +94 -0
- package/server/src/util/ndjson.js +27 -0
- package/server/src/util/proc.js +80 -0
- package/server/src/util/resolveFolder.js +78 -0
- package/server/src/util/sanitize.js +45 -0
- package/server/src/util/sizedCache.js +74 -0
- package/server/src/util/textFiles.js +9 -0
- package/server/src/util/tokens.js +21 -0
- package/server/src/version.js +12 -0
- package/web/dist/LICENSE.txt +21 -0
- package/web/dist/THIRD-PARTY-LICENSES.txt +2267 -0
- package/web/dist/assets/ArasakaGame-B0V_LZLl.css +1 -0
- package/web/dist/assets/ArasakaGame-C1Wkmqik.js +36 -0
- package/web/dist/assets/index-BgL1OaU4.css +2 -0
- package/web/dist/assets/index-Ec2XBK4-.js +66 -0
- package/web/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/web/dist/index.html +15 -0
package/.env.example
ADDED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# 변경 기록
|
|
2
|
+
|
|
3
|
+
새 버전의 항목은 `npm run package`가 내려받기 페이지의 meta.json에 서명과 함께 넣고, 앱의 "새 버전" 표시를 누르면 보입니다. 한 줄이 한 항목입니다.
|
|
4
|
+
|
|
5
|
+
## 0.3.2 (2026-11-02)
|
|
6
|
+
|
|
7
|
+
- 영어·일본어 화면에서 숫자·이름·명령이 끼는 문장이 그 언어의 어순으로 자연스럽게 나옵니다.
|
|
8
|
+
- 검색이 아직 확인 중인 결과에도 즐겨찾기 별을 누를 수 있습니다. 별을 바꾸지 못하면 별 아래에 이유가 보입니다.
|
|
9
|
+
- 설치 확인 창의 skills CLI 명령이 어디서나 실행할 수 있는 npx skills@1.7.4 모양으로 보입니다.
|
|
10
|
+
- 설정 화면에서 라이선스(MIT)와 함께 들어 있는 오픈소스의 라이선스 목록을 볼 수 있습니다.
|
|
11
|
+
|
|
12
|
+
## 0.3.1 (2026-11-02)
|
|
13
|
+
|
|
14
|
+
- 즐겨찾기에 로그인이 필요 없습니다. 별을 누르면 바로 모이고, 위쪽 메뉴의 즐겨찾기(★)에서 봅니다.
|
|
15
|
+
- 0.3.0에서 만든 계정의 즐겨찾기는 그대로 옮겨지고, 계정 정보는 지워집니다.
|
|
16
|
+
|
|
17
|
+
## 0.3.0 (2026-11-02)
|
|
18
|
+
|
|
19
|
+
- 영어·일본어 화면을 고를 수 있습니다(설정).
|
|
20
|
+
- 회원가입·로그인 뒤 스킬에 별을 눌러 즐겨찾기로 모읍니다. 계정은 이 PC 안에만 저장됩니다.
|
|
21
|
+
- 팀 프리셋: 설치된 스킬을 파일로 내보내고, 팀원의 파일로 같은 스킬을 확인 창을 거쳐 설치합니다.
|
|
22
|
+
- 공유 링크: 앱이 없는 사람도 스킬의 위험 신호와 설치 명령을 볼 수 있는 페이지를 만듭니다.
|
|
23
|
+
- npx로 macOS·Linux에서도 실행하고, Claude Code 안에서 쓰는 읽기 전용 명령(skill-finder search 등)을 더했습니다.
|
|
24
|
+
- 업데이트는 서명을 확인한 뒤에만 설치합니다.
|
|
25
|
+
- Anthropic 공식·파트너 스킬은 별 기준과 상관없이 보입니다.
|
|
26
|
+
- "스크립트 포함"만 있는 스킬은 이제 "주의"가 아니라 "낮음"입니다(공식 pdf·docx 등).
|
|
27
|
+
- 한 단어 검색에서 같은 이름 저장소의 기여자용 스킬이 위를 채우지 않습니다.
|
|
28
|
+
- README가 없으면 CLAUDE.md·AGENTS.md로 추천합니다.
|
|
29
|
+
- 결과 카드의 점수 막대를 빼고, 쪽당 결과는 기본 10개입니다.
|
|
30
|
+
|
|
31
|
+
## 0.2.0 (2026-10-30)
|
|
32
|
+
|
|
33
|
+
- 실행기(SkillFinder.exe): 바탕화면 바로가기, 쓰는 중인 포트를 피하는 포트 선택, 자동 업데이트와 실패 시 되돌리기.
|
|
34
|
+
- 설치 확인 창이 함께 들어오는 파일을 검사하고, 같은 이름 폴더를 덮어쓰기 전에 묻습니다.
|
|
35
|
+
- 검색·설치 화면의 접근성·좁은 화면·공유·내보내기를 다듬었습니다.
|
|
36
|
+
|
|
37
|
+
## 0.1.0 (2026-10-27)
|
|
38
|
+
|
|
39
|
+
- 첫 배포: 마켓플레이스·skills.sh·GitHub 통합 검색, 위험 신호와 점수, README 추천, 설치·삭제.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Parkseojin08
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,87 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Skill Finder
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Claude Code 스킬을 한곳에서 찾아 점수·위험 신호와 함께 보고, 설치 전에 무엇이 함께 들어오는지 확인한 뒤 설치·삭제하는 로컬 웹 앱입니다. 화면은 한국어·영어·일본어입니다.
|
|
4
|
+
|
|
5
|
+
> **English:** A local web app (127.0.0.1) that searches Claude Code skills across plugin marketplaces, skills.sh and GitHub, scores them, flags risky signals, and installs or removes them only after showing the exact command, the install location and the files that come along. No AI calls; the UI is in Korean, English or Japanese (Settings). Run it with `npx @seojin08/skill-finder` on Windows, macOS or Linux.
|
|
6
|
+
|
|
7
|
+
## 무엇을 하나요
|
|
8
|
+
|
|
9
|
+
- **세 곳을 한 번에 검색**: Claude Code 플러그인 마켓플레이스, skills.sh, GitHub(토큰을 넣으면 코드 검색까지). 같은 스킬의 사본은 하나로 묶습니다.
|
|
10
|
+
- **설치 전에 보여 주기**: 실행할 정확한 명령, 설치 위치, 함께 들어오는 파일 검사(내려받아 실행하는 코드 등), 위험 신호(훅, MCP 서버, Bash 사전 승인, 셸 명령 주입, 원격 스크립트 실행). 위험 등급은 규칙으로 판정한 것이며 안전 보증이 아닙니다.
|
|
11
|
+
- **점수**: 관련도·인기(다운로드·별)·최신성·신뢰도(Anthropic 공식·파트너·커뮤니티). 결과는 이름 일치 → 다운로드 → 별 순서로 정렬합니다.
|
|
12
|
+
- **README 추천**: 프로젝트 폴더의 README(없으면 package.json 같은 설명 파일과 CLAUDE.md·AGENTS.md)에서 단어를 뽑아 비슷한 스킬을 보여 줍니다.
|
|
13
|
+
- **설치됨**: 설치된 플러그인·스킬, 매 턴·사용 시 컨텍스트 토큰 추정, 설치 기록, 삭제.
|
|
14
|
+
- **공유·내보내기**: 설치 명령·Markdown 한 줄·공유용 글·공유 링크(앱 없이 열리는 페이지) 복사, 검색 결과와 설치 목록을 JSON·Markdown으로 저장(브라우저 안에서만).
|
|
15
|
+
- **팀 프리셋·즐겨찾기**: 설치된 스킬을 파일로 내보내 팀원이 같은 스킬을 확인 창을 거쳐 설치하고, 별을 눌러 즐겨찾기를 모읍니다(로그인 없음, 이 PC에만 저장).
|
|
16
|
+
- **언어**: 한국어·English·日本語 (설정에서 고름).
|
|
17
|
+
|
|
18
|
+
## 설치
|
|
19
|
+
|
|
20
|
+
### Windows
|
|
21
|
+
|
|
22
|
+
[소개 페이지](https://skill-finder.a01075011775.workers.dev)에서 zip을 받아 압축을 풀고 `SkillFinder.exe`를 실행합니다. 서버를 켜고 브라우저를 열며, 새 버전이 나오면 서명을 확인한 뒤 업데이트합니다.
|
|
23
|
+
|
|
24
|
+
### macOS·Linux·Windows (npx)
|
|
25
|
+
|
|
26
|
+
Node.js 22.18 이상이 있으면 설치 없이 실행합니다. 데이터는 사용자 폴더(macOS `~/Library/Application Support/Skill Finder`, Linux `~/.local/share/skill-finder`, Windows `%LOCALAPPDATA%\SkillFinder\npx-data`)에 남습니다.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx @seojin08/skill-finder
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Claude Code 안에서 쓰기
|
|
33
|
+
|
|
34
|
+
같은 명령에 읽기 전용 하위 명령이 있습니다. 앱이 꺼져 있으면 백그라운드로 켜고 결과만 출력하므로, Claude Code에게 "skill-finder로 pdf 스킬 찾아 줘"처럼 시킬 수 있습니다. 설치·삭제는 하지 않습니다(앱 화면의 확인 창에서만).
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx @seojin08/skill-finder search "pdf tables" --limit 5
|
|
38
|
+
npx @seojin08/skill-finder show gh:anthropics/skills:skills/pdf
|
|
39
|
+
npx @seojin08/skill-finder recommend # 지금 폴더의 README와 비슷한 스킬
|
|
40
|
+
npx @seojin08/skill-finder installed --json
|
|
41
|
+
npx @seojin08/skill-finder stop # 백그라운드로 켠 앱 끄기
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`--json`은 기계가 읽는 출력, `--lang ko|en|ja`는 출력 언어입니다. 자주 쓰면 `npm install -g @seojin08/skill-finder`로 설치해 `skill-finder search …`처럼 짧게 부를 수 있습니다.
|
|
45
|
+
|
|
46
|
+
### 소스에서
|
|
47
|
+
|
|
48
|
+
필요한 것: Node.js 22.18 이상(24 권장), git, Claude Code(`claude` 명령).
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install
|
|
52
|
+
npm start
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
이 저장소를 내려받은 폴더에서 실행한 뒤 브라우저로 http://127.0.0.1:4317 을 엽니다. 첫 실행 때는 스킬 색인을 모으느라 몇 분 걸립니다.
|
|
56
|
+
|
|
57
|
+
GitHub 토큰은 선택입니다. 설정 화면에서 넣거나 `.env`의 `GITHUB_TOKEN`에 적습니다(`.env.example` 참고). 공개 저장소 읽기만 되는 fine-grained 토큰이면 되고, GitHub 코드 검색과 빠른 색인에 쓰입니다.
|
|
58
|
+
|
|
59
|
+
## 안전과 개인 정보
|
|
60
|
+
|
|
61
|
+
- 서버는 127.0.0.1에만 열리고, Host 허용 목록·전용 요청 헤더·같은 출처 검사로 다른 사이트의 요청을 막습니다.
|
|
62
|
+
- 설치·삭제는 확인 창을 거친 뒤 `claude plugin` 또는 skills CLI를 셸 없이 실행합니다. 앱이 원격 스킬의 코드를 직접 실행하지 않습니다.
|
|
63
|
+
- 검색·추천에 AI를 부르지 않습니다. 설명 번역은 브라우저에 들어 있는 Translator API만 씁니다.
|
|
64
|
+
- 밖으로 나가는 요청은 GitHub API, skills.sh 검색, 마켓플레이스 저장소, 업데이트 확인뿐입니다.
|
|
65
|
+
- 색인과 설정은 `data/`(SQLite)에 남습니다. 검색 기록은 설정에서 끌 수 있습니다.
|
|
66
|
+
|
|
67
|
+
## 개발
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm run dev # 서버(--watch) + Vite
|
|
71
|
+
npm test # 서버·웹 테스트
|
|
72
|
+
npm run typecheck
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- `server/`: Node가 TypeScript를 그대로 실행합니다(타입만 지우는 문법, import에 `.ts`). Express 5, `node:sqlite`.
|
|
76
|
+
- `web/`: React 19 + Vite, Tailwind v4 + shadcn/ui.
|
|
77
|
+
- `docs/DECISIONS.md`: 처음 명세와 다르게 정한 것들의 기록입니다. 고칠 때 먼저 확인하세요.
|
|
78
|
+
|
|
79
|
+
## 문제 신고
|
|
80
|
+
|
|
81
|
+
버그와 제안은 GitHub 이슈로 남겨 주세요. 설정 화면의 "진단 정보 복사" 내용을 붙이면 원인을 찾기 쉽습니다. 보안 문제는 [SECURITY.md](SECURITY.md)를 보세요.
|
|
82
|
+
|
|
83
|
+
## 라이선스
|
|
84
|
+
|
|
85
|
+
[MIT](LICENSE) © 2026 Parkseojin08. 누구나 쓰고 고치고 배포할 수 있으며, 복사본에는 저작권 표시와 라이선스 문구를 함께 두면 됩니다(보증 없음).
|
|
86
|
+
|
|
87
|
+
웹 화면에 함께 묶이는 오픈소스(React, Base UI, react-markdown 등)의 라이선스는 빌드할 때 `web/dist/THIRD-PARTY-LICENSES.txt`로 만들어지고, 앱의 설정 화면 "라이선스"에서 볼 수 있습니다.
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Skill Finder on Windows, macOS and Linux: `npx @seojin08/skill-finder` (the npm package) or `node bin/skill-finder.mjs`
|
|
3
|
+
// (a checkout). With no command it starts the local app and opens the browser. `search`, `show`, `recommend` and
|
|
4
|
+
// `installed` read from the app, starting it in the background when it is not running, and print text or --json,
|
|
5
|
+
// so Claude Code can call them from its shell. Nothing here installs or removes a skill: that stays in the app,
|
|
6
|
+
// behind its confirm dialog (user decision 2026-11-02).
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { createServer } from 'node:net';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { dirname, join, resolve } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
15
|
+
const VERSION = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version ?? '?';
|
|
16
|
+
// the npm package ships compiled JavaScript (Node does not strip types under node_modules); a checkout runs the TS
|
|
17
|
+
const PACKAGED = existsSync(join(ROOT, 'server', 'src', 'index.js'));
|
|
18
|
+
const ENTRY = join(ROOT, 'server', 'src', PACKAGED ? 'index.js' : 'index.ts');
|
|
19
|
+
const DEFAULT_PORT = 4317;
|
|
20
|
+
|
|
21
|
+
// ── messages ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
const LANGS = ['ko', 'en', 'ja'];
|
|
23
|
+
const T = {
|
|
24
|
+
ko: {
|
|
25
|
+
usage: `Skill Finder ${VERSION} — Claude Code 스킬 검색 (읽기 전용 명령)
|
|
26
|
+
|
|
27
|
+
사용법:
|
|
28
|
+
skill-finder 앱을 켜고 브라우저를 엽니다
|
|
29
|
+
skill-finder search <검색어> 스킬을 찾습니다 (--limit N, --risk low|medium|high)
|
|
30
|
+
skill-finder show <id> 스킬 하나의 위험 신호와 설치 명령
|
|
31
|
+
skill-finder recommend [폴더] 프로젝트 README와 비슷한 스킬 (기본: 지금 폴더)
|
|
32
|
+
skill-finder installed [폴더] 설치된 플러그인·스킬
|
|
33
|
+
skill-finder stop 이 명령이 백그라운드로 켠 앱을 끕니다
|
|
34
|
+
|
|
35
|
+
공통: --json(기계가 읽는 출력), --port N, --no-open, --lang ko|en|ja
|
|
36
|
+
설치·삭제는 앱 화면의 확인 창에서만 합니다.`,
|
|
37
|
+
node: (v) => `Node.js ${v}는 너무 오래됐습니다. 22.18 이상이 필요합니다: https://nodejs.org`,
|
|
38
|
+
noWeb: '웹 화면이 빌드되지 않았습니다. 먼저 npm run build를 실행하세요.',
|
|
39
|
+
starting: (p) => `Skill Finder를 켜는 중입니다 (127.0.0.1:${p})…`,
|
|
40
|
+
background: (p) => `Skill Finder를 백그라운드로 켰습니다 (127.0.0.1:${p}). 끄려면: skill-finder stop`,
|
|
41
|
+
running: (p) => `Skill Finder가 이미 켜져 있습니다: http://127.0.0.1:${p}`,
|
|
42
|
+
ready: (p) => `준비됐습니다: http://127.0.0.1:${p} (끄려면 Ctrl+C)`,
|
|
43
|
+
busy: (p, q) => `포트 ${p}를 다른 프로그램이 쓰고 있어 ${q}를 씁니다.`,
|
|
44
|
+
timeout: '서버가 60초 안에 응답하지 않았습니다.',
|
|
45
|
+
first: '처음 켜면 스킬 색인을 모으느라 몇 분 걸릴 수 있습니다.',
|
|
46
|
+
stopped: 'Skill Finder를 껐습니다.',
|
|
47
|
+
notMine: '이 명령이 켠 Skill Finder가 없습니다 (앱 창이나 실행기로 켠 것은 거기서 끄세요).',
|
|
48
|
+
none: '결과가 없습니다.',
|
|
49
|
+
needQuery: '검색어를 넣으세요. 예: skill-finder search "pdf tables"',
|
|
50
|
+
needId: '스킬 id를 넣으세요 (search 결과의 id). 예: skill-finder show gh:anthropics/skills:skills/pdf',
|
|
51
|
+
install: '설치',
|
|
52
|
+
noInstall: '앱에서 설치 불가',
|
|
53
|
+
risk: { low: '위험 낮음', medium: '주의', high: '위험 높음' },
|
|
54
|
+
note: '위험 등급은 규칙 판정이며 안전 보증이 아닙니다. 설치는 앱의 확인 창에서 하세요.',
|
|
55
|
+
plugins: '플러그인',
|
|
56
|
+
skills: '스킬',
|
|
57
|
+
readFrom: (f) => `읽은 파일: ${f}`,
|
|
58
|
+
failed: (m) => `실패: ${m}`,
|
|
59
|
+
},
|
|
60
|
+
en: {
|
|
61
|
+
usage: `Skill Finder ${VERSION} — find Claude Code skills (read-only commands)
|
|
62
|
+
|
|
63
|
+
Usage:
|
|
64
|
+
skill-finder start the app and open the browser
|
|
65
|
+
skill-finder search <query> find skills (--limit N, --risk low|medium|high)
|
|
66
|
+
skill-finder show <id> one skill's risk signals and install command
|
|
67
|
+
skill-finder recommend [folder] skills like the project's README (default: this folder)
|
|
68
|
+
skill-finder installed [folder] installed plugins and skills
|
|
69
|
+
skill-finder stop stop the app this command started in the background
|
|
70
|
+
|
|
71
|
+
Common: --json (machine-readable), --port N, --no-open, --lang ko|en|ja
|
|
72
|
+
Installing and removing happen only in the app's confirm dialog.`,
|
|
73
|
+
node: (v) => `Node.js ${v} is too old. 22.18 or later is needed: https://nodejs.org`,
|
|
74
|
+
noWeb: 'The web app is not built. Run npm run build first.',
|
|
75
|
+
starting: (p) => `Starting Skill Finder (127.0.0.1:${p})…`,
|
|
76
|
+
background: (p) => `Started Skill Finder in the background (127.0.0.1:${p}). To stop it: skill-finder stop`,
|
|
77
|
+
running: (p) => `Skill Finder is already running: http://127.0.0.1:${p}`,
|
|
78
|
+
ready: (p) => `Ready: http://127.0.0.1:${p} (Ctrl+C to stop)`,
|
|
79
|
+
busy: (p, q) => `Port ${p} is used by another program; using ${q}.`,
|
|
80
|
+
timeout: 'The server did not answer within 60 seconds.',
|
|
81
|
+
first: 'The first start gathers the skill index and can take a few minutes.',
|
|
82
|
+
stopped: 'Stopped Skill Finder.',
|
|
83
|
+
notMine: 'No Skill Finder started by this command (stop one started by the app or launcher there).',
|
|
84
|
+
none: 'No results.',
|
|
85
|
+
needQuery: 'Give a query. Example: skill-finder search "pdf tables"',
|
|
86
|
+
needId: 'Give a skill id (the id in search results). Example: skill-finder show gh:anthropics/skills:skills/pdf',
|
|
87
|
+
install: 'Install',
|
|
88
|
+
noInstall: 'not installable from the app',
|
|
89
|
+
risk: { low: 'Low risk', medium: 'Caution', high: 'High risk' },
|
|
90
|
+
note: 'Risk levels come from rules and are not a safety guarantee. Install from the app’s confirm dialog.',
|
|
91
|
+
plugins: 'Plugins',
|
|
92
|
+
skills: 'Skills',
|
|
93
|
+
readFrom: (f) => `Read: ${f}`,
|
|
94
|
+
failed: (m) => `Failed: ${m}`,
|
|
95
|
+
},
|
|
96
|
+
ja: {
|
|
97
|
+
usage: `Skill Finder ${VERSION} — Claude Code スキル検索(読み取り専用のコマンド)
|
|
98
|
+
|
|
99
|
+
使い方:
|
|
100
|
+
skill-finder アプリを起動してブラウザを開く
|
|
101
|
+
skill-finder search <検索語> スキルを探す (--limit N, --risk low|medium|high)
|
|
102
|
+
skill-finder show <id> スキルのリスク信号とインストールコマンド
|
|
103
|
+
skill-finder recommend [フォルダ] プロジェクトの README に似たスキル(既定: 今のフォルダ)
|
|
104
|
+
skill-finder installed [フォルダ] インストール済みのプラグイン・スキル
|
|
105
|
+
skill-finder stop このコマンドがバックグラウンドで起動したアプリを止める
|
|
106
|
+
|
|
107
|
+
共通: --json(機械向け出力), --port N, --no-open, --lang ko|en|ja
|
|
108
|
+
インストールと削除はアプリの確認画面でのみ行います。`,
|
|
109
|
+
node: (v) => `Node.js ${v} は古すぎます。22.18 以上が必要です: https://nodejs.org`,
|
|
110
|
+
noWeb: 'Web 画面がビルドされていません。先に npm run build を実行してください。',
|
|
111
|
+
starting: (p) => `Skill Finder を起動しています (127.0.0.1:${p})…`,
|
|
112
|
+
background: (p) => `Skill Finder をバックグラウンドで起動しました (127.0.0.1:${p})。止めるには: skill-finder stop`,
|
|
113
|
+
running: (p) => `Skill Finder はすでに起動しています: http://127.0.0.1:${p}`,
|
|
114
|
+
ready: (p) => `準備ができました: http://127.0.0.1:${p} (Ctrl+C で終了)`,
|
|
115
|
+
busy: (p, q) => `ポート ${p} は別のプログラムが使っているため ${q} を使います。`,
|
|
116
|
+
timeout: 'サーバーが 60 秒以内に応答しませんでした。',
|
|
117
|
+
first: '初回起動ではスキルの索引を集めるため数分かかることがあります。',
|
|
118
|
+
stopped: 'Skill Finder を止めました。',
|
|
119
|
+
notMine: 'このコマンドが起動した Skill Finder はありません(アプリやランチャーで起動したものはそちらで止めてください)。',
|
|
120
|
+
none: '結果がありません。',
|
|
121
|
+
needQuery: '検索語を入れてください。例: skill-finder search "pdf tables"',
|
|
122
|
+
needId: 'スキル id を入れてください(search 結果の id)。例: skill-finder show gh:anthropics/skills:skills/pdf',
|
|
123
|
+
install: 'インストール',
|
|
124
|
+
noInstall: 'アプリからインストールできません',
|
|
125
|
+
risk: { low: 'リスク低', medium: '注意', high: 'リスク高' },
|
|
126
|
+
note: 'リスクの判定はルールによる目安で、安全の保証ではありません。インストールはアプリの確認画面で行ってください。',
|
|
127
|
+
plugins: 'プラグイン',
|
|
128
|
+
skills: 'スキル',
|
|
129
|
+
readFrom: (f) => `読んだファイル: ${f}`,
|
|
130
|
+
failed: (m) => `失敗: ${m}`,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// ── arguments ────────────────────────────────────────────────────────────────────────────────────────────────
|
|
135
|
+
const args = process.argv.slice(2);
|
|
136
|
+
const flag = (name) => {
|
|
137
|
+
const i = args.indexOf(name);
|
|
138
|
+
if (i < 0) return false;
|
|
139
|
+
args.splice(i, 1);
|
|
140
|
+
return true;
|
|
141
|
+
};
|
|
142
|
+
const option = (name) => {
|
|
143
|
+
const i = args.indexOf(name);
|
|
144
|
+
if (i < 0 || i === args.length - 1) return null;
|
|
145
|
+
const v = args[i + 1];
|
|
146
|
+
args.splice(i, 2);
|
|
147
|
+
return v;
|
|
148
|
+
};
|
|
149
|
+
const json = flag('--json');
|
|
150
|
+
const noOpen = flag('--no-open');
|
|
151
|
+
const langArg = option('--lang');
|
|
152
|
+
const portArg = option('--port');
|
|
153
|
+
const limit = Math.min(50, Math.max(1, Number(option('--limit')) || 10));
|
|
154
|
+
const risk = option('--risk');
|
|
155
|
+
const systemLang = (process.env.SKILL_FINDER_LANG || Intl.DateTimeFormat().resolvedOptions().locale || 'en').slice(0, 2).toLowerCase();
|
|
156
|
+
const lang = LANGS.includes(langArg) ? langArg : LANGS.includes(systemLang) ? systemLang : 'en';
|
|
157
|
+
const t = T[lang];
|
|
158
|
+
const [command = 'start', ...rest] = args;
|
|
159
|
+
|
|
160
|
+
// ── where the app lives ──────────────────────────────────────────────────────────────────────────────────────
|
|
161
|
+
/** A checkout keeps its data in ./data as `npm start` does; the package keeps it per user, outside the npm cache. */
|
|
162
|
+
function dataDir() {
|
|
163
|
+
if (process.env.SKILL_FINDER_DATA_DIR) return process.env.SKILL_FINDER_DATA_DIR;
|
|
164
|
+
if (!PACKAGED) return join(ROOT, 'data');
|
|
165
|
+
if (process.platform === 'win32') return join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'SkillFinder', 'npx-data');
|
|
166
|
+
if (process.platform === 'darwin') return join(homedir(), 'Library', 'Application Support', 'Skill Finder');
|
|
167
|
+
return join(process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share'), 'skill-finder');
|
|
168
|
+
}
|
|
169
|
+
const DATA = dataDir();
|
|
170
|
+
const PID_FILE = join(DATA, 'cli-server.json');
|
|
171
|
+
|
|
172
|
+
async function health(port) {
|
|
173
|
+
try {
|
|
174
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/health`, { headers: { 'X-Skill-Finder': '1' }, signal: AbortSignal.timeout(1500) });
|
|
175
|
+
const h = res.ok ? await res.json() : null;
|
|
176
|
+
return h?.app === 'skill-finder' ? h : null;
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const portFree = (port) =>
|
|
182
|
+
new Promise((done) => {
|
|
183
|
+
const s = createServer();
|
|
184
|
+
s.once('error', () => done(false));
|
|
185
|
+
s.listen(port, '127.0.0.1', () => s.close(() => done(true)));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
/** A running Skill Finder's port, or null. */
|
|
189
|
+
async function findRunning() {
|
|
190
|
+
// a port given with --port is the only one: another Skill Finder elsewhere is not the one asked for
|
|
191
|
+
const ports = Number(portArg) ? [Number(portArg)] : [...new Set([Number(process.env.PORT) || 0, DEFAULT_PORT, readPid()?.port ?? 0].filter(Boolean))];
|
|
192
|
+
for (const p of ports) if (await health(p)) return p;
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function readPid() {
|
|
197
|
+
try {
|
|
198
|
+
return JSON.parse(readFileSync(PID_FILE, 'utf8'));
|
|
199
|
+
} catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Starts the server: in this terminal (`start`), or detached for the read commands. Resolves its port. */
|
|
205
|
+
async function startServer(background) {
|
|
206
|
+
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
207
|
+
if (major < 22 || (major === 22 && minor < 18)) throw new Error(t.node(process.versions.node));
|
|
208
|
+
if (!existsSync(join(ROOT, 'web', 'dist', 'index.html'))) throw new Error(t.noWeb);
|
|
209
|
+
let port = Number(portArg) || Number(process.env.PORT) || DEFAULT_PORT;
|
|
210
|
+
if (!(await portFree(port))) {
|
|
211
|
+
let next = port + 1;
|
|
212
|
+
while (next < port + 50 && !(await portFree(next))) next++;
|
|
213
|
+
if (!json) console.error(t.busy(port, next));
|
|
214
|
+
port = next;
|
|
215
|
+
}
|
|
216
|
+
mkdirSync(DATA, { recursive: true });
|
|
217
|
+
const env = { ...process.env, PORT: String(port), SKILL_FINDER_DATA_DIR: DATA };
|
|
218
|
+
if (PACKAGED) env.SKILL_FINDER_ENV_PATH = process.env.SKILL_FINDER_ENV_PATH || join(DATA, '.env');
|
|
219
|
+
if (!json) console.error(background ? t.starting(port) : `${t.starting(port)}\n${t.first}`);
|
|
220
|
+
const child = spawn(process.execPath, ['--disable-warning=ExperimentalWarning', ENTRY], {
|
|
221
|
+
cwd: ROOT,
|
|
222
|
+
env,
|
|
223
|
+
stdio: background ? 'ignore' : 'inherit',
|
|
224
|
+
detached: background,
|
|
225
|
+
windowsHide: true,
|
|
226
|
+
});
|
|
227
|
+
if (background) {
|
|
228
|
+
writeFileSync(PID_FILE, JSON.stringify({ pid: child.pid, port }));
|
|
229
|
+
child.unref();
|
|
230
|
+
}
|
|
231
|
+
const until = Date.now() + 60_000;
|
|
232
|
+
while (Date.now() < until) {
|
|
233
|
+
if (await health(port)) return { port, child };
|
|
234
|
+
if (child.exitCode != null) break;
|
|
235
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
236
|
+
}
|
|
237
|
+
throw new Error(t.timeout);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function openBrowser(url) {
|
|
241
|
+
const [cmd, cmdArgs] =
|
|
242
|
+
process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]] : process.platform === 'darwin' ? ['open', [url]] : ['xdg-open', [url]];
|
|
243
|
+
spawn(cmd, cmdArgs, { stdio: 'ignore', detached: true, windowsHide: true }).on('error', () => {}).unref();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** The app's API, the way its own page calls it (header, same origin). */
|
|
247
|
+
async function api(port, method, path, body) {
|
|
248
|
+
const res = await fetch(`http://127.0.0.1:${port}/api${path}`, {
|
|
249
|
+
method,
|
|
250
|
+
headers: { 'X-Skill-Finder': '1', Origin: `http://127.0.0.1:${port}`, ...(body ? { 'Content-Type': 'application/json' } : {}) },
|
|
251
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
252
|
+
});
|
|
253
|
+
const text = await res.text();
|
|
254
|
+
if (!res.ok) throw new Error((() => { try { return JSON.parse(text).error; } catch { return `${res.status}`; } })());
|
|
255
|
+
return text;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function ensureRunning() {
|
|
259
|
+
const running = await findRunning();
|
|
260
|
+
if (running) return running;
|
|
261
|
+
const { port } = await startServer(true);
|
|
262
|
+
if (!json) console.error(t.background(port));
|
|
263
|
+
return port;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── output ───────────────────────────────────────────────────────────────────────────────────────────────────
|
|
267
|
+
const count = (n) => (n == null ? '-' : n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n));
|
|
268
|
+
const repoOf = (id) => id.match(/^gh:([^:]+):/)?.[1] ?? '';
|
|
269
|
+
const brief = (it) => ({
|
|
270
|
+
id: it.id,
|
|
271
|
+
name: it.name,
|
|
272
|
+
description: it.description,
|
|
273
|
+
repo: repoOf(it.id) || null,
|
|
274
|
+
installs: it.installs ?? null,
|
|
275
|
+
stars: it.stars ?? null,
|
|
276
|
+
riskLevel: it.riskLevel,
|
|
277
|
+
riskFlags: it.riskFlags,
|
|
278
|
+
installMethod: it.install?.method ?? null,
|
|
279
|
+
installCommand: it.install?.command || null,
|
|
280
|
+
url: it.sources?.find((s) => s.url)?.url ?? null,
|
|
281
|
+
});
|
|
282
|
+
function printItems(items) {
|
|
283
|
+
if (json) return console.log(JSON.stringify(items.map(brief), null, 2));
|
|
284
|
+
if (!items.length) return console.log(t.none);
|
|
285
|
+
for (const it of items) {
|
|
286
|
+
const b = brief(it);
|
|
287
|
+
console.log(`${b.name} [${t.risk[b.riskLevel] ?? b.riskLevel}] ↓${count(b.installs)} ★${count(b.stars)}${b.repo ? ` ${b.repo}` : ''}`);
|
|
288
|
+
if (b.description) console.log(` ${b.description.replace(/\s+/g, ' ').slice(0, 160)}`);
|
|
289
|
+
console.log(` id: ${b.id}`);
|
|
290
|
+
}
|
|
291
|
+
console.log(`\n${t.note}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── commands ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
295
|
+
async function main() {
|
|
296
|
+
if (command === '--help' || command === '-h' || command === 'help') return console.log(t.usage);
|
|
297
|
+
if (command === '--version' || command === '-v') return console.log(VERSION);
|
|
298
|
+
|
|
299
|
+
if (command === 'start') {
|
|
300
|
+
const running = await findRunning();
|
|
301
|
+
if (running) {
|
|
302
|
+
console.log(t.running(running));
|
|
303
|
+
if (!noOpen) openBrowser(`http://127.0.0.1:${running}`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const { port, child } = await startServer(false);
|
|
307
|
+
console.log(t.ready(port));
|
|
308
|
+
if (!noOpen) openBrowser(`http://127.0.0.1:${port}`);
|
|
309
|
+
for (const sig of ['SIGINT', 'SIGTERM']) process.on(sig, () => child.kill(sig));
|
|
310
|
+
await new Promise((r) => child.on('exit', r));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (command === 'stop') {
|
|
315
|
+
const p = readPid();
|
|
316
|
+
// only the server this command started, still running as the same process (a stale pid may be anything now)
|
|
317
|
+
const h = p?.pid ? await health(p.port) : null;
|
|
318
|
+
if (!h || h.pid !== p.pid) {
|
|
319
|
+
rmSync(PID_FILE, { force: true });
|
|
320
|
+
return console.log(t.notMine);
|
|
321
|
+
}
|
|
322
|
+
process.kill(p.pid);
|
|
323
|
+
rmSync(PID_FILE, { force: true });
|
|
324
|
+
return console.log(t.stopped);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (command === 'search') {
|
|
328
|
+
const query = rest.join(' ').trim();
|
|
329
|
+
if (!query) throw new Error(t.needQuery);
|
|
330
|
+
const port = await ensureRunning();
|
|
331
|
+
const filters = ['low', 'medium', 'high'].includes(risk) ? { maxRisk: risk } : undefined;
|
|
332
|
+
const lines = (await api(port, 'POST', '/search', { query, filters })).trim().split('\n').map((l) => JSON.parse(l));
|
|
333
|
+
// the last one: a slow search sends interim results first, then the final ones
|
|
334
|
+
const results = lines.filter((e) => e.type === 'results').at(-1);
|
|
335
|
+
return printItems((results?.items ?? []).slice(0, limit));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (command === 'show') {
|
|
339
|
+
const id = rest[0];
|
|
340
|
+
if (!id) throw new Error(t.needId);
|
|
341
|
+
const port = await ensureRunning();
|
|
342
|
+
const d = JSON.parse(await api(port, 'GET', `/skills/${encodeURIComponent(id)}`));
|
|
343
|
+
if (json) return console.log(JSON.stringify({ ...brief(d), whenToUse: d.whenToUse ?? null }, null, 2));
|
|
344
|
+
printItems([d]);
|
|
345
|
+
const cmd = d.install?.command;
|
|
346
|
+
console.log(cmd ? `${t.install}: ${cmd}` : t.noInstall);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (command === 'recommend') {
|
|
351
|
+
const projectPath = resolve(rest[0] ?? process.cwd());
|
|
352
|
+
const port = await ensureRunning();
|
|
353
|
+
const r = JSON.parse(await api(port, 'POST', '/readme-skills', { projectPath }));
|
|
354
|
+
if (!json && r.readmePath) console.log(t.readFrom([r.readmePath, ...(r.alsoRead ?? [])].join(' + ')));
|
|
355
|
+
return printItems(r.skills.map((s) => s.item).slice(0, limit));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (command === 'installed') {
|
|
359
|
+
const projectPath = rest[0] ? resolve(rest[0]) : undefined;
|
|
360
|
+
const port = await ensureRunning();
|
|
361
|
+
const q = projectPath ? `?projectPath=${encodeURIComponent(projectPath)}` : '';
|
|
362
|
+
const d = JSON.parse(await api(port, 'GET', `/installed${q}`));
|
|
363
|
+
if (json) {
|
|
364
|
+
return console.log(
|
|
365
|
+
JSON.stringify(
|
|
366
|
+
{
|
|
367
|
+
plugins: d.plugins.map((p) => ({ id: p.id, scope: p.scope, version: p.version, topRisk: p.topRisk })),
|
|
368
|
+
skills: d.skills.map((s) => ({ name: s.name, scope: s.scope, path: s.path, riskLevel: s.risk.level, id: s.skillId })),
|
|
369
|
+
},
|
|
370
|
+
null,
|
|
371
|
+
2,
|
|
372
|
+
),
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
console.log(`${t.plugins} (${d.plugins.length})`);
|
|
376
|
+
for (const p of d.plugins) console.log(` ${p.id} [${p.scope}]${p.topRisk ? ` ${t.risk[p.topRisk]}` : ''}`);
|
|
377
|
+
console.log(`${t.skills} (${d.skills.length})`);
|
|
378
|
+
for (const s of d.skills) console.log(` ${s.name} [${s.scope}] ${t.risk[s.risk.level]}`);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
console.log(t.usage);
|
|
383
|
+
process.exitCode = 2;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
main().catch((e) => {
|
|
387
|
+
console.error(t.failed(String(e?.message ?? e)));
|
|
388
|
+
process.exitCode = 1;
|
|
389
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seojin08/skill-finder",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"description": "Find Claude Code skills across marketplaces, skills.sh and GitHub, see their risk signals, and install them after checking what comes along. Local web app and read-only CLI.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"skill-finder": "bin/skill-finder.mjs"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=22.18"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"express": "5.3.0",
|
|
15
|
+
"skills": "1.7.4",
|
|
16
|
+
"yaml": "2.9.1",
|
|
17
|
+
"zod": "4.6.7"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/Parkseojin08/skill-finder.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://skill-finder.a01075011775.workers.dev",
|
|
24
|
+
"bugs": "https://github.com/Parkseojin08/skill-finder/issues",
|
|
25
|
+
"keywords": [
|
|
26
|
+
"claude-code",
|
|
27
|
+
"claude-skills",
|
|
28
|
+
"agent-skills",
|
|
29
|
+
"skills",
|
|
30
|
+
"cli"
|
|
31
|
+
],
|
|
32
|
+
"files": [
|
|
33
|
+
"bin",
|
|
34
|
+
"server",
|
|
35
|
+
"web/dist",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE",
|
|
38
|
+
"CHANGELOG.md",
|
|
39
|
+
".env.example"
|
|
40
|
+
]
|
|
41
|
+
}
|