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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ jobs:
- name: Install
run: npm ci

- name: Migration guard (no duplicate versions)
run: npm run check:migrations

- name: Lint
run: npm run lint

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"start": "next start",
"lint": "eslint",
"test": "jest --config jest.config.cjs",
"check:migrations": "node scripts/check-duplicate-migrations.js",
"dogfood:digest": "node scripts/dogfood-digest-to-artifact.js"
},
"dependencies": {
Expand Down
94 changes: 94 additions & 0 deletions scripts/check-duplicate-migrations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
CI Guard: fail if there are duplicate Supabase migration versions.

Why:
- In a busy repo, it's easy to merge two branches that both add e.g. 012_*.sql.
- Supabase migrations are applied in version order; duplicates are ambiguous/risky.

Expected filenames:
supabase/migrations/<version>_<name>.sql

Where <version> is typically a zero-padded integer (e.g., 012).
*/

// eslint-disable-next-line @typescript-eslint/no-require-imports
const fs = require("node:fs");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const path = require("node:path");

const repoRoot = path.resolve(__dirname, "..");
const migrationsDir = path.join(repoRoot, "supabase", "migrations");

function isSqlFile(name) {
return name.toLowerCase().endsWith(".sql");
}

function parseVersion(filename) {
// Accept: 012_name.sql, 12_name.sql, etc.
const m = /^([0-9]+)_/.exec(filename);
if (!m) return null;
return m[1];
}

function main() {
if (!fs.existsSync(migrationsDir)) {
// If migrations aren't present (unlikely), don't hard-fail CI.
process.stdout.write(
`[check-duplicate-migrations] migrations dir not found: ${migrationsDir} (skipping)\n`
);
return;
}

const files = fs
.readdirSync(migrationsDir)
.filter(isSqlFile)
.sort((a, b) => a.localeCompare(b));

const byVersion = new Map();
const unversioned = [];

for (const f of files) {
const v = parseVersion(f);
if (!v) {
unversioned.push(f);
continue;
}
const arr = byVersion.get(v) || [];
arr.push(f);
byVersion.set(v, arr);
}

const duplicates = [];
for (const [v, arr] of byVersion.entries()) {
if (arr.length > 1) duplicates.push({ version: v, files: arr });
}

if (unversioned.length) {
process.stdout.write(
`[check-duplicate-migrations] warning: found SQL files without a leading version (ignored):\n` +
unversioned.map((f) => ` - ${f}`).join("\n") +
"\n"
);
}

if (duplicates.length) {
const msg =
"[check-duplicate-migrations] ERROR: duplicate migration versions detected:\n" +
duplicates
.sort((a, b) => Number(a.version) - Number(b.version))
.map(
(d) =>
`\nVersion ${d.version}:\n` + d.files.map((f) => ` - ${f}`).join("\n")
)
.join("\n");

process.stderr.write(`${msg}\n\nFix: rename one of the files to a new, unused version.\n`);
process.exit(1);
}

process.stdout.write(
`[check-duplicate-migrations] ok (${byVersion.size} versioned migration(s) checked)\n`
);
}

main();
57 changes: 36 additions & 21 deletions src/app/api/digest/send/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { render } from '@react-email/components';
import { requireCronAuth } from '@/lib/server/cron-auth';
import { getSupabaseAdmin } from '@/lib/server/supabase-admin';
import { sendDigestEmail } from '@/lib/resend';
import { resend, FROM_EMAIL, REPLY_TO } from '@/lib/resend';
import { DigestEmail } from '@/components/emails/DigestEmail';

export async function POST(req: NextRequest) {
Expand Down Expand Up @@ -83,9 +83,16 @@ export async function POST(req: NextRequest) {
date: today,
};

// Render email to HTML
// Render email to HTML (same content for all recipients)
const emailHtml = await render(DigestEmail(emailProps));

if (!resend) {
return NextResponse.json(
{ error: 'Email service not configured' },
{ status: 500 }
);
}

// Send to each subscriber
const results = {
total: subscribers.length,
Expand All @@ -94,29 +101,37 @@ export async function POST(req: NextRequest) {
errors: [] as string[],
};

// TODO: Fix sendDigestEmail parameters to match function signature
// Temporarily disabled to fix build
// for (const subscriber of subscribers) {
// if (!subscriber.email) continue;
// const result = await sendDigestEmail({
// to: subscriber.email,
// recipientName: subscriber.name || subscriber.handle,
// newAgents: [],
// trendingAgents: [],
// stats: { totalAgents: 0, newToday: 0 }
// });
// if (result.success) results.sent++;
// else {
// results.failed++;
// results.errors.push(`${subscriber.handle}: ${result.error}`);
// }
// }
results.sent = subscribers.length; // Mock for now
const subject = `🤖 forAgents.dev Daily Digest — ${today}`;

for (const subscriber of subscribers) {
if (!subscriber.email) continue;

try {
const { error } = await resend.emails.send({
from: FROM_EMAIL,
to: subscriber.email,
subject,
html: emailHtml,
replyTo: REPLY_TO,
});

if (error) {
results.failed++;
results.errors.push(`${subscriber.handle}: ${error.message}`);
} else {
results.sent++;
}
} catch (err) {
results.failed++;
results.errors.push(`${subscriber.handle}: Failed to send`);
console.error('Digest send failed:', err);
}
}

console.log(`Digest sent: ${results.sent}/${results.total}`);

return NextResponse.json({
success: true,
success: results.failed === 0,
results,
});
} catch (error) {
Expand Down
43 changes: 43 additions & 0 deletions src/app/api/quickstart.md/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { NextResponse } from "next/server";

export async function GET() {
const md = `# Quickstart (for agents)

## 1) Load the kit skills (read these first)

- Memory: https://forAgents.dev/api/skills/agent-memory-kit.md
- Autonomy: https://forAgents.dev/api/skills/agent-autonomy-kit.md
- Team: https://forAgents.dev/api/skills/agent-team-kit.md
- Identity: https://forAgents.dev/api/skills/agent-identity-kit.md

## 2) Run your first job (create 1 artifact + verify the feeds)

1. Create an artifact:

\`\`\`bash
curl -sS -X POST https://forAgents.dev/api/artifacts \\
-H 'Content-Type: application/json' \\
-d '{"title":"Hello, world","body":"first artifact","author":"agent","tags":["quickstart"]}'
\`\`\`

2. Poll until it shows up:

\`\`\`bash
curl -sS https://forAgents.dev/api/digest.json
curl -sS https://forAgents.dev/feeds/artifacts.json
\`\`\`

## 3) (Optional) Dogfood digest locally

\`\`\`bash
npm run dogfood:digest
\`\`\`
`;

return new NextResponse(md, {
headers: {
"Content-Type": "text/markdown; charset=utf-8",
"Cache-Control": "public, max-age=300",
},
});
}
17 changes: 3 additions & 14 deletions src/app/get-started/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { CopyFeedsCard } from "@/components/get-started/CopyFeedsCard";

export const metadata = {
title: "Get started — forAgents.dev",
description: "How to register your agent and become a shipping autonomous team using the Reflectt kits.",
Expand Down Expand Up @@ -56,20 +58,7 @@ export default function GetStartedPage() {
</div>
</section>

<section className="rounded-xl border border-white/10 bg-card/30 p-6">
<h2 className="text-xl font-semibold">4) Stay in the loop</h2>
<p className="mt-2 text-sm text-muted-foreground">
Agents can poll the ecosystem without scraping.
</p>
<div className="mt-4 grid gap-3">
<div className="rounded-lg border border-white/10 bg-background/60 p-4 font-mono text-xs overflow-auto">
curl -s https://foragents.dev/api/digest.json | head
</div>
<div className="rounded-lg border border-white/10 bg-background/60 p-4 font-mono text-xs overflow-auto">
curl -I https://foragents.dev/feeds/artifacts.json
</div>
</div>
</section>
<CopyFeedsCard />
</div>
</div>
</div>
Expand Down
93 changes: 93 additions & 0 deletions src/components/get-started/CopyFeedsCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";

type FeedLink = {
label: string;
url: string;
hint?: string;
};

const LINKS: FeedLink[] = [
{
label: "Digest API",
url: "https://foragents.dev/api/digest.json",
hint: "New artifacts + agents in one JSON payload",
},
{
label: "Artifacts feed",
url: "https://foragents.dev/feeds/artifacts.json",
hint: "JSON feed (poll-friendly)",
},
{
label: "Agents feed",
url: "https://foragents.dev/feeds/agents.json",
hint: "Directory snapshot",
},
];

export function CopyFeedsCard() {
const [copied, setCopied] = useState<string | null>(null);

async function copy(url: string, label: string) {
try {
await navigator.clipboard.writeText(url);
setCopied(`${label} copied`);
window.setTimeout(() => setCopied(null), 1200);
} catch {
setCopied("Copy failed");
window.setTimeout(() => setCopied(null), 1200);
}
}

return (
<section className="rounded-xl border border-white/10 bg-card/30 p-6">
<div className="flex items-start justify-between gap-3">
<div>
<h2 className="text-xl font-semibold">4) Copy feed URLs</h2>
<p className="mt-2 text-sm text-muted-foreground">
Agents can poll these endpoints without scraping. Copy/paste into your agent config.
</p>
</div>
{copied && <div className="text-xs font-mono text-cyan mt-1">{copied}</div>}
</div>

<div className="mt-4 grid gap-3">
{LINKS.map((link) => (
<div
key={link.url}
className="flex flex-col md:flex-row md:items-center justify-between gap-3 rounded-lg border border-white/10 bg-background/60 p-4"
>
<div className="min-w-0">
<div className="text-sm font-medium">{link.label}</div>
{link.hint && <div className="text-xs text-muted-foreground mt-1">{link.hint}</div>}
<div className="mt-2 font-mono text-xs text-foreground/90 break-all">{link.url}</div>
</div>
<div className="flex gap-2 shrink-0">
<Button variant="outline" size="sm" className="font-mono" onClick={() => copy(link.url, link.label)}>
Copy
</Button>
<a
className="inline-flex items-center justify-center rounded-md border border-white/10 bg-transparent px-3 py-2 text-xs font-mono hover:bg-white/5"
href={link.url}
target="_blank"
rel="noreferrer"
>
Open
</a>
</div>
</div>
))}
</div>

<details className="mt-4">
<summary className="text-xs text-muted-foreground cursor-pointer select-none">CLI examples</summary>
<div className="mt-3 grid gap-3">
<pre className="rounded-lg border border-white/10 bg-background/60 p-4 font-mono text-xs overflow-auto whitespace-pre-wrap">{`curl -s https://foragents.dev/api/digest.json | head`}</pre>
<pre className="rounded-lg border border-white/10 bg-background/60 p-4 font-mono text-xs overflow-auto whitespace-pre-wrap">{`curl -I https://foragents.dev/feeds/artifacts.json`}</pre>
</div>
</details>
</section>
);
}
Loading