diff --git a/.github/assets/hero-dark.png b/.github/assets/hero-dark.png new file mode 100644 index 0000000..88ccf87 Binary files /dev/null and b/.github/assets/hero-dark.png differ diff --git a/.github/assets/hero-light.png b/.github/assets/hero-light.png new file mode 100644 index 0000000..9724c12 Binary files /dev/null and b/.github/assets/hero-light.png differ diff --git a/README.md b/README.md index c5af502..7d6b555 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,13 @@ Chrome Web Store

+

+ + + An agent driving a real browser through reins: a login page with e3/e7 element refs, and an agent session running reins snapshot then reins click --ref e7 + +

+ reins gives agents (Claude Code, Cursor, Codex, Copilot, … — anything with a shell) full control of your actual, logged-in Chromium browsers — Chrome, Dia, Brave, Edge, Arc — through a CLI and a Manifest V3 extension. No MCP diff --git a/packages/protocol/tsdown.config.ts b/packages/protocol/tsdown.config.ts index 5184815..37b4aa9 100644 --- a/packages/protocol/tsdown.config.ts +++ b/packages/protocol/tsdown.config.ts @@ -4,7 +4,11 @@ export default defineConfig({ entry: ["src/index.ts"], format: ["esm"], dts: true, - clean: true, + // Never clean: in `pnpm dev`, tsdown --watch would otherwise wipe dist/ on + // startup while the extension/cli builds resolve @reins/protocol from it in + // parallel, racing to "failed to resolve import". The single fixed entry + // (src/index.ts → index.js/index.d.ts) overwrites in place, so no orphans. + clean: false, // Emit .js/.d.ts (not .mjs/.d.mts) so the package's exports map and the // mcp package's bin paths resolve. Valid because every package is "type":"module". fixedExtension: false, diff --git a/packages/web/public/llms.txt b/packages/web/public/llms.txt new file mode 100644 index 0000000..166d3bd --- /dev/null +++ b/packages/web/public/llms.txt @@ -0,0 +1,29 @@ +# reins + +> reins lets coding agents (Claude Code, Cursor, Codex, anything with a shell) drive the user's real, logged-in Chromium browser through a local CLI, an auto-spawned daemon, and a Manifest V3 browser extension. Everything runs on 127.0.0.1 — no cloud, no telemetry. Open source, MIT licensed. + +Key facts: + +- Install: `npm i -g @karnstack/reins`, then add the reins extension from the Chrome Web Store in any Chromium browser (Chrome, Brave, Edge, Arc, Dia). +- Teach an agent: `npx skills add karnstack/reins`; agents without skill support can read `reins help`. +- Core loop: `reins snapshot` (interactive elements with refs) → `reins click --ref e7` / `reins type --ref e3 --text "…"` → `reins text` or `reins screenshot` to verify. +- Site permissions: every site resolves to a per-site tier — deny, read-only, or full — enforced inside the extension; grants happen only via a click in the extension popup. +- Security: daemon binds 127.0.0.1 with Host-header validation; only allowlisted chrome-extension:// origins may connect; Chrome shows its native debug banner while attached. + +## Docs + +- [Getting started](https://reins.tech/docs): install the CLI and extension, teach your agent, verify the loop +- [Commands](https://reins.tech/docs/commands): full command reference — tabs, snapshot, click, type, fill, screenshot, console, network, eval, cdp +- [Site permissions](https://reins.tech/docs/permissions): per-site deny/read/full tiers and how grants work +- [Architecture](https://reins.tech/docs/architecture): how the CLI, daemon, and extension fit together +- [Security](https://reins.tech/docs/security): the localhost-only security model +- [Install without the store](https://reins.tech/docs/sideload): stage the bundled extension for Chrome's Load unpacked +- [How it compares](https://reins.tech/docs/comparison): reins vs agent-browser, dev3000, and playwright-mcp +- [FAQ](https://reins.tech/docs/faq): common questions + +## Optional + +- [Changelog](https://reins.tech/changelog): release notes for the CLI and extension +- [Privacy policy](https://reins.tech/privacy): nothing leaves your machine +- [GitHub repository](https://github.com/karnstack/reins): source for the CLI, daemon, extension, and this site +- [npm package](https://www.npmjs.com/package/@karnstack/reins) diff --git a/packages/web/public/og.png b/packages/web/public/og.png index 4174dc8..eb5a437 100644 Binary files a/packages/web/public/og.png and b/packages/web/public/og.png differ diff --git a/packages/web/public/robots.txt b/packages/web/public/robots.txt new file mode 100644 index 0000000..b3ca936 --- /dev/null +++ b/packages/web/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://reins.tech/sitemap.xml diff --git a/packages/web/public/sitemap.xml b/packages/web/public/sitemap.xml new file mode 100644 index 0000000..482d107 --- /dev/null +++ b/packages/web/public/sitemap.xml @@ -0,0 +1,15 @@ + + + https://reins.tech/ + https://reins.tech/docs + https://reins.tech/docs/sideload + https://reins.tech/docs/commands + https://reins.tech/docs/permissions + https://reins.tech/docs/architecture + https://reins.tech/docs/security + https://reins.tech/docs/comparison + https://reins.tech/docs/faq + https://reins.tech/changelog + https://reins.tech/changelog/extension + https://reins.tech/privacy + diff --git a/packages/web/src/components/docs-toc.tsx b/packages/web/src/components/docs-toc.tsx index f3e4389..677663a 100644 --- a/packages/web/src/components/docs-toc.tsx +++ b/packages/web/src/components/docs-toc.tsx @@ -238,7 +238,7 @@ export function DocsToc({ contentRef, className }: DocsTocProps) {
{heading.text} diff --git a/packages/web/src/components/hero.tsx b/packages/web/src/components/hero.tsx new file mode 100644 index 0000000..a7fae5d --- /dev/null +++ b/packages/web/src/components/hero.tsx @@ -0,0 +1,167 @@ +import { Link } from "@tanstack/react-router"; +import { Globe } from "lucide-react"; +import { AnimatedTerminal, output, prompt } from "@/components/animated-terminal"; +import { Button } from "@/components/ui/button"; + +const GITHUB_URL = "https://github.com/karnstack/reins"; + +/* The agent transcript shown inside the browser mock — kept short so it fits + the docked panel and mirrors the highlighted e3/e7 refs on the page. */ +const AGENT_PANEL_LINES = [ + prompt("reins snapshot"), + output(' e3: input "Email"\n e7: button "Sign in"'), + prompt("reins click --ref e7"), + output(" ✓ clicked e7"), +]; + +function RefChip({ id, className }: { id: string; className?: string }) { + return ( + + {id} + + ); +} + +/* Small arrow cursor, positioned to look like it is about to click e7. */ +function Cursor({ className }: { className?: string }) { + return ( + + ); +} + +export function Hero() { + return ( +
+
+ ); +} diff --git a/packages/web/src/components/terminal.tsx b/packages/web/src/components/terminal.tsx index ede8036..6ab3b44 100644 --- a/packages/web/src/components/terminal.tsx +++ b/packages/web/src/components/terminal.tsx @@ -13,14 +13,14 @@ export function Terminal({ return (
-
diff --git a/packages/web/src/lib/seo.ts b/packages/web/src/lib/seo.ts new file mode 100644 index 0000000..d242478 --- /dev/null +++ b/packages/web/src/lib/seo.ts @@ -0,0 +1,28 @@ +export const SITE_URL = "https://reins.tech"; + +/** + * Per-page head tags: title, description, canonical, and Open Graph + * overrides. Root-level tags (og:image, twitter:card, site defaults) live in + * __root.tsx; TanStack dedupes by name/property so these win per page. + */ +export function seo({ + title, + description, + path, +}: { + title: string; + description: string; + path: string; +}) { + const url = path === "/" ? SITE_URL : `${SITE_URL}${path}`; + return { + meta: [ + { title }, + { name: "description", content: description }, + { property: "og:title", content: title }, + { property: "og:description", content: description }, + { property: "og:url", content: url }, + ], + links: [{ rel: "canonical", href: url }], + }; +} diff --git a/packages/web/src/routes/__root.tsx b/packages/web/src/routes/__root.tsx index 3baf351..c6206fd 100644 --- a/packages/web/src/routes/__root.tsx +++ b/packages/web/src/routes/__root.tsx @@ -17,9 +17,18 @@ export const Route = createRootRoute({ { property: "og:title", content: TITLE }, { property: "og:description", content: DESCRIPTION }, { property: "og:type", content: "website" }, + { property: "og:site_name", content: "reins" }, { property: "og:url", content: SITE_URL }, { property: "og:image", content: `${SITE_URL}/og.png` }, + { property: "og:image:width", content: "1200" }, + { property: "og:image:height", content: "630" }, + { + property: "og:image:alt", + content: "reins — drive your real browser from your coding agent", + }, { name: "twitter:card", content: "summary_large_image" }, + { name: "theme-color", media: "(prefers-color-scheme: light)", content: "#ffffff" }, + { name: "theme-color", media: "(prefers-color-scheme: dark)", content: "#0a0a0a" }, ], links: [ { rel: "icon", href: "/favicon.svg", type: "image/svg+xml" }, diff --git a/packages/web/src/routes/changelog/extension.tsx b/packages/web/src/routes/changelog/extension.tsx index 3d4ae43..c6e6984 100644 --- a/packages/web/src/routes/changelog/extension.tsx +++ b/packages/web/src/routes/changelog/extension.tsx @@ -1,8 +1,16 @@ import { createFileRoute } from "@tanstack/react-router"; import { ReleaseList } from "@/components/release-list"; +import { seo } from "@/lib/seo"; export const Route = createFileRoute("/changelog/extension")({ - head: () => ({ meta: [{ title: "Extension changelog · reins" }] }), + head: () => ({ + ...seo({ + title: "Extension changelog · reins", + description: + "Release notes for the reins Chrome extension, straight from the package changelog.", + path: "/changelog/extension", + }), + }), component: ExtensionChangelogPage, }); diff --git a/packages/web/src/routes/changelog/index.tsx b/packages/web/src/routes/changelog/index.tsx index c0a89f7..38998a1 100644 --- a/packages/web/src/routes/changelog/index.tsx +++ b/packages/web/src/routes/changelog/index.tsx @@ -1,8 +1,16 @@ import { createFileRoute } from "@tanstack/react-router"; import { ReleaseList } from "@/components/release-list"; +import { seo } from "@/lib/seo"; export const Route = createFileRoute("/changelog/")({ - head: () => ({ meta: [{ title: "Changelog · reins" }] }), + head: () => ({ + ...seo({ + title: "Changelog · reins", + description: + "Release notes for the reins CLI (@karnstack/reins), straight from the package changelog.", + path: "/changelog", + }), + }), component: CliChangelogPage, }); diff --git a/packages/web/src/routes/docs/architecture.tsx b/packages/web/src/routes/docs/architecture.tsx index a484434..e28b3b0 100644 --- a/packages/web/src/routes/docs/architecture.tsx +++ b/packages/web/src/routes/docs/architecture.tsx @@ -1,8 +1,16 @@ import { createFileRoute, Link } from "@tanstack/react-router"; import { Fragment } from "react"; +import { seo } from "@/lib/seo"; export const Route = createFileRoute("/docs/architecture")({ - head: () => ({ meta: [{ title: "Architecture · reins" }] }), + head: () => ({ + ...seo({ + title: "Architecture · reins", + description: + "How the reins CLI, local daemon, and Chrome extension fit together — one WebSocket on 127.0.0.1, no cloud, nothing to keep running.", + path: "/docs/architecture", + }), + }), component: ArchitecturePage, }); diff --git a/packages/web/src/routes/docs/commands.tsx b/packages/web/src/routes/docs/commands.tsx index a3446a1..17df028 100644 --- a/packages/web/src/routes/docs/commands.tsx +++ b/packages/web/src/routes/docs/commands.tsx @@ -1,9 +1,17 @@ import { createFileRoute } from "@tanstack/react-router"; import { Fragment } from "react"; import { CodeBlock } from "@/components/code-block"; +import { seo } from "@/lib/seo"; export const Route = createFileRoute("/docs/commands")({ - head: () => ({ meta: [{ title: "Commands · reins" }] }), + head: () => ({ + ...seo({ + title: "Commands · reins", + description: + "The full reins command reference: tabs, snapshot, click, type, fill, screenshot, console, network, eval, and raw Chrome DevTools Protocol access.", + path: "/docs/commands", + }), + }), component: CommandsPage, }); diff --git a/packages/web/src/routes/docs/comparison.tsx b/packages/web/src/routes/docs/comparison.tsx index 547cd47..e369c7d 100644 --- a/packages/web/src/routes/docs/comparison.tsx +++ b/packages/web/src/routes/docs/comparison.tsx @@ -1,7 +1,15 @@ import { createFileRoute } from "@tanstack/react-router"; +import { seo } from "@/lib/seo"; export const Route = createFileRoute("/docs/comparison")({ - head: () => ({ meta: [{ title: "How it compares · reins" }] }), + head: () => ({ + ...seo({ + title: "How it compares · reins", + description: + "How reins compares to agent-browser, dev3000, and playwright-mcp — and when to pick each for agent-driven browser automation.", + path: "/docs/comparison", + }), + }), component: ComparisonPage, }); diff --git a/packages/web/src/routes/docs/faq.tsx b/packages/web/src/routes/docs/faq.tsx index 70d98a4..e77c344 100644 --- a/packages/web/src/routes/docs/faq.tsx +++ b/packages/web/src/routes/docs/faq.tsx @@ -5,9 +5,31 @@ import { AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; +import { seo } from "@/lib/seo"; export const Route = createFileRoute("/docs/faq")({ - head: () => ({ meta: [{ title: "FAQ · reins" }] }), + head: () => ({ + ...seo({ + title: "FAQ · reins", + description: + "Answers to common questions about reins: supported browsers and agents, the local-only daemon, Chrome's debugging banner, and installing without the store.", + path: "/docs/faq", + }), + scripts: [ + { + type: "application/ld+json", + children: JSON.stringify({ + "@context": "https://schema.org", + "@type": "FAQPage", + mainEntity: FAQS.map((faq) => ({ + "@type": "Question", + name: faq.question, + acceptedAnswer: { "@type": "Answer", text: faq.answer }, + })), + }), + }, + ], + }), component: FaqPage, }); @@ -63,11 +85,20 @@ function FaqPage() { return (

FAQ

+

+ Quick answers about how reins works. Anything missing? Ask on{" "} + + GitHub + + . +

{FAQS.map((faq) => ( - {faq.question} - + + {faq.question} + + {faq.answer} diff --git a/packages/web/src/routes/docs/index.tsx b/packages/web/src/routes/docs/index.tsx index 37d76ad..bbef6bc 100644 --- a/packages/web/src/routes/docs/index.tsx +++ b/packages/web/src/routes/docs/index.tsx @@ -1,11 +1,19 @@ import { createFileRoute, Link } from "@tanstack/react-router"; import { CodeBlock } from "@/components/code-block"; +import { seo } from "@/lib/seo"; const CHROME_WEB_STORE_URL = "https://chromewebstore.google.com/detail/reins/hnjcfgochepemjndccfblpmfmlblkofo"; export const Route = createFileRoute("/docs/")({ - head: () => ({ meta: [{ title: "Getting started · reins" }] }), + head: () => ({ + ...seo({ + title: "Getting started · reins", + description: + "Install the reins CLI and Chrome extension, teach your coding agent the command loop, and have it driving your real logged-in browser in minutes.", + path: "/docs", + }), + }), component: GettingStarted, }); diff --git a/packages/web/src/routes/docs/permissions.tsx b/packages/web/src/routes/docs/permissions.tsx index 51f8535..3438b99 100644 --- a/packages/web/src/routes/docs/permissions.tsx +++ b/packages/web/src/routes/docs/permissions.tsx @@ -1,8 +1,16 @@ import { createFileRoute, Link } from "@tanstack/react-router"; import { CodeBlock } from "@/components/code-block"; +import { seo } from "@/lib/seo"; export const Route = createFileRoute("/docs/permissions")({ - head: () => ({ meta: [{ title: "Site permissions · reins" }] }), + head: () => ({ + ...seo({ + title: "Site permissions · reins", + description: + "How reins site permissions work: per-site deny, read-only, or full tiers enforced inside the extension, granted only by a click in the popup.", + path: "/docs/permissions", + }), + }), component: PermissionsPage, }); diff --git a/packages/web/src/routes/docs/security.tsx b/packages/web/src/routes/docs/security.tsx index 9783274..4b6d1ff 100644 --- a/packages/web/src/routes/docs/security.tsx +++ b/packages/web/src/routes/docs/security.tsx @@ -1,7 +1,15 @@ import { createFileRoute, Link } from "@tanstack/react-router"; +import { seo } from "@/lib/seo"; export const Route = createFileRoute("/docs/security")({ - head: () => ({ meta: [{ title: "Security · reins" }] }), + head: () => ({ + ...seo({ + title: "Security · reins", + description: + "The reins security model: localhost-only daemon, DNS-rebinding protection, extension-origin allowlisting, Chrome's native debug banner, and instant disconnect.", + path: "/docs/security", + }), + }), component: SecurityPage, }); diff --git a/packages/web/src/routes/docs/sideload.tsx b/packages/web/src/routes/docs/sideload.tsx index d86efff..77d65c9 100644 --- a/packages/web/src/routes/docs/sideload.tsx +++ b/packages/web/src/routes/docs/sideload.tsx @@ -1,8 +1,16 @@ import { createFileRoute } from "@tanstack/react-router"; import { CodeBlock } from "@/components/code-block"; +import { seo } from "@/lib/seo"; export const Route = createFileRoute("/docs/sideload")({ - head: () => ({ meta: [{ title: "Install without the store · reins" }] }), + head: () => ({ + ...seo({ + title: "Install without the store · reins", + description: + "Install the bundled reins extension without the Chrome Web Store: reins extension stages it for Chrome's Load unpacked, no store access required.", + path: "/docs/sideload", + }), + }), component: SideloadPage, }); diff --git a/packages/web/src/routes/index.tsx b/packages/web/src/routes/index.tsx index ecaf10d..fda145d 100644 --- a/packages/web/src/routes/index.tsx +++ b/packages/web/src/routes/index.tsx @@ -8,33 +8,52 @@ import { ScanSearch, ShieldCheck, } from "lucide-react"; -import { AnimatedTerminal, output, prompt } from "@/components/animated-terminal"; import { CopyCommand } from "@/components/copy-command"; +import { Hero } from "@/components/hero"; import { IdeasSection } from "@/components/ideas-section"; import { PopupMock } from "@/components/popup-mock"; import { Reveal } from "@/components/reveal"; import { SiteFooter } from "@/components/site-footer"; import { SiteHeader } from "@/components/site-header"; import { Button } from "@/components/ui/button"; +import { seo } from "@/lib/seo"; const CHROME_WEB_STORE_URL = "https://chromewebstore.google.com/detail/reins/hnjcfgochepemjndccfblpmfmlblkofo"; export const Route = createFileRoute("/")({ + head: () => ({ + ...seo({ + title: "reins: drive your real browser from your coding agent", + description: + "reins lets coding agents drive the logged-in Chromium browser you already use, through a local CLI, daemon, and extension. Everything stays on 127.0.0.1.", + path: "/", + }), + scripts: [ + { + type: "application/ld+json", + children: JSON.stringify({ + "@context": "https://schema.org", + "@type": "SoftwareApplication", + name: "reins", + applicationCategory: "DeveloperApplication", + operatingSystem: "macOS, Linux, Windows", + description: + "reins lets coding agents drive the logged-in Chromium browser you already use, through a local CLI, daemon, and extension.", + url: "https://reins.tech", + offers: { "@type": "Offer", price: "0", priceCurrency: "USD" }, + license: "https://github.com/karnstack/reins/blob/main/LICENSE", + sameAs: [ + "https://github.com/karnstack/reins", + "https://www.npmjs.com/package/@karnstack/reins", + ], + }), + }, + ], + }), component: LandingPage, }); -const HERO_LINES = [ - prompt("reins tabs"), - output(" b1 chrome tab 12 * Dashboard — localhost:3000"), - prompt("reins snapshot"), - output(' e3: input "Email"\n e7: button "Sign in"'), - prompt('reins type --ref e3 --text "you@work.dev"'), - prompt("reins click --ref e7"), - prompt("reins screenshot"), - output(" ~/.reins/shots/tab-12.png"), -]; - const FEATURES = [ { icon: AppWindow, @@ -96,32 +115,7 @@ function LandingPage() { <>
-
-
-
-

- Take the reins of your real browser -

-

- reins lets coding agents (Claude Code, Cursor, Codex, anything with a shell) drive - the logged-in browser you already use. No debug profile, no launch flags, no MCP - server to register. -

-
- - -
- -
- -
-
+ @@ -296,7 +290,11 @@ function LandingPage() { -
+
+