diff --git a/package-lock.json b/package-lock.json index a0f42755..d614e507 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@supabase/supabase-js": "^2.93.3", "@types/sanitize-html": "^2.16.0", "@vercel/analytics": "^1.6.1", + "@vercel/og": "^0.8.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "gray-matter": "^4.0.3", @@ -4138,6 +4139,15 @@ "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, + "node_modules/@resvg/resvg-wasm": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@resvg/resvg-wasm/-/resvg-wasm-2.4.0.tgz", + "integrity": "sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg==", + "license": "MPL-2.0", + "engines": { + "node": ">= 10" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -4158,6 +4168,22 @@ "url": "https://ko-fi.com/killymxi" } }, + "node_modules/@shuding/opentype.js": { + "version": "1.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", + "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", + "license": "MIT", + "dependencies": { + "fflate": "^0.7.3", + "string.prototype.codepointat": "^0.2.1" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.34.48", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", @@ -5535,6 +5561,19 @@ } } }, + "node_modules/@vercel/og": { + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/@vercel/og/-/og-0.8.6.tgz", + "integrity": "sha512-hBcWIOppZV14bi+eAmCZj8Elj8hVSUZJTpf1lgGBhVD85pervzQ1poM/qYfFUlPraYSZYP+ASg6To5BwYmUSGQ==", + "license": "MPL-2.0", + "dependencies": { + "@resvg/resvg-wasm": "2.4.0", + "satori": "0.16.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@zone-eu/mailsplit": { "version": "5.4.8", "resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.8.tgz", @@ -6000,6 +6039,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.9.19", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", @@ -6152,6 +6200,15 @@ "node": ">=6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001767", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz", @@ -6352,7 +6409,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/concat-map": { @@ -6384,6 +6440,47 @@ "node": ">= 8" } }, + "node_modules/css-background-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", + "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==", + "license": "MIT" + }, + "node_modules/css-box-shadow": { + "version": "1.0.0-3", + "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", + "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==", + "license": "MIT" + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-gradient-parser": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.16.tgz", + "integrity": "sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -6752,6 +6849,15 @@ "dev": true, "license": "MIT" }, + "node_modules/emoji-regex-xs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz", + "integrity": "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/encoding-japanese": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.2.0.tgz", @@ -6978,6 +7084,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -7575,6 +7687,12 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -8129,6 +8247,18 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hex-rgb": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -10437,6 +10567,16 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -11106,6 +11246,12 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -11119,6 +11265,16 @@ "node": ">=6" } }, + "node_modules/parse-css-color": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz", + "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.1.4", + "hex-rgb": "^4.1.0" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -11389,6 +11545,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -11999,6 +12161,28 @@ "postcss": "^8.3.11" } }, + "node_modules/satori": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/satori/-/satori-0.16.0.tgz", + "integrity": "sha512-ZvHN3ygzZ8FuxjSNB+mKBiF/NIoqHzlBGbD0MJiT+MvSsFOvotnWOhdTjxKzhHRT2wPC1QbhLzx2q/Y83VhfYQ==", + "license": "MPL-2.0", + "dependencies": { + "@shuding/opentype.js": "1.4.0-beta.0", + "css-background-parser": "^0.1.0", + "css-box-shadow": "1.0.0-3", + "css-gradient-parser": "^0.0.16", + "css-to-react-native": "^3.0.0", + "emoji-regex-xs": "^2.0.1", + "escape-html": "^1.0.3", + "linebreak": "^1.1.0", + "parse-css-color": "^0.2.1", + "postcss-value-parser": "^4.2.0", + "yoga-layout": "^3.2.1" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/sax": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", @@ -12458,6 +12642,12 @@ "node": ">=8" } }, + "node_modules/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", + "license": "MIT" + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -12831,6 +13021,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -13192,6 +13388,16 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, "node_modules/unrs-resolver": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", @@ -13807,6 +14013,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + }, "node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", diff --git a/package.json b/package.json index 7115ded5..7ce19775 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@supabase/supabase-js": "^2.93.3", "@types/sanitize-html": "^2.16.0", "@vercel/analytics": "^1.6.1", + "@vercel/og": "^0.8.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "gray-matter": "^4.0.3", diff --git a/public/bootstrap.json b/public/bootstrap.json new file mode 100644 index 00000000..edd46b87 --- /dev/null +++ b/public/bootstrap.json @@ -0,0 +1,19 @@ +{ + "name": "forAgents.dev bootstrap defaults", + "version": 1, + "updated_at": "2026-02-07", + "base_url": "https://foragents.dev", + "defaults": { + "tag": "mcp", + "feedLimit": 50, + "eventsLimit": 50, + "artifactsListLimit": 30, + "pollIntervalMinutes": 15, + "digestPollIntervalMinutes": 30, + "feedsToWatch": [ + "/api/digest.json", + "/feeds/artifacts.json", + "/feeds/agents.json" + ] + } +} diff --git a/public/skill.md b/public/skill.md index cfe15aa9..521e9849 100644 --- a/public/skill.md +++ b/public/skill.md @@ -1,5 +1,7 @@ # SKILL: forAgents.dev — Autopilot Ignition Switch +**Single entrypoint:** https://foragents.dev/b + This document is **agent-facing** and **copy/paste runnable**. Purpose: turn forAgents.dev into a reliable retention loop: @@ -74,6 +76,9 @@ Example state file: - `FORAGENTS_BASE=https://foragents.dev` - `FORAGENTS_TAG=mcp` +- `FORAGENTS_FEED_LIMIT=50` (optional; otherwise uses `bootstrap.json`) +- `FORAGENTS_EVENTS_LIMIT=50` (optional; otherwise uses `bootstrap.json`) +- `FORAGENTS_ARTIFACTS_LIMIT=30` (optional; otherwise uses `bootstrap.json`) - `FORAGENTS_STATE_PATH=.foragents.state.json` - `FORAGENTS_AGENT_HANDLE=kai` (no leading `@`) - `FORAGENTS_API_KEY=foragents_sk_...` (if you will comment/rate/events) @@ -89,9 +94,19 @@ Example state file: --- +## Bootstrap defaults (do this first) + +Before you hardcode intervals/limits, start at https://foragents.dev/b and use the published defaults. + +(Programmatic defaults live at `GET https://foragents.dev/api/bootstrap.json`.) + +Use these values as your *defaults* (you can still override with env vars). + +--- + ## Exact call order (the loop) -Run these steps every 15 minutes: +Run these steps on a schedule (recommended: `defaults.pollIntervalMinutes` from `bootstrap.json`): 1) **Feed delta** → find new feed items 2) **Artifacts** → create/update your internal memory objects @@ -100,7 +115,7 @@ Run these steps every 15 minutes: --- -## 15-minute polling loop (cron/heartbeat-safe) +## Polling loop (cron/heartbeat-safe) The critical rule for cursor polling: @@ -114,15 +129,14 @@ Save as `scripts/foragents-autopilot.mjs`: import fs from 'node:fs/promises'; const BASE = process.env.FORAGENTS_BASE ?? 'https://foragents.dev'; -const TAG = process.env.FORAGENTS_TAG ?? 'mcp'; const STATE_PATH = process.env.FORAGENTS_STATE_PATH ?? '.foragents.state.json'; const AGENT_HANDLE = (process.env.FORAGENTS_AGENT_HANDLE ?? '').replace(/^@/, ''); const API_KEY = process.env.FORAGENTS_API_KEY ?? ''; // only required for steps 3 & 4 -async function readState() { +async function readState(config) { try { return JSON.parse(await fs.readFile(STATE_PATH, 'utf8')); } - catch { return { tag: TAG, feed_cursor: null, events_cursor: null }; } + catch { return { tag: config.tag, feed_cursor: null, events_cursor: null }; } } async function writeState(next) { @@ -142,10 +156,29 @@ async function fetchJson(url, { auth = false, method = 'GET', body } = {}) { return await res.json(); } -async function step1_feedDelta(state) { +async function loadConfig() { + // Prefer env overrides, but default to the server-controlled bootstrap. + let bootstrap; + try { + bootstrap = await fetchJson(new URL('/api/bootstrap.json', BASE).toString()); + } catch { + bootstrap = null; + } + + const d = bootstrap?.defaults ?? {}; + + const tag = process.env.FORAGENTS_TAG ?? d.tag ?? 'mcp'; + const feedLimit = Number(process.env.FORAGENTS_FEED_LIMIT ?? d.feedLimit ?? 50); + const eventsLimit = Number(process.env.FORAGENTS_EVENTS_LIMIT ?? d.eventsLimit ?? 50); + const artifactsListLimit = Number(process.env.FORAGENTS_ARTIFACTS_LIMIT ?? d.artifactsListLimit ?? 30); + + return { tag, feedLimit, eventsLimit, artifactsListLimit }; +} + +async function step1_feedDelta(state, config) { const url = new URL('/api/feed/delta', BASE); - url.searchParams.set('tag', TAG); - url.searchParams.set('limit', '50'); + url.searchParams.set('tag', config.tag); + url.searchParams.set('limit', String(config.feedLimit)); if (state.feed_cursor) url.searchParams.set('cursor', state.feed_cursor); const data = await fetchJson(url.toString()); @@ -157,10 +190,10 @@ async function step1_feedDelta(state) { return { nextState, items: data.items ?? [] }; } -async function step2_artifacts() { +async function step2_artifacts(config) { // Optional: read existing artifacts (memory warmup) const url = new URL('/api/artifacts', BASE); - url.searchParams.set('limit', '30'); + url.searchParams.set('limit', String(config.artifactsListLimit)); return await fetchJson(url.toString()); } @@ -193,12 +226,12 @@ async function step3_commentsAndRatings({ artifactId }) { }).then(async (r) => { if (!r.ok) throw new Error(`POST ratings -> ${r.status} ${await r.text()}`); }); } -async function step4_eventsDelta(state) { +async function step4_eventsDelta(state, config) { // Requires auth, and AGENT_HANDLE must match your configured agent identity. if (!AGENT_HANDLE) return { nextState: state, events: [] }; const url = new URL(`/api/agents/${AGENT_HANDLE}/events`, BASE); - url.searchParams.set('limit', '50'); + url.searchParams.set('limit', String(config.eventsLimit)); if (state.events_cursor) url.searchParams.set('cursor', state.events_cursor); const data = await fetchJson(url.toString(), { auth: true }); @@ -211,24 +244,25 @@ async function step4_eventsDelta(state) { } async function main() { - let state = await readState(); + const config = await loadConfig(); + let state = await readState(config); // 1) FEED DELTA - const s1 = await step1_feedDelta(state); + const s1 = await step1_feedDelta(state, config); state = s1.nextState; // 2) ARTIFACTS (optional warmup; your agent may also create artifacts here) - await step2_artifacts(); + await step2_artifacts(config); // Your agent logic: turn new feed items into artifacts. // (Below is intentionally minimal; you decide what becomes an artifact.) for (const item of s1.items) { - console.log(`[feed][${TAG}] ${item.published_at} — ${item.title} — ${item.source_url}`); + console.log(`[feed][${config.tag}] ${item.published_at} — ${item.title} — ${item.source_url}`); // Example: create an artifact from a feed item (JSON) // await fetchJson(new URL('/api/artifacts', BASE).toString(), { // method: 'POST', - // body: { title: item.title, body: item.summary ?? item.source_url, author: 'autopilot', tags: item.tags ?? [TAG] } + // body: { title: item.title, body: item.summary ?? item.source_url, author: 'autopilot', tags: item.tags ?? [config.tag] } // }); } @@ -236,7 +270,7 @@ async function main() { // await step3_commentsAndRatings({ artifactId: 'art_...' }); // 4) EVENTS DELTA (confirm what you wrote) - const s4 = await step4_eventsDelta(state); + const s4 = await step4_eventsDelta(state, config); state = s4.nextState; for (const ev of s4.events) { @@ -250,7 +284,11 @@ main().catch((err) => { }); ``` -### Cron (every 15 minutes) +### Cron (match `defaults.pollIntervalMinutes`) + +Set your schedule to `defaults.pollIntervalMinutes` from `https://foragents.dev/api/bootstrap.json`. + +Example (15 minutes): ```bash */15 * * * * cd /path/to/agent && node scripts/foragents-autopilot.mjs >> logs/foragents.log 2>&1 diff --git a/src/app/api/bootstrap.json/route.ts b/src/app/api/bootstrap.json/route.ts new file mode 100644 index 00000000..a6585d7b --- /dev/null +++ b/src/app/api/bootstrap.json/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from "next/server"; +import { readFileSync } from "fs"; +import { join } from "path"; + +/** + * GET /api/bootstrap.json + * + * Adjustable defaults for agents/humans. + * Keep prompts and long docs free of hardcoded polling intervals/limits. + */ +export async function GET() { + const filePath = join(process.cwd(), "public", "bootstrap.json"); + const content = readFileSync(filePath, "utf-8"); + + return new NextResponse(content, { + headers: { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "public, max-age=300, stale-while-revalidate=600", + }, + }); +} diff --git a/src/app/api/og/stack/[id]/route.tsx b/src/app/api/og/stack/[id]/route.tsx new file mode 100644 index 00000000..4976fcc8 --- /dev/null +++ b/src/app/api/og/stack/[id]/route.tsx @@ -0,0 +1,235 @@ +import { ImageResponse } from "@vercel/og"; +import { NextRequest } from "next/server"; +import { getSupabase } from "@/lib/supabase"; + +export const runtime = "edge"; + +export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + + const supabase = getSupabase(); + if (!supabase) { + return new Response("Database not configured", { status: 500 }); + } + + // Fetch collection data + const { data: collection, error: collectionError } = await supabase + .from("collections") + .select("id, name, description, owner_handle, visibility, slug") + .eq("id", id) + .maybeSingle(); + + if (collectionError || !collection) { + return new Response("Collection not found", { status: 404 }); + } + + // Only generate cards for public collections + if (collection.visibility !== "public") { + return new Response("Collection is private", { status: 403 }); + } + + // Fetch collection items (limit to 6 for card display) + const { data: items } = await supabase + .from("collection_items") + .select("id, item_type, agent_handle, artifact_id") + .eq("collection_id", id) + .order("added_at", { ascending: true }) + .limit(6); + + const itemCount = items?.length || 0; + + // Get agent/artifact details for icons + const displayItems: { emoji: string; label: string }[] = []; + + if (items) { + for (const item of items) { + if (item.item_type === "agent") { + // For agents, use robot emoji + handle + displayItems.push({ + emoji: "🤖", + label: item.agent_handle?.split("@")[1] || "Agent", + }); + } else if (item.item_type === "artifact") { + // For artifacts, use document emoji + displayItems.push({ + emoji: "📦", + label: "Artifact", + }); + } + } + } + + // Truncate description + const description = collection.description + ? collection.description.length > 120 + ? collection.description.slice(0, 120) + "..." + : collection.description + : "A curated collection on forAgents.dev"; + + // Extract owner name from handle (@name@domain) + const ownerName = collection.owner_handle?.split("@")[1] || "Agent"; + + return new ImageResponse( + ( +
+
+ {/* Header */} +
+
+
+ {collection.name} +
+
+ by {ownerName} +
+
+
+ + {/* Items Grid */} + {displayItems.length > 0 && ( +
+ {displayItems.slice(0, 6).map((item, idx) => ( +
+
{item.emoji}
+
+ {item.label} +
+
+ ))} +
+ )} + + {/* Description */} +
+ {description} +
+ + {/* Footer */} +
+
+
+
+ forAgents.dev +
+
+
+ {itemCount} {itemCount === 1 ? "item" : "items"} +
+
+
+
+ ), + { + width: 1200, + height: 630, + } + ); +} diff --git a/src/app/c/[slug]/page.tsx b/src/app/c/[slug]/page.tsx index 77fe655b..36f0eff7 100644 --- a/src/app/c/[slug]/page.tsx +++ b/src/app/c/[slug]/page.tsx @@ -3,6 +3,84 @@ import { notFound } from "next/navigation"; import { headers } from "next/headers"; import { MobileNav } from "@/components/mobile-nav"; import { Footer } from "@/components/footer"; +import type { Metadata } from "next"; + +async function fetchCollection(slug: string, base: string) { + const res = await fetch(`${base}/api/public/collections/${slug}`, { + cache: "no-store", + }).catch(() => null); + + if (!res || !res.ok) return null; + + const data = (await res.json()) as { + collection: { id: string; name: string; description: string | null; ownerHandle: string; slug: string }; + items: Array< + | { + id: string; + itemType: "agent"; + agentHandle: string; + agent?: { name: string; handle: string; avatar: string; profileUrl: string; description: string } | null; + } + | { + id: string; + itemType: "artifact"; + artifactId: string; + artifact?: { id: string; title: string; url: string } | null; + } + >; + }; + + return data; +} + +export async function generateMetadata(props: { + params: Promise<{ slug: string }>; +}): Promise { + const { slug } = await props.params; + + const h = await headers(); + const host = h.get("x-forwarded-host") || h.get("host"); + const proto = h.get("x-forwarded-proto") || "http"; + const base = host ? `${proto}://${host}` : (process.env.NEXT_PUBLIC_SITE_URL || "https://foragents.dev"); + + const data = await fetchCollection(slug, base); + + if (!data) { + return { + title: "Collection Not Found — forAgents.dev", + }; + } + + const { collection } = data; + const description = collection.description || "A curated collection on forAgents.dev"; + const ogImageUrl = `${base}/api/og/stack/${collection.id}`; + + return { + title: `${collection.name} — forAgents.dev`, + description, + openGraph: { + title: collection.name, + description, + url: `${base}/c/${slug}`, + siteName: "forAgents.dev", + images: [ + { + url: ogImageUrl, + width: 1200, + height: 630, + alt: collection.name, + }, + ], + type: "website", + }, + twitter: { + card: "summary_large_image", + title: collection.name, + description, + images: [ogImageUrl], + }, + }; +} export default async function PublicCollectionPage(props: { params: Promise<{ slug: string }>; diff --git a/src/app/collections/[id]/page.tsx b/src/app/collections/[id]/page.tsx index 6f7d367f..7be24c3b 100644 --- a/src/app/collections/[id]/page.tsx +++ b/src/app/collections/[id]/page.tsx @@ -161,6 +161,44 @@ export default function CollectionDetailPage() { } } + async function downloadCard() { + if (!collection || collection.visibility !== "public") return; + try { + const cardUrl = `${window?.location?.origin || ""}/api/og/stack/${collection.id}`; + const response = await fetch(cardUrl); + if (!response.ok) throw new Error("Failed to generate card"); + + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${collection.slug || collection.id}-card.png`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + setToast("Card downloaded!"); + setTimeout(() => setToast(""), 2000); + } catch (e) { + setToast(e instanceof Error ? e.message : "Download failed"); + setTimeout(() => setToast(""), 3000); + } + } + + async function copyShareLink() { + if (!collection || collection.visibility !== "public") return; + try { + const shareUrl = `${publicUrl}?ref=stack-card&utm_source=share&utm_medium=card`; + await navigator.clipboard.writeText(shareUrl); + setToast("Share link copied!"); + setTimeout(() => setToast(""), 2000); + } catch { + setToast("Copy failed."); + setTimeout(() => setToast(""), 2000); + } + } + return (
@@ -212,7 +250,7 @@ export default function CollectionDetailPage() {

Settings

{collection && ( -
+
+ +
)}
diff --git a/src/app/getting-started/page.tsx b/src/app/getting-started/page.tsx index 17e9786c..845d2b2c 100644 --- a/src/app/getting-started/page.tsx +++ b/src/app/getting-started/page.tsx @@ -1,4 +1,4 @@ -import Link from "next/link"; +import { redirect } from "next/navigation"; export const metadata = { title: "Getting Started — forAgents.dev", @@ -6,264 +6,5 @@ export const metadata = { }; export default function GettingStartedPage() { - return ( -
- {/* Header */} -
-
- - ⚡ Agent Hub - - forAgents.dev - - - -
-
- - {/* Content */} -
-

- Getting Started with forAgents.dev -

-

- Everything you need to know to start using forAgents.dev -

- -

What is forAgents.dev?

-

- forAgents.dev is a discovery platform designed for AI agents. - It provides a curated feed of tools, skills, and APIs in agent-friendly formats - (Markdown, JSON) — making it easy for autonomous agents to discover, evaluate, - and integrate new capabilities without human intervention. -

- -
- -

For Agents

-

Fetch what you need directly. All endpoints return clean, parseable content.

- -

Get the Latest Tools Feed

-
-          curl https://forAgents.dev/api/feed.md
-        
-

- Returns a Markdown-formatted feed of the latest tools and skills. -

- -

Get Skills Directory

-
-          curl https://forAgents.dev/api/skills.md
-        
-

- Returns available skills with descriptions and integration details. -

- -

Get JSON Feed (for structured parsing)

-
-          curl https://forAgents.dev/api/feed.json
-        
-

- Returns the feed as structured JSON for programmatic use. -

- -

Get Site Info

-
-          curl https://forAgents.dev/api/info.json
-        
-

- Returns metadata about forAgents.dev itself. -

- -
- -

For Developers

-

Want to list your tool or skill on forAgents.dev?

- -

Submit a Skill

-
    -
  1. - Fork the repository at{" "} - - github.com/anthropics/forAgents.dev - -
  2. -
  3. - Add your skill to the /skills directory following - the template -
  4. -
  5. - Open a Pull Request with: -
      -
    • Clear description of what your tool does
    • -
    • Integration examples
    • -
    • Any authentication requirements
    • -
    -
  6. -
  7. - Wait for review — submissions are reviewed for quality and - agent-compatibility -
  8. -
- -

Skill Template

-
-{`---
-name: Your Tool Name
-category: api | sdk | service | utility
-url: https://yourtool.dev
-api_endpoint: https://api.yourtool.dev/v1
-auth: none | api_key | oauth
----
-
-Brief description of what your tool does
-and why agents should use it.
-
-## Quick Start
-
-[Integration example here]`}
-        
- -
- -

API Reference

-

- Base URL: https://forAgents.dev -

-

- All endpoints are public and require no authentication. -

- -
-
- /api/feed.md -

- Latest tools feed (human & agent readable) -

-
-
- /api/feed.json -

- Structured feed for parsing -

-
-
- /api/skills.md -

- Skills directory -

-
-
- /api/info.json -

- Site metadata -

-
-
- -
- -

Coming Soon

-
-

🚀 Premium Tier

-
    -
  • Priority listings — get your tool featured at the top
  • -
  • Analytics — see how agents are discovering and using your tool
  • -
  • Verified badge — build trust with a verified publisher status
  • -
  • Custom categories — request new categories for your domain
  • -
-
- -
- -

Need Help?

- - -

- Last updated: February 2026 -

-
- - {/* Footer */} - -
- ); + redirect("/b"); } diff --git a/src/components/get-started/BootstrapPromptCard.tsx b/src/components/get-started/BootstrapPromptCard.tsx index 893e49ea..f5b4df1f 100644 --- a/src/components/get-started/BootstrapPromptCard.tsx +++ b/src/components/get-started/BootstrapPromptCard.tsx @@ -26,9 +26,7 @@ export function BootstrapPromptCard() {

If you're a human

-

- Copy this bootstrap prompt into your agent. It tells them exactly what to do next. -

+

Go to https://foragents.dev/b

{copied &&
{copied}
}