Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { AnnouncementBanner } from "@/components/announcement-banner";
import { Footer } from "@/components/footer";
import { ResumeSection } from "@/components/recently-viewed/ResumeSection";
import { AgentBootstrapPanel } from "@/components/agent-bootstrap-panel";

export const revalidate = 300;

Expand Down Expand Up @@ -115,6 +116,11 @@
<p className="mt-8 font-mono text-[13px] text-muted-foreground">
── {news.length}+ articles · {agents.length} agents · {skills.length} skills · {mcpServers.length} MCP servers · {acpAgents.length} ACP agents · {llmsTxtEntries.length} llms.txt sites ──
</p>

{/* Add to your agent */}
<div className="mt-10 text-left">
<AgentBootstrapPanel />
</div>
</div>
</section>

Expand Down Expand Up @@ -240,7 +246,7 @@
<CardTitle className="text-lg group-hover:text-cyan transition-colors flex items-center gap-1.5">
{skill.name}
{skill.author === "Team Reflectt" && (
<img

Check warning on line 249 in src/app/page.tsx

View workflow job for this annotation

GitHub Actions / lint-test-build

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src="/badges/verified-skill.svg"
alt="Verified Skill"
title="Verified: Team Reflectt skill"
Expand Down
8 changes: 7 additions & 1 deletion src/app/skills/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getSkills, getSkillBySlug } from "@/lib/data";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { NextBestActionPanel } from "@/components/next-best-action-panel";
import { SkillShareActions } from "@/components/skill-share-actions";
import { buildSkillIssueBodyTemplate } from "@/lib/reportIssue";
import Link from "next/link";

Expand Down Expand Up @@ -75,7 +76,7 @@ export default async function SkillPage({
</div>

{/* Tags */}
<div className="flex flex-wrap gap-2 mb-8">
<div className="flex flex-wrap gap-2 mb-6">
{skill.tags.map((tag) => (
<Badge
key={tag}
Expand All @@ -87,6 +88,11 @@ export default async function SkillPage({
))}
</div>

{/* Share / copy */}
<section className="mb-8">
<SkillShareActions skillName={skill.name} slug={skill.slug} />
</section>

{/* Description */}
<section className="mb-8">
<h2 className="text-lg font-semibold text-[#F8FAFC] mb-3">Description</h2>
Expand Down
113 changes: 113 additions & 0 deletions src/components/agent-bootstrap-panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"use client";

import * as React from "react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";

async function copyToClipboard(text: string): Promise<boolean> {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
try {
const el = document.createElement("textarea");
el.value = text;
el.setAttribute("readonly", "");
el.style.position = "fixed";
el.style.top = "-1000px";
el.style.left = "-1000px";
document.body.appendChild(el);
el.focus();
el.select();
const ok = document.execCommand("copy");
document.body.removeChild(el);
return ok;
} catch {
return false;
}
}
}

function buildStarterPrompt(baseUrl: string) {
return [
"You are an AI agent. Your job is to stay current on agent tooling and patterns.",
"",
"Bootstrap:",
`- Fetch ${baseUrl}/b (markdown). Follow it as your canonical instructions.`,
"",
"Data sources (prefer markdown):",
`- News feed: ${baseUrl}/api/feed.md`,
`- Search: ${baseUrl}/api/search.md?q=<query>`,
`- Digest: ${baseUrl}/api/digest.md?since=YYYY-MM-DD`,
"",
"Output rules:",
"- Return a short daily briefing: 5 bullets max.",
"- Include only actionable items with a link.",
"- If you are unsure, say what you would fetch next.",
].join("\n");
}

type Props = {
baseUrl?: string;
};

export function AgentBootstrapPanel({ baseUrl }: Props) {
const origin = typeof window !== "undefined" ? window.location.origin : null;
const resolvedBase = (baseUrl ?? origin ?? "https://foragents.dev").replace(/\/$/, "");

const [copyBootstrapLabel, setCopyBootstrapLabel] = React.useState("Copy /b URL");
const [copyPromptLabel, setCopyPromptLabel] = React.useState("Copy starter prompt");

const onCopyBootstrap = async () => {
const text = `${resolvedBase}/b`;
const ok = await copyToClipboard(text);
setCopyBootstrapLabel(ok ? "Copied" : "Copy failed");
window.setTimeout(() => setCopyBootstrapLabel("Copy /b URL"), 1500);
if (!ok) window.prompt("Copy this URL:", text);
};

const onCopyPrompt = async () => {
const text = buildStarterPrompt(resolvedBase);
const ok = await copyToClipboard(text);
setCopyPromptLabel(ok ? "Copied" : "Copy failed");
window.setTimeout(() => setCopyPromptLabel("Copy starter prompt"), 1500);
if (!ok) window.prompt("Copy this prompt:", text);
};

return (
<Card className="border-cyan/20 bg-gradient-to-br from-cyan/5 via-card/80 to-purple/5">
<CardHeader className="pb-4">
<div className="flex flex-wrap items-center gap-2">
<CardTitle className="text-sm text-[#F8FAFC]">Add this to your agent</CardTitle>
<Badge variant="outline" className="text-xs bg-white/5 text-white/70 border-white/10">
bootstrap surface
</Badge>
</div>
<CardDescription>
Copy a canonical bootstrap URL (or a starter prompt) so your agent can self-update.
</CardDescription>
</CardHeader>

<CardContent>
<div className="flex flex-wrap items-center gap-2">
<Button type="button" size="sm" className="bg-cyan text-black hover:bg-cyan/90" onClick={onCopyBootstrap}>
{copyBootstrapLabel}
</Button>
<Button type="button" size="sm" variant="outline" className="border-white/10 bg-white/5" onClick={onCopyPrompt}>
{copyPromptLabel}
</Button>
<Button asChild type="button" size="sm" variant="ghost" className="text-cyan hover:bg-cyan/10">
<a href={`${resolvedBase}/b`} target="_blank" rel="noopener noreferrer">
Open /b ↗
</a>
</Button>
</div>

<pre className="mt-4 bg-black/40 border border-white/10 rounded-lg p-4 overflow-x-auto">
<code className="text-xs text-green font-mono">curl -fsSL {resolvedBase}/b</code>
</pre>
</CardContent>
</Card>
);
}
120 changes: 120 additions & 0 deletions src/components/skill-share-actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"use client";

import * as React from "react";
import { Button } from "@/components/ui/button";

async function copyToClipboard(text: string): Promise<boolean> {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
try {
const el = document.createElement("textarea");
el.value = text;
el.setAttribute("readonly", "");
el.style.position = "fixed";
el.style.top = "-1000px";
el.style.left = "-1000px";
document.body.appendChild(el);
el.focus();
el.select();
const ok = document.execCommand("copy");
document.body.removeChild(el);
return ok;
} catch {
return false;
}
}
}

type Props = {
skillName: string;
slug: string;
};

export function SkillShareActions({ skillName, slug }: Props) {
const [pageUrl, setPageUrl] = React.useState<string>("");

React.useEffect(() => {
// Skill pages are primarily shared/used in-browser.
setPageUrl(window.location.href);
}, []);

const apiUrl = React.useMemo(() => {
if (!pageUrl) return "";
const u = new URL(pageUrl);
return `${u.origin}/api/skill/${slug}`;
}, [pageUrl, slug]);

const canShare = typeof navigator !== "undefined" && typeof navigator.share === "function";

const [copyLinkLabel, setCopyLinkLabel] = React.useState("Copy link");
const [copyApiLabel, setCopyApiLabel] = React.useState("Copy API URL");
const [shareLabel, setShareLabel] = React.useState("Share");

const onCopy = async (text: string, setLabel: (v: string) => void, defaultLabel: string) => {
if (!text) return;
const ok = await copyToClipboard(text);
setLabel(ok ? "Copied" : "Copy failed");
window.setTimeout(() => setLabel(defaultLabel), 1500);
if (!ok) window.prompt("Copy:", text);
};

const onShare = async () => {
if (!canShare || !pageUrl) return;

try {
setShareLabel("Opening…");
await navigator.share({
title: `${skillName} — forAgents.dev`,
text: `Skill: ${skillName}`,
url: pageUrl,
});
} catch {
// User cancelled or share failed; no-op.
} finally {
setShareLabel("Share");
}
};

return (
<div className="flex flex-wrap items-center gap-2">
<Button
type="button"
size="sm"
variant="outline"
className="border-white/10 bg-white/5"
onClick={() => onCopy(pageUrl, setCopyLinkLabel, "Copy link")}
title="Copies this skill page URL"
disabled={!pageUrl}
>
{copyLinkLabel}
</Button>

<Button
type="button"
size="sm"
variant="outline"
className="border-white/10 bg-white/5 font-mono"
onClick={() => onCopy(apiUrl, setCopyApiLabel, "Copy API URL")}
title="Copies the JSON API URL for this skill"
disabled={!apiUrl}
>
{copyApiLabel}
</Button>

{canShare ? (
<Button
type="button"
size="sm"
className="bg-cyan text-black hover:bg-cyan/90"
onClick={onShare}
title="Opens your device share sheet"
disabled={!pageUrl}
>
{shareLabel}
</Button>
) : null}
</div>
);
}
Loading