Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .github/assets/hero-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/assets/hero-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
<a href="https://chromewebstore.google.com/detail/reins/hnjcfgochepemjndccfblpmfmlblkofo"><img src="https://img.shields.io/chrome-web-store/v/hnjcfgochepemjndccfblpmfmlblkofo" alt="Chrome Web Store"></a>
</p>

<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".github/assets/hero-dark.png">
<img src=".github/assets/hero-light.png" alt="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" width="820">
</picture>
</p>

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
Expand Down
6 changes: 5 additions & 1 deletion packages/protocol/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
29 changes: 29 additions & 0 deletions packages/web/public/llms.txt
Original file line number Diff line number Diff line change
@@ -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)
Binary file modified packages/web/public/og.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions packages/web/public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
User-agent: *
Allow: /

Sitemap: https://reins.tech/sitemap.xml
15 changes: 15 additions & 0 deletions packages/web/public/sitemap.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://reins.tech/</loc></url>
<url><loc>https://reins.tech/docs</loc></url>
<url><loc>https://reins.tech/docs/sideload</loc></url>
<url><loc>https://reins.tech/docs/commands</loc></url>
<url><loc>https://reins.tech/docs/permissions</loc></url>
<url><loc>https://reins.tech/docs/architecture</loc></url>
<url><loc>https://reins.tech/docs/security</loc></url>
<url><loc>https://reins.tech/docs/comparison</loc></url>
<url><loc>https://reins.tech/docs/faq</loc></url>
<url><loc>https://reins.tech/changelog</loc></url>
<url><loc>https://reins.tech/changelog/extension</loc></url>
<url><loc>https://reins.tech/privacy</loc></url>
</urlset>
6 changes: 3 additions & 3 deletions packages/web/src/components/docs-toc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ export function DocsToc({ contentRef, className }: DocsTocProps) {
<div
ref={thumbRef}
className={cn(
"absolute w-full bg-primary",
"absolute w-full bg-primary dark:bg-violet-300",
"top-[var(--toc-top)] h-[var(--toc-height)]",
"transition-[top,height] duration-150 ease-linear",
"data-[hidden=true]:opacity-0",
Expand All @@ -263,8 +263,8 @@ export function DocsToc({ contentRef, className }: DocsTocProps) {
}}
style={{ paddingInlineStart: getItemOffset(heading.level) }}
className={cn(
"relative block py-1.5 text-xs leading-snug transition-colors hover:text-foreground",
isActive ? "font-medium text-primary" : "text-muted-foreground",
"relative block py-1.5 text-xs/5 transition-colors hover:text-foreground",
isActive ? "text-primary dark:text-violet-300" : "text-muted-foreground",
)}
>
{heading.text}
Expand Down
167 changes: 167 additions & 0 deletions packages/web/src/components/hero.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<span
className={`absolute z-20 rounded-md bg-primary px-1.5 py-0.5 font-mono text-[0.6875rem] leading-none text-primary-foreground shadow-sm ${className ?? ""}`}
>
{id}
</span>
);
}

/* Small arrow cursor, positioned to look like it is about to click e7. */
function Cursor({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 16 16"
className={`absolute z-20 size-4 drop-shadow-md ${className ?? ""}`}
aria-hidden="true"
>
<path
d="M1 1l4.5 13 2.2-5.3L13 6.5 1 1z"
fill="white"
stroke="black"
strokeWidth="1"
strokeLinejoin="round"
/>
</svg>
);
}

