@olegkoval/agent-skills 1.38.3 → 1.40.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.
Files changed (29) hide show
  1. package/.github/copilot-instructions.md +1 -0
  2. package/.github/prompts/veto-routing.prompt.md +65 -0
  3. package/.kiro/steering/veto-routing.md +64 -0
  4. package/.windsurf/rules/veto-routing.md +63 -0
  5. package/README.md +3 -2
  6. package/adapters/claude/olko-skill-meta/skills/veto-routing/SKILL.md +76 -0
  7. package/adapters/codex/olko-skill-meta/README.md +1 -0
  8. package/adapters/cursor/olko-skill-meta/skills/veto-routing/SKILL.md +76 -0
  9. package/adapters/grok/olko-skill-meta/skills/veto-routing/SKILL.md +76 -0
  10. package/catalog/skills.json +23 -0
  11. package/package.json +1 -1
  12. package/plugins/olko-apple-kit/.claude-plugin/plugin.json +1 -1
  13. package/plugins/olko-creative/.claude-plugin/plugin.json +1 -1
  14. package/plugins/olko-garmin-kit/.claude-plugin/plugin.json +1 -1
  15. package/plugins/olko-git-tools/.claude-plugin/plugin.json +1 -1
  16. package/plugins/olko-github-pr/.claude-plugin/plugin.json +1 -1
  17. package/plugins/olko-obsidian/.claude-plugin/plugin.json +1 -1
  18. package/plugins/olko-product/.claude-plugin/plugin.json +1 -1
  19. package/plugins/olko-reflection/.claude-plugin/plugin.json +1 -1
  20. package/plugins/olko-release/.claude-plugin/plugin.json +1 -1
  21. package/plugins/olko-skill-meta/.claude-plugin/plugin.json +1 -1
  22. package/plugins/olko-skill-meta/skills/veto-routing/SKILL.md +75 -0
  23. package/plugins/olko-web-ops/.claude-plugin/plugin.json +1 -1
  24. package/scripts/lib/catalog.mjs +1 -1
  25. package/site/assets/catalog.css +1064 -0
  26. package/site/assets/catalog.js +232 -0
  27. package/site/build.mjs +218 -166
  28. package/site/assets/paperbag.css +0 -707
  29. package/site/assets/paperbag.js +0 -218
package/site/build.mjs CHANGED
@@ -2,14 +2,13 @@
2
2
  /*
3
3
  * Static site generator for the agent-skills catalog.
4
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.
5
+ * Reads the machine-readable catalog and emits a dependency-free site into
6
+ * `_site/`. The catalog stays the source of truth for plugin and skill data.
9
7
  */
10
8
 
11
9
  import { mkdir, readFile, readdir, copyFile, writeFile, rm } from "node:fs/promises";
12
10
  import { existsSync } from "node:fs";
11
+ import { createHash } from "node:crypto";
13
12
  import { dirname, join } from "node:path";
14
13
  import { fileURLToPath } from "node:url";
15
14
  import { loadCatalog } from "../scripts/lib/catalog.mjs";
@@ -19,10 +18,16 @@ const root = join(here, "..");
19
18
  const out = join(root, "_site");
20
19
 
21
20
  const SITE_URL = "https://skills.olegkoval.com";
21
+ const PORTFOLIO = "https://www.olegkoval.com";
22
22
  const REPO = "https://github.com/oleg-koval/agent-skills";
23
23
  const CNAME = "skills.olegkoval.com";
24
-
25
- /* ------------------------------------------------------------- utilities */
24
+ const GA_MEASUREMENT_ID = "G-NV8Q2H8YV0";
25
+ const MARKETPLACE_NAME = "olko-agent-skills";
26
+ const assetVersion = createHash("sha256")
27
+ .update(await readFile(join(here, "assets", "catalog.css")))
28
+ .update(await readFile(join(here, "assets", "catalog.js")))
29
+ .digest("hex")
30
+ .slice(0, 10);
26
31
 
27
32
  const esc = (value) =>
28
33
  String(value)
@@ -34,7 +39,8 @@ const esc = (value) =>
34
39
  const titleCase = (slug) =>
35
40
  slug.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
