diff --git a/.agents/skills/build-agent/SKILL.md b/.agents/skills/build-agent/SKILL.md new file mode 100644 index 0000000..6dcac18 --- /dev/null +++ b/.agents/skills/build-agent/SKILL.md @@ -0,0 +1,115 @@ +--- +name: build-agent +description: Build and ship a new installable agent in this repo, following the web-agent pattern from development to CLI to production. USE WHEN adding a new agent, adding tools to an agent, exposing an agent through the agentcn CLI/registry, writing agent docs, or wiring the docs demo. Trigger words - new agent, add agent, add a tool, register agent, agent registry, agentcn add, agent docs demo. +--- + +# Build an Agent (dev → CLI → prod) + +This skill is the canonical, repeatable recipe for adding a new agent to this +repo. It mirrors the reference implementation, the **web-agent** +(`ai/agents/web/`), which is distributed to users through the `agentcn` CLI. + +Follow the checklist top-to-bottom. Each phase links to a deeper reference file +under `references/`. Always copy the existing web-agent conventions instead of +inventing new ones — consistency is what makes the registry + CLI work. + +## Mental model + +An agent in this repo has **five layers**. Adding an agent means touching each: + +1. **Source** — the agent + tools live in `ai/agents//`. This is the code + users actually run. (→ `references/agent-anatomy.md`) +2. **Registry** — `registry/registry-agents.ts` declares which files, deps, and + env vars make up the agent. A build script turns this into static JSON at + `apps/web/public/r/.json`. (→ `references/registry-cli-prod.md`) +3. **CLI** — `agentcn add ` fetches that JSON and installs the agent into a + user's project. The CLI is generic; you don't edit it per-agent. (→ + `references/registry-cli-prod.md`) +4. **Docs** — `apps/web/content/docs/agents/.mdx` documents install + + wiring. (→ `references/docs-and-demo.md`) +5. **Demo** — `apps/web/lib/agent-demos/.ts` powers the zero-cost + simulated preview shown in the docs. (→ `references/docs-and-demo.md`) + +## Conventions (do not deviate) + +- **Package manager:** `pnpm` (see `packageManager` in root `package.json`). +- **Task runner:** Nx. Prefer `pnpm exec nx run ...` or the root `package.json` + scripts over calling tools directly. +- **Model:** `anthropic("claude-sonnet-4-5-20250929")` via `@ai-sdk/anthropic`. +- **AI SDK:** Vercel `ai` (`streamText`, `tool`, `stepCountIs`). +- **Tool schemas:** `zod`, defined in a dedicated `schema.ts`, imported by tools. +- **Agent name:** kebab-case, matches the folder, the registry `name`, the docs + slug, and the demo `agentId` — all identical. E.g. `web-agent`. +- **Env vars:** never hardcode secrets. Read from `process.env` and throw a clear + error if missing (see `tools/core.ts`). + +## Checklist + +### Phase 1 — Scaffold the source (`ai/agents//`) + +Copy the web-agent layout. Full templates in `references/agent-anatomy.md`. + +- [ ] `ai/agents//index.ts` — re-export the agent: `export { Agent } from "./agent"`. +- [ ] `ai/agents//agent.ts` — `streamText` call with model, system prompt, tools, `stopWhen`. +- [ ] `ai/agents//prompt.ts` — the `SYSTEM_PROMPT` string. +- [ ] `ai/agents//tools/schema.ts` — one zod schema per tool. +- [ ] `ai/agents//tools/core.ts` — shared clients/helpers + env-var guards. +- [ ] `ai/agents//tools/.ts` — one file per tool using `tool({...})`. +- [ ] `ai/agents//tools/toolset.ts` — map tool names → tool defs. +- [ ] `ai/agents//tools/index.ts` — `export { Toolset } from "./toolset"`. +- [ ] `ai/agents//tools/types.ts` — shared TS types (optional). +- [ ] `ai/agents//tools/services/*.ts` — external API clients (optional). + +### Phase 2 — Tests (`ai/agents//test/`) + +- [ ] `test/test-helpers.ts` — `describeIf` guards keyed on env vars. +- [ ] `test/.test.ts` — one suite per tool; live-API suites use the guards. +- [ ] Run `pnpm test:web-agent`-equivalent: `pnpm jest ai/agents/`. + +### Phase 3 — Register for distribution + +- [ ] Add an entry to the `agents` array in `registry/registry-agents.ts` + (name, description, title, categories, `dependencies`, `envVars`, and every + file from Phase 1 with its `type`). +- [ ] Build the registry: `pnpm agentcn:registry:build`. +- [ ] Confirm `apps/web/public/r/.json` and updated `index.json` exist. + +### Phase 4 — Docs + demo + +- [ ] `apps/web/content/docs/agents/.mdx` — frontmatter (`title`, + `description`, `component: true`), ``, + install tabs, wiring, tools reference. +- [ ] Add the slug to `pages` in `apps/web/content/docs/agents/meta.json`. +- [ ] `apps/web/lib/agent-demos/.ts` — an `AgentDemoConfig` with scenarios. +- [ ] Register it in `apps/web/lib/agent-demos/index.ts` (`agentDemos` map). + +### Phase 5 — Verify (dev) and ship (prod) + +- [ ] `pnpm exec nx run @kit/web:typecheck` +- [ ] `pnpm exec nx run @kit/web:build` +- [ ] `pnpm deploy:build` (registry build + web build — what prod runs). +- [ ] Optional live check: `pnpm agentcn:registry:verify-live`. +- [ ] Ship: registry JSON deploys with the web app; the CLI is published via + `nx release` on an `agentcn@*` tag. See `references/registry-cli-prod.md`. + +## Quick command reference + +```bash +pnpm jest ai/agents/ # run agent tests +pnpm agentcn:registry:build # regenerate public/r/*.json (REQUIRED after registry edits) +pnpm exec nx run @kit/web:typecheck # typecheck the web app + agent source +pnpm exec nx run @kit/web:build # build docs/marketing site +pnpm deploy:build # registry build + web build (prod parity) +npx agentcn@latest add # what a user runs to install your agent +``` + +## Common mistakes + +- **Forgetting `pnpm agentcn:registry:build`** after editing source or the + registry — the CLI serves stale JSON and installs the old files. +- **Name drift** — folder, registry `name`, docs slug, and demo `agentId` must be + identical. +- **Adding a file to `ai/agents//` but not to `files` in the registry** — + the CLI won't install it, so the agent breaks in the user's project. +- **New dependency not listed in the registry `dependencies`** — user install + fails at runtime. Keep `dependencies`/`envVars` in sync with the source. diff --git a/.agents/skills/build-agent/references/agent-anatomy.md b/.agents/skills/build-agent/references/agent-anatomy.md new file mode 100644 index 0000000..1c1eb64 --- /dev/null +++ b/.agents/skills/build-agent/references/agent-anatomy.md @@ -0,0 +1,202 @@ +# Agent anatomy — source layout & templates + +Reference implementation: `ai/agents/web/`. Copy this structure verbatim for a +new agent, replacing `web`/`` and the tool set. + +## Directory layout + +``` +ai/agents// +├── index.ts # public entry: re-exports the agent +├── agent.ts # the agent (streamText call) +├── prompt.ts # SYSTEM_PROMPT string +├── tools/ +│ ├── index.ts # re-exports the toolset +│ ├── toolset.ts # { tool_name: toolDef } passed to the agent +│ ├── schema.ts # zod input schemas (one per tool) +│ ├── core.ts # shared clients + env-var guards + helpers +│ ├── types.ts # shared TS types (optional) +│ ├── .ts # one file per tool (tool({...})) +│ └── services/ +│ └── .ts # external API client wrappers (optional) +└── test/ + ├── test-helpers.ts # describeIf guards + └── .test.ts # one suite per tool +``` + +## Layer 1 — `index.ts` + +Thin public surface. Consumers import from `ai/agents/`. + +```ts +export { webAgent } from "./agent"; +``` + +## Layer 2 — `agent.ts` + +The agent is a plain function that returns a `streamText` result. It wires the +model, system prompt, tools, and a stop condition. Keep it this small — all the +behavior lives in the prompt and tools. + +```ts +import { anthropic } from "@ai-sdk/anthropic"; +import { streamText, type ModelMessage, stepCountIs } from "ai"; +import { SYSTEM_PROMPT } from "./prompt"; +import { webToolset } from "./tools"; + +export function webAgent(messages: ModelMessage[]) { + return streamText({ + model: anthropic("claude-sonnet-4-5-20250929"), + system: SYSTEM_PROMPT, + messages, + tools: webToolset, + stopWhen: [stepCountIs(20)], + }); +} +``` + +Notes: +- **`messages: ModelMessage[]`** in, streaming result out. The caller (an API + route, script, or the demo app) owns the transport. +- **`stopWhen: [stepCountIs(20)]`** caps the tool-use loop. Tune per agent. +- Keep the model choice consistent across agents unless there's a reason. + +## Layer 3 — `prompt.ts` + +Export a single `SYSTEM_PROMPT` constant. Describe the agent's role, when to use +each tool, and output/citation rules. Keep tool names in the prompt exactly +matching the `toolset.ts` keys. + +```ts +export const SYSTEM_PROMPT = `You are a web research agent. +... +Use web_search for quick lookups. Use deep_research for multi-source reports. +Always cite sources as markdown links.`; +``` + +## Layer 4 — Tools + +### `tools/schema.ts` — validation + +One zod schema per tool. Use `.describe()` on every field — the model reads +these descriptions. + +```ts +import { z } from "zod"; + +export const webSearchSchema = z.object({ + query: z.string().describe("The search query."), + num_results: z + .number() + .int() + .min(1) + .max(25) + .optional() + .default(5) + .describe("Maximum number of search results."), +}); +``` + +### `tools/core.ts` — shared clients & env guards + +Centralize provider clients and secret access. Every secret read throws a clear +error when missing (never silently fall back). + +```ts +import Exa from "exa-js"; + +export function getExaClient() { + const exaApiKey = process.env["EXA_API_KEY"]; + if (!exaApiKey) { + throw new Error("EXA_API_KEY is not set."); + } + return new Exa(exaApiKey); +} +``` + +Put filesystem/artifact helpers here too if the agent persists output (the +web-agent writes to `data/.local/` via `saveArtifact`). + +### `tools/.ts` — a tool + +Each tool is `tool({ description, inputSchema, execute })`. `execute` receives +the validated, typed args. Return a serializable object; a `success` flag plus a +`content` payload is the house style. + +```ts +import { tool } from "ai"; +import { getExaClient } from "./core"; +import { webSearchSchema } from "./schema"; + +export const webSearchTool = tool({ + description: "Search the web for up-to-date information.", + inputSchema: webSearchSchema, + execute: async ({ query, num_results }) => { + const exa = getExaClient(); + const { results } = await exa.searchAndContents(query, { + numResults: num_results, + highlights: true, + }); + return { + success: true, + content: results.map((r) => ({ + title: r.title, + url: r.url, + content: r.highlights.join("\n"), + })), + }; + }, +}); +``` + +### `tools/toolset.ts` — the map + +Keys are the tool names the model calls (and what you name in the prompt). + +```ts +import { ToolSet } from "ai"; +import { webSearchTool } from "./web-search"; +import { answerQuestionTool } from "./answer-question"; + +export const webToolset = { + web_search: webSearchTool, + answer_question: answerQuestionTool, +} as ToolSet; +``` + +### `tools/index.ts` + +```ts +export { webToolset } from "./toolset"; +``` + +## Layer 5 — Tests + +Live-API tests are gated so CI/local runs without keys don't fail. + +`test/test-helpers.ts`: + +```ts +import { describe } from "@jest/globals"; +declare const process: { env: Record }; + +export const describeIfExa = + process.env.EXA_API_KEY ? describe : describe.skip; +``` + +`test/web-search.test.ts`: + +```ts +import { describeIfExa } from "./test-helpers"; + +describeIfExa("web_search (live)", () => { + it("returns results", async () => { + // ... call the tool's execute and assert on shape + }); +}); +``` + +Run: `pnpm jest ai/agents/`. + +> Tests that hit a real provider will fail without network + keys — that's +> expected. Gate them behind `describeIf` so they skip cleanly. diff --git a/.agents/skills/build-agent/references/docs-and-demo.md b/.agents/skills/build-agent/references/docs-and-demo.md new file mode 100644 index 0000000..ef1ed6c --- /dev/null +++ b/.agents/skills/build-agent/references/docs-and-demo.md @@ -0,0 +1,210 @@ +# Docs page & simulated demo + +Every agent gets a docs page and a zero-cost simulated demo. The demo replays a +scripted conversation (no live API calls, no keys, no spend) so visitors can see +the agent "work" in the docs. + +## Part A — Docs page (MDX) + +### 1. Create the page + +`apps/web/content/docs/agents/.mdx`. Frontmatter must set +`component: true` (enables the MDX components used below). + +```mdx +--- +title: Web Agent +description: Web research agent with search, deep research, browser, and websets. +component: true +--- + + + +## Installation + + + + + CLI + Manual + + + + +```bash +npx agentcn@latest add web-agent +``` + + + + + + + +Install the following dependencies: + +```bash +npm install ai @ai-sdk/anthropic zod exa-js playwright-core +``` + +Set environment variables in your `.env` file. Get each key from its provider: + +- [ANTHROPIC_API_KEY](https://console.anthropic.com/settings/keys) — Anthropic Console +- [EXA_API_KEY](https://dashboard.exa.ai/api-keys) — Exa Dashboard +- [ANCHOR_API_KEY](https://app.anchorbrowser.io/) — Anchor Browser + +```bash +ANTHROPIC_API_KEY= +EXA_API_KEY= +ANCHOR_API_KEY= +``` + +Copy the agent files into your project. + + + + + + + +## Usage + +... show importing and calling the agent ... + +## Tools + +... document each tool: name, purpose, key inputs ... +``` + +- `` renders the simulated player. The + `agentId` **must** match the demo's `agentId` (Part B) and the registry name. +- The dependency list and env vars must match `registry/registry-agents.ts`. + +### 2. Register the page in nav + +Add the slug to `pages` in `apps/web/content/docs/agents/meta.json`: + +```json +{ + "title": "Agents", + "pages": ["web-agent", ""] +} +``` + +(The top-level `apps/web/content/docs/meta.json` already includes the `agents` +section — no change needed there.) + +## Part B — Simulated demo data + +The demo is pure data. You script the prompt, the tool calls, and the final +answer; the player animates it. + +### 1. Create the demo config + +`apps/web/lib/agent-demos/.ts` exporting an `AgentDemoConfig`. + +Types (`apps/web/lib/agent-demos/types.ts`): + +```ts +type AgentDemoConfig = { + agentId: string // === registry name / docs slug + label: string + description: string + defaultScenarioId: string + scenarios: AgentDemoScenario[] +} + +type AgentDemoScenario = { + id: string + label: string // chip label in the UI + prompt: string // the simulated user message + assistantParts: DemoMessagePart[] +} +``` + +`DemoMessagePart` is a discriminated union — mix and match to script the run: + +- `{ type: "tool", tool: DemoToolName, input: Record }` — renders a + tool step (pending → running → done). `tool` must be one of the known + `DemoToolName`s; add new names to the union in `types.ts` if your agent has new + tools. +- `{ type: "text", text: string }` — the assistant's answer. Supports inline + markdown (bold, links, bullet lines). +- `{ type: "browser_view", ... }` — a simulated browser panel (url, pageTitle, + navigationSteps, pageContent). +- `{ type: "webset_view", ... }` — a simulated result table (title, columns, + rows, entityCount). + +Example: + +```ts +import type { AgentDemoConfig } from "./types" + +export const webAgentDemo: AgentDemoConfig = { + agentId: "web-agent", + label: "Web Agent", + description: "Uses web search, deep research, browser automation, and websets.", + defaultScenarioId: "search", + scenarios: [ + { + id: "search", + label: "Search & cite", + prompt: "Find top AI coding agents launched this month with citations.", + assistantParts: [ + { type: "tool", tool: "web_search", input: { query: "AI coding agents 2026" } }, + { type: "tool", tool: "answer_question", input: { question: "Top agents this month?" } }, + { + type: "text", + text: `Here are notable agents:\n\n• **Cursor Agent** — IDE-native. [cursor.com](https://cursor.com)`, + }, + ], + }, + // add scenarios per capability (deep research, browser, webset, ...) + ], +} +``` + +Guidelines: +- Give each scenario a distinct `label` — these become the capability chips. +- Set `defaultScenarioId` to the most representative scenario. +- Keep tool `input` realistic; it's shown to users as the tool call. +- Order `assistantParts` in execution order: tool calls first, rich views next, + final `text` last. + +### 2. Register the demo + +Add it to the `agentDemos` map in `apps/web/lib/agent-demos/index.ts`: + +```ts +import { webAgentDemo } from "./web-agent" + +const agentDemos: Record = { + [webAgentDemo.agentId]: webAgentDemo, +} +``` + +That's it — `` looks the agent up here. + +## Rendering internals (for reference only) + +You normally don't edit these; they're generic and already handle any config: + +- `apps/web/components/agent-demo-preview.tsx` — the MDX entry component. +- `apps/web/components/agent-demo/agent-demo-player.tsx` — orchestrates replay, + scenario chips, thinking state, and reveals parts in sequence. +- `agent-demo-message.tsx`, `agent-demo-tool-steps.tsx`, `agent-demo-thinking.tsx`, + `agent-demo-browser-panel.tsx`, `agent-demo-webset-table.tsx`, + `agent-demo-formatted-text.tsx`, `agent-demo-input.tsx` — the individual pieces. +- `apps/web/lib/agent-demos/tool-labels.ts` — human labels + view extractors. + +Only touch these if you're adding a **new part type** (e.g. a new rich view). In +that case: add the type to `types.ts`, render it in `agent-demo-message.tsx`, and +handle its timing in `agent-demo-player.tsx`. + +## Definition of done + +- [ ] `apps/web/content/docs/agents/.mdx` created with ``. +- [ ] Slug added to `apps/web/content/docs/agents/meta.json`. +- [ ] `apps/web/lib/agent-demos/.ts` created with ≥1 scenario. +- [ ] Registered in `apps/web/lib/agent-demos/index.ts`. +- [ ] `pnpm exec nx run @kit/web:build` passes and the demo replays in the docs. diff --git a/.agents/skills/build-agent/references/registry-cli-prod.md b/.agents/skills/build-agent/references/registry-cli-prod.md new file mode 100644 index 0000000..262295c --- /dev/null +++ b/.agents/skills/build-agent/references/registry-cli-prod.md @@ -0,0 +1,143 @@ +# Registry, CLI & production + +How agent source in `ai/agents//` becomes something a user installs with +`npx agentcn@latest add `, and how it reaches production. + +## Pipeline overview + +``` +ai/agents//* registry/registry-agents.ts + (source) → (declaration) + │ + │ pnpm agentcn:registry:build + ▼ + apps/web/public/r/.json + index.json + │ + │ served as static files by the web app + ▼ + agentcn add (CLI fetches JSON, writes files) + ▼ + user's project (installed agent) +``` + +Two independently shipped artifacts: +1. **Registry JSON** — deploys with the web app (`apps/web/public/r/`). +2. **`agentcn` CLI** — published to npm via `nx release`. + +## Step 1 — Declare the agent in the registry + +Edit `registry/registry-agents.ts` and add an entry to the `agents` array. The +shape is `RegistryAgentItem`: + +```ts +{ + name: "web-agent", // === folder / docs slug / demo agentId + type: "registry:agent", + description: "Web research agent with search, deep research, browser, websets.", + title: "Web Agent", + categories: ["web", "research", "browser"], + dependencies: ["ai", "@ai-sdk/anthropic", "zod", "exa-js", "playwright-core"], + envVars: { + ANTHROPIC_API_KEY: "", + EXA_API_KEY: "", + ANCHOR_API_KEY: "", + }, + files: [ + { path: "ai/agents/web/index.ts", type: "registry:agent" }, + { path: "ai/agents/web/agent.ts", type: "registry:agent" }, + { path: "ai/agents/web/prompt.ts", type: "registry:agent" }, + { path: "ai/agents/web/tools/index.ts", type: "registry:lib" }, + { path: "ai/agents/web/tools/toolset.ts", type: "registry:lib" }, + { path: "ai/agents/web/tools/schema.ts", type: "registry:lib" }, + { path: "ai/agents/web/tools/core.ts", type: "registry:lib" }, + { path: "ai/agents/web/tools/web-search.ts", type: "registry:lib" }, + // ...every file the agent needs to run + ], + meta: { providers: ["anthropic", "exa", "anchor"] }, +} +``` + +Rules: +- **Every file** the agent imports at runtime must be listed in `files`. If it's + not listed, the CLI won't install it. +- `type` is `registry:agent` for the core agent files and `registry:lib` for + tools/helpers. (An optional `target` overrides the install path; default is the + same `path`.) +- Keep `dependencies` and `envVars` in sync with what the source actually uses. + These drive the CLI's dependency install and `.env` scaffolding. + +## Step 2 — Build the registry JSON + +```bash +pnpm agentcn:registry:build +``` + +This runs `scripts/build-agent-registry.mts`, which: +- reads each entry from `registry/registry-agents.ts`, +- inlines the **content** of every listed file, +- writes `apps/web/public/r/.json` per agent, +- writes an aggregate `apps/web/public/r/index.json` (drives `agentcn list`). + +> ALWAYS re-run this after editing agent source or the registry. The CLI serves +> whatever JSON is committed/deployed, not the live source. + +Verify the output exists and includes your new files: +`apps/web/public/r/.json`. + +## Step 3 — How the CLI consumes it (no per-agent edits) + +The `agentcn` CLI lives in `packages/agentcn/cli/`. It's generic — you do not +change it when adding an agent. For reference: + +- `src/commands/add.ts` — `agentcn add `: fetches `r/.json`, builds an + install plan, installs deps, writes files, scaffolds env vars. +- `src/commands/list.ts` / `info.ts` — read `index.json` / a single item. +- `src/lib/registry.ts` — resolves the registry URL and fetches JSON. +- `src/lib/{deps,package-json,project,config,install-plan}.ts` — install logic. +- Tests: `src/tests/*.test.ts` (run with the CLI's `nx test` target). + +You'd only touch the CLI to change *install behavior* for all agents (e.g. a new +`type`, a new config option) — not to add a single agent. + +## Step 4 — Verify (dev) + +```bash +pnpm jest ai/agents/ # agent unit/live tests +pnpm exec nx run @kit/web:typecheck # typecheck source + web app +pnpm exec nx run @kit/web:build # build the site (includes docs) +pnpm deploy:build # registry build + web build (prod parity) +``` + +Optional end-to-end registry check against a running/deployed site: + +```bash +pnpm agentcn:registry:verify-live # scripts/verify-agentcn-registry-live.sh +pnpm agentcn:runner-matrix-smoke # CLI install smoke across package managers +``` + +## Step 5 — Production + +- **Registry JSON** ships automatically: `pnpm deploy:build` runs + `agentcn:registry:build` then `web:build`, and the JSON under + `apps/web/public/r/` is deployed as static assets with the web app. Once the + site deploys, `agentcn add ` can install your agent. +- **CLI publishing** (only when the CLI package itself changed): the repo uses + Nx release. + + ```bash + pnpm release # version + changelog + publish + pnpm release:version # bump versions only + pnpm release:changelog # generate changelog only + pnpm release:publish # publish only + ``` + + Publishing is triggered on an `agentcn@*` git tag (see the publish workflow in + `.github/workflows/`). Adding a new agent usually does **not** require a CLI + release — only a site deploy with the rebuilt registry. + +## Definition of done + +- [ ] Entry added to `registry/registry-agents.ts` with all files, deps, env vars. +- [ ] `pnpm agentcn:registry:build` run; `public/r/.json` + `index.json` updated. +- [ ] `pnpm deploy:build` passes. +- [ ] (If applicable) CLI released via `nx release`. diff --git a/.env.example b/.env.example index f8e47eb..a504fa6 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,3 @@ # Required for agent ANTHROPIC_API_KEY= -AGENTCN_REGISTRY_URL=https://agentcn.dev/r -NEXT_PUBLIC_AGENT_DEMO_URL= \ No newline at end of file +AGENTCN_REGISTRY_URL=https://agentcn.dev/r \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81b4c45..e57189e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,6 @@ jobs: timeout-minutes: 15 env: NEXT_PUBLIC_APP_URL: https://agentcn.dev - NEXT_PUBLIC_AGENT_DEMO_URL: https://kit-demo-agentcn-ui.vercel.app steps: - name: Checkout repository uses: actions/checkout@v4 diff --git a/README.md b/README.md index d0ed136..c42372f 100644 --- a/README.md +++ b/README.md @@ -78,16 +78,18 @@ pnpm web Open [http://localhost:3000](http://localhost:3000) for the marketing site and docs. -### Run the agent demo embed +### Run the agent demo app (optional) -The docs live preview expects a demo app on port **3001**: +For live agent testing with your API keys, run the demo app on port **3001**: ```bash cd examples/agent-ui-template pnpm install -pnpm dev:embed # port 3001 — matches NEXT_PUBLIC_AGENT_DEMO_URL +pnpm dev:embed # port 3001 ``` +Docs agent pages use simulated examples and do not require this app. + ### Build for production ```bash @@ -112,7 +114,6 @@ For the docs site (`apps/web/.env`): ```env NEXT_PUBLIC_APP_URL=http://localhost:3000 -NEXT_PUBLIC_AGENT_DEMO_URL=http://localhost:3001 ``` ### Getting API keys diff --git a/apps/web/.env.example b/apps/web/.env.example index 408d605..0090f17 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1,2 +1 @@ NEXT_PUBLIC_APP_URL=https://agentcn.dev -NEXT_PUBLIC_AGENT_DEMO_URL=http://localhost:3001 diff --git a/apps/web/app/(view)/view/[name]/page.tsx b/apps/web/app/(view)/view/[name]/page.tsx index 6e3434b..0c48e5c 100644 --- a/apps/web/app/(view)/view/[name]/page.tsx +++ b/apps/web/app/(view)/view/[name]/page.tsx @@ -44,7 +44,7 @@ export async function generateMetadata({ url: absoluteUrl(`/view/${item.name}`), images: [ { - url: siteConfig.ogImage, + url: absoluteUrl(siteConfig.ogImage), width: 1200, height: 630, alt: siteConfig.name, @@ -55,7 +55,7 @@ export async function generateMetadata({ card: "summary_large_image", title, description, - images: [siteConfig.ogImage], + images: [absoluteUrl(siteConfig.ogImage)], creator: "@shadcn", }, } diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 0fed024..4104ee8 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -4,6 +4,7 @@ import Script from "next/script" import { META_THEME_COLORS, siteConfig } from "@/lib/config" import { fontVariables } from "@/lib/fonts" +import { defaultOgImage } from "@/lib/metadata" import { cn, getSiteUrl } from "@/lib/utils" import { LayoutProvider } from "@/hooks/use-layout" import { Toaster } from "@/components/ui/sonner" @@ -62,20 +63,13 @@ export const metadata: Metadata = { title: siteConfig.name, description: siteConfig.description, siteName: siteConfig.name, - images: [ - { - url: `${siteUrl}/opengraph-image.png`, - width: 1200, - height: 630, - alt: siteConfig.name, - }, - ], + images: [defaultOgImage], }, twitter: { card: "summary_large_image", title: siteConfig.name, description: siteConfig.description, - images: [`${siteUrl}/opengraph-image.png`], + images: [defaultOgImage.url], creator: `@${siteConfig.social.twitterHandle}`, }, manifest: `${siteConfig.url}/site.webmanifest`, diff --git a/apps/web/app/opengraph-image.tsx b/apps/web/app/opengraph-image.tsx new file mode 100644 index 0000000..469ca59 --- /dev/null +++ b/apps/web/app/opengraph-image.tsx @@ -0,0 +1,98 @@ +import { ImageResponse } from "next/og" + +import { siteConfig } from "@/lib/config" + +export const alt = siteConfig.name +export const size = { + width: 1200, + height: 630, +} +export const contentType = "image/png" + +export default function OpenGraphImage() { + return new ImageResponse( + ( +
+
+ + + + + + + AgentCN + +
+ +
+
+ Installable AI agents for your workflow +
+
+ {siteConfig.description} +
+
+ +
+ agentcn.dev + CLI · Registry · Docs +
+
+ ), + { + ...size, + } + ) +} diff --git a/apps/web/app/twitter-image.tsx b/apps/web/app/twitter-image.tsx new file mode 100644 index 0000000..ee29dc8 --- /dev/null +++ b/apps/web/app/twitter-image.tsx @@ -0,0 +1 @@ +export { default, alt, contentType, size } from "./opengraph-image" diff --git a/apps/web/components/agent-demo-preview.tsx b/apps/web/components/agent-demo-preview.tsx index e3f31e5..ba8463c 100644 --- a/apps/web/components/agent-demo-preview.tsx +++ b/apps/web/components/agent-demo-preview.tsx @@ -3,33 +3,28 @@ import Link from "next/link" import { CodeBlockCommand } from "@/components/code-block-command" +import { AgentDemoPlayer } from "@/components/agent-demo/agent-demo-player" import { ComponentPreviewTabs } from "@/components/component-preview-tabs" -const demoBaseUrl = - process.env.NEXT_PUBLIC_AGENT_DEMO_URL ?? "http://localhost:3001" - export function AgentDemoPreview({ agentId = "web-agent", - align = "center", + align = "start", replayable = true, }: { agentId?: string align?: "center" | "start" | "end" replayable?: boolean }) { - const embedUrl = `${demoBaseUrl.replace(/\/$/, "")}/embed/${agentId}` - return ( +
+ +
} source={
@@ -48,11 +43,11 @@ export function AgentDemoPreview({ __bun__="bunx agentcn@latest add web-agent" />

- Run the demo app locally (requires API keys in{" "} + Run the demo app locally for a live agent with your API keys in{" "} .env.local - ): + :

diff --git a/apps/web/components/agent-demo/agent-demo-browser-panel.tsx b/apps/web/components/agent-demo/agent-demo-browser-panel.tsx
new file mode 100644
index 0000000..30fc3d9
--- /dev/null
+++ b/apps/web/components/agent-demo/agent-demo-browser-panel.tsx
@@ -0,0 +1,121 @@
+"use client"
+
+import * as React from "react"
+import { Globe, Loader2, Lock } from "lucide-react"
+
+import type { DemoBrowserViewPart } from "@/lib/agent-demos/types"
+import { cn } from "@/lib/utils"
+import { Badge } from "@/components/ui/badge"
+
+export function AgentDemoBrowserPanel({
+  view,
+  isReplaying = false,
+}: {
+  view: DemoBrowserViewPart
+  isReplaying?: boolean
+}) {
+  const [stepIndex, setStepIndex] = React.useState(
+    isReplaying ? -1 : view.navigationSteps.length - 1
+  )
+  const [showContent, setShowContent] = React.useState(!isReplaying)
+
+  React.useEffect(() => {
+    if (!isReplaying) {
+      setStepIndex(view.navigationSteps.length - 1)
+      setShowContent(true)
+      return
+    }
+
+    setStepIndex(-1)
+    setShowContent(false)
+
+    const timers: number[] = []
+    view.navigationSteps.forEach((_step, index) => {
+      timers.push(
+        window.setTimeout(() => {
+          setStepIndex(index)
+        }, (index + 1) * 700)
+      )
+    })
+
+    timers.push(
+      window.setTimeout(() => {
+        setShowContent(true)
+      }, view.navigationSteps.length * 700 + 400)
+    )
+
+    return () => {
+      timers.forEach((id) => window.clearTimeout(id))
+    }
+  }, [isReplaying, view.navigationSteps])
+
+  const isNavigating = isReplaying && !showContent
+  const currentStep =
+    stepIndex >= 0 ? view.navigationSteps[stepIndex] : "Starting browser session…"
+
+  return (
+    
+
+
+ + + +
+ + {view.url} +
+
+ +
+
+ {isNavigating ? ( + + ) : ( + + )} + {currentStep} +
+ + {view.provider} + +
+ +
+
+

+ {view.pageTitle} +

+

{view.url}

+
+ +
+ {view.pageContent.map((item) => ( +
+

+ {item.heading} +

+

+ {item.detail} +

+
+ ))} +
+
+
+ +

+ Simulated preview. Real agent uses{" "} + {view.provider} to + open live browser sessions and return results + artifacts. +

+
+ ) +} diff --git a/apps/web/components/agent-demo/agent-demo-formatted-text.tsx b/apps/web/components/agent-demo/agent-demo-formatted-text.tsx new file mode 100644 index 0000000..08c2ed3 --- /dev/null +++ b/apps/web/components/agent-demo/agent-demo-formatted-text.tsx @@ -0,0 +1,126 @@ +"use client" + +import Link from "next/link" +import type { ReactNode } from "react" + +type InlineNode = + | { type: "text"; value: string } + | { type: "bold"; value: string } + | { type: "link"; label: string; href: string } + +function parseInline(text: string): InlineNode[] { + const nodes: InlineNode[] = [] + const pattern = /(\*\*[^*]+\*\*|\[[^\]]+\]\([^)]+\))/g + let lastIndex = 0 + let match: RegExpExecArray | null + + while ((match = pattern.exec(text)) !== null) { + if (match.index > lastIndex) { + nodes.push({ type: "text", value: text.slice(lastIndex, match.index) }) + } + + const token = match[0] + if (token.startsWith("**")) { + nodes.push({ type: "bold", value: token.slice(2, -2) }) + } else { + const linkMatch = token.match(/^\[([^\]]+)\]\(([^)]+)\)$/) + if (linkMatch) { + nodes.push({ + type: "link", + label: linkMatch[1], + href: linkMatch[2], + }) + } else { + nodes.push({ type: "text", value: token }) + } + } + + lastIndex = match.index + token.length + } + + if (lastIndex < text.length) { + nodes.push({ type: "text", value: text.slice(lastIndex) }) + } + + return nodes.length > 0 ? nodes : [{ type: "text", value: text }] +} + +function InlineContent({ text }: { text: string }) { + return ( + <> + {parseInline(text).map((node, index) => { + if (node.type === "bold") { + return ( + + {node.value} + + ) + } + + if (node.type === "link") { + return ( + + {node.label} + + ) + } + + return {node.value} + })} + + ) +} + +export function AgentDemoFormattedText({ text }: { text: string }) { + const lines = text.split("\n") + const blocks: ReactNode[] = [] + let listItems: string[] = [] + + const flushList = () => { + if (listItems.length === 0) return + blocks.push( +
    + {listItems.map((item, index) => ( +
  • + + + + +
  • + ))} +
+ ) + listItems = [] + } + + for (const line of lines) { + const trimmed = line.trim() + + if (!trimmed) { + flushList() + continue + } + + if (trimmed.startsWith("•")) { + listItems.push(trimmed.replace(/^•\s*/, "")) + continue + } + + flushList() + blocks.push( +

+ +

+ ) + } + + flushList() + + return
{blocks}
+} diff --git a/apps/web/components/agent-demo/agent-demo-input.tsx b/apps/web/components/agent-demo/agent-demo-input.tsx new file mode 100644 index 0000000..84cddfc --- /dev/null +++ b/apps/web/components/agent-demo/agent-demo-input.tsx @@ -0,0 +1,74 @@ +"use client" + +import { ArrowUp, Loader2 } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" + +export function AgentDemoInput({ + value, + onChange, + onSubmit, + disabled, + hint, + isLoading, +}: { + value: string + onChange: (value: string) => void + onSubmit: () => void + disabled?: boolean + hint?: string + isLoading?: boolean +}) { + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + onSubmit() + } + + return ( +
+ {hint ? ( +

+ {hint} +

+ ) : null} +
+
+