readme: update roadmap + add MCP and skills (#213)
This commit is contained in:
parent
5cef024c68
commit
90f369ad46
192
README.md
192
README.md
@ -12,13 +12,15 @@ Easy to self-host and extend, but we have a managed version too:
|
||||
|
||||
- [Why Use This](#why-use-this)
|
||||
- [Main SEO Workflows](#main-seo-workflows)
|
||||
- [OpenSEO MCP](#openseo-mcp)
|
||||
- [OpenSEO Skills](#openseo-skills)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Community](#community)
|
||||
- [Pricing / Costs (Free + API costs)](#pricing--costs)
|
||||
- [DataForSEO API Key Setup](#dataforseo-api-key-setup)
|
||||
- [Self-hosting](#self-hosting)
|
||||
- [Cloudflare Self-Hosting](#cloudflare-self-hosting)
|
||||
- [Docker Self Hosting](#docker-self-hosting)
|
||||
- [Cloudflare Self-Hosting](#cloudflare-self-hosting)
|
||||
- [Local Development](#local-development)
|
||||
- [Contributing](#contributing)
|
||||
- [SEO API Cost Reference](#seo-api-cost-reference)
|
||||
@ -30,6 +32,7 @@ Easy to self-host and extend, but we have a managed version too:
|
||||
- Pay only for what you use.
|
||||
- Fork and vibe code your own features.
|
||||
- Focused workflows instead of a bloated, complex SEO suite.
|
||||
- Best in class MCP and AI Skills.
|
||||
|
||||
## Main SEO Workflows
|
||||
|
||||
@ -43,20 +46,122 @@ Easy to self-host and extend, but we have a managed version too:
|
||||
- See who links to your site, which pages attract links, and where links are newly won or lost.
|
||||
- Site Audits
|
||||
- Catch technical issues early so your site is easier for search engines to crawl and rank.
|
||||
- AI brand visibility
|
||||
- See how your brand appears in AI answers, including competitor mentions and source coverage.
|
||||
- AI search prompt explorer
|
||||
- Track and explore the prompts people might use when they ask AI tools for recommendations in your market.
|
||||
|
||||
## OpenSEO MCP
|
||||
|
||||
OpenSEO exposes an MCP server so AI agents can use your SEO data directly.
|
||||
|
||||
Connect Codex, Claude Code, Claude Desktop, or another MCP client to:
|
||||
|
||||
- Run keyword research
|
||||
- Inspect SERPs
|
||||
- Compare domains
|
||||
- Review backlinks
|
||||
- Work through SEO decisions from your editor or chat
|
||||
|
||||
In the app, open **AI & MCP** and copy your MCP server URL. Point your agent at whichever OpenSEO instance you use.
|
||||
|
||||
Hosted app:
|
||||
|
||||
```sh
|
||||
codex mcp add openseo --url https://app.openseo.so/mcp
|
||||
claude mcp add --transport http --scope user openseo https://app.openseo.so/mcp
|
||||
```
|
||||
|
||||
Cloudflare self-hosted:
|
||||
|
||||
```sh
|
||||
codex mcp add openseo --url https://your-openseo-domain.com/mcp
|
||||
claude mcp add --transport http --scope user openseo https://your-openseo-domain.com/mcp
|
||||
```
|
||||
|
||||
Local Docker:
|
||||
|
||||
```sh
|
||||
codex mcp add openseo --url http://localhost:3001/mcp
|
||||
claude mcp add --transport http --scope user openseo http://localhost:3001/mcp
|
||||
```
|
||||
|
||||
Approve the OpenSEO login when your agent asks.
|
||||
|
||||
## OpenSEO Skills
|
||||
|
||||
OpenSEO skills are reusable workflows for Codex and Claude Code. They guide your agent through SEO tasks and can use the OpenSEO MCP for live keyword, SERP, backlink, and domain data.
|
||||
|
||||
### Installation Options
|
||||
|
||||
Install with `skills add`:
|
||||
|
||||
```sh
|
||||
npx skills add every-app/open-seo
|
||||
```
|
||||
|
||||
Auto-accept each OpenSEO skill:
|
||||
|
||||
```sh
|
||||
npx skills add every-app/open-seo --skill '*'
|
||||
```
|
||||
|
||||
Install for Claude Code only:
|
||||
|
||||
```sh
|
||||
npx skills add every-app/open-seo --skill '*' --agent claude-code
|
||||
```
|
||||
|
||||
Install for OpenAI Codex only:
|
||||
|
||||
```sh
|
||||
npx skills add every-app/open-seo --skill '*' --agent codex
|
||||
```
|
||||
|
||||
You can also pick skills directly from the GitHub repo and copy them into your agent's skills folder:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/every-app/open-seo.git
|
||||
|
||||
# Codex
|
||||
mkdir -p ~/.codex/skills
|
||||
cp -R open-seo/.agents/skills/* ~/.codex/skills/
|
||||
|
||||
# Claude Code
|
||||
mkdir -p ~/.claude/skills
|
||||
cp -R open-seo/.agents/skills/* ~/.claude/skills/
|
||||
```
|
||||
|
||||
Start with `/onboarding-checklist`. It will ask about your project and help configure your workspace.
|
||||
|
||||
### Available Skills
|
||||
|
||||
- `onboarding-checklist`
|
||||
- `seo-coach`
|
||||
- `keyword-research`
|
||||
- `keyword-clustering`
|
||||
- `competitive-landscape`
|
||||
- `competitor-analysis`
|
||||
- `link-prospecting`
|
||||
|
||||
## Roadmap
|
||||
|
||||
Top priorities:
|
||||
|
||||
- AI SEO, GEO, LLM Visibility
|
||||
- MCP for Claude
|
||||
- Making the best agentic workflows for SEO
|
||||
- Google Search Console Integration + MCP
|
||||
- Custom Reports for Clients
|
||||
- Improved and Scheduled Site Audits
|
||||
- In App AI Agent
|
||||
- Support Multiple Projects
|
||||
|
||||
Our top priority is always refining the current product and making existing features better based on user feedback.
|
||||
|
||||
If something important is missing, please join the [Discord](https://discord.gg/c9uGs3cFXr) or email me at ben@openseo.so and request it.
|
||||
|
||||
## Community
|
||||
|
||||
Email me: ben@openseo.so
|
||||
|
||||
Join Discord to chat: [Discord](https://discord.gg/c9uGs3cFXr)
|
||||
|
||||
Follow along for updates:
|
||||
@ -99,8 +204,8 @@ printf '%s' 'YOUR_LOGIN:YOUR_PASSWORD' | base64
|
||||
|
||||
OpenSEO supports two self-hosting paths:
|
||||
|
||||
- Docker for your homelab or local use (Recommended).
|
||||
- Cloudflare for use across multiple devices or for your team.
|
||||
- Docker for personal use and testing (Recommended for local use).
|
||||
- Cloudflare for internet-facing self-hosting across multiple devices or for your team.
|
||||
|
||||
_Docker_
|
||||
|
||||
@ -112,6 +217,9 @@ If you love OpenSEO and want to use it across multiple devices or with your team
|
||||
|
||||
## Docker Self Hosting
|
||||
|
||||
> [!WARNING]
|
||||
> By default, the Docker version is intended for local use only. It runs in single-user mode with no authentication. For internet-facing self-hosting, use Cloudflare (free plan compatible). Or read [`docs/SELF_HOSTING_DOCKER.md`](./docs/SELF_HOSTING_DOCKER.md) before exposing to the internet.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Install Docker: https://www.docker.com/products/docker-desktop/
|
||||
@ -140,12 +248,6 @@ Or use a single command:
|
||||
docker compose up -d --pull always
|
||||
```
|
||||
|
||||
Use a pinned version tag in `.env` if preferred:
|
||||
|
||||
```sh
|
||||
OPEN_SEO_IMAGE=ghcr.io/every-app/open-seo:v1.2.3
|
||||
```
|
||||
|
||||
For more info, see [`docs/SELF_HOSTING_DOCKER.md`](./docs/SELF_HOSTING_DOCKER.md).
|
||||
|
||||
## Cloudflare Self-Hosting
|
||||
@ -160,71 +262,7 @@ Reference these docs while deploying since the Cloudflare UI doesn't indicate wh
|
||||
|
||||
## Local Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 20+
|
||||
- [pnpm](https://pnpm.io/)
|
||||
- A DataForSEO account/API credentials
|
||||
|
||||
### Local Development Workflow
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
|
||||
# Run once per fresh local DB
|
||||
pnpm run db:migrate:local
|
||||
```
|
||||
|
||||
Configure .env.local:
|
||||
|
||||
1. `cp .env.example .env.local`
|
||||
2. Add `DATAFORSEO_API_KEY` as a base64-encoded `login:password` value:
|
||||
|
||||
`printf '%s' 'YOUR_LOGIN:YOUR_PASSWORD' | base64`
|
||||
|
||||
Run Locally:
|
||||
|
||||
```sh
|
||||
# Option 1
|
||||
pnpm run dev
|
||||
|
||||
# Option 2 (Recommended)
|
||||
# This log file makes it easier for your coding agent to debug.
|
||||
mkdir .logs
|
||||
touch .logs/dev-server.log
|
||||
|
||||
# This command uses portless, which is great for worktrees. It also pipes logs to that fixed file, which is helpful for agent debugging output.
|
||||
pnpm dev:agents
|
||||
```
|
||||
|
||||
`pnpm dev:agents` runs through [portless](https://github.com/vercel-labs/portless) at `http://open-seo.localhost:1355` by default.
|
||||
|
||||
When using a git worktree, [portless](https://github.com/vercel-labs/portless) prefixes the branch name, for example `http://feature-name.open-seo.localhost:1355`.
|
||||
|
||||
### Database Commands
|
||||
|
||||
Generate migration:
|
||||
|
||||
```sh
|
||||
pnpm run db:generate
|
||||
```
|
||||
|
||||
Migrate local DB:
|
||||
|
||||
```sh
|
||||
pnpm run db:migrate:local
|
||||
```
|
||||
|
||||
### Auth modes
|
||||
|
||||
- `AUTH_MODE=cloudflare_access` (default): validates Cloudflare Access JWTs (`cf-access-jwt-assertion`) using `TEAM_DOMAIN` + `POLICY_AUD`.
|
||||
- `AUTH_MODE=local_noauth`: local trusted mode, no auth check, injects `admin@localhost`.
|
||||
- `AUTH_MODE=hosted`: Better Auth-backed email/password mode. Requires Better Auth schema generation plus `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL`.
|
||||
|
||||
Local scripts (`pnpm dev` and `pnpm dev:agents`) set `AUTH_MODE=local_noauth` automatically.
|
||||
Use `AUTH_MODE=cloudflare_access pnpm dev` when you specifically want to test Access validation locally.
|
||||
|
||||
For Cloudflare deployments, ensure Cloudflare Access is enabled on your Worker route/domain and provide `TEAM_DOMAIN` + `POLICY_AUD` in environment variables.
|
||||
See [`docs/LOCAL_DEVELOPMENT.md`](./docs/LOCAL_DEVELOPMENT.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
67
docs/LOCAL_DEVELOPMENT.md
Normal file
67
docs/LOCAL_DEVELOPMENT.md
Normal file
@ -0,0 +1,67 @@
|
||||
# Local Development
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 20+
|
||||
- [pnpm](https://pnpm.io/)
|
||||
- A DataForSEO account/API credentials
|
||||
|
||||
## Local Development Workflow
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
|
||||
# Run once per fresh local DB
|
||||
pnpm run db:migrate:local
|
||||
```
|
||||
|
||||
Configure `.env.local`:
|
||||
|
||||
1. `cp .env.example .env.local`
|
||||
2. Add `DATAFORSEO_API_KEY` as a base64-encoded `login:password` value:
|
||||
|
||||
`printf '%s' 'YOUR_LOGIN:YOUR_PASSWORD' | base64`
|
||||
|
||||
Run locally:
|
||||
|
||||
```sh
|
||||
# Option 1
|
||||
pnpm run dev
|
||||
|
||||
# Option 2 (Recommended)
|
||||
# This log file makes it easier for your coding agent to debug.
|
||||
mkdir .logs
|
||||
touch .logs/dev-server.log
|
||||
|
||||
# This command uses portless, which is great for worktrees. It also pipes logs to that fixed file, which is helpful for agent debugging output.
|
||||
pnpm dev:agents
|
||||
```
|
||||
|
||||
`pnpm dev:agents` runs through [portless](https://github.com/vercel-labs/portless) at `http://open-seo.localhost:1355` by default.
|
||||
|
||||
When using a git worktree, [portless](https://github.com/vercel-labs/portless) prefixes the branch name, for example `http://feature-name.open-seo.localhost:1355`.
|
||||
|
||||
## Database Commands
|
||||
|
||||
Generate migration:
|
||||
|
||||
```sh
|
||||
pnpm run db:generate
|
||||
```
|
||||
|
||||
Migrate local DB:
|
||||
|
||||
```sh
|
||||
pnpm run db:migrate:local
|
||||
```
|
||||
|
||||
## Auth Modes
|
||||
|
||||
- `AUTH_MODE=cloudflare_access` (default): validates Cloudflare Access JWTs (`cf-access-jwt-assertion`) using `TEAM_DOMAIN` + `POLICY_AUD`.
|
||||
- `AUTH_MODE=local_noauth`: local trusted mode, no auth check, injects `admin@localhost`.
|
||||
- `AUTH_MODE=hosted`: Better Auth-backed email/password mode. Requires Better Auth schema generation plus `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL`.
|
||||
|
||||
Local scripts (`pnpm dev` and `pnpm dev:agents`) set `AUTH_MODE=local_noauth` automatically.
|
||||
Use `AUTH_MODE=cloudflare_access pnpm dev` when you specifically want to test Access validation locally.
|
||||
|
||||
For Cloudflare deployments, ensure Cloudflare Access is enabled on your Worker route/domain and provide `TEAM_DOMAIN` + `POLICY_AUD` in environment variables.
|
||||
136
src/client/features/ai-mcp/SetupControls.tsx
Normal file
136
src/client/features/ai-mcp/SetupControls.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
import { Check, ChevronDown, Copy } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function Collapsible({
|
||||
id,
|
||||
title,
|
||||
subtitle,
|
||||
icon,
|
||||
children,
|
||||
}: {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
icon?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const contentId = `collapsible-${id}`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
aria-expanded={open}
|
||||
aria-controls={contentId}
|
||||
className="flex w-full items-center justify-between gap-3 px-4 py-3.5 text-left transition-colors hover:bg-base-300/50"
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
{icon ? (
|
||||
<span className="flex size-5 shrink-0 items-center justify-center text-base-content">
|
||||
{icon}
|
||||
</span>
|
||||
) : null}
|
||||
<div className="flex min-w-0 flex-col gap-0.5">
|
||||
<span className="text-sm font-medium text-base-content">
|
||||
{title}
|
||||
</span>
|
||||
{subtitle ? (
|
||||
<span className="text-xs text-base-content/55">{subtitle}</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={`size-4 shrink-0 text-base-content/50 transition-transform ${
|
||||
open ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
{open ? (
|
||||
<div id={contentId} className="space-y-4 px-4 pb-6 pt-3">
|
||||
{children}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CodeBlock({ code }: { code: string }) {
|
||||
return (
|
||||
<div className="flex items-stretch overflow-hidden rounded-md border border-base-300 bg-base-100">
|
||||
<pre className="min-w-0 flex-1 overflow-x-auto p-3 text-xs leading-relaxed text-base-content">
|
||||
<code className="font-mono">{code}</code>
|
||||
</pre>
|
||||
<div className="flex shrink-0 items-start border-l border-base-300 p-1.5">
|
||||
<CopyButton
|
||||
value={code}
|
||||
successMessage="Copied to clipboard"
|
||||
iconOnly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CopyButton({
|
||||
value,
|
||||
successMessage,
|
||||
label,
|
||||
iconOnly = false,
|
||||
}: {
|
||||
value: string;
|
||||
successMessage: string;
|
||||
label?: string;
|
||||
iconOnly?: boolean;
|
||||
}) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
|
||||
toast.error("Clipboard not available");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
toast.success(successMessage);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
toast.error("Could not copy to clipboard");
|
||||
}
|
||||
};
|
||||
|
||||
if (iconOnly) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
aria-label="Copy"
|
||||
className="flex size-7 items-center justify-center rounded-md text-base-content/60 transition-colors hover:bg-base-200 hover:text-base-content"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="size-3.5 text-success" />
|
||||
) : (
|
||||
<Copy className="size-3.5" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="inline-flex items-center gap-1.5 rounded-md border border-base-300 bg-base-100 px-2 py-1 text-xs font-medium text-base-content/70 transition-colors hover:bg-base-300/50 hover:text-base-content"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="size-3 text-success" />
|
||||
) : (
|
||||
<Copy className="size-3" />
|
||||
)}
|
||||
{label ?? "Copy"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@ -1,13 +1,38 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ArrowUpRight, Check, ChevronDown, Copy } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
import { ClaudeIcon, CodexIcon } from "@/client/features/ai-mcp/AgentIcons";
|
||||
import { AvailableTools } from "@/client/features/ai-mcp/AvailableTools";
|
||||
import {
|
||||
CodeBlock,
|
||||
Collapsible,
|
||||
CopyButton,
|
||||
} from "@/client/features/ai-mcp/SetupControls";
|
||||
|
||||
const DISCORD_URL = "https://discord.gg/c9uGs3cFXr";
|
||||
const SUPPORT_EMAIL = "ben@openseo.so";
|
||||
const SAM_GITHUB_URL = "https://github.com/every-app/sam";
|
||||
const SKILL_NAMES = [
|
||||
"onboarding-checklist",
|
||||
"seo-coach",
|
||||
"keyword-research",
|
||||
"keyword-clustering",
|
||||
"competitive-landscape",
|
||||
"competitor-analysis",
|
||||
"link-prospecting",
|
||||
];
|
||||
const SKILLS_INSTALL = `npx skills add every-app/open-seo`;
|
||||
const ALL_SKILLS_INSTALL = `npx skills add every-app/open-seo --skill '*'`;
|
||||
const CLAUDE_CODE_SKILLS_INSTALL = `npx skills add every-app/open-seo --skill '*' --agent claude-code`;
|
||||
const CODEX_SKILLS_INSTALL = `npx skills add every-app/open-seo --skill '*' --agent codex`;
|
||||
const SKILLS_MANUAL_INSTALL = `git clone https://github.com/every-app/open-seo.git
|
||||
|
||||
# Codex
|
||||
mkdir -p ~/.codex/skills
|
||||
cp -R open-seo/.agents/skills/* ~/.codex/skills/
|
||||
|
||||
# Claude Code
|
||||
mkdir -p ~/.claude/skills
|
||||
cp -R open-seo/.agents/skills/* ~/.claude/skills/`;
|
||||
|
||||
export const Route = createFileRoute("/_app/ai")({
|
||||
component: AiPage,
|
||||
@ -25,7 +50,7 @@ function AiPage() {
|
||||
<h1 className="text-2xl font-semibold">AI & MCP</h1>
|
||||
<p className="mt-2 text-sm text-base-content/70 leading-relaxed">
|
||||
Connect your AI agent to OpenSEO. Run keyword research, SERP analysis,
|
||||
and domain lookups from your editor or chat.
|
||||
domain lookups, and backlink reviews from your editor or chat.
|
||||
</p>
|
||||
|
||||
<section className="mt-8">
|
||||
@ -45,7 +70,9 @@ function AiPage() {
|
||||
</code>
|
||||
</div>
|
||||
<p className="mt-2.5 text-xs text-base-content/55 leading-relaxed">
|
||||
Paste this into any MCP client. Sign in with OpenSEO when prompted.
|
||||
Paste this into any MCP client. This URL points at the OpenSEO
|
||||
instance you are using now, whether hosted, self-hosted, or local.
|
||||
Sign in with OpenSEO when prompted.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@ -152,6 +179,72 @@ function AiPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-12">
|
||||
<h2 className="text-base font-semibold">OpenSEO Skills</h2>
|
||||
<p className="mt-1.5 text-sm text-base-content/70 leading-relaxed">
|
||||
Skills give Codex and Claude Code reusable SEO workflows that can
|
||||
call your OpenSEO MCP tools when live SERP, keyword, backlink, or
|
||||
domain data is needed.
|
||||
</p>
|
||||
<div className="mt-4 divide-y divide-base-300 overflow-hidden rounded-lg border border-base-300 bg-base-200">
|
||||
<Collapsible
|
||||
id="skills-add"
|
||||
title="Install with skills add"
|
||||
subtitle="Recommended cross-agent installer"
|
||||
>
|
||||
<CodeBlock code={SKILLS_INSTALL} />
|
||||
<p className="text-sm text-base-content/70">
|
||||
You can also auto-accept each OpenSEO skill:
|
||||
</p>
|
||||
<CodeBlock code={ALL_SKILLS_INSTALL} />
|
||||
</Collapsible>
|
||||
<Collapsible
|
||||
id="claude-code-skills"
|
||||
title="Install for Claude Code"
|
||||
subtitle="Target Claude Code only"
|
||||
icon={<ClaudeIcon className="size-5" />}
|
||||
>
|
||||
<CodeBlock code={CLAUDE_CODE_SKILLS_INSTALL} />
|
||||
</Collapsible>
|
||||
<Collapsible
|
||||
id="codex-skills"
|
||||
title="Install for Codex"
|
||||
subtitle="Target OpenAI Codex only"
|
||||
icon={<CodexIcon className="size-5" />}
|
||||
>
|
||||
<CodeBlock code={CODEX_SKILLS_INSTALL} />
|
||||
</Collapsible>
|
||||
<Collapsible
|
||||
id="manual-skills"
|
||||
title="Manual GitHub install"
|
||||
subtitle="Clone the repo and copy the skills"
|
||||
>
|
||||
<CodeBlock code={SKILLS_MANUAL_INSTALL} />
|
||||
</Collapsible>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<p className="text-sm text-base-content/70 leading-relaxed">
|
||||
Start with{" "}
|
||||
<span className="font-mono text-base-content">
|
||||
/onboarding-checklist
|
||||
</span>
|
||||
. It will ask about your project and help configure your
|
||||
workspace.
|
||||
</p>
|
||||
<p className="mt-4 text-xs font-medium uppercase tracking-wide text-base-content/50">
|
||||
Available skills
|
||||
</p>
|
||||
<ul className="mt-2 grid gap-1.5 text-sm text-base-content/70 sm:grid-cols-2">
|
||||
{SKILL_NAMES.map((skill) => (
|
||||
<li key={skill} className="flex gap-2">
|
||||
<span className="text-base-content/35">-</span>
|
||||
<span>{skill}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-12">
|
||||
<h2 className="text-base font-semibold">Available tools</h2>
|
||||
<div className="mt-5">
|
||||
@ -228,136 +321,3 @@ function AiPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Collapsible({
|
||||
id,
|
||||
title,
|
||||
subtitle,
|
||||
icon,
|
||||
children,
|
||||
}: {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
icon?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const contentId = `collapsible-${id}`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
aria-expanded={open}
|
||||
aria-controls={contentId}
|
||||
className="flex w-full items-center justify-between gap-3 px-4 py-3.5 text-left transition-colors hover:bg-base-300/50"
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
{icon ? (
|
||||
<span className="flex size-5 shrink-0 items-center justify-center text-base-content">
|
||||
{icon}
|
||||
</span>
|
||||
) : null}
|
||||
<div className="flex min-w-0 flex-col gap-0.5">
|
||||
<span className="text-sm font-medium text-base-content">
|
||||
{title}
|
||||
</span>
|
||||
{subtitle ? (
|
||||
<span className="text-xs text-base-content/55">{subtitle}</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={`size-4 shrink-0 text-base-content/50 transition-transform ${
|
||||
open ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
{open ? (
|
||||
<div id={contentId} className="space-y-3 px-4 pb-4 pt-1">
|
||||
{children}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CodeBlock({ code }: { code: string }) {
|
||||
return (
|
||||
<div className="flex items-stretch overflow-hidden rounded-md border border-base-300 bg-base-100">
|
||||
<pre className="min-w-0 flex-1 overflow-x-auto p-3 text-xs leading-relaxed text-base-content">
|
||||
<code className="font-mono">{code}</code>
|
||||
</pre>
|
||||
<div className="flex shrink-0 items-start border-l border-base-300 p-1.5">
|
||||
<CopyButton
|
||||
value={code}
|
||||
successMessage="Copied to clipboard"
|
||||
iconOnly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CopyButton({
|
||||
value,
|
||||
successMessage,
|
||||
label,
|
||||
iconOnly = false,
|
||||
}: {
|
||||
value: string;
|
||||
successMessage: string;
|
||||
label?: string;
|
||||
iconOnly?: boolean;
|
||||
}) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
|
||||
toast.error("Clipboard not available");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
toast.success(successMessage);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
toast.error("Could not copy to clipboard");
|
||||
}
|
||||
};
|
||||
|
||||
if (iconOnly) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
aria-label="Copy"
|
||||
className="flex size-7 items-center justify-center rounded-md text-base-content/60 transition-colors hover:bg-base-200 hover:text-base-content"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="size-3.5 text-success" />
|
||||
) : (
|
||||
<Copy className="size-3.5" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="inline-flex items-center gap-1.5 rounded-md border border-base-300 bg-base-100 px-2 py-1 text-xs font-medium text-base-content/70 transition-colors hover:bg-base-300/50 hover:text-base-content"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="size-3 text-success" />
|
||||
) : (
|
||||
<Copy className="size-3" />
|
||||
)}
|
||||
{label ?? "Copy"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user