36
41
 
37
- /** Pulls the YAML-ish frontmatter block off a SKILL.md without a YAML dep. */
42
+ const displayPlugin = (name) => name.replace(/^olko-/, "").replace(/-/g, " ");
43
+
38
44
  const readFrontmatter = async (path) => {
39
45
  if (!existsSync(path)) return {};
40
46
  const raw = await readFile(path, "utf8");
@@ -49,13 +55,12 @@ const readFrontmatter = async (path) => {
49
55
  return fields;
50
56
  };
51
57
 
52
- /** Counts the extra reference material shipped alongside a skill. */
53
58
  const countSupportFiles = async (dir) => {
54
59
  let files = 0;
55
60
  const walk = async (current) => {
56
61
  const entries = await readdir(current, { withFileTypes: true });
57
62
  for (const entry of entries) {
58
- if (entry.name === "adapters") continue; // wrappers, not content
63
+ if (entry.name === "adapters") continue;
59
64
  if (entry.isDirectory()) await walk(join(current, entry.name));
60
65
  else if (entry.name !== "SKILL.md") files += 1;
61
66
  }
@@ -64,9 +69,7 @@ const countSupportFiles = async (dir) => {
64
69
  return files;
65
70
  };
66
71
 
67
- /* ----------------------------------------------------------------- chrome */
68
-
69
- const layout = ({ title, description, canonical, body, ogTitle }) => `<!doctype html>
72
+ const layout = ({ title, description, canonical, body, ogTitle, pageType }) => `<!doctype html>
70
73
  <html lang="en">
71
74
  <head>
72
75
  <meta charset="utf-8">
@@ -80,219 +83,283 @@ const layout = ({ title, description, canonical, body, ogTitle }) => `<!doctype
80
83
  <meta property="og:description" content="${esc(description)}">
81
84
  <meta property="og:url" content="${esc(canonical)}">
82
85
  <meta name="twitter:card" content="summary">
83
- <meta name="theme-color" content="#c3ac84">
86
+ <meta name="theme-color" content="#f5f7f4">
84
87
  <link rel="icon" href="/assets/favicon.svg" type="image/svg+xml">
85
- <link rel="stylesheet" href="/assets/paperbag.css">
88
+ <link rel="stylesheet" href="/assets/catalog.css?v=${assetVersion}">
86
89
  </head>
87
- <body>
88
- <div class="grain" aria-hidden="true"></div>
89
- <div class="halftone" aria-hidden="true"></div>
90
- <div class="blotch" aria-hidden="true"></div>
91
- <div class="smudge" aria-hidden="true"></div>
90
+ <body data-page-type="${esc(pageType)}" data-ga-measurement-id="${GA_MEASUREMENT_ID}">
91
+ <a class="skip-link" href="#main">Skip to content</a>
92
92
  <div class="shell">
93
93
  <header class="masthead">
94
- <a class="wordmark" href="/">agent&nbsp;skills</a>
94
+ <div class="identity">
95
+ <a class="wordmark" href="/" aria-label="Agent Skills home"><span aria-hidden="true">as</span>agent skills</a>
96
+ <a class="maker" href="${PORTFOLIO}" data-analytics-event="portfolio_visit" data-analytics-location="header">by Oleg Koval <span aria-hidden="true">↗</span></a>
97
+ </div>
95
98
  <nav aria-label="Primary">
96
- <a href="/#catalog">Catalog</a>
99
+ <a href="/#marketplace">Marketplace</a>
100
+ <a href="/#catalog">Skills</a>
97
101
  <a href="/#install">Install</a>
98
- <a href="/#about">About</a>
99
- <a href="${REPO}">GitHub</a>
102
+ <a href="${REPO}" data-analytics-event="github_visit" data-analytics-location="header">GitHub <span aria-hidden="true">↗</span></a>
100
103
  </nav>
101
104
  </header>
102
- ${body}
105
+ <main id="main">${body}</main>
103
106
  <footer class="colophon">
104
- <span>MIT licensed &middot; <a href="${REPO}">oleg-koval/agent-skills</a></span>
105
- <span>Lo-fi homage to p2output.com, c. 1998</span>
107
+ <p>Open source under MIT. Built and maintained by <a href="${PORTFOLIO}" data-analytics-event="portfolio_visit" data-analytics-location="footer">Oleg Koval</a>.</p>
108
+ <div>
109
+ <button class="text-button" type="button" data-consent-open>Analytics preferences</button>
110
+ <a href="${REPO}">Source</a>
111
+ </div>
106
112
  </footer>
107
113
  </div>
108
- <script src="/assets/paperbag.js" defer></script>
114
+ <section class="consent" data-consent-banner hidden aria-label="Analytics preference">
115
+ <div>
116
+ <strong>Optional traffic analytics</strong>
117
+ <p>Off until you allow. Page and install interaction counts help improve this catalog. Search text is never sent.</p>
118
+ </div>
119
+ <div class="consent-actions">
120
+ <button class="button button-secondary" type="button" data-consent-decline>Decline</button>
121
+ <button class="button button-primary" type="button" data-consent-accept>Allow analytics</button>
122
+ </div>
123
+ </section>
124
+ <script src="/assets/catalog.js?v=${assetVersion}" defer></script>
109
125
  </body>
110
126
  </html>
111
127
  `;
112
128
 
113
- const slab = (label, code) => `<div class="slab">
114
- <header><span>${esc(label)}</span><button class="copy" type="button">Copy</button></header>
129
+ const command = (label, code, analytics = {}) => `<div class="command">
130
+ <div class="command-head"><span>${esc(label)}</span><button class="copy" type="button" data-copy ${Object.entries(analytics)
131
+ .map(([key, value]) => `data-analytics-${esc(key)}="${esc(value)}"`)
132
+ .join(" ")}>Copy</button></div>
115
133
  <pre>${esc(code)}</pre>
116
134
  </div>`;
117
135
 
118
- /* ------------------------------------------------------------ index page */
136
+ const pluginCard = (plugin) => {
137
+ const install = `/plugin install ${plugin.name}@${MARKETPLACE_NAME}`;
138
+ return `<article class="plugin-card">
139
+ <div class="plugin-card-main">
140
+ <span class="eyebrow">${plugin.count} skill${plugin.count === 1 ? "" : "s"}</span>
141
+ <h3><a href="/?category=${encodeURIComponent(plugin.name)}#catalog">${esc(plugin.name)}</a></h3>
142
+ <p>${esc(plugin.description)}</p>
143
+ </div>
144
+ <div class="install-line">
145
+ <code>${esc(install)}</code>
146
+ <button class="copy-icon" type="button" data-copy data-analytics-event="marketplace_install_copy" data-analytics-plugin="${esc(plugin.name)}" aria-label="Copy install command for ${esc(plugin.name)}">Copy</button>
147
+ </div>
148
+ </article>`;
149
+ };
119
150
 
120
- const installSection = (sample) => `<section class="band" id="install">
121
- <h2 data-reactive>Get them</h2>
122
- <p class="sub">Pick your agent. The canonical package is the same either way; only the wrapper changes.</p>
123
- <div class="cols">
151
+ const installSection = (sample) => `<section class="section" id="install">
152
+ <div class="section-heading">
124
153
  <div>
125
- <h3>Claude Code</h3>
126
- ${slab("marketplace", "/plugin marketplace add oleg-koval/agent-skills\n/plugin install olko-agent-skills@olko-agent-skills")}
154
+ <p class="eyebrow">Installation</p>
155
+ <h2>Use the same catalog anywhere.</h2>
127
156
  </div>
128
- <div>
157
+ <p>Marketplace installs are quickest in Claude Code and Grok. Codex and the other adapters use the same canonical skill packages.</p>
158
+ </div>
159
+ <div class="install-grid">
160
+ <article>
161
+ <h3>Claude Code</h3>
162
+ <p>Add the marketplace once. Then copy a plugin command from the shelf above.</p>
163
+ ${command("Marketplace", "/plugin marketplace add oleg-koval/agent-skills", { event: "marketplace_add_copy", agent: "claude" })}
164
+ </article>
165
+ <article>
129
166
  <h3>Codex</h3>
130
- ${slab("symlink install", "git clone https://github.com/oleg-koval/agent-skills.git\ncd agent-skills\n./scripts/install-codex-symlinks.sh")}
131
- </div>
132
- <div>
167
+ <p>Clone the catalog and link its canonical packages into your local skills directory.</p>
168
+ ${command("Local install", "git clone https://github.com/oleg-koval/agent-skills.git\ncd agent-skills\n./scripts/install-codex-symlinks.sh", { event: "catalog_install_copy", agent: "codex" })}
169
+ </article>
170
+ <article>
133
171
  <h3>Grok</h3>
134
- ${slab("marketplace", "grok plugin marketplace add oleg-koval/agent-skills")}
135
- </div>
136
- <div>
137
- <h3>Then just ask</h3>
138
- ${slab("any session", `Use the ${sample} skill.`)}
139
- </div>
172
+ <p>Add the public marketplace. Its generated wrappers stay in sync with the source skills.</p>
173
+ ${command("Marketplace", "grok plugin marketplace add oleg-koval/agent-skills", { event: "marketplace_add_copy", agent: "grok" })}
174
+ </article>
175
+ <article>
176
+ <h3>Then ask directly</h3>
177
+ <p>Skills can be discovered automatically or named explicitly in a session.</p>
178
+ ${command("Any agent", `Use the ${sample} skill.`, { event: "skill_prompt_copy", agent: "any" })}
179
+ </article>
140
180
  </div>
141
- <p class="sub">Cursor, Copilot, Windsurf and Kiro consume the generated wrappers under <code>adapters/&lt;tool&gt;/&lt;plugin&gt;/</code>. See the <a href="${REPO}#quick-start">repository README</a>.</p>
181
+ <p class="section-note">Cursor, Copilot, Windsurf, Kiro, Pi and Hermes wrappers live under <code>adapters/</code>. See the <a href="${REPO}#quick-start">installation guide</a> for agent-specific paths.</p>
142
182
  </section>`;
143
183
 
144
- const card = (pkg) => {
184
+ const skillCard = (pkg) => {
145
185
  const search = [pkg.name, pkg.description, pkg.plugin.name, ...(pkg.tags || [])]
146
186
  .join(" ")
147
187
  .toLowerCase();
148
- return `<li class="card"
188
+ return `<li class="skill-card"
149
189
  data-category="${esc(pkg.plugin.name)}"
150
190
  data-adapters="${esc((pkg.adapters || []).join(" "))}"
151
191
  data-search="${esc(search)}">
152
- <span class="cat">${esc(pkg.plugin.name.replace(/^olko-/, "").replace(/-/g, " "))}</span>
153
- <h2><a href="/skills/${esc(pkg.name)}/" data-reactive>${esc(titleCase(pkg.name))}</a></h2>
192
+ <div class="skill-card-top">
193
+ <span>${esc(displayPlugin(pkg.plugin.name))}</span>
194
+ <code>${esc(pkg.lookupName)}</code>
195
+ </div>
196
+ <h3><a href="/skills/${esc(pkg.name)}/" data-analytics-event="skill_open" data-analytics-skill="${esc(pkg.name)}">${esc(titleCase(pkg.name))}</a></h3>
154
197
  <p>${esc(pkg.description)}</p>
155
- <span class="lookup">${esc(pkg.lookupName)}</span>
156
198
  <ul class="tags">${(pkg.tags || [])
157
- .slice(0, 6)
158
- .map((t) => `<li>${esc(t)}</li>`)
199
+ .slice(0, 4)
200
+ .map((tag) => `<li>${esc(tag)}</li>`)
159
201
  .join("")}</ul>
160
202
  </li>`;
161
203
  };
162
204
 
163
- const indexPage = ({ packages, categories, adapters }) => {
205
+ const indexPage = ({ packages, plugins, categories, adapters }) => {
164
206
  const body = `<section class="hero">
165
- <p class="kicker">Agent-agnostic skill catalog</p>
166
- <h1 data-reactive>Skills in a paper bag</h1>
167
- <div class="stamp" aria-hidden="true">Opinionated<br>by design</div>
168
- <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>
207
+ <div class="hero-copy">
208
+ <p class="eyebrow">Agent workflows by Oleg Koval</p>
209
+ <h1>Useful workflows, ready for your agent.</h1>
210
+ <p class="lede">A public marketplace of opinionated skills for shipping software, running products, and maintaining the work around them. Install by plugin, inspect every source file, and keep the same workflow across agents.</p>
211
+ <div class="hero-actions">
212
+ <a class="button button-primary" href="#marketplace">Browse marketplace</a>
213
+ <a class="button button-secondary" href="${REPO}" data-analytics-event="github_visit" data-analytics-location="hero">View source</a>
214
+ </div>
215
+ <p class="facts"><strong>${packages.length}</strong> skills <span>·</span> <strong>${plugins.length}</strong> plugins <span>·</span> <strong>${adapters.length}</strong> agent adapters</p>
216
+ </div>
217
+ <aside class="hero-command" aria-label="Quick marketplace install">
218
+ <p class="eyebrow">Start here</p>
219
+ <h2>Add the marketplace once.</h2>
220
+ ${command("Claude Code", "/plugin marketplace add oleg-koval/agent-skills", { event: "marketplace_add_copy", agent: "claude" })}
221
+ <p>Then choose one focused plugin below. You can add more later without installing the whole catalog.</p>
222
+ </aside>
169
223
  </section>
170
224
 
171
- <div class="ticker" aria-hidden="true"><span>${packages
172
- .map((p) => p.lookupName)
173
- .join(" &nbsp;&bull;&nbsp; ")}</span></div>
174
-
175
- <div class="statbar">
176
- <div><b>${packages.length}</b><small>Skills</small></div>
177
- <div><b>${adapters.length}</b><small>Agent adapters</small></div>
178
- <div><b>${categories.length}</b><small>Plugins</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&hellip;">
185
- <div class="chips" role="group" aria-label="Filter by plugin">
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(/^olko-/, "").replace(/-/g, " "))} <span aria-hidden="true">${c.count}</span></button>`,
190
- )
191
- .join("\n ")}
225
+ <section class="section marketplace" id="marketplace">
226
+ <div class="section-heading">
227
+ <div>
228
+ <p class="eyebrow">Marketplace plugins</p>
229
+ <h2>Install by the work you do.</h2>
192
230
  </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 ")}
231
+ <p>Each plugin is a focused bundle. Copy its Claude Code install command, or open it to see the individual skills it includes.</p>
232
+ </div>
233
+ <div class="plugin-grid">
234
+ ${plugins.map(pluginCard).join("\n ")}
235
+ </div>
236
+ </section>
237
+
238
+ <section class="section catalog" id="catalog">
239
+ <div class="section-heading">
240
+ <div>
241
+ <p class="eyebrow">Skill catalog</p>
242
+ <h2>Inspect before you install.</h2>
200
243
  </div>
244
+ <p>Search the complete source-backed catalog, then narrow it by plugin or supported agent.</p>
245
+ </div>
246
+ <div class="controls">
247
+ <label for="q">Search skills</label>
248
+ <input id="q" type="search" autocomplete="off" placeholder="Pull requests, releases, analytics, watch faces...">
249
+ <details>
250
+ <summary>Filter by plugin <span aria-hidden="true"></span></summary>
251
+ <div class="chips" role="group" aria-label="Filter by plugin">
252
+ ${categories
253
+ .map(
254
+ (category) =>
255
+ `<button class="chip" type="button" aria-pressed="false" data-kind="category" data-value="${esc(category.name)}">${esc(displayPlugin(category.name))} <span>${category.count}</span></button>`,
256
+ )
257
+ .join("\n ")}
258
+ </div>
259
+ </details>
260
+ <details>
261
+ <summary>Filter by agent <span aria-hidden="true"></span></summary>
262
+ <div class="chips" role="group" aria-label="Filter by agent">
263
+ ${adapters
264
+ .map(
265
+ (adapter) =>
266
+ `<button class="chip" type="button" aria-pressed="false" data-kind="adapter" data-value="${esc(adapter.name)}">${esc(adapter.name)} <span>${adapter.count}</span></button>`,
267
+ )
268
+ .join("\n ")}
269
+ </div>
270
+ </details>
201
271
  <p class="count" id="count" role="status">${packages.length} skills</p>
202
272
  </div>
203
- <ul class="grid" id="grid">
204
- ${packages.map(card).join("\n ")}
273
+ <ul class="skill-grid" id="grid">
274
+ ${packages.map(skillCard).join("\n ")}
205
275
  </ul>
206
- <p class="empty" id="empty" hidden>Nothing in the bag matches that.</p>
276
+ <p class="empty" id="empty" hidden>No skills match those filters.</p>
207
277
  </section>
208
278
 
209
279
  ${installSection(packages[0].lookupName)}
210
280
 
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>
281
+ <section class="section about" id="about">
282
+ <div class="section-heading">
283
+ <div>
284
+ <p class="eyebrow">How it stays useful</p>
285
+ <h2>One source, many agents.</h2>
286
+ </div>
287
+ <p>Every workflow has one canonical skill package. Tool-specific adapters are generated from it, so the marketplace, source files, and this site stay aligned.</p>
288
+ </div>
289
+ <a class="quiet-link" href="${PORTFOLIO}" data-analytics-event="portfolio_visit" data-analytics-location="about">More open-source work and writing at olegkoval.com <span aria-hidden="true">↗</span></a>
215
290
  </section>`;
216
291
 
217
292
  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.`,
293
+ title: "Agent Skills by Oleg Koval",
294
+ ogTitle: "Agent Skills",
295
+ description: `${packages.length} open-source agent skills packaged for Claude Code, Codex, Grok, Cursor, Copilot, Windsurf, Kiro, Pi and Hermes.`,
221
296
  canonical: `${SITE_URL}/`,
222
297
  body,
298
+ pageType: "catalog",
223
299
  });
224
300
  };
225
301
 
226
- /* ----------------------------------------------------------- detail page */
227
-
228
302
  const detailPage = (pkg, prev, next) => {
229
303
  const label = titleCase(pkg.name);
230
- const body = `<p class="crumb"><a href="/">Catalog</a> / ${esc(pkg.plugin.name.replace(/^olko-/, "").replace(/-/g, " "))}</p>
231
- <div class="detail-head">
232
- <span class="cat">${esc(pkg.lookupName)}</span>
233
- <h1 data-reactive>${esc(label)}</h1>
304
+ const body = `<p class="crumb"><a href="/">Skills</a><span>/</span><a href="/?category=${encodeURIComponent(pkg.plugin.name)}#catalog">${esc(displayPlugin(pkg.plugin.name))}</a></p>
305
+ <header class="detail-head">
306
+ <p class="eyebrow">${esc(pkg.lookupName)}</p>
307
+ <h1>${esc(label)}</h1>
234
308
  <p>${esc(pkg.longDescription || pkg.description)}</p>
235
- </div>
309
+ </header>
236
310
 
237
311
  <dl class="meta">
238
- <div><dt>Lookup name</dt><dd>${esc(pkg.lookupName)}</dd></div>
239
- <div><dt>Plugin</dt><dd>${esc(pkg.plugin.name.replace(/^olko-/, "").replace(/-/g, " "))}</dd></div>
312
+ <div><dt>Plugin</dt><dd>${esc(pkg.plugin.name)}</dd></div>
240
313
  <div><dt>Agents</dt><dd>${esc((pkg.adapters || []).join(", "))}</dd></div>
241
314
  <div><dt>Bundled references</dt><dd>${pkg.supportFiles} file${pkg.supportFiles === 1 ? "" : "s"}</dd></div>
242
315
  </dl>
243
316
 
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> &middot; <a href="${REPO}/tree/main/${esc(pkg.path)}">Browse the package</a></p>
317
+ <section class="section detail-use">
318
+ <div class="section-heading">
319
+ <div><p class="eyebrow">Install and use</p><h2>Add its plugin, then ask.</h2></div>
320
+ <p>The marketplace command is needed once per Claude Code setup. The plugin command installs this skill and its related workflows.</p>
321
+ </div>
322
+ <div class="install-grid">
323
+ <article>${command("Add marketplace", "/plugin marketplace add oleg-koval/agent-skills", { event: "marketplace_add_copy", agent: "claude" })}</article>
324
+ <article>${command("Install plugin", `/plugin install ${pkg.plugin.name}@${MARKETPLACE_NAME}`, { event: "marketplace_install_copy", plugin: pkg.plugin.name })}</article>
325
+ <article>${command("Invoke skill", `Use the ${pkg.lookupName} skill.`, { event: "skill_prompt_copy", skill: pkg.name })}</article>
326
+ </div>
327
+ <p class="section-note"><a href="${REPO}/blob/main/${esc(pkg.path)}/SKILL.md">Read the full SKILL.md</a> <span>·</span> <a href="${REPO}/tree/main/${esc(pkg.path)}">Browse package files</a></p>
250
328
  </section>
251
329
 
252
330
  ${
253
331
  (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>`
332
+ ? `<section class="section compact-section"><p class="eyebrow">Tags</p><ul class="tags detail-tags">${pkg.tags.map((tag) => `<li><a href="/?q=${encodeURIComponent(tag)}#catalog">${esc(tag)}</a></li>`).join("")}</ul></section>`
258
333
  : ""
259
334
  }
260
335
 
261
- <nav class="pager" aria-label="Catalog">
262
- <a href="/skills/${esc(prev.name)}/">&larr; ${esc(titleCase(prev.name))}</a>
263
- <a href="/skills/${esc(next.name)}/">${esc(titleCase(next.name))} &rarr;</a>
336
+ <nav class="pager" aria-label="Skill catalog">
337
+ <a href="/skills/${esc(prev.name)}/"><span>Previous</span>${esc(titleCase(prev.name))}</a>
338
+ <a href="/skills/${esc(next.name)}/"><span>Next</span>${esc(titleCase(next.name))}</a>
264
339
  </nav>`;
265
340
 
266
341
  return layout({
267
- title: `${label}, agent skills`,
342
+ title: `${label} | Agent Skills`,
268
343
  description: pkg.description,
269
344
  canonical: `${SITE_URL}/skills/${pkg.name}/`,
270
345
  body,
346
+ pageType: "skill",
271
347
  });
272
348
  };
273
349
 
274
- /* ------------------------------------------------------------------ main */
275
-
276
350
  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"/>
351
+ <rect width="32" height="32" rx="8" fill="#365b4a"/>
352
+ <path d="M9 10h14M9 16h14M9 22h9" fill="none" stroke="#f5f7f4" stroke-width="2.5" stroke-linecap="round"/>
281
353
  </svg>
282
354
  `;
283
355
 
284
356
  const main = async () => {
285
357
  const catalog = loadCatalog(join(root, "catalog", "skills.json"));
286
-
287
- const packages = [...catalog.skills].sort((a, b) =>
288
- a.name.localeCompare(b.name),
289
- );
358
+ const packages = [...catalog.skills].sort((a, b) => a.name.localeCompare(b.name));
290
359
 
291
360
  for (const pkg of packages) {
292
361
  const dir = join(root, pkg.path);
293
362
  const front = await readFrontmatter(join(dir, "SKILL.md"));
294
- // The catalog description is the source of truth; frontmatter only fills
295
- // the longer detail-page blurb when it is genuinely richer.
296
363
  if (front.description && front.description.length > pkg.description.length) {
297
364
  pkg.longDescription = front.description;
298
365
  }
@@ -302,8 +369,7 @@ const main = async () => {
302
369
  const tally = (getValues) => {
303
370
  const counts = new Map();
304
371
  for (const pkg of packages) {
305
- const values = getValues(pkg);
306
- for (const value of values) counts.set(value, (counts.get(value) || 0) + 1);
372
+ for (const value of getValues(pkg)) counts.set(value, (counts.get(value) || 0) + 1);
307
373
  }
308
374
  return [...counts]
309
375
  .map(([name, count]) => ({ name, count }))
@@ -312,28 +378,22 @@ const main = async () => {
312
378
 
313
379
  const categories = tally((pkg) => [pkg.plugin.name]);
314
380
  const adapters = tally((pkg) => pkg.adapters || []);
381
+ const plugins = catalog.plugins.map((plugin) => ({
382
+ name: plugin.name,
383
+ description: plugin.description,
384
+ count: plugin.skills.length,
385
+ }));
315
386
 
316
387
  await rm(out, { recursive: true, force: true });
317
388
  await mkdir(join(out, "assets"), { recursive: true });
318
-
319
- await copyFile(
320
- join(here, "assets", "paperbag.css"),
321
- join(out, "assets", "paperbag.css"),
322
- );
323
- await copyFile(
324
- join(here, "assets", "paperbag.js"),
325
- join(out, "assets", "paperbag.js"),
326
- );
389
+ await copyFile(join(here, "assets", "catalog.css"), join(out, "assets", "catalog.css"));
390
+ await copyFile(join(here, "assets", "catalog.js"), join(out, "assets", "catalog.js"));
327
391
  await writeFile(join(out, "assets", "favicon.svg"), favicon);
392
+ await writeFile(join(out, "index.html"), indexPage({ packages, plugins, categories, adapters }));
328
393
 
329
- await writeFile(
330
- join(out, "index.html"),
331
- indexPage({ packages, categories, adapters }),
332
- );
333
-
334
- for (const [i, pkg] of packages.entries()) {
335
- const prev = packages[(i - 1 + packages.length) % packages.length];
336
- const next = packages[(i + 1) % packages.length];
394
+ for (const [index, pkg] of packages.entries()) {
395
+ const prev = packages[(index - 1 + packages.length) % packages.length];
396
+ const next = packages[(index + 1) % packages.length];
337
397
  const dir = join(out, "skills", pkg.name);
338
398
  await mkdir(dir, { recursive: true });
339
399
  await writeFile(join(dir, "index.html"), detailPage(pkg, prev, next));
@@ -342,34 +402,26 @@ const main = async () => {
342
402
  await writeFile(
343
403
  join(out, "404.html"),
344
404
  layout({
345
- title: "Not in the bag, agent skills",
405
+ title: "Page not found | Agent Skills",
346
406
  description: "That page does not exist.",
347
407
  canonical: `${SITE_URL}/404.html`,
348
- body: `<section class="hero">
349
- <p class="kicker">404</p>
350
- <h1 data-reactive>Not in the bag</h1>
351
- <p class="lede">Whatever you reached for is not here. <a href="/">Go back to the catalog</a>.</p>
352
- </section>`,
408
+ pageType: "not-found",
409
+ body: `<section class="not-found"><p class="eyebrow">404</p><h1>That skill is not here.</h1><p>Return to the catalog to find another workflow.</p><a class="button button-primary" href="/">Browse skills</a></section>`,
353
410
  }),
354
411
  );
355
412
 
356
- const urls = [`${SITE_URL}/`, ...packages.map((p) => `${SITE_URL}/skills/${p.name}/`)];
413
+ const urls = [`${SITE_URL}/`, ...packages.map((pkg) => `${SITE_URL}/skills/${pkg.name}/`)];
357
414
  await writeFile(
358
415
  join(out, "sitemap.xml"),
359
416
  `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls
360
- .map((u) => ` <url><loc>${u}</loc></url>`)
417
+ .map((url) => ` <url><loc>${url}</loc></url>`)
361
418
  .join("\n")}\n</urlset>\n`,
362
419
  );
363
- await writeFile(
364
- join(out, "robots.txt"),
365
- `User-agent: *\nAllow: /\nSitemap: ${SITE_URL}/sitemap.xml\n`,
366
- );
420
+ await writeFile(join(out, "robots.txt"), `User-agent: *\nAllow: /\nSitemap: ${SITE_URL}/sitemap.xml\n`);
367
421
  await writeFile(join(out, "CNAME"), `${CNAME}\n`);
368
422
  await writeFile(join(out, ".nojekyll"), "");
369
423
 
370
- console.log(
371
- `site: ${packages.length} skills, ${categories.length} categories, ${adapters.length} adapters -> _site/`,
372
- );
424
+ console.log(`site: ${packages.length} skills, ${categories.length} categories, ${adapters.length} adapters -> _site/`);
373
425
  };
374
426
 
375
427
  await main();