Havre: the skill where agents look (.well-known/skills, the alternate link, robots) (#129)

Co-authored-by: Time <time@users.noreply.teahub.xyz>
This commit was merged in pull request #129.
This commit is contained in:
time authored and time committed 2026-10-18 07:07:39 +00:00
1 parent ce258536ed
commit 35eb92828a
5 files changed
+59

No files matched your search

@@ -0,0 +1,11 @@
{
"$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
"skills": [
{
"name": "havre",
"type": "skill-md",
"description": "Write AI characters as Havre cards (.havre.json), export them as Character Card V3 for SillyTavern-style apps, talk to them through Venice's API, and remix someone else's character into an original one, with credit.",
"url": "/skill.md"
}
]
}
+1
View File
@@ -19,6 +19,7 @@
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
<link rel="icon" href="favicon.svg" type="image/svg+xml"> <link rel="icon" href="favicon.svg" type="image/svg+xml">
<link rel="alternate" type="text/plain" href="llms.txt" title="For agents"> <link rel="alternate" type="text/plain" href="llms.txt" title="For agents">
<link rel="alternate" type="text/markdown" href="skill.md" title="Skill">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<script type="module" src="app.js"></script> <script type="module" src="app.js"></script>
</head> </head>
+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://havre.yolopreneur.lol/sitemap.txt
+37
View File
@@ -0,0 +1,37 @@
// node --test tests/*.test.mjs: where agents look: .well-known/skills/index.json, the alternate links, robots.txt
import { test } from "node:test";
import assert from "node:assert/strict";
import { existsSync, readFileSync } from "node:fs";
const root = new URL("../", import.meta.url);
const read = (p) => readFileSync(new URL(p, root), "utf8");
test("the skills index parses and points at a skill file that exists, with the name its front matter gives", () => {
const index = JSON.parse(read(".well-known/skills/index.json"));
assert.ok(Array.isArray(index.skills) && index.skills.length >= 1, "at least one skill");
for (const s of index.skills) {
assert.ok(s.name && s.description, "a name and a description");
assert.equal(s.type, "skill-md");
assert.match(s.url, /^\/[^/]/, "a path from the site root");
assert.ok(existsSync(new URL("." + s.url, root)), `${s.url} is served from the site root`);
}
const havre = index.skills.find((s) => s.name === "havre");
assert.ok(havre, "Havre's skill is listed");
assert.equal(havre.url, "/skill.md");
assert.match(read("skill.md"), /^---\nname: havre\n/, "skill.md's own front matter agrees on the name");
});
test("index.html carries the alternate links to llms.txt and skill.md, in the head", () => {
const head = read("index.html").split("</head>")[0];
assert.match(head, /<link rel="alternate" type="text\/plain" href="llms\.txt" title="For agents">/);
assert.match(head, /<link rel="alternate" type="text\/markdown" href="skill\.md" title="Skill">/);
for (const href of ["llms.txt", "skill.md"]) assert.ok(existsSync(new URL(href, root)), `${href} exists`);
});
test("robots.txt allows everything and names the sitemap", () => {
const robots = read("robots.txt");
assert.match(robots, /^User-agent: \*$/m);
assert.match(robots, /^Allow: \/$/m);
assert.doesNotMatch(robots, /^Disallow: *\S/m, "nothing is shut out");
assert.match(robots, /^Sitemap: https:\/\/havre\.yolopreneur\.lol\/sitemap\.txt$/m);
});
+6
View File
@@ -14,6 +14,12 @@ cd "$(dirname "$0")/.."
# this list.) # this list.)
rm -rf art fonts tools/paint.py tools/draw.py tools/vendor tests/smoke.py rm -rf art fonts tools/paint.py tools/draw.py tools/vendor tests/smoke.py
# what agents look for stays: .well-known/ (the skills index) and robots.txt are served as they are, so the build
# never clears them, and fails if they're gone
for f in .well-known/skills/index.json robots.txt; do
[ -f "$f" ] || { echo "tools/build.sh: $f is missing: agents look for it" >&2; exit 1; }
done
node tools/cast.mjs # cast/index.js, the harbor's own as the module the page imports, from cast/*.havre.json node tools/cast.mjs # cast/index.js, the harbor's own as the module the page imports, from cast/*.havre.json
node tools/pages.mjs # c/ (a page and a V3 card for each of them, and the list) and sitemap.txt, from the same cards node tools/pages.mjs # c/ (a page and a V3 card for each of them, and the list) and sitemap.txt, from the same cards
node --test tests/*.test.mjs node --test tests/*.test.mjs