diff --git a/README.md b/README.md index cac99f4..2fac380 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,9 @@

-reins gives your coding agent (Claude Code, Cursor, Codex, Copilot — anything -with a shell) control of the real, logged-in Chromium browser you already use, -through a CLI and a Manifest V3 extension. No MCP server to register, no debug +reins gives your coding agent control of the real, logged-in Chromium browser +you already use, through a CLI and a Manifest V3 extension. Claude Code, Cursor, +Codex, Copilot: anything with a shell. No MCP server to register, no debug profile, no launch flags, no tokens. ## Quick start @@ -38,35 +38,45 @@ npm i -g @karnstack/reins # the CLI (daemon included, starts on demand) npx skills add karnstack/reins # the skill, into your agent(s) of choice ``` -Then install the extension in every Chromium browser you want agents to reach — -Chrome, Brave, Edge, Arc, Dia: +Then install the extension in every Chromium browser you want agents to reach: +Chrome, Brave, Edge, Arc, Dia. -**[Add reins from the Chrome Web Store](https://chromewebstore.google.com/detail/reins/hnjcfgochepemjndccfblpmfmlblkofo)** — it finds the local daemon and connects on its own. +**[Add reins from the Chrome Web Store](https://chromewebstore.google.com/detail/reins/hnjcfgochepemjndccfblpmfmlblkofo)**. It finds the local daemon and connects on its own. -No store access? `reins extension` installs it via Load unpacked instead — see -[docs/SIDELOAD.md](docs/SIDELOAD.md). That's the whole setup; `reins status` -shows what's connected. +No store access? `reins extension` installs it through Load unpacked instead. +See [docs/SIDELOAD.md](docs/SIDELOAD.md). That is the whole setup, and +`reins status` shows what is connected. ## The loop ```bash -reins snapshot # list interactive elements with refs → e5: button "Submit" -reins click --ref e5 # act by ref -reins text # verify (or: reins screenshot → prints an image path) +reins snapshot # look: interactive elements with refs → e5: button "Submit" +reins click --ref e5 # act: by ref +reins text # check: or reins screenshot, which prints an image path ``` -Every command takes `--tab ` (default: active tab), `--browser ` (only -when several are connected), and `--json`. `reins help` is self-describing; -`reins cdp` is the escape hatch to the full Chrome DevTools Protocol. +Shared flags, on the commands that act on a page or a tab: `--tab ` +(default: active tab), `--browser ` (only when several are connected), and +`--json` for raw output. `reins help` is self-describing, and +`reins cdp` reaches the full Chrome DevTools Protocol when the curated commands +are not enough. + +## Limits + +Chromium only, so no Firefox and no WebKit. No headless mode: reins drives a +browser you already have open, which also makes it the wrong tool for CI. With +two browsers connected, commands need `--browser `, because reins never +guesses which one you meant. Releases are still `0.x`, so commands, flags, and +output can change. ## Learn more The full story lives on the site: -- **[Docs](https://reins.tech/docs)** — getting started, architecture, and the complete command reference -- **[How it compares](https://reins.tech/docs/comparison)** — vs agent-browser, dev3000, and playwright-mcp -- **[Security](https://reins.tech/docs/security)** — per-site permissions, `127.0.0.1`-only binding, and the threat model -- **[Site permissions](https://reins.tech/docs/permissions)** — the `deny` / `read` / `full` tiers and how to tighten them +- **[Docs](https://reins.tech/docs)**: getting started, architecture, and the complete command reference +- **[How it compares](https://reins.tech/docs/comparison)**: reins next to agent-browser, dev3000, and playwright-mcp +- **[Security](https://reins.tech/docs/security)**: per-site permissions, `127.0.0.1`-only binding, and the threat model +- **[Site permissions](https://reins.tech/docs/permissions)**: the `deny`, `read`, and `full` tiers, and how to tighten them Report vulnerabilities privately via [GitHub security advisories](https://github.com/karnstack/reins/security/advisories/new). diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index e78833e..5bef114 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -4,7 +4,7 @@ ### Minor Changes -- 65ce7f3: `reins audit` — a per-action audit trail. The extension stamps each response with the resolved host, permission tier, and tab; the daemon writes one redacted JSONL line per action (policy denials included) to `~/.reins/logs/audit-YYYY-MM-DD.jsonl`, pruned after 30 days. Value-bearing params (typed text, fill values, eval code, CDP payloads) are redacted before anything reaches disk. +- 65ce7f3: `reins audit`, a per-action audit trail. The extension stamps each response with the resolved host, permission tier, and tab; the daemon writes one redacted JSONL line per action (policy denials included) to `~/.reins/logs/audit-YYYY-MM-DD.jsonl`, pruned after 30 days. Value-bearing params (typed text, fill values, eval code, CDP payloads) are redacted before anything reaches disk. ## 0.3.0 @@ -19,7 +19,7 @@ ### Patch Changes -- 6885954: Trust the published Chrome Web Store extension (`hnjcfgochepemjndccfblpmfmlblkofo`) out of the box — store installs connect without `reins allow`. +- 6885954: Trust the published Chrome Web Store extension (`hnjcfgochepemjndccfblpmfmlblkofo`) out of the box. Store installs connect without `reins allow`. ## 0.2.0 @@ -27,7 +27,7 @@ - f6b30a4: New `reins extension` command: install the extension without the Chrome Web Store. The npm package now bundles the extension build with a key-pinned, - pre-allowlisted id — `reins extension` stages it at `~/.reins/extension` for + pre-allowlisted id. `reins extension` stages it at `~/.reins/extension` for Chrome's Load unpacked, no `reins allow` step. See docs/SIDELOAD.md. ### Patch Changes diff --git a/packages/cli/src/bridge.ts b/packages/cli/src/bridge.ts index 000f6d4..56546a9 100644 --- a/packages/cli/src/bridge.ts +++ b/packages/cli/src/bridge.ts @@ -232,14 +232,14 @@ export class BridgeHost implements BridgePort { const entry = this.#browsers.get(browserId); if (!entry || entry.ws.readyState !== WebSocket.OPEN) { throw new Error( - `unknown browserId "${browserId}"${roster ? ` — connected: ${roster}` : " — no browsers connected"}`, + `unknown browserId "${browserId}"${roster ? `. Connected: ${roster}` : ". No browsers connected"}`, ); } return { id: browserId, ws: entry.ws }; } if (live.length === 0) throw new Error("extension not connected"); if (live.length > 1) { - throw new Error(`several browsers connected — pass browserId. Connected: ${roster}`); + throw new Error(`several browsers connected. Pass browserId. Connected: ${roster}`); } const only = live[0] as BrowserInfo; const entry = this.#browsers.get(only.id) as ConnectedBrowser; diff --git a/packages/cli/src/integration.test.ts b/packages/cli/src/integration.test.ts index 2105c53..8626dc7 100644 --- a/packages/cli/src/integration.test.ts +++ b/packages/cli/src/integration.test.ts @@ -78,7 +78,7 @@ function standInExtension(port: number): Promise { error: { code: "policy_denied", message: - "blocked by policy: x.com is read-only — grant full access from the reins extension popup", + "blocked by policy: x.com is read-only. Grant full access from the reins extension popup", }, }), ); diff --git a/packages/cli/src/rpc.test.ts b/packages/cli/src/rpc.test.ts index 73a211e..981c023 100644 --- a/packages/cli/src/rpc.test.ts +++ b/packages/cli/src/rpc.test.ts @@ -90,7 +90,7 @@ describe("listAllTabs", () => { it("errors on an unknown browserId, naming the roster", async () => { const bridge = fakeBridge(); await expect(listAllTabs(bridge, "b9")).rejects.toThrow( - 'unknown browserId "b9" — connected: b1 (Chrome)', + 'unknown browserId "b9". Connected: b1 (Chrome)', ); }); }); diff --git a/packages/cli/src/rpc.ts b/packages/cli/src/rpc.ts index 29c1fb4..2ecfe99 100644 --- a/packages/cli/src/rpc.ts +++ b/packages/cli/src/rpc.ts @@ -14,7 +14,7 @@ export async function listAllTabs(bridge: BridgePort, browserId?: string): Promi const targets = browserId ? bridge.browsers.filter((b) => b.id === browserId) : bridge.browsers; if (browserId !== undefined && targets.length === 0) { const roster = bridge.browsers.map((b) => `${b.id} (${b.browser})`).join(", "); - throw new Error(`unknown browserId "${browserId}"${roster ? ` — connected: ${roster}` : ""}`); + throw new Error(`unknown browserId "${browserId}"${roster ? `. Connected: ${roster}` : ""}`); } const results = await Promise.all( targets.map(async (b) => { diff --git a/packages/extension/CHANGELOG.md b/packages/extension/CHANGELOG.md index 4dc24fd..7f22beb 100644 --- a/packages/extension/CHANGELOG.md +++ b/packages/extension/CHANGELOG.md @@ -4,7 +4,7 @@ ### Minor Changes -- 65ce7f3: `reins audit` — a per-action audit trail. The extension stamps each response with the resolved host, permission tier, and tab; the daemon writes one redacted JSONL line per action (policy denials included) to `~/.reins/logs/audit-YYYY-MM-DD.jsonl`, pruned after 30 days. Value-bearing params (typed text, fill values, eval code, CDP payloads) are redacted before anything reaches disk. +- 65ce7f3: `reins audit`, a per-action audit trail. The extension stamps each response with the resolved host, permission tier, and tab; the daemon writes one redacted JSONL line per action (policy denials included) to `~/.reins/logs/audit-YYYY-MM-DD.jsonl`, pruned after 30 days. Value-bearing params (typed text, fill values, eval code, CDP payloads) are redacted before anything reaches disk. ## 0.3.0 @@ -21,5 +21,5 @@ - f6b30a4: New `reins extension` command: install the extension without the Chrome Web Store. The npm package now bundles the extension build with a key-pinned, - pre-allowlisted id — `reins extension` stages it at `~/.reins/extension` for + pre-allowlisted id. `reins extension` stages it at `~/.reins/extension` for Chrome's Load unpacked, no `reins allow` step. See docs/SIDELOAD.md. diff --git a/packages/extension/src/lib/policy.ts b/packages/extension/src/lib/policy.ts index a5bf7e4..21f83fd 100644 --- a/packages/extension/src/lib/policy.ts +++ b/packages/extension/src/lib/policy.ts @@ -82,7 +82,7 @@ export function tightenPolicy(patternInput: string, tier: Tier): Promise existing?.tier ?? effectiveTier(p, pattern.startsWith("*.") ? pattern.slice(2) : pattern); if (!tighterThan(tier, current)) { throw new PolicyDenied( - `policy_tighten can only restrict: "${pattern}" is already ${current} — grants require the extension popup`, + `policy_tighten can only restrict: "${pattern}" is already ${current}. Grants require the extension popup`, ); } const rules = existing @@ -134,8 +134,8 @@ export async function ensureAllowed(method: GatedMethod, host: string | undefine const label = host ?? "this tab"; const err = new PolicyDenied( tier === "deny" - ? `blocked by policy: ${label} is denied — change its tier from the reins extension popup` - : `blocked by policy: ${label} is read-only — grant full access from the reins extension popup`, + ? `blocked by policy: ${label} is denied. Change its tier from the reins extension popup` + : `blocked by policy: ${label} is read-only. Grant full access from the reins extension popup`, ); err.meta = { host, tier }; throw err; diff --git a/packages/web/package.json b/packages/web/package.json index 0fbba31..99b2072 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "dev": "vite dev --port 3000", - "build": "vite build && pagefind --site dist/client", + "build": "vite build && pagefind --site dist/client && node scripts/check-prose.mjs", + "check:prose": "node scripts/check-prose.mjs", "preview": "vite preview", "typecheck": "tsc --noEmit", "deploy": "wrangler deploy" diff --git a/packages/web/public/404.html b/packages/web/public/404.html index 61bc559..e41e451 100644 --- a/packages/web/public/404.html +++ b/packages/web/public/404.html @@ -4,7 +4,7 @@ - Page not found — reins + Page not found · reins