@olegkoval/agent-skills 1.26.0 → 1.28.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 +2 -1
- package/README.md +7 -3
- package/catalog/skills.json +18 -0
- package/package.json +5 -3
- package/packages/software-development/lekker-review/SKILL.md +519 -0
- package/packages/software-development/lekker-review/adapters/claude/plugin.json +5 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/SKILL.md +520 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/completeness-critic.md +21 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/conventions.md +124 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/fix-verifier.md +84 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/fixer.md +120 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/implementation.md +53 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/prover.md +135 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/quality.md +72 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/simplification.md +45 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/test-quality.md +170 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/triage-logic.md +27 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/triage-quality.md +41 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/verifier.md +295 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/artifact-page.md +143 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/context-gathering.md +162 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/fix-mode.md +329 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/github-post.md +205 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/house-rules.md +76 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/output-format.md +232 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/scripts/changed-files.sh +77 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/scripts/setup-worktree.sh +337 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/scripts/verify-fixes.sh +231 -0
- package/packages/software-development/lekker-review/fix-workflow.js +273 -0
- package/packages/software-development/lekker-review/references/agents/completeness-critic.md +21 -0
- package/packages/software-development/lekker-review/references/agents/conventions.md +124 -0
- package/packages/software-development/lekker-review/references/agents/fix-verifier.md +84 -0
- package/packages/software-development/lekker-review/references/agents/fixer.md +120 -0
- package/packages/software-development/lekker-review/references/agents/implementation.md +53 -0
- package/packages/software-development/lekker-review/references/agents/prover.md +135 -0
- package/packages/software-development/lekker-review/references/agents/quality.md +72 -0
- package/packages/software-development/lekker-review/references/agents/simplification.md +45 -0
- package/packages/software-development/lekker-review/references/agents/test-quality.md +170 -0
- package/packages/software-development/lekker-review/references/agents/triage-logic.md +27 -0
- package/packages/software-development/lekker-review/references/agents/triage-quality.md +41 -0
- package/packages/software-development/lekker-review/references/agents/verifier.md +295 -0
- package/packages/software-development/lekker-review/references/artifact-page.md +143 -0
- package/packages/software-development/lekker-review/references/context-gathering.md +162 -0
- package/packages/software-development/lekker-review/references/fix-mode.md +329 -0
- package/packages/software-development/lekker-review/references/github-post.md +205 -0
- package/packages/software-development/lekker-review/references/house-rules.md +76 -0
- package/packages/software-development/lekker-review/references/output-format.md +232 -0
- package/packages/software-development/lekker-review/scripts/changed-files.sh +77 -0
- package/packages/software-development/lekker-review/scripts/setup-worktree.sh +337 -0
- package/packages/software-development/lekker-review/scripts/verify-fixes.sh +231 -0
- package/packages/software-development/lekker-review/workflow.js +602 -0
- package/site/assets/paperbag.css +707 -0
- package/site/assets/paperbag.js +218 -0
- package/site/build.mjs +380 -0
|
@@ -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();
|