export function Hero() {
return (
<section className="relative isolate overflow-hidden py-20 sm:py-24">
<div aria-hidden="true" className="hero-grid pointer-events-none absolute inset-0 -z-10" />
<div className="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
<div className="mx-auto max-w-3xl text-center">
<a
href={GITHUB_URL}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 rounded-full border border-border bg-foreground/2 py-1 pr-3 pl-1.5 text-sm text-muted-foreground transition-colors hover:bg-foreground/5 hover:text-foreground"
>
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
Open source
</span>
MIT licensed, runs on 127.0.0.1
</a>
<h1 className="mx-auto mt-6 max-w-[20ch] font-display text-5xl font-semibold tracking-tight text-balance sm:text-6xl">
Hand your agent the browser you already use
</h1>
<p className="mx-auto mt-5 max-w-[52ch] text-lg text-pretty text-muted-foreground">
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.
</p>
<div className="mt-8 flex flex-wrap items-center justify-center gap-4">
<Button asChild size="lg">
<Link to="/docs">Get started</Link>
</Button>
<Button asChild size="lg" variant="ghost">
<a href={GITHUB_URL} target="_blank" rel="noreferrer">
View on GitHub
</a>
</Button>
</div>
</div>

{/* Browser window mock being driven by an agent. */}
<div className="relative mx-auto mt-16 max-w-5xl">
{/* Soft radial glow (blurred blob, not a rectangle). */}
<div
aria-hidden="true"
className="pointer-events-none absolute -top-10 left-1/2 -z-10 h-64 w-4/5 -translate-x-1/2 rounded-[50%] bg-primary/25 blur-[110px]"
/>
<div className="rounded-xl border border-border bg-card shadow-2xl dark:shadow-none dark:inset-ring dark:inset-ring-white/10">
{/* Chrome bar */}
<div className="flex items-center gap-3 rounded-t-xl border-b border-border px-4 py-2.5">
<div className="flex gap-1.5">
<span className="size-2.5 rounded-full bg-[#ff5f57]" />
<span className="size-2.5 rounded-full bg-[#febc2e]" />
<span className="size-2.5 rounded-full bg-[#28c840]" />
</div>
<div className="mx-auto flex w-full max-w-sm items-center gap-2 rounded-md bg-foreground/5 px-3 py-1 font-mono text-xs text-muted-foreground">
<Globe className="size-3.5 shrink-0" aria-hidden="true" />
app.acme.com/checkout
</div>
<div className="w-11 shrink-0" />
</div>
{/* Debugging banner (mirrors Chrome's native one) */}
<div className="flex items-center gap-2 border-b border-amber-500/20 bg-amber-500/10 px-4 py-1.5 text-xs text-amber-700 dark:text-amber-400">
<span className="size-1.5 shrink-0 rounded-full bg-current" />
<span className="truncate">reins is controlling this browser</span>
</div>
{/* Page body */}
<div className="relative overflow-hidden rounded-b-xl">
{/* faux site chrome */}
<div className="flex items-center justify-between border-b border-border px-6 py-3">
<div className="flex items-center gap-2">
<span className="size-4 rounded bg-foreground/15" />
<span className="h-2 w-14 rounded-full bg-foreground/10" />
</div>
<div className="flex items-center gap-4 max-sm:hidden">
<span className="h-2 w-10 rounded-full bg-foreground/10" />
<span className="h-2 w-10 rounded-full bg-foreground/10" />
<span className="h-5 w-14 rounded-md bg-foreground/10" />
</div>
</div>

<div className="grid gap-8 p-8 sm:p-12 lg:min-h-88 lg:grid-cols-2 lg:items-center">
{/* the page's sign-in form */}
<div className="mx-auto w-full max-w-xs">
<p className="text-lg font-semibold">Sign in to Dashboard</p>
<p className="mt-1 text-sm text-muted-foreground">Welcome back.</p>
<div className="mt-6">
<p className="mb-1.5 text-xs font-medium text-muted-foreground">Email</p>
<div className="relative rounded-lg border border-primary/50 px-3 py-2 text-left font-mono text-xs ring-2 ring-primary/25">
<RefChip id="e3" className="-top-2.5 right-2" />
you@work.dev
</div>
</div>
<div className="mt-4">
<p className="mb-1.5 text-xs font-medium text-muted-foreground">Password</p>
<div className="rounded-lg border border-border px-3 py-2 text-left font-mono text-xs text-muted-foreground">
••••••••••
</div>
</div>
<div className="relative mt-6">
<div className="relative rounded-lg bg-primary px-3 py-2.5 text-center text-sm font-medium text-primary-foreground ring-2 ring-primary/30">
<RefChip id="e7" className="-top-2.5 right-2 bg-neutral-950 text-white" />
Sign in
</div>
<Cursor className="right-6 -bottom-3" />
</div>
</div>

{/* the agent session driving it */}
<div className="lg:pl-4">
<AnimatedTerminal
title="agent session"
lines={AGENT_PANEL_LINES}
startDelay={600}
className="shadow-xl"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
8 changes: 4 additions & 4 deletions packages/web/src/components/terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ export function Terminal({
return (
<div
className={cn(
"overflow-hidden rounded-xl bg-neutral-950 shadow-xl ring-1 ring-black/10 dark:shadow-none dark:ring-white/10",
"overflow-hidden rounded-xl bg-neutral-950 shadow-xl ring-1 ring-black/10 dark:bg-neutral-900 dark:shadow-none dark:ring-white/10",
className,
)}
>
<div className="flex items-center gap-2 border-b border-white/10 px-4 py-3">
<span aria-hidden="true" className="size-2.5 rounded-full bg-white/20" />
<span aria-hidden="true" className="size-2.5 rounded-full bg-white/20" />
<span aria-hidden="true" className="size-2.5 rounded-full bg-white/20" />
<span aria-hidden="true" className="size-2.5 rounded-full bg-[#ff5f57]" />
<span aria-hidden="true" className="size-2.5 rounded-full bg-[#febc2e]" />
<span aria-hidden="true" className="size-2.5 rounded-full bg-[#28c840]" />
<p className="ml-2 font-mono text-xs text-neutral-500">{title}</p>
</div>
<div className="overflow-x-auto p-4 font-mono text-[0.8125rem]/6 whitespace-pre text-neutral-300">
Expand Down
28 changes: 28 additions & 0 deletions packages/web/src/lib/seo.ts
Original file line number Diff line number Diff line change
@@ -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 }],
};
}
9 changes: 9 additions & 0 deletions packages/web/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
10 changes: 9 additions & 1 deletion packages/web/src/routes/changelog/extension.tsx
Original file line number Diff line number Diff line change
@@ -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,
});

Expand Down
10 changes: 9 additions & 1 deletion packages/web/src/routes/changelog/index.tsx
Original file line number Diff line number Diff line change
@@ -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,
});

Expand Down
10 changes: 9 additions & 1 deletion packages/web/src/routes/docs/architecture.tsx
Original file line number Diff line number Diff line change
@@ -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,
});

Expand Down
Loading
Loading