yuri-skills 0.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.
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/bin/yuri-skills.js +449 -0
- package/package.json +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yuri Yu
|
|
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
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# yuri-skills
|
|
2
|
+
|
|
3
|
+
A curated collection of Agent Skills for AI workflows, engineering, research, and productivity.
|
|
4
|
+
|
|
5
|
+
Each skill is a self-contained folder under `skills/`. Pre-packaged `.skill` files for one-click install live in `dist/`.
|
|
6
|
+
|
|
7
|
+
Skills may be authored in English or Chinese. Chinese skills are suffixed (e.g. `-zh`) in the folder name.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Pick the method that matches your AI client.
|
|
14
|
+
|
|
15
|
+
### Option A: Claude Code + Codex via npx
|
|
16
|
+
|
|
17
|
+
Install every Yuri skill globally for both Claude Code and Codex:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx yuri-skills install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The npm package only ships the installer. Skills are downloaded from GitHub at install time, so skill updates only need to be pushed to this repository.
|
|
24
|
+
|
|
25
|
+
Install one or more skills:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx yuri-skills install grill-me-harder
|
|
29
|
+
npx yuri-skills install grill-me-harder ruthless-paper-reviewer
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Limit the target agent or install into the current project:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx yuri-skills install --agent codex
|
|
36
|
+
npx yuri-skills install --agent claude
|
|
37
|
+
npx yuri-skills install --scope project
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Pin installs to a branch, tag, or commit:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx yuri-skills install --ref v0.1.0
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Remove Yuri skills from the same default global locations:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx yuri-skills remove
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Defaults: all skills, both Claude and Codex, user-global install, GitHub `main` branch. The installer supports macOS, Linux, and Windows.
|
|
53
|
+
|
|
54
|
+
## Triggering skills
|
|
55
|
+
|
|
56
|
+
After installation, Claude Code and Codex can auto-trigger skills based on each skill's `description` field in `SKILL.md`.
|
|
57
|
+
|
|
58
|
+
You can also invoke the intended behavior directly in natural language, for example:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
grill me on this design
|
|
62
|
+
pressure test this plan
|
|
63
|
+
拷打我这个方案
|
|
64
|
+
帮我锐评这篇论文
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Chinese-language skills use the `-zh` suffix and include Chinese trigger phrasing in their own `SKILL.md`.
|
|
68
|
+
|
|
69
|
+
### Option B: Claude.ai web / desktop app (recommended for non-technical users)
|
|
70
|
+
|
|
71
|
+
1. Open the [dist/](dist/) directory.
|
|
72
|
+
2. Download the `<skill-name>.skill` file you want.
|
|
73
|
+
3. In Claude.ai, go to **Settings → Capabilities → Skills → Upload skill**.
|
|
74
|
+
4. Select the downloaded `.skill` file. Done.
|
|
75
|
+
|
|
76
|
+
### Option C: Manual install
|
|
77
|
+
|
|
78
|
+
You can also copy a skill folder directly into your agent's skills directory.
|
|
79
|
+
|
|
80
|
+
**Claude Code global install** (available in every project):
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# macOS / Linux
|
|
84
|
+
cp -r skills/<skill-name> ~/.claude/skills/
|
|
85
|
+
|
|
86
|
+
# Windows (PowerShell)
|
|
87
|
+
Copy-Item -Recurse skills\<skill-name> $env:USERPROFILE\.claude\skills\
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Claude Code project-scoped install** (current repo only):
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
cp -r skills/<skill-name> <your-project>/.claude/skills/
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Codex project-scoped install** (current repo only):
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# macOS / Linux
|
|
100
|
+
mkdir -p .agents/skills
|
|
101
|
+
cp -r skills/<skill-name> .agents/skills/
|
|
102
|
+
|
|
103
|
+
# Windows (PowerShell)
|
|
104
|
+
New-Item -ItemType Directory -Force .agents\skills
|
|
105
|
+
Copy-Item -Recurse skills\<skill-name> .agents\skills\
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Codex user-scoped install** (available in every project):
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# macOS / Linux
|
|
112
|
+
mkdir -p ~/.agents/skills
|
|
113
|
+
cp -r skills/<skill-name> ~/.agents/skills/
|
|
114
|
+
|
|
115
|
+
# Windows (PowerShell)
|
|
116
|
+
New-Item -ItemType Directory -Force $HOME\.agents\skills
|
|
117
|
+
Copy-Item -Recurse skills\<skill-name> $HOME\.agents\skills\
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Restart Claude Code or Codex if the new skill does not appear. Some older Codex setups may use `~/.codex/skills`; prefer `.agents/skills` for new installs.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Available skills
|
|
125
|
+
|
|
126
|
+
> This list grows as new skills are added. Each skill's `SKILL.md` has full details and trigger examples.
|
|
127
|
+
|
|
128
|
+
| Skill | What it does | Source |
|
|
129
|
+
| --- | --- | --- |
|
|
130
|
+
| [grill-me-harder](skills/grill-me-harder/) | Adversarially interviews you about a plan or design, one branch at a time, until every decision is concrete. | Adapted from [mattpocock/skills · grill-me](https://github.com/mattpocock/skills) |
|
|
131
|
+
| [grill-me-harder-zh](skills/grill-me-harder-zh/) | 中文版"拷打我"。用调侃但不留情面的语气,逐个分支把你的方案追问到落地,直到你"悟了"。 | Adapted from [mattpocock/skills · grill-me](https://github.com/mattpocock/skills) |
|
|
132
|
+
| [ruthless-paper-reviewer](skills/ruthless-paper-reviewer/) | Ruthlessly roasts an academic paper. Hunts for fatal logic flaws, tech-washing, dataset-timeline mismatches, and unsupported conclusions. Evidence-grounded, no "pros and cons" essays. | Original |
|
|
133
|
+
| [ruthless-paper-reviewer-zh](skills/ruthless-paper-reviewer-zh/) | 中文版"学术论文锐评"。B 站锐评味儿打底,第一性原理收尾——专治缝合怪、A+B 灌水、跑分游戏、Math/Tech-washing。皮调侃,骨头硬。 | Original |
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## For contributors
|
|
138
|
+
|
|
139
|
+
See [CLAUDE.md](CLAUDE.md) for the full skill-authoring workflow — you can hand it to Claude Code and have it walk through the process.
|
|
140
|
+
|
|
141
|
+
Package a skill:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
python scripts/package_skill.py skills/<skill-name>
|
|
145
|
+
# Output: dist/<skill-name>.skill
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const https = require("https");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
|
|
8
|
+
const GITHUB_REPO = "MSWinds/yuri-skills";
|
|
9
|
+
const DEFAULT_REF = "main";
|
|
10
|
+
const MARKER_FILE = ".yuri-skills.json";
|
|
11
|
+
const VALID_AGENTS = new Set(["all", "claude", "codex"]);
|
|
12
|
+
const VALID_SCOPES = new Set(["user", "project", "repo", "global", "local"]);
|
|
13
|
+
const SKILL_NAME_RE = /^[a-z0-9-]+$/;
|
|
14
|
+
|
|
15
|
+
function usage() {
|
|
16
|
+
console.log(`yuri-skills
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
yuri-skills list [--ref main]
|
|
20
|
+
yuri-skills install [skill...] [--agent claude|codex|all] [--scope user|project] [--ref main] [--dry-run]
|
|
21
|
+
yuri-skills remove [skill...] [--agent claude|codex|all] [--scope user|project] [--dry-run]
|
|
22
|
+
|
|
23
|
+
Defaults:
|
|
24
|
+
install all Yuri skills from GitHub
|
|
25
|
+
remove all skills installed by yuri-skills
|
|
26
|
+
--agent all
|
|
27
|
+
--scope user
|
|
28
|
+
--ref main
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
yuri-skills install
|
|
32
|
+
yuri-skills install grill-me-harder --agent codex
|
|
33
|
+
yuri-skills install --scope project --ref v0.1.0
|
|
34
|
+
yuri-skills remove
|
|
35
|
+
yuri-skills remove --agent claude --dry-run
|
|
36
|
+
`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function fail(message) {
|
|
40
|
+
console.error(`Error: ${message}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseArgs(argv) {
|
|
45
|
+
const out = {
|
|
46
|
+
command: argv[0],
|
|
47
|
+
skills: [],
|
|
48
|
+
agent: "all",
|
|
49
|
+
scope: "user",
|
|
50
|
+
ref: DEFAULT_REF,
|
|
51
|
+
dryRun: false,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
for (let i = 1; i < argv.length; i += 1) {
|
|
55
|
+
const arg = argv[i];
|
|
56
|
+
|
|
57
|
+
if (arg === "--dry-run") {
|
|
58
|
+
out.dryRun = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (arg === "--agent" || arg === "-a") {
|
|
63
|
+
out.agent = argv[i + 1];
|
|
64
|
+
i += 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (arg.startsWith("--agent=")) {
|
|
69
|
+
out.agent = arg.slice("--agent=".length);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (arg === "--scope" || arg === "-s") {
|
|
74
|
+
out.scope = argv[i + 1];
|
|
75
|
+
i += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (arg.startsWith("--scope=")) {
|
|
80
|
+
out.scope = arg.slice("--scope=".length);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (arg === "--ref" || arg === "-r") {
|
|
85
|
+
out.ref = argv[i + 1];
|
|
86
|
+
i += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (arg.startsWith("--ref=")) {
|
|
91
|
+
out.ref = arg.slice("--ref=".length);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (arg.startsWith("-")) {
|
|
96
|
+
fail(`unknown option: ${arg}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
out.skills.push(arg);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!VALID_AGENTS.has(out.agent)) {
|
|
103
|
+
fail(`--agent must be one of: all, claude, codex`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!VALID_SCOPES.has(out.scope)) {
|
|
107
|
+
fail(`--scope must be one of: user, project`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (out.scope === "global") {
|
|
111
|
+
out.scope = "user";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (out.scope === "repo" || out.scope === "local") {
|
|
115
|
+
out.scope = "project";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!out.ref) {
|
|
119
|
+
fail(`--ref requires a value`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const name of out.skills) {
|
|
123
|
+
if (!SKILL_NAME_RE.test(name)) {
|
|
124
|
+
fail(`invalid skill name: ${name}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function selectedAgents(agent) {
|
|
132
|
+
if (agent === "all") {
|
|
133
|
+
return ["claude", "codex"];
|
|
134
|
+
}
|
|
135
|
+
return [agent];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function targetRoot(agent, scope) {
|
|
139
|
+
const base = scope === "user" ? os.homedir() : process.cwd();
|
|
140
|
+
|
|
141
|
+
if (agent === "claude") {
|
|
142
|
+
return path.join(base, ".claude", "skills");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return path.join(base, ".agents", "skills");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function request(url, parseJson = false) {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
const req = https.get(
|
|
151
|
+
url,
|
|
152
|
+
{
|
|
153
|
+
headers: {
|
|
154
|
+
"Accept": "application/vnd.github+json",
|
|
155
|
+
"User-Agent": "yuri-skills-installer",
|
|
156
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
(res) => {
|
|
160
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
|
|
161
|
+
res.resume();
|
|
162
|
+
request(res.headers.location, parseJson).then(resolve, reject);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const chunks = [];
|
|
167
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
168
|
+
res.on("end", () => {
|
|
169
|
+
const body = Buffer.concat(chunks);
|
|
170
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
171
|
+
reject(new Error(`GitHub request failed (${res.statusCode}): ${body.toString("utf8")}`));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (parseJson) {
|
|
176
|
+
try {
|
|
177
|
+
resolve(JSON.parse(body.toString("utf8")));
|
|
178
|
+
} catch (error) {
|
|
179
|
+
reject(error);
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
resolve(body);
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
req.on("error", reject);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function githubContents(apiPath, ref) {
|
|
194
|
+
const url = `https://api.github.com/repos/${GITHUB_REPO}/contents/${apiPath}?ref=${encodeURIComponent(ref)}`;
|
|
195
|
+
return request(url, true);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function remoteSkillNames(ref) {
|
|
199
|
+
const entries = await githubContents("skills", ref);
|
|
200
|
+
if (!Array.isArray(entries)) {
|
|
201
|
+
fail(`GitHub path is not a directory: skills`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return entries
|
|
205
|
+
.filter((entry) => entry.type === "dir" && SKILL_NAME_RE.test(entry.name))
|
|
206
|
+
.map((entry) => entry.name)
|
|
207
|
+
.sort();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function selectedSkills(requested, ref) {
|
|
211
|
+
const available = await remoteSkillNames(ref);
|
|
212
|
+
if (requested.length === 0) {
|
|
213
|
+
return available;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const name of requested) {
|
|
217
|
+
if (!available.includes(name)) {
|
|
218
|
+
fail(`unknown skill: ${name}. Available at ${GITHUB_REPO}@${ref}: ${available.join(", ")}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return requested;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function frontmatterName(skillMdPath) {
|
|
226
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const text = fs.readFileSync(skillMdPath, "utf8");
|
|
231
|
+
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
232
|
+
if (!match) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const nameLine = match[1]
|
|
237
|
+
.split(/\r?\n/)
|
|
238
|
+
.find((line) => line.trim().startsWith("name:"));
|
|
239
|
+
|
|
240
|
+
if (!nameLine) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return nameLine.split(":").slice(1).join(":").trim().replace(/^["']|["']$/g, "");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function readMarker(targetDir) {
|
|
248
|
+
const markerPath = path.join(targetDir, MARKER_FILE);
|
|
249
|
+
if (!fs.existsSync(markerPath)) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
return JSON.parse(fs.readFileSync(markerPath, "utf8"));
|
|
255
|
+
} catch {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function writeMarker(targetDir, skillName, ref, agent, scope) {
|
|
261
|
+
const marker = {
|
|
262
|
+
installer: "yuri-skills",
|
|
263
|
+
source: `github:${GITHUB_REPO}`,
|
|
264
|
+
skill: skillName,
|
|
265
|
+
ref,
|
|
266
|
+
agent,
|
|
267
|
+
scope,
|
|
268
|
+
installedAt: new Date().toISOString(),
|
|
269
|
+
};
|
|
270
|
+
fs.writeFileSync(path.join(targetDir, MARKER_FILE), `${JSON.stringify(marker, null, 2)}\n`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function isYuriSkillTarget(targetDir, skillName) {
|
|
274
|
+
const marker = readMarker(targetDir);
|
|
275
|
+
if (marker) {
|
|
276
|
+
return marker.installer === "yuri-skills" && marker.skill === skillName;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return frontmatterName(path.join(targetDir, "SKILL.md")) === skillName;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function ensureSafeExistingTarget(targetDir, skillName) {
|
|
283
|
+
if (!fs.existsSync(targetDir)) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!isYuriSkillTarget(targetDir, skillName)) {
|
|
288
|
+
fail(`refusing to overwrite ${targetDir}; it does not look like ${skillName} installed by yuri-skills`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function downloadDirectory(apiPath, targetDir, ref) {
|
|
293
|
+
const entries = await githubContents(apiPath, ref);
|
|
294
|
+
if (!Array.isArray(entries)) {
|
|
295
|
+
fail(`GitHub path is not a directory: ${apiPath}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
299
|
+
|
|
300
|
+
for (const entry of entries) {
|
|
301
|
+
const entryTarget = path.join(targetDir, entry.name);
|
|
302
|
+
|
|
303
|
+
if (entry.type === "dir") {
|
|
304
|
+
await downloadDirectory(entry.path, entryTarget, ref);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (entry.type === "file") {
|
|
309
|
+
const content = await request(entry.download_url, false);
|
|
310
|
+
fs.writeFileSync(entryTarget, content);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function downloadSkill(skillName, ref) {
|
|
316
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `yuri-skills-${skillName}-`));
|
|
317
|
+
try {
|
|
318
|
+
const skillDir = path.join(tempDir, skillName);
|
|
319
|
+
await downloadDirectory(`skills/${skillName}`, skillDir, ref);
|
|
320
|
+
const actualName = frontmatterName(path.join(skillDir, "SKILL.md"));
|
|
321
|
+
if (actualName !== skillName) {
|
|
322
|
+
fail(`downloaded skill ${skillName} has mismatched SKILL.md name: ${actualName || "missing"}`);
|
|
323
|
+
}
|
|
324
|
+
return { tempDir, skillDir };
|
|
325
|
+
} catch (error) {
|
|
326
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function installSkill(skillName, root, options, agent) {
|
|
332
|
+
const targetDir = path.join(root, skillName);
|
|
333
|
+
ensureSafeExistingTarget(targetDir, skillName);
|
|
334
|
+
|
|
335
|
+
if (options.dryRun) {
|
|
336
|
+
console.log(`[dry-run] install ${skillName} from ${GITHUB_REPO}@${options.ref} -> ${targetDir}`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const { tempDir, skillDir } = await downloadSkill(skillName, options.ref);
|
|
341
|
+
try {
|
|
342
|
+
fs.mkdirSync(root, { recursive: true });
|
|
343
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
344
|
+
fs.cpSync(skillDir, targetDir, { recursive: true });
|
|
345
|
+
writeMarker(targetDir, skillName, options.ref, agent, options.scope);
|
|
346
|
+
console.log(`Installed ${skillName} -> ${targetDir}`);
|
|
347
|
+
} finally {
|
|
348
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function installedYuriSkills(root) {
|
|
353
|
+
if (!fs.existsSync(root)) {
|
|
354
|
+
return [];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return fs
|
|
358
|
+
.readdirSync(root, { withFileTypes: true })
|
|
359
|
+
.filter((entry) => entry.isDirectory())
|
|
360
|
+
.map((entry) => entry.name)
|
|
361
|
+
.filter((name) => {
|
|
362
|
+
const marker = readMarker(path.join(root, name));
|
|
363
|
+
return marker && marker.installer === "yuri-skills" && marker.skill === name;
|
|
364
|
+
})
|
|
365
|
+
.sort();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function removeSkill(skillName, root, dryRun) {
|
|
369
|
+
const targetDir = path.join(root, skillName);
|
|
370
|
+
if (!fs.existsSync(targetDir)) {
|
|
371
|
+
console.log(`Skipped ${skillName}; not installed at ${targetDir}`);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!isYuriSkillTarget(targetDir, skillName)) {
|
|
376
|
+
fail(`refusing to remove ${targetDir}; it does not look like ${skillName} installed by yuri-skills`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (dryRun) {
|
|
380
|
+
console.log(`[dry-run] remove ${targetDir}`);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
385
|
+
console.log(`Removed ${targetDir}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function listSkills(options) {
|
|
389
|
+
for (const skill of await remoteSkillNames(options.ref)) {
|
|
390
|
+
console.log(skill);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function install(options) {
|
|
395
|
+
const skills = await selectedSkills(options.skills, options.ref);
|
|
396
|
+
for (const agent of selectedAgents(options.agent)) {
|
|
397
|
+
const root = targetRoot(agent, options.scope);
|
|
398
|
+
for (const skill of skills) {
|
|
399
|
+
await installSkill(skill, root, options, agent);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function remove(options) {
|
|
405
|
+
for (const agent of selectedAgents(options.agent)) {
|
|
406
|
+
const root = targetRoot(agent, options.scope);
|
|
407
|
+
const skills = options.skills.length > 0 ? options.skills : installedYuriSkills(root);
|
|
408
|
+
|
|
409
|
+
if (skills.length === 0) {
|
|
410
|
+
console.log(`No yuri-skills installs found at ${root}`);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
for (const skill of skills) {
|
|
415
|
+
removeSkill(skill, root, options.dryRun);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function main() {
|
|
421
|
+
const options = parseArgs(process.argv.slice(2));
|
|
422
|
+
|
|
423
|
+
if (!options.command || options.command === "--help" || options.command === "-h") {
|
|
424
|
+
usage();
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (options.command === "list") {
|
|
429
|
+
await listSkills(options);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (options.command === "install") {
|
|
434
|
+
await install(options);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (options.command === "remove" || options.command === "uninstall") {
|
|
439
|
+
remove(options);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
fail(`unknown command: ${options.command}`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
main().catch((error) => {
|
|
447
|
+
console.error(`Error: ${error.message}`);
|
|
448
|
+
process.exit(1);
|
|
449
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "yuri-skills",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A curated collection of Agent Skills for AI workflows, engineering, research, and productivity.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"yuri-skills": "bin/yuri-skills.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"check": "node bin/yuri-skills.js list && node bin/yuri-skills.js install --agent codex --scope project --dry-run && node bin/yuri-skills.js remove --agent codex --scope project --dry-run"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/MSWinds/yuri-skills.git"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [],
|
|
16
|
+
"files": [
|
|
17
|
+
"bin/",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"author": "",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"type": "commonjs",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/MSWinds/yuri-skills/issues"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/MSWinds/yuri-skills#readme"
|
|
28
|
+
}
|