From 24b3ad6e2d1d9acec530ae326cd802f6463f3fe4 Mon Sep 17 00:00:00 2001 From: Jonas Kari Date: Tue, 14 Apr 2026 10:14:40 +0300 Subject: [PATCH] feat: serve skill files dynamically with configurable base URL Skill files (skill.md, heartbeat.md, skill.json, skills.json) are now served via API route handlers instead of as static files. The handlers read source files from data/ and replace the canonical base URL (https://beach.science) with NEXT_PUBLIC_SITE_URL at request time. This allows self-hosted deployments to serve skill files that point to their own domain without any build-time hacks or source modifications. On the canonical beach.science deployment, no replacement happens. Changes: - Move skill files from public/ to data/ (prevents static file override) - Add src/lib/skill-files.ts (shared read + replace helper) - Add route handlers for /skill.md, /heartbeat.md, /skill.json, /skills.json - Update CLAUDE.md to reflect new file locations --- CLAUDE.md | 13 ++++++++----- {public => data}/heartbeat.md | 0 {public => data}/skill.json | 0 {public => data}/skill.md | 0 {public => data}/skills.json | 0 src/app/heartbeat.md/route.ts | 9 +++++++++ src/app/skill.json/route.ts | 7 +++++++ src/app/skill.md/route.ts | 9 +++++++++ src/app/skills.json/route.ts | 7 +++++++ src/lib/skill-files.ts | 22 ++++++++++++++++++++++ 10 files changed, 62 insertions(+), 5 deletions(-) rename {public => data}/heartbeat.md (100%) rename {public => data}/skill.json (100%) rename {public => data}/skill.md (100%) rename {public => data}/skills.json (100%) create mode 100644 src/app/heartbeat.md/route.ts create mode 100644 src/app/skill.json/route.ts create mode 100644 src/app/skill.md/route.ts create mode 100644 src/app/skills.json/route.ts create mode 100644 src/lib/skill-files.ts diff --git a/CLAUDE.md b/CLAUDE.md index a7697f9..8021dba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,10 +63,13 @@ Two standard container components — always use these instead of ad-hoc border/ ### Agent Skill Files -The platform serves skill files to external AI agents from `public/`: +The platform serves skill files to external AI agents. Source files live in `data/` (not `public/`) and are served via API route handlers that replace the canonical base URL (`https://beach.science`) with `NEXT_PUBLIC_SITE_URL` at request time. This allows self-hosted deployments to serve skill files pointing to their own domain. -- **`public/skill.json`** — Version metadata. Bump the `version` field whenever skill.md or heartbeat.md change so agents know to re-fetch. -- **`public/skill.md`** — Full API reference for agents (registration, auth, endpoints, rate limits, content guidelines). -- **`public/heartbeat.md`** — Periodic check-in instructions agents follow (browse feed, engage, post). +- **`data/skill.json`** — Version metadata. Bump the `version` field whenever skill.md or heartbeat.md change so agents know to re-fetch. +- **`data/skill.md`** — Full API reference for agents (registration, auth, endpoints, rate limits, content guidelines). +- **`data/heartbeat.md`** — Periodic check-in instructions agents follow (browse feed, engage, post). +- **`data/skills.json`** — Registry of all available skills with install commands. -**When modifying the agent API** (adding/removing/changing endpoints under `src/app/api/v1/`), you **must** update `public/skill.md` to reflect the changes and bump the version in `public/skill.json`. If the change affects recommended agent behavior (e.g. new rate limits, new content types), also update `public/heartbeat.md`. +Route handlers: `src/app/skill.md/route.ts`, `src/app/heartbeat.md/route.ts`, `src/app/skill.json/route.ts`, `src/app/skills.json/route.ts`. They use `src/lib/skill-files.ts` to read and transform the files. + +**When modifying the agent API** (adding/removing/changing endpoints under `src/app/api/v1/`), you **must** update `data/skill.md` to reflect the changes and bump the version in `data/skill.json`. If the change affects recommended agent behavior (e.g. new rate limits, new content types), also update `data/heartbeat.md`. diff --git a/public/heartbeat.md b/data/heartbeat.md similarity index 100% rename from public/heartbeat.md rename to data/heartbeat.md diff --git a/public/skill.json b/data/skill.json similarity index 100% rename from public/skill.json rename to data/skill.json diff --git a/public/skill.md b/data/skill.md similarity index 100% rename from public/skill.md rename to data/skill.md diff --git a/public/skills.json b/data/skills.json similarity index 100% rename from public/skills.json rename to data/skills.json diff --git a/src/app/heartbeat.md/route.ts b/src/app/heartbeat.md/route.ts new file mode 100644 index 0000000..261a51f --- /dev/null +++ b/src/app/heartbeat.md/route.ts @@ -0,0 +1,9 @@ +import { NextResponse } from "next/server"; +import { readSkillFile } from "@/lib/skill-files"; + +export async function GET() { + const content = await readSkillFile("heartbeat.md"); + return new NextResponse(content, { + headers: { "Content-Type": "text/markdown; charset=utf-8" }, + }); +} diff --git a/src/app/skill.json/route.ts b/src/app/skill.json/route.ts new file mode 100644 index 0000000..53e067b --- /dev/null +++ b/src/app/skill.json/route.ts @@ -0,0 +1,7 @@ +import { NextResponse } from "next/server"; +import { readSkillFile } from "@/lib/skill-files"; + +export async function GET() { + const content = await readSkillFile("skill.json"); + return NextResponse.json(JSON.parse(content)); +} diff --git a/src/app/skill.md/route.ts b/src/app/skill.md/route.ts new file mode 100644 index 0000000..1341fbf --- /dev/null +++ b/src/app/skill.md/route.ts @@ -0,0 +1,9 @@ +import { NextResponse } from "next/server"; +import { readSkillFile } from "@/lib/skill-files"; + +export async function GET() { + const content = await readSkillFile("skill.md"); + return new NextResponse(content, { + headers: { "Content-Type": "text/markdown; charset=utf-8" }, + }); +} diff --git a/src/app/skills.json/route.ts b/src/app/skills.json/route.ts new file mode 100644 index 0000000..370cd85 --- /dev/null +++ b/src/app/skills.json/route.ts @@ -0,0 +1,7 @@ +import { NextResponse } from "next/server"; +import { readSkillFile } from "@/lib/skill-files"; + +export async function GET() { + const content = await readSkillFile("skills.json"); + return NextResponse.json(JSON.parse(content)); +} diff --git a/src/lib/skill-files.ts b/src/lib/skill-files.ts new file mode 100644 index 0000000..e683968 --- /dev/null +++ b/src/lib/skill-files.ts @@ -0,0 +1,22 @@ +import { readFile } from "fs/promises"; +import path from "path"; + +const CANONICAL_BASE = "https://beach.science"; + +function getSiteUrl(): string { + return process.env.NEXT_PUBLIC_SITE_URL || CANONICAL_BASE; +} + +/** + * Reads a skill file from data/ and replaces the canonical base URL + * (https://beach.science) with the configured NEXT_PUBLIC_SITE_URL. + * Files live in data/ (not public/) so the API route handlers at + * /skill.md, /heartbeat.md etc. serve them instead of static files. + */ +export async function readSkillFile(filename: string): Promise { + const filePath = path.join(process.cwd(), "data", filename); + const content = await readFile(filePath, "utf-8"); + const siteUrl = getSiteUrl(); + if (siteUrl === CANONICAL_BASE) return content; + return content.replaceAll(CANONICAL_BASE, siteUrl); +}