@olegkoval/agent-skills 1.26.0 → 1.27.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/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -0
- package/package.json +5 -3
- package/site/assets/paperbag.css +707 -0
- package/site/assets/paperbag.js +218 -0
- package/site/build.mjs +380 -0
package/README.md
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
Agent-agnostic skill collection for Codex, Claude, Cursor, Grok, and other skill-aware tools.
|
|
13
13
|
|
|
14
|
+
**Browse the catalog: [skills.olegkoval.com](https://skills.olegkoval.com)**
|
|
15
|
+
|
|
14
16
|
These skills are opinionated by design. They encode working defaults, preferred tools, and repeatable workflows instead of trying to be neutral snippets. Treat them as starting points with taste: useful out of the box, easy to inspect, and specific enough for an agent to execute consistently.
|
|
15
17
|
|
|
16
18
|
## Structure
|
|
@@ -19,6 +21,7 @@ These skills are opinionated by design. They encode working defaults, preferred
|
|
|
19
21
|
- `catalog/` — machine-readable inventory
|
|
20
22
|
- `collections/` — grouped package bundles
|
|
21
23
|
- `scripts/` — sync and validation helpers
|
|
24
|
+
- `site/` — generator for the browsable catalog at [skills.olegkoval.com](https://skills.olegkoval.com) (`npm run build:site`)
|
|
22
25
|
|
|
23
26
|
## Principles
|
|
24
27
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olegkoval/agent-skills",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -30,13 +30,15 @@
|
|
|
30
30
|
".kiro",
|
|
31
31
|
"scripts",
|
|
32
32
|
"README.md",
|
|
33
|
-
"LICENSE"
|
|
33
|
+
"LICENSE",
|
|
34
|
+
"site"
|
|
34
35
|
],
|
|
35
36
|
"scripts": {
|
|
36
37
|
"build": "sh ./scripts/build-adapters.sh",
|
|
37
38
|
"test": "sh ./scripts/validate-catalog.sh",
|
|
38
39
|
"semantic-release": "semantic-release",
|
|
39
|
-
"release:dry-run": "semantic-release --dry-run --no-ci --verify-conditions=@semantic-release/changelog --verify-conditions=@semantic-release/git --verify-conditions=@semantic-release/github"
|
|
40
|
+
"release:dry-run": "semantic-release --dry-run --no-ci --verify-conditions=@semantic-release/changelog --verify-conditions=@semantic-release/git --verify-conditions=@semantic-release/github",
|
|
41
|
+
"build:site": "node site/build.mjs"
|
|
40
42
|
},
|
|
41
43
|
"devDependencies": {
|
|
42
44
|
"@semantic-release/changelog": "^6.0.3",
|
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* paperbag.css — lo-fi / grunge "brown paper bag" styling.
|
|
3
|
+
* Homage to p2output.com (late-1990s), documented in "Fresh Styles for Web
|
|
4
|
+
* Designers: Eye Candy from the Underground" for its busy background graphics
|
|
5
|
+
* and cursor-reactive text highlighting.
|
|
6
|
+
*
|
|
7
|
+
* Everything here is self-contained: no webfonts, no images, no network.
|
|
8
|
+
* The textures are procedural (SVG feTurbulence + gradients).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/* ---------------------------------------------------------------- tokens */
|
|
12
|
+
:root {
|
|
13
|
+
--bag: #c3ac84;
|
|
14
|
+
--bag-lit: #d6c19c;
|
|
15
|
+
--bag-dim: #a78f68;
|
|
16
|
+
--bag-deep: #8d7752;
|
|
17
|
+
--ink: #191410;
|
|
18
|
+
--ink-soft: #3d3225;
|
|
19
|
+
--ink-faint: #6b5b43;
|
|
20
|
+
--stamp: #a8321f;
|
|
21
|
+
--marker: #f6e14b;
|
|
22
|
+
--overprint: #1f6f7a;
|
|
23
|
+
|
|
24
|
+
--edge: 2px solid var(--ink);
|
|
25
|
+
--rule: 1px solid rgba(25, 20, 16, 0.28);
|
|
26
|
+
|
|
27
|
+
/* Period-correct: Impact was *the* late-90s display face and ships on both
|
|
28
|
+
macOS and Windows. Condensed grotesques stand in everywhere else. */
|
|
29
|
+
--display: "Impact", "Haettenschweiler", "Franklin Gothic Bold",
|
|
30
|
+
"Arial Narrow Bold", "Arial Narrow", "Liberation Sans Narrow",
|
|
31
|
+
"DejaVu Sans Condensed", sans-serif;
|
|
32
|
+
--mono: ui-monospace, "SF Mono", "SFMono-Regular", Menlo, Consolas,
|
|
33
|
+
"DejaVu Sans Mono", monospace;
|
|
34
|
+
|
|
35
|
+
--shadow-hard: 5px 5px 0 var(--ink);
|
|
36
|
+
--shadow-soft: 3px 3px 0 rgba(25, 20, 16, 0.55);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
*,
|
|
40
|
+
*::before,
|
|
41
|
+
*::after {
|
|
42
|
+
box-sizing: border-box;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* ------------------------------------------------------------ the paper */
|
|
46
|
+
html {
|
|
47
|
+
scroll-behavior: smooth;
|
|
48
|
+
-webkit-text-size-adjust: 100%;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
body {
|
|
52
|
+
margin: 0;
|
|
53
|
+
min-height: 100vh;
|
|
54
|
+
color: var(--ink);
|
|
55
|
+
background-color: var(--bag);
|
|
56
|
+
font-family: var(--mono);
|
|
57
|
+
font-size: 15px;
|
|
58
|
+
line-height: 1.55;
|
|
59
|
+
overflow-x: hidden;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/*
|
|
63
|
+
* Three stacked procedural layers make the "busy background": kraft fibre
|
|
64
|
+
* grain, a print halftone, and the soft blotches of a bag that has been
|
|
65
|
+
* folded and re-opened. They sit behind content and never intercept input.
|
|
66
|
+
*/
|
|
67
|
+
.grain,
|
|
68
|
+
.halftone,
|
|
69
|
+
.blotch {
|
|
70
|
+
position: fixed;
|
|
71
|
+
inset: 0;
|
|
72
|
+
pointer-events: none;
|
|
73
|
+
z-index: 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.grain {
|
|
77
|
+
opacity: 0.42;
|
|
78
|
+
mix-blend-mode: multiply;
|
|
79
|
+
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='220' height='220'><filter id='f'><feTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='4' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='220' height='220' filter='url(%23f)' opacity='0.55'/></svg>");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.halftone {
|
|
83
|
+
opacity: 0.2;
|
|
84
|
+
mix-blend-mode: multiply;
|
|
85
|
+
background-image: radial-gradient(
|
|
86
|
+
circle at 1px 1px,
|
|
87
|
+
rgba(25, 20, 16, 0.9) 0.7px,
|
|
88
|
+
transparent 1.1px
|
|
89
|
+
);
|
|
90
|
+
background-size: 5px 5px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.blotch {
|
|
94
|
+
opacity: 0.5;
|
|
95
|
+
mix-blend-mode: multiply;
|
|
96
|
+
background-image:
|
|
97
|
+
radial-gradient(60% 42% at 12% 8%, rgba(120, 96, 60, 0.5), transparent 70%),
|
|
98
|
+
radial-gradient(48% 38% at 88% 22%, rgba(90, 72, 44, 0.42), transparent 70%),
|
|
99
|
+
radial-gradient(70% 46% at 30% 92%, rgba(122, 100, 66, 0.4), transparent 72%),
|
|
100
|
+
repeating-linear-gradient(
|
|
101
|
+
104deg,
|
|
102
|
+
transparent 0 78px,
|
|
103
|
+
rgba(25, 20, 16, 0.05) 78px 79px
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/*
|
|
108
|
+
* The cursor drags a smudge of ink across the bag. Positioned from JS via
|
|
109
|
+
* --mx / --my; hidden entirely when the pointer is coarse or motion is
|
|
110
|
+
* reduced.
|
|
111
|
+
*/
|
|
112
|
+
.smudge {
|
|
113
|
+
position: fixed;
|
|
114
|
+
inset: 0;
|
|
115
|
+
z-index: 1;
|
|
116
|
+
pointer-events: none;
|
|
117
|
+
opacity: 0;
|
|
118
|
+
transition: opacity 400ms ease;
|
|
119
|
+
mix-blend-mode: multiply;
|
|
120
|
+
background: radial-gradient(
|
|
121
|
+
220px 220px at var(--mx, -999px) var(--my, -999px),
|
|
122
|
+
rgba(25, 20, 16, 0.28),
|
|
123
|
+
rgba(25, 20, 16, 0.1) 45%,
|
|
124
|
+
transparent 72%
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
body.pointer-fine .smudge {
|
|
129
|
+
opacity: 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* --------------------------------------------------------------- layout */
|
|
133
|
+
.shell {
|
|
134
|
+
position: relative;
|
|
135
|
+
z-index: 2;
|
|
136
|
+
max-width: 1180px;
|
|
137
|
+
margin: 0 auto;
|
|
138
|
+
padding: 0 20px 96px;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
a {
|
|
142
|
+
color: var(--ink);
|
|
143
|
+
text-decoration-thickness: 2px;
|
|
144
|
+
text-underline-offset: 3px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
a:hover {
|
|
148
|
+
background: var(--marker);
|
|
149
|
+
color: var(--ink);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
a:focus-visible,
|
|
153
|
+
button:focus-visible,
|
|
154
|
+
input:focus-visible {
|
|
155
|
+
outline: 3px solid var(--overprint);
|
|
156
|
+
outline-offset: 2px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* -------------------------------------------------------------- masthead */
|
|
160
|
+
.masthead {
|
|
161
|
+
display: flex;
|
|
162
|
+
flex-wrap: wrap;
|
|
163
|
+
gap: 14px;
|
|
164
|
+
align-items: baseline;
|
|
165
|
+
justify-content: space-between;
|
|
166
|
+
padding: 18px 0 14px;
|
|
167
|
+
border-bottom: var(--edge);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.wordmark {
|
|
171
|
+
font-family: var(--display);
|
|
172
|
+
font-size: clamp(22px, 3.4vw, 30px);
|
|
173
|
+
letter-spacing: 0.06em;
|
|
174
|
+
text-transform: uppercase;
|
|
175
|
+
text-decoration: none;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.wordmark:hover {
|
|
179
|
+
background: none;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.masthead nav {
|
|
183
|
+
display: flex;
|
|
184
|
+
flex-wrap: wrap;
|
|
185
|
+
gap: 16px;
|
|
186
|
+
font-size: 12px;
|
|
187
|
+
letter-spacing: 0.14em;
|
|
188
|
+
text-transform: uppercase;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* ----------------------------------------------------------------- hero */
|
|
192
|
+
.hero {
|
|
193
|
+
position: relative;
|
|
194
|
+
padding: 54px 0 40px;
|
|
195
|
+
border-bottom: var(--edge);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.kicker {
|
|
199
|
+
font-size: 11px;
|
|
200
|
+
letter-spacing: 0.34em;
|
|
201
|
+
text-transform: uppercase;
|
|
202
|
+
color: var(--ink-soft);
|
|
203
|
+
margin: 0 0 18px;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.hero h1 {
|
|
207
|
+
font-family: var(--display);
|
|
208
|
+
font-size: clamp(44px, 11vw, 128px);
|
|
209
|
+
line-height: 0.86;
|
|
210
|
+
max-width: 12ch;
|
|
211
|
+
letter-spacing: -0.005em;
|
|
212
|
+
text-transform: uppercase;
|
|
213
|
+
margin: 0;
|
|
214
|
+
/* Cheap offset-litho misregistration. */
|
|
215
|
+
text-shadow:
|
|
216
|
+
2px 0 0 rgba(168, 50, 31, 0.4),
|
|
217
|
+
-2px 1px 0 rgba(31, 111, 122, 0.3);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.hero p.lede {
|
|
221
|
+
max-width: 62ch;
|
|
222
|
+
margin: 26px 0 0;
|
|
223
|
+
font-size: 16px;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.stamp {
|
|
227
|
+
position: absolute;
|
|
228
|
+
top: 46px;
|
|
229
|
+
right: 4px;
|
|
230
|
+
display: grid;
|
|
231
|
+
place-items: center;
|
|
232
|
+
width: 158px;
|
|
233
|
+
height: 158px;
|
|
234
|
+
padding: 14px;
|
|
235
|
+
border: 4px double var(--stamp);
|
|
236
|
+
border-radius: 50%;
|
|
237
|
+
background: rgba(195, 172, 132, 0.75);
|
|
238
|
+
color: var(--stamp);
|
|
239
|
+
font-family: var(--display);
|
|
240
|
+
font-size: 21px;
|
|
241
|
+
line-height: 1.02;
|
|
242
|
+
letter-spacing: 0.06em;
|
|
243
|
+
text-align: center;
|
|
244
|
+
text-transform: uppercase;
|
|
245
|
+
opacity: 0.82;
|
|
246
|
+
transform: rotate(-13deg);
|
|
247
|
+
mix-blend-mode: multiply;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@media (max-width: 720px) {
|
|
251
|
+
.stamp {
|
|
252
|
+
position: static;
|
|
253
|
+
margin: 28px 0 0;
|
|
254
|
+
transform: rotate(-6deg);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* --------------------------------------------------------------- strips */
|
|
259
|
+
.ticker {
|
|
260
|
+
overflow: hidden;
|
|
261
|
+
border-bottom: var(--edge);
|
|
262
|
+
padding: 9px 0;
|
|
263
|
+
font-size: 11px;
|
|
264
|
+
letter-spacing: 0.3em;
|
|
265
|
+
text-transform: uppercase;
|
|
266
|
+
color: var(--ink-soft);
|
|
267
|
+
white-space: nowrap;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.ticker span {
|
|
271
|
+
display: inline-block;
|
|
272
|
+
padding-left: 100%;
|
|
273
|
+
animation: drift 34s linear infinite;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
@keyframes drift {
|
|
277
|
+
to {
|
|
278
|
+
transform: translateX(-100%);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.statbar {
|
|
283
|
+
display: grid;
|
|
284
|
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
285
|
+
border-bottom: var(--edge);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.statbar div {
|
|
289
|
+
padding: 16px 14px;
|
|
290
|
+
border-right: var(--rule);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.statbar div:last-child {
|
|
294
|
+
border-right: 0;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.statbar b {
|
|
298
|
+
display: block;
|
|
299
|
+
font-family: var(--display);
|
|
300
|
+
font-size: 40px;
|
|
301
|
+
line-height: 1;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.statbar small {
|
|
305
|
+
font-size: 10px;
|
|
306
|
+
letter-spacing: 0.22em;
|
|
307
|
+
text-transform: uppercase;
|
|
308
|
+
color: var(--ink-soft);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/* --------------------------------------------------------------- filter */
|
|
312
|
+
.controls {
|
|
313
|
+
position: sticky;
|
|
314
|
+
top: 0;
|
|
315
|
+
z-index: 5;
|
|
316
|
+
padding: 16px 0;
|
|
317
|
+
border-bottom: var(--edge);
|
|
318
|
+
background: var(--bag);
|
|
319
|
+
box-shadow: 0 10px 18px -14px rgba(25, 20, 16, 0.8);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.controls label {
|
|
323
|
+
display: block;
|
|
324
|
+
font-size: 10px;
|
|
325
|
+
letter-spacing: 0.24em;
|
|
326
|
+
text-transform: uppercase;
|
|
327
|
+
color: var(--ink-soft);
|
|
328
|
+
margin-bottom: 6px;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
#q {
|
|
332
|
+
width: 100%;
|
|
333
|
+
padding: 11px 13px;
|
|
334
|
+
border: var(--edge);
|
|
335
|
+
border-radius: 0;
|
|
336
|
+
background: var(--bag-lit);
|
|
337
|
+
color: var(--ink);
|
|
338
|
+
font: inherit;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
#q::placeholder {
|
|
342
|
+
color: var(--ink-faint);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.chips {
|
|
346
|
+
display: flex;
|
|
347
|
+
flex-wrap: wrap;
|
|
348
|
+
gap: 7px;
|
|
349
|
+
margin-top: 12px;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.chip {
|
|
353
|
+
padding: 5px 11px;
|
|
354
|
+
border: 2px solid var(--ink);
|
|
355
|
+
background: transparent;
|
|
356
|
+
color: var(--ink);
|
|
357
|
+
font: inherit;
|
|
358
|
+
font-size: 11px;
|
|
359
|
+
letter-spacing: 0.12em;
|
|
360
|
+
text-transform: uppercase;
|
|
361
|
+
cursor: pointer;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.chip:hover {
|
|
365
|
+
background: var(--bag-lit);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.chip[aria-pressed="true"] {
|
|
369
|
+
background: var(--ink);
|
|
370
|
+
color: var(--bag-lit);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.count {
|
|
374
|
+
margin-top: 12px;
|
|
375
|
+
font-size: 11px;
|
|
376
|
+
letter-spacing: 0.2em;
|
|
377
|
+
text-transform: uppercase;
|
|
378
|
+
color: var(--ink-soft);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/* ---------------------------------------------------------------- cards */
|
|
382
|
+
.grid {
|
|
383
|
+
display: grid;
|
|
384
|
+
grid-template-columns: repeat(auto-fill, minmax(310px, 1fr));
|
|
385
|
+
gap: 20px;
|
|
386
|
+
padding: 28px 0 0;
|
|
387
|
+
list-style: none;
|
|
388
|
+
margin: 0;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.card {
|
|
392
|
+
display: flex;
|
|
393
|
+
flex-direction: column;
|
|
394
|
+
gap: 12px;
|
|
395
|
+
padding: 18px;
|
|
396
|
+
border: var(--edge);
|
|
397
|
+
background: var(--bag-lit);
|
|
398
|
+
box-shadow: var(--shadow-soft);
|
|
399
|
+
transition:
|
|
400
|
+
transform 120ms ease,
|
|
401
|
+
box-shadow 120ms ease;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.card:hover,
|
|
405
|
+
.card:focus-within {
|
|
406
|
+
transform: translate(-2px, -2px);
|
|
407
|
+
box-shadow: var(--shadow-hard);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.card[hidden] {
|
|
411
|
+
display: none;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.card .cat {
|
|
415
|
+
font-size: 10px;
|
|
416
|
+
letter-spacing: 0.2em;
|
|
417
|
+
text-transform: uppercase;
|
|
418
|
+
color: var(--stamp);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.card h2 {
|
|
422
|
+
margin: 0;
|
|
423
|
+
font-family: var(--display);
|
|
424
|
+
font-size: 27px;
|
|
425
|
+
line-height: 1.02;
|
|
426
|
+
letter-spacing: 0.01em;
|
|
427
|
+
text-transform: uppercase;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.card h2 a {
|
|
431
|
+
text-decoration: none;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.card h2 a:hover {
|
|
435
|
+
background: none;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/* Full-card click target without nesting interactive elements. */
|
|
439
|
+
.card h2 a::after {
|
|
440
|
+
content: "";
|
|
441
|
+
position: absolute;
|
|
442
|
+
inset: 0;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.card {
|
|
446
|
+
position: relative;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.card p {
|
|
450
|
+
margin: 0;
|
|
451
|
+
font-size: 13.5px;
|
|
452
|
+
color: var(--ink-soft);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.card .lookup {
|
|
456
|
+
font-size: 12px;
|
|
457
|
+
color: var(--overprint);
|
|
458
|
+
word-break: break-all;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.tags {
|
|
462
|
+
display: flex;
|
|
463
|
+
flex-wrap: wrap;
|
|
464
|
+
gap: 5px;
|
|
465
|
+
margin: auto 0 0;
|
|
466
|
+
padding: 10px 0 0;
|
|
467
|
+
border-top: var(--rule);
|
|
468
|
+
list-style: none;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.tags li {
|
|
472
|
+
padding: 2px 7px;
|
|
473
|
+
border: 1px solid rgba(25, 20, 16, 0.45);
|
|
474
|
+
font-size: 10px;
|
|
475
|
+
letter-spacing: 0.1em;
|
|
476
|
+
text-transform: uppercase;
|
|
477
|
+
color: var(--ink-soft);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.empty {
|
|
481
|
+
padding: 48px 0;
|
|
482
|
+
font-family: var(--display);
|
|
483
|
+
font-size: 30px;
|
|
484
|
+
text-transform: uppercase;
|
|
485
|
+
color: var(--ink-faint);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.empty[hidden] {
|
|
489
|
+
display: none;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/* -------------------------------------------------------------- sections */
|
|
493
|
+
section.band {
|
|
494
|
+
padding: 56px 0 0;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
section.band > h2 {
|
|
498
|
+
font-family: var(--display);
|
|
499
|
+
font-size: clamp(30px, 6vw, 60px);
|
|
500
|
+
line-height: 0.94;
|
|
501
|
+
text-transform: uppercase;
|
|
502
|
+
margin: 0 0 6px;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
section.band > .sub {
|
|
506
|
+
margin: 0 0 26px;
|
|
507
|
+
font-size: 13px;
|
|
508
|
+
color: var(--ink-soft);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/* ------------------------------------------------------------ code slabs */
|
|
512
|
+
.slab {
|
|
513
|
+
border: var(--edge);
|
|
514
|
+
background: var(--ink);
|
|
515
|
+
color: var(--bag-lit);
|
|
516
|
+
margin: 0 0 18px;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.slab header {
|
|
520
|
+
display: flex;
|
|
521
|
+
align-items: center;
|
|
522
|
+
justify-content: space-between;
|
|
523
|
+
gap: 12px;
|
|
524
|
+
padding: 8px 12px;
|
|
525
|
+
border-bottom: 1px solid rgba(214, 193, 156, 0.3);
|
|
526
|
+
font-size: 10px;
|
|
527
|
+
letter-spacing: 0.22em;
|
|
528
|
+
text-transform: uppercase;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.slab pre {
|
|
532
|
+
margin: 0;
|
|
533
|
+
padding: 14px 12px;
|
|
534
|
+
overflow-x: auto;
|
|
535
|
+
font-family: var(--mono);
|
|
536
|
+
font-size: 13px;
|
|
537
|
+
line-height: 1.7;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.copy {
|
|
541
|
+
padding: 3px 9px;
|
|
542
|
+
border: 1px solid var(--bag-lit);
|
|
543
|
+
background: transparent;
|
|
544
|
+
color: var(--bag-lit);
|
|
545
|
+
font: inherit;
|
|
546
|
+
font-size: 10px;
|
|
547
|
+
letter-spacing: 0.16em;
|
|
548
|
+
text-transform: uppercase;
|
|
549
|
+
cursor: pointer;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.copy:hover {
|
|
553
|
+
background: var(--bag-lit);
|
|
554
|
+
color: var(--ink);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.cols {
|
|
558
|
+
display: grid;
|
|
559
|
+
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
|
560
|
+
gap: 20px;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/* --------------------------------------------------------------- detail */
|
|
564
|
+
.crumb {
|
|
565
|
+
padding: 22px 0 0;
|
|
566
|
+
font-size: 11px;
|
|
567
|
+
letter-spacing: 0.18em;
|
|
568
|
+
text-transform: uppercase;
|
|
569
|
+
color: var(--ink-soft);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.detail-head {
|
|
573
|
+
padding: 10px 0 34px;
|
|
574
|
+
border-bottom: var(--edge);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.detail-head h1 {
|
|
578
|
+
font-family: var(--display);
|
|
579
|
+
font-size: clamp(38px, 8.5vw, 96px);
|
|
580
|
+
line-height: 0.88;
|
|
581
|
+
text-transform: uppercase;
|
|
582
|
+
margin: 8px 0 0;
|
|
583
|
+
text-shadow:
|
|
584
|
+
2px 0 0 rgba(168, 50, 31, 0.36),
|
|
585
|
+
-2px 1px 0 rgba(31, 111, 122, 0.26);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.detail-head p {
|
|
589
|
+
max-width: 68ch;
|
|
590
|
+
margin: 20px 0 0;
|
|
591
|
+
font-size: 16px;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.meta {
|
|
595
|
+
display: grid;
|
|
596
|
+
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
|
|
597
|
+
gap: 0;
|
|
598
|
+
border-bottom: var(--edge);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.meta div {
|
|
602
|
+
padding: 16px 14px 16px 0;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.meta dt {
|
|
606
|
+
font-size: 10px;
|
|
607
|
+
letter-spacing: 0.22em;
|
|
608
|
+
text-transform: uppercase;
|
|
609
|
+
color: var(--ink-soft);
|
|
610
|
+
margin-bottom: 6px;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.meta dd {
|
|
614
|
+
margin: 0;
|
|
615
|
+
font-size: 13px;
|
|
616
|
+
word-break: normal;
|
|
617
|
+
overflow-wrap: anywhere;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.pager {
|
|
621
|
+
display: flex;
|
|
622
|
+
flex-wrap: wrap;
|
|
623
|
+
justify-content: space-between;
|
|
624
|
+
gap: 16px;
|
|
625
|
+
margin-top: 56px;
|
|
626
|
+
padding-top: 20px;
|
|
627
|
+
border-top: var(--edge);
|
|
628
|
+
font-size: 12px;
|
|
629
|
+
letter-spacing: 0.1em;
|
|
630
|
+
text-transform: uppercase;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/* --------------------------------------------------------------- footer */
|
|
634
|
+
footer.colophon {
|
|
635
|
+
margin-top: 72px;
|
|
636
|
+
padding-top: 20px;
|
|
637
|
+
border-top: var(--edge);
|
|
638
|
+
font-size: 11px;
|
|
639
|
+
letter-spacing: 0.1em;
|
|
640
|
+
text-transform: uppercase;
|
|
641
|
+
color: var(--ink-soft);
|
|
642
|
+
display: flex;
|
|
643
|
+
flex-wrap: wrap;
|
|
644
|
+
gap: 16px;
|
|
645
|
+
justify-content: space-between;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/* ----------------------------------------------------- cursor highlight */
|
|
649
|
+
/*
|
|
650
|
+
* The p2output signature: glyphs light up as the pointer sweeps over them.
|
|
651
|
+
* JS sets --h (0..1) per character; everything visual happens here so the
|
|
652
|
+
* effect degrades to nothing when JS is off.
|
|
653
|
+
*/
|
|
654
|
+
.reactive .w {
|
|
655
|
+
display: inline-block;
|
|
656
|
+
white-space: nowrap;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.reactive .g {
|
|
660
|
+
display: inline-block;
|
|
661
|
+
color: color-mix(in srgb, var(--stamp) calc(var(--h, 0) * 100%), currentColor);
|
|
662
|
+
transform: translateY(calc(var(--h, 0) * -5px)) rotate(calc(var(--h, 0) * -3deg));
|
|
663
|
+
background: linear-gradient(
|
|
664
|
+
var(--marker),
|
|
665
|
+
var(--marker)
|
|
666
|
+
) no-repeat;
|
|
667
|
+
background-size: 100% calc(var(--h, 0) * 100%);
|
|
668
|
+
background-position: 0 100%;
|
|
669
|
+
will-change: transform;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
@media (prefers-reduced-motion: reduce) {
|
|
673
|
+
html {
|
|
674
|
+
scroll-behavior: auto;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.ticker span {
|
|
678
|
+
animation: none;
|
|
679
|
+
padding-left: 0;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.reactive .g {
|
|
683
|
+
transform: none;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
.smudge {
|
|
687
|
+
display: none;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.card {
|
|
691
|
+
transition: none;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
@media print {
|
|
696
|
+
.grain,
|
|
697
|
+
.halftone,
|
|
698
|
+
.blotch,
|
|
699
|
+
.smudge,
|
|
700
|
+
.controls {
|
|
701
|
+
display: none;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
body {
|
|
705
|
+
background: #fff;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* paperbag.js — progressive enhancement only. With JS off the page is a
|
|
3
|
+
* complete, readable, fully linked catalog; everything below just adds the
|
|
4
|
+
* p2output-era cursor theatrics and client-side filtering.
|
|
5
|
+
*/
|
|
6
|
+
(() => {
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
10
|
+
const finePointer = window.matchMedia("(hover: hover) and (pointer: fine)");
|
|
11
|
+
|
|
12
|
+
/* ------------------------------------------------ cursor-lit lettering */
|
|
13
|
+
|
|
14
|
+
// Radius of the pointer's influence, in CSS pixels.
|
|
15
|
+
const RADIUS = 130;
|
|
16
|
+
|
|
17
|
+
const glyphs = [];
|
|
18
|
+
|
|
19
|
+
const splitIntoGlyphs = (el) => {
|
|
20
|
+
const text = el.textContent;
|
|
21
|
+
// Keep the accessible name intact; the per-character spans are decorative.
|
|
22
|
+
el.setAttribute("aria-label", text.trim());
|
|
23
|
+
const frag = document.createDocumentFragment();
|
|
24
|
+
// Characters are inline-block, which would otherwise let the browser break
|
|
25
|
+
// a line mid-word. Wrapping each word keeps normal wrapping behaviour.
|
|
26
|
+
for (const word of text.split(/(\s+)/)) {
|
|
27
|
+
if (word === "") continue;
|
|
28
|
+
if (/^\s+$/.test(word)) {
|
|
29
|
+
frag.appendChild(document.createTextNode(word));
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const wrap = document.createElement("span");
|
|
33
|
+
wrap.className = "w";
|
|
34
|
+
wrap.setAttribute("aria-hidden", "true");
|
|
35
|
+
for (const ch of word) {
|
|
36
|
+
const span = document.createElement("span");
|
|
37
|
+
span.className = "g";
|
|
38
|
+
span.textContent = ch;
|
|
39
|
+
wrap.appendChild(span);
|
|
40
|
+
}
|
|
41
|
+
frag.appendChild(wrap);
|
|
42
|
+
}
|
|
43
|
+
el.textContent = "";
|
|
44
|
+
el.appendChild(frag);
|
|
45
|
+
el.classList.add("reactive");
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const measure = () => {
|
|
49
|
+
for (const glyph of glyphs) {
|
|
50
|
+
const r = glyph.el.getBoundingClientRect();
|
|
51
|
+
// Page coordinates so scrolling never invalidates the cache.
|
|
52
|
+
glyph.x = r.left + r.width / 2 + window.scrollX;
|
|
53
|
+
glyph.y = r.top + r.height / 2 + window.scrollY;
|
|
54
|
+
glyph.visible = r.width > 0;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const initGlyphs = () => {
|
|
59
|
+
if (!finePointer.matches) return;
|
|
60
|
+
document.querySelectorAll("[data-reactive]").forEach((el) => {
|
|
61
|
+
splitIntoGlyphs(el);
|
|
62
|
+
el.querySelectorAll(".g").forEach((span) => {
|
|
63
|
+
glyphs.push({ el: span, x: 0, y: 0, lit: 0, visible: true });
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
if (glyphs.length) measure();
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
let pointerX = -9999;
|
|
70
|
+
let pointerY = -9999;
|
|
71
|
+
let queued = false;
|
|
72
|
+
|
|
73
|
+
const paint = () => {
|
|
74
|
+
queued = false;
|
|
75
|
+
const top = window.scrollY - RADIUS;
|
|
76
|
+
const bottom = window.scrollY + window.innerHeight + RADIUS;
|
|
77
|
+
|
|
78
|
+
for (const glyph of glyphs) {
|
|
79
|
+
let next = 0;
|
|
80
|
+
if (glyph.visible && glyph.y > top && glyph.y < bottom) {
|
|
81
|
+
const dx = glyph.x - pointerX;
|
|
82
|
+
const dy = glyph.y - pointerY;
|
|
83
|
+
const d = Math.hypot(dx, dy);
|
|
84
|
+
if (d < RADIUS) {
|
|
85
|
+
// Ease-out so the falloff reads as a soft brush, not a hard disc.
|
|
86
|
+
const t = 1 - d / RADIUS;
|
|
87
|
+
next = Math.round(t * t * 100) / 100;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (next !== glyph.lit) {
|
|
91
|
+
glyph.lit = next;
|
|
92
|
+
if (next === 0) glyph.el.style.removeProperty("--h");
|
|
93
|
+
else glyph.el.style.setProperty("--h", String(next));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const onPointerMove = (event) => {
|
|
99
|
+
pointerX = event.clientX + window.scrollX;
|
|
100
|
+
pointerY = event.clientY + window.scrollY;
|
|
101
|
+
document.body.style.setProperty("--mx", `${event.clientX}px`);
|
|
102
|
+
document.body.style.setProperty("--my", `${event.clientY}px`);
|
|
103
|
+
if (!queued && glyphs.length) {
|
|
104
|
+
queued = true;
|
|
105
|
+
requestAnimationFrame(paint);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (finePointer.matches && !reduceMotion.matches) {
|
|
110
|
+
document.body.classList.add("pointer-fine");
|
|
111
|
+
initGlyphs();
|
|
112
|
+
window.addEventListener("pointermove", onPointerMove, { passive: true });
|
|
113
|
+
window.addEventListener("resize", measure);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* -------------------------------------------------------- catalog filter */
|
|
117
|
+
|
|
118
|
+
const q = document.getElementById("q");
|
|
119
|
+
const grid = document.getElementById("grid");
|
|
120
|
+
|
|
121
|
+
if (q && grid) {
|
|
122
|
+
const cards = Array.from(grid.querySelectorAll(".card"));
|
|
123
|
+
const chips = Array.from(document.querySelectorAll(".chip"));
|
|
124
|
+
const countEl = document.getElementById("count");
|
|
125
|
+
const emptyEl = document.getElementById("empty");
|
|
126
|
+
const active = { category: null, adapter: null };
|
|
127
|
+
|
|
128
|
+
const apply = () => {
|
|
129
|
+
const needle = q.value.trim().toLowerCase();
|
|
130
|
+
let shown = 0;
|
|
131
|
+
|
|
132
|
+
for (const card of cards) {
|
|
133
|
+
const matchesText = !needle || card.dataset.search.includes(needle);
|
|
134
|
+
const matchesCat =
|
|
135
|
+
!active.category || card.dataset.category === active.category;
|
|
136
|
+
const matchesAdapter =
|
|
137
|
+
!active.adapter ||
|
|
138
|
+
card.dataset.adapters.split(" ").includes(active.adapter);
|
|
139
|
+
const visible = matchesText && matchesCat && matchesAdapter;
|
|
140
|
+
card.hidden = !visible;
|
|
141
|
+
if (visible) shown += 1;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
countEl.textContent =
|
|
145
|
+
shown === cards.length
|
|
146
|
+
? `${cards.length} skills`
|
|
147
|
+
: `${shown} of ${cards.length} skills`;
|
|
148
|
+
emptyEl.hidden = shown !== 0;
|
|
149
|
+
|
|
150
|
+
// Cards moved, so the glyph rect cache is stale.
|
|
151
|
+
if (glyphs.length) requestAnimationFrame(measure);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
q.addEventListener("input", apply);
|
|
155
|
+
|
|
156
|
+
for (const chip of chips) {
|
|
157
|
+
chip.addEventListener("click", () => {
|
|
158
|
+
const kind = chip.dataset.kind;
|
|
159
|
+
const value = chip.dataset.value;
|
|
160
|
+
const isOn = active[kind] === value;
|
|
161
|
+
active[kind] = isOn ? null : value;
|
|
162
|
+
for (const other of chips) {
|
|
163
|
+
if (other.dataset.kind !== kind) continue;
|
|
164
|
+
other.setAttribute(
|
|
165
|
+
"aria-pressed",
|
|
166
|
+
String(!isOn && other.dataset.value === value),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
apply();
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Deep links like /?q=release or /?category=marketing.
|
|
174
|
+
const params = new URLSearchParams(window.location.search);
|
|
175
|
+
if (params.get("q")) q.value = params.get("q");
|
|
176
|
+
for (const kind of ["category", "adapter"]) {
|
|
177
|
+
const value = params.get(kind);
|
|
178
|
+
if (!value) continue;
|
|
179
|
+
const chip = chips.find(
|
|
180
|
+
(c) => c.dataset.kind === kind && c.dataset.value === value,
|
|
181
|
+
);
|
|
182
|
+
if (chip) {
|
|
183
|
+
active[kind] = value;
|
|
184
|
+
chip.setAttribute("aria-pressed", "true");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
apply();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* ------------------------------------------------------- copy buttons */
|
|
191
|
+
|
|
192
|
+
document.querySelectorAll(".copy").forEach((button) => {
|
|
193
|
+
const idle = button.textContent;
|
|
194
|
+
let resetTimer = 0;
|
|
195
|
+
|
|
196
|
+
button.addEventListener("click", async () => {
|
|
197
|
+
const pre = button.closest(".slab").querySelector("pre");
|
|
198
|
+
try {
|
|
199
|
+
await navigator.clipboard.writeText(pre.textContent.trim());
|
|
200
|
+
button.textContent = "copied";
|
|
201
|
+
// A second click before the first timer fires would otherwise capture
|
|
202
|
+
// "copied" as the label to restore, wedging the button in that state.
|
|
203
|
+
clearTimeout(resetTimer);
|
|
204
|
+
resetTimer = setTimeout(() => {
|
|
205
|
+
button.textContent = idle;
|
|
206
|
+
}, 1400);
|
|
207
|
+
} catch {
|
|
208
|
+
// Clipboard blocked (insecure context, denied permission): select the
|
|
209
|
+
// text so the reader can copy it by hand.
|
|
210
|
+
const range = document.createRange();
|
|
211
|
+
range.selectNodeContents(pre);
|
|
212
|
+
const selection = window.getSelection();
|
|
213
|
+
selection.removeAllRanges();
|
|
214
|
+
selection.addRange(range);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
})();
|
package/site/build.mjs
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Static site generator for the agent-skills catalog.
|
|
4
|
+
*
|
|
5
|
+
* Reads the machine-readable catalog (the same file `npm test` validates) and
|
|
6
|
+
* emits a dependency-free static site into `_site/` for GitHub Pages.
|
|
7
|
+
* There is no templating library on purpose: the catalog is small, the output
|
|
8
|
+
* must stay auditable, and CI should not need a lockfile refresh to publish.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { mkdir, readFile, readdir, copyFile, writeFile, rm } from "node:fs/promises";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const root = join(here, "..");
|
|
18
|
+
const out = join(root, "_site");
|
|
19
|
+
|
|
20
|
+
const SITE_URL = "https://skills.olegkoval.com";
|
|
21
|
+
const REPO = "https://github.com/oleg-koval/agent-skills";
|
|
22
|
+
const CNAME = "skills.olegkoval.com";
|
|
23
|
+
|
|
24
|
+
/* ------------------------------------------------------------- utilities */
|
|
25
|
+
|
|
26
|
+
const esc = (value) =>
|
|
27
|
+
String(value)
|
|
28
|
+
.replace(/&/g, "&")
|
|
29
|
+
.replace(/</g, "<")
|
|
30
|
+
.replace(/>/g, ">")
|
|
31
|
+
.replace(/"/g, """);
|
|
32
|
+
|
|
33
|
+
const titleCase = (slug) =>
|
|
34
|
+
slug.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
35
|
+
|
|
36
|
+
/** Pulls the YAML-ish frontmatter block off a SKILL.md without a YAML dep. */
|
|
37
|
+
const readFrontmatter = async (path) => {
|
|
38
|
+
if (!existsSync(path)) return {};
|
|
39
|
+
const raw = await readFile(path, "utf8");
|
|
40
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
41
|
+
if (!match) return {};
|
|
42
|
+
const fields = {};
|
|
43
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
44
|
+
const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
45
|
+
if (!kv) continue;
|
|
46
|
+
fields[kv[1]] = kv[2].replace(/^["']|["']$/g, "").trim();
|
|
47
|
+
}
|
|
48
|
+
return fields;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Counts the extra reference material shipped alongside a skill. */
|
|
52
|
+
const countSupportFiles = async (dir) => {
|
|
53
|
+
let files = 0;
|
|
54
|
+
const walk = async (current) => {
|
|
55
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (entry.name === "adapters") continue; // wrappers, not content
|
|
58
|
+
if (entry.isDirectory()) await walk(join(current, entry.name));
|
|
59
|
+
else if (entry.name !== "SKILL.md") files += 1;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
if (existsSync(dir)) await walk(dir);
|
|
63
|
+
return files;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/* ----------------------------------------------------------------- chrome */
|
|
67
|
+
|
|
68
|
+
const layout = ({ title, description, canonical, body, ogTitle }) => `<!doctype html>
|
|
69
|
+
<html lang="en">
|
|
70
|
+
<head>
|
|
71
|
+
<meta charset="utf-8">
|
|
72
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
73
|
+
<title>${esc(title)}</title>
|
|
74
|
+
<meta name="description" content="${esc(description)}">
|
|
75
|
+
<link rel="canonical" href="${esc(canonical)}">
|
|
76
|
+
<meta property="og:type" content="website">
|
|
77
|
+
<meta property="og:site_name" content="agent skills">
|
|
78
|
+
<meta property="og:title" content="${esc(ogTitle || title)}">
|
|
79
|
+
<meta property="og:description" content="${esc(description)}">
|
|
80
|
+
<meta property="og:url" content="${esc(canonical)}">
|
|
81
|
+
<meta name="twitter:card" content="summary">
|
|
82
|
+
<meta name="theme-color" content="#c3ac84">
|
|
83
|
+
<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml">
|
|
84
|
+
<link rel="stylesheet" href="/assets/paperbag.css">
|
|
85
|
+
</head>
|
|
86
|
+
<body>
|
|
87
|
+
<div class="grain" aria-hidden="true"></div>
|
|
88
|
+
<div class="halftone" aria-hidden="true"></div>
|
|
89
|
+
<div class="blotch" aria-hidden="true"></div>
|
|
90
|
+
<div class="smudge" aria-hidden="true"></div>
|
|
91
|
+
<div class="shell">
|
|
92
|
+
<header class="masthead">
|
|
93
|
+
<a class="wordmark" href="/">agent skills</a>
|
|
94
|
+
<nav aria-label="Primary">
|
|
95
|
+
<a href="/#catalog">Catalog</a>
|
|
96
|
+
<a href="/#install">Install</a>
|
|
97
|
+
<a href="/#about">About</a>
|
|
98
|
+
<a href="${REPO}">GitHub</a>
|
|
99
|
+
</nav>
|
|
100
|
+
</header>
|
|
101
|
+
${body}
|
|
102
|
+
<footer class="colophon">
|
|
103
|
+
<span>MIT licensed · <a href="${REPO}">oleg-koval/agent-skills</a></span>
|
|
104
|
+
<span>Lo-fi homage to p2output.com, c. 1998</span>
|
|
105
|
+
</footer>
|
|
106
|
+
</div>
|
|
107
|
+
<script src="/assets/paperbag.js" defer></script>
|
|
108
|
+
</body>
|
|
109
|
+
</html>
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
const slab = (label, code) => `<div class="slab">
|
|
113
|
+
<header><span>${esc(label)}</span><button class="copy" type="button">Copy</button></header>
|
|
114
|
+
<pre>${esc(code)}</pre>
|
|
115
|
+
</div>`;
|
|
116
|
+
|
|
117
|
+
/* ------------------------------------------------------------ index page */
|
|
118
|
+
|
|
119
|
+
const installSection = (sample) => `<section class="band" id="install">
|
|
120
|
+
<h2 data-reactive>Get them</h2>
|
|
121
|
+
<p class="sub">Pick your agent. The canonical package is the same either way; only the wrapper changes.</p>
|
|
122
|
+
<div class="cols">
|
|
123
|
+
<div>
|
|
124
|
+
<h3>Claude Code</h3>
|
|
125
|
+
${slab("marketplace", "/plugin marketplace add oleg-koval/agent-skills\n/plugin install olko-agent-skills@olko-agent-skills")}
|
|
126
|
+
</div>
|
|
127
|
+
<div>
|
|
128
|
+
<h3>Codex</h3>
|
|
129
|
+
${slab("symlink install", "git clone https://github.com/oleg-koval/agent-skills.git\ncd agent-skills\n./scripts/install-codex-symlinks.sh")}
|
|
130
|
+
</div>
|
|
131
|
+
<div>
|
|
132
|
+
<h3>Grok</h3>
|
|
133
|
+
${slab("marketplace", "grok plugin marketplace add oleg-koval/agent-skills")}
|
|
134
|
+
</div>
|
|
135
|
+
<div>
|
|
136
|
+
<h3>Then just ask</h3>
|
|
137
|
+
${slab("any session", `Use the ${sample} skill.`)}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
<p class="sub">Cursor, Copilot, Windsurf and Kiro consume the generated wrappers under each package's <code>adapters/</code> directory — see the <a href="${REPO}#quick-start">repository README</a>.</p>
|
|
141
|
+
</section>`;
|
|
142
|
+
|
|
143
|
+
const card = (pkg) => {
|
|
144
|
+
const search = [pkg.name, pkg.description, pkg.category, ...(pkg.tags || [])]
|
|
145
|
+
.join(" ")
|
|
146
|
+
.toLowerCase();
|
|
147
|
+
return `<li class="card"
|
|
148
|
+
data-category="${esc(pkg.category)}"
|
|
149
|
+
data-adapters="${esc((pkg.adapters || []).join(" "))}"
|
|
150
|
+
data-search="${esc(search)}">
|
|
151
|
+
<span class="cat">${esc(pkg.category.replace(/-/g, " "))}</span>
|
|
152
|
+
<h2><a href="/skills/${esc(pkg.name)}/" data-reactive>${esc(titleCase(pkg.name))}</a></h2>
|
|
153
|
+
<p>${esc(pkg.description)}</p>
|
|
154
|
+
<span class="lookup">${esc(pkg.lookupName)}</span>
|
|
155
|
+
<ul class="tags">${(pkg.tags || [])
|
|
156
|
+
.slice(0, 6)
|
|
157
|
+
.map((t) => `<li>${esc(t)}</li>`)
|
|
158
|
+
.join("")}</ul>
|
|
159
|
+
</li>`;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const indexPage = ({ packages, categories, adapters, collections }) => {
|
|
163
|
+
const body = `<section class="hero">
|
|
164
|
+
<p class="kicker">Agent-agnostic skill catalog</p>
|
|
165
|
+
<h1 data-reactive>Skills in a paper bag</h1>
|
|
166
|
+
<div class="stamp" aria-hidden="true">Opinionated<br>by design</div>
|
|
167
|
+
<p class="lede">${packages.length} canonical skills for Codex, Claude, Cursor, Grok, Copilot, Windsurf, Kiro, Pi and Hermes. They encode working defaults and repeatable workflows instead of neutral snippets — starting points with taste, easy to inspect, specific enough for an agent to execute the same way twice.</p>
|
|
168
|
+
</section>
|
|
169
|
+
|
|
170
|
+
<div class="ticker" aria-hidden="true"><span>${packages
|
|
171
|
+
.map((p) => p.lookupName)
|
|
172
|
+
.join(" • ")}</span></div>
|
|
173
|
+
|
|
174
|
+
<div class="statbar">
|
|
175
|
+
<div><b>${packages.length}</b><small>Skills</small></div>
|
|
176
|
+
<div><b>${adapters.length}</b><small>Agent adapters</small></div>
|
|
177
|
+
<div><b>${categories.length}</b><small>Categories</small></div>
|
|
178
|
+
<div><b>${collections.length}</b><small>Collections</small></div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<section class="band" id="catalog">
|
|
182
|
+
<div class="controls">
|
|
183
|
+
<label for="q">Search the catalog</label>
|
|
184
|
+
<input id="q" type="search" autocomplete="off" placeholder="release, pull requests, obsidian, watch face…">
|
|
185
|
+
<div class="chips" role="group" aria-label="Filter by category">
|
|
186
|
+
${categories
|
|
187
|
+
.map(
|
|
188
|
+
(c) =>
|
|
189
|
+
`<button class="chip" type="button" aria-pressed="false" data-kind="category" data-value="${esc(c.name)}">${esc(c.name.replace(/-/g, " "))} <span aria-hidden="true">${c.count}</span></button>`,
|
|
190
|
+
)
|
|
191
|
+
.join("\n ")}
|
|
192
|
+
</div>
|
|
193
|
+
<div class="chips" role="group" aria-label="Filter by agent">
|
|
194
|
+
${adapters
|
|
195
|
+
.map(
|
|
196
|
+
(a) =>
|
|
197
|
+
`<button class="chip" type="button" aria-pressed="false" data-kind="adapter" data-value="${esc(a.name)}">${esc(a.name)} <span aria-hidden="true">${a.count}</span></button>`,
|
|
198
|
+
)
|
|
199
|
+
.join("\n ")}
|
|
200
|
+
</div>
|
|
201
|
+
<p class="count" id="count" role="status">${packages.length} skills</p>
|
|
202
|
+
</div>
|
|
203
|
+
<ul class="grid" id="grid">
|
|
204
|
+
${packages.map(card).join("\n ")}
|
|
205
|
+
</ul>
|
|
206
|
+
<p class="empty" id="empty" hidden>Nothing in the bag matches that.</p>
|
|
207
|
+
</section>
|
|
208
|
+
|
|
209
|
+
${installSection(packages[0].lookupName)}
|
|
210
|
+
|
|
211
|
+
<section class="band" id="about">
|
|
212
|
+
<h2 data-reactive>About</h2>
|
|
213
|
+
<p class="sub">One canonical package per workflow. Agent-specific wrappers live in <code>adapters/</code> rather than duplicating the skill. Catalogs stay neutral and machine-readable, so this page is generated from <code>catalog/skills.json</code> and can never drift from what actually ships.</p>
|
|
214
|
+
<p class="sub">The styling is a deliberate homage to <strong>p2output.com</strong>, a late-1990s experimental web design and motion graphics project written up in <em>Fresh Styles for Web Designers: Eye Candy from the Underground</em> for its lo-fi paper-bag surface, busy background graphics, and text that lit up as your cursor swept across it. The original domain has been dark for years. Move your pointer over the headlines.</p>
|
|
215
|
+
</section>`;
|
|
216
|
+
|
|
217
|
+
return layout({
|
|
218
|
+
title: "agent skills — an agent-agnostic skill catalog",
|
|
219
|
+
ogTitle: "agent skills",
|
|
220
|
+
description: `${packages.length} opinionated, agent-agnostic skills for Codex, Claude, Cursor, Grok, Copilot, Windsurf and Kiro.`,
|
|
221
|
+
canonical: `${SITE_URL}/`,
|
|
222
|
+
body,
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/* ----------------------------------------------------------- detail page */
|
|
227
|
+
|
|
228
|
+
const detailPage = (pkg, prev, next) => {
|
|
229
|
+
const label = titleCase(pkg.name);
|
|
230
|
+
const body = `<p class="crumb"><a href="/">Catalog</a> / ${esc(pkg.category.replace(/-/g, " "))}</p>
|
|
231
|
+
<div class="detail-head">
|
|
232
|
+
<span class="cat">${esc(pkg.lookupName)}</span>
|
|
233
|
+
<h1 data-reactive>${esc(label)}</h1>
|
|
234
|
+
<p>${esc(pkg.longDescription || pkg.description)}</p>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<dl class="meta">
|
|
238
|
+
<div><dt>Lookup name</dt><dd>${esc(pkg.lookupName)}</dd></div>
|
|
239
|
+
<div><dt>Category</dt><dd>${esc(pkg.category.replace(/-/g, " "))}</dd></div>
|
|
240
|
+
<div><dt>Agents</dt><dd>${esc((pkg.adapters || []).join(", "))}</dd></div>
|
|
241
|
+
<div><dt>Bundled references</dt><dd>${pkg.supportFiles} file${pkg.supportFiles === 1 ? "" : "s"}</dd></div>
|
|
242
|
+
</dl>
|
|
243
|
+
|
|
244
|
+
<section class="band">
|
|
245
|
+
<h2 data-reactive>Use it</h2>
|
|
246
|
+
<p class="sub">Once the catalog is installed, name the skill in a session.</p>
|
|
247
|
+
${slab("any agent", `Use the ${pkg.lookupName} skill.`)}
|
|
248
|
+
${slab("claude code", `/plugin marketplace add oleg-koval/agent-skills\n/plugin install olko-agent-skills@olko-agent-skills`)}
|
|
249
|
+
<p class="sub"><a href="${REPO}/blob/main/${esc(pkg.path)}/SKILL.md">Read the full SKILL.md on GitHub</a> · <a href="${REPO}/tree/main/${esc(pkg.path)}">Browse the package</a></p>
|
|
250
|
+
</section>
|
|
251
|
+
|
|
252
|
+
${
|
|
253
|
+
(pkg.tags || []).length
|
|
254
|
+
? `<section class="band">
|
|
255
|
+
<h2 data-reactive>Tags</h2>
|
|
256
|
+
<ul class="tags">${pkg.tags.map((t) => `<li><a href="/?q=${encodeURIComponent(t)}">${esc(t)}</a></li>`).join("")}</ul>
|
|
257
|
+
</section>`
|
|
258
|
+
: ""
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
<nav class="pager" aria-label="Catalog">
|
|
262
|
+
<a href="/skills/${esc(prev.name)}/">← ${esc(titleCase(prev.name))}</a>
|
|
263
|
+
<a href="/skills/${esc(next.name)}/">${esc(titleCase(next.name))} →</a>
|
|
264
|
+
</nav>`;
|
|
265
|
+
|
|
266
|
+
return layout({
|
|
267
|
+
title: `${label} — agent skills`,
|
|
268
|
+
description: pkg.description,
|
|
269
|
+
canonical: `${SITE_URL}/skills/${pkg.name}/`,
|
|
270
|
+
body,
|
|
271
|
+
});
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
/* ------------------------------------------------------------------ main */
|
|
275
|
+
|
|
276
|
+
const favicon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
277
|
+
<rect width="32" height="32" fill="#c3ac84"/>
|
|
278
|
+
<path d="M7 9h18v16H7z" fill="none" stroke="#191410" stroke-width="2.5"/>
|
|
279
|
+
<path d="M7 9l4-4h10l4 4" fill="none" stroke="#191410" stroke-width="2.5"/>
|
|
280
|
+
<circle cx="16" cy="18" r="3.4" fill="#a8321f"/>
|
|
281
|
+
</svg>
|
|
282
|
+
`;
|
|
283
|
+
|
|
284
|
+
const main = async () => {
|
|
285
|
+
const catalog = JSON.parse(
|
|
286
|
+
await readFile(join(root, "catalog", "skills.json"), "utf8"),
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const collectionFiles = await readdir(join(root, "collections"));
|
|
290
|
+
const collections = collectionFiles.filter((f) => f.endsWith(".json"));
|
|
291
|
+
|
|
292
|
+
const packages = [...catalog.packages].sort((a, b) =>
|
|
293
|
+
a.name.localeCompare(b.name),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
for (const pkg of packages) {
|
|
297
|
+
const dir = join(root, pkg.path);
|
|
298
|
+
const front = await readFrontmatter(join(dir, "SKILL.md"));
|
|
299
|
+
// The catalog description is the source of truth; frontmatter only fills
|
|
300
|
+
// the longer detail-page blurb when it is genuinely richer.
|
|
301
|
+
if (front.description && front.description.length > pkg.description.length) {
|
|
302
|
+
pkg.longDescription = front.description;
|
|
303
|
+
}
|
|
304
|
+
pkg.supportFiles = await countSupportFiles(dir);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const tally = (key) => {
|
|
308
|
+
const counts = new Map();
|
|
309
|
+
for (const pkg of packages) {
|
|
310
|
+
const values = Array.isArray(pkg[key]) ? pkg[key] : [pkg[key]];
|
|
311
|
+
for (const value of values) counts.set(value, (counts.get(value) || 0) + 1);
|
|
312
|
+
}
|
|
313
|
+
return [...counts]
|
|
314
|
+
.map(([name, count]) => ({ name, count }))
|
|
315
|
+
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const categories = tally("category");
|
|
319
|
+
const adapters = tally("adapters");
|
|
320
|
+
|
|
321
|
+
await rm(out, { recursive: true, force: true });
|
|
322
|
+
await mkdir(join(out, "assets"), { recursive: true });
|
|
323
|
+
|
|
324
|
+
await copyFile(
|
|
325
|
+
join(here, "assets", "paperbag.css"),
|
|
326
|
+
join(out, "assets", "paperbag.css"),
|
|
327
|
+
);
|
|
328
|
+
await copyFile(
|
|
329
|
+
join(here, "assets", "paperbag.js"),
|
|
330
|
+
join(out, "assets", "paperbag.js"),
|
|
331
|
+
);
|
|
332
|
+
await writeFile(join(out, "assets", "favicon.svg"), favicon);
|
|
333
|
+
|
|
334
|
+
await writeFile(
|
|
335
|
+
join(out, "index.html"),
|
|
336
|
+
indexPage({ packages, categories, adapters, collections }),
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
for (const [i, pkg] of packages.entries()) {
|
|
340
|
+
const prev = packages[(i - 1 + packages.length) % packages.length];
|
|
341
|
+
const next = packages[(i + 1) % packages.length];
|
|
342
|
+
const dir = join(out, "skills", pkg.name);
|
|
343
|
+
await mkdir(dir, { recursive: true });
|
|
344
|
+
await writeFile(join(dir, "index.html"), detailPage(pkg, prev, next));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
await writeFile(
|
|
348
|
+
join(out, "404.html"),
|
|
349
|
+
layout({
|
|
350
|
+
title: "Not in the bag — agent skills",
|
|
351
|
+
description: "That page does not exist.",
|
|
352
|
+
canonical: `${SITE_URL}/404.html`,
|
|
353
|
+
body: `<section class="hero">
|
|
354
|
+
<p class="kicker">404</p>
|
|
355
|
+
<h1 data-reactive>Not in the bag</h1>
|
|
356
|
+
<p class="lede">Whatever you reached for is not here. <a href="/">Go back to the catalog</a>.</p>
|
|
357
|
+
</section>`,
|
|
358
|
+
}),
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const urls = [`${SITE_URL}/`, ...packages.map((p) => `${SITE_URL}/skills/${p.name}/`)];
|
|
362
|
+
await writeFile(
|
|
363
|
+
join(out, "sitemap.xml"),
|
|
364
|
+
`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls
|
|
365
|
+
.map((u) => ` <url><loc>${u}</loc></url>`)
|
|
366
|
+
.join("\n")}\n</urlset>\n`,
|
|
367
|
+
);
|
|
368
|
+
await writeFile(
|
|
369
|
+
join(out, "robots.txt"),
|
|
370
|
+
`User-agent: *\nAllow: /\nSitemap: ${SITE_URL}/sitemap.xml\n`,
|
|
371
|
+
);
|
|
372
|
+
await writeFile(join(out, "CNAME"), `${CNAME}\n`);
|
|
373
|
+
await writeFile(join(out, ".nojekyll"), "");
|
|
374
|
+
|
|
375
|
+
console.log(
|
|
376
|
+
`site: ${packages.length} skills, ${categories.length} categories, ${adapters.length} adapters -> _site/`,
|
|
377
|
+
);
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
await main();
|