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); +}