[
        {
          "id": "blog-2026-08-equipr-cross-agent-skill-manager",
          "title": "equipr: Cross-Agent Skill and MCP Server Manager",
          "collection": {
            "label": "posts",
            "name": "Blog"
          },
          "categories": "Announcements, Release, Tools",
          "tags": "",
          "url": "/blog/2026/08/equipr-cross-agent-skill-manager/",
          "content": "equipr installs skills, commands, and MCP servers from a marketplace or an Agent Plugins source into whichever coding agents are present on a machine: Claude Code, Codex, OpenCode, and Pi. It works through each agent’s own personal config surfaces rather than a plugin system. It is a single Go binary, MIT licensed, and available now via Homebrew or as a standalone install script.\n\nThe problem it addresses\n\nClaude Code has a marketplace mechanism for distributing and updating skills that none of the other three agents come close to matching. Codex, OpenCode, and Pi have no built-in equivalent. The alternatives are copying a skill’s files by hand, which then has to be kept current on its own since nothing does that automatically, or installing an npm package for the skills some maintainers publish that way. The Agent Plugins specification exists to standardize the package format itself, but it does not yet define how any individual agent installs from a source or keeps that installation up to date. That part is still left to each agent’s own tooling, or to something else.\n\nThe same gap shows up again with isolated per-agent containers, of the kind Fragua uses: each container needs the same sources installed, and each one is a separate place that update has to reach.\n\nWhat equipr resolves\n\nequipr treats a source as one of two shapes. A repository with .claude-plugin/marketplace.json at its root is a marketplace holding one or more plugins. A repository with plugin.json at its root is a single Agent Plugins-conformant package. Both resolve to the same internal model, addressed the same way:\n\n&lt;source-id&gt;/&lt;plugin&gt;:&lt;component&gt;\n\n\n$ equipr add https://github.com/coreyhaines31/marketingskills\nFetching https://github.com/coreyhaines31/marketingskills\nAdded marketingskills (marketplace, fetched via git) with 1 plugin(s)\n  - marketing-skills 2.10.0\n\n$ equipr list\nmarketingskills     marketplace\n  marketing-skills  2.10.0  49 skills\n\n$ equipr install marketingskills/marketing-skills:seo-audit --yes\nInstalled 1 component(s) into 3 agent(s):\n  [claude-code] seo-audit (copy -&gt; ~/.claude/skills/seo-audit)\n  [opencode] seo-audit (symlink -&gt; ~/.config/opencode/skills/seo-audit)\n  [pi] seo-audit (symlink -&gt; ~/.pi/agent/skills/seo-audit)\n\n\nequipr status reports whether each recorded install still matches its source. equipr update re-fetches every registered source and re-applies anything that changed. equipr doctor reports broken symlinks and orphaned records.\n\nWhere each agent differs\n\nThe four agents agree on very little beyond the skill file format itself. A SKILL.md is the one point of real convergence across all four. Everywhere else, they differ:\n\n\n  \n    \n      Agent\n      Skills directory\n      MCP config\n    \n  \n  \n    \n      Claude Code\n      ~/.claude/skills/\n      ~/.claude.json\n    \n    \n      Codex\n      ~/.codex/skills/\n      ~/.codex/config.toml\n    \n    \n      OpenCode\n      ~/.config/opencode/skills/\n      ~/.config/opencode/opencode.json\n    \n    \n      Pi\n      ~/.pi/agent/skills/\n      ~/.config/mcp/mcp.json\n    \n  \n\n\nThree different config formats, and OpenCode’s MCP schema is not merely a different filename: it requires an explicit type and a command array where Claude Code infers both from a single string. equipr’s install step translates a source’s MCP configuration into each target’s native shape rather than writing one format everywhere.\n\nInstalling into each agent\n\nequipr places skill and command files where each agent expects them, and merges MCP servers into each agent’s existing config without disturbing anything else already there: no unrelated key is touched, no unrelated setting is reformatted. Installing is the same command regardless of which agent it’s going into; equipr absorbs the difference in how each one stores things.\n\nnpm sources without an npm install\n\nequipr accepts an npm package as a source. It does not run npm install. It runs npm pack, extracts the resulting tarball into its own cache alongside git and archive sources, and reads plugin.json, skills/, commands/, and mcp.json out of the result. No dependency tree is installed alongside it, and nothing registers as a plugin or a global package in the process.\n\nWhat equipr does not do\n\nequipr never registers itself as a native plugin inside any agent: no entry in Claude Code’s /plugin list, no npm-global registration, nothing to unregister if the binary is removed. It writes to personal config directories and stops.\n\nWhat this reinforced\n\nTreating each agent’s personal config surface as the integration point, rather than any agent’s own plugin system, is what let one tool support four incompatible agents from a single codebase. The harder requirement turned out to be the opposite of adding capability: writing less to any given file, and writing it more carefully, specifically the single-key-merge discipline for MCP config. That constraint did more to make the tool trustworthy than any feature did.\n\nInstall\n\ncurl -fsSL https://github.com/maquina-app/equipr/releases/latest/download/install.sh | sh\n\n\nChecksum-verified, installs to /usr/local/bin when writable, ~/.local/bin otherwise. A Homebrew tap is also available: brew install maquina-app/tap/equipr.\n\nThe equipr documentation covers every command, every flag, where each file lands per agent, and exit codes for scripting. Source is on GitHub, MIT licensed.\n\nMaquina’s own Claude Code plugins are distributed as a marketplace equipr can install from: see AI Tools."
        },
        {
          "id": "blog-2026-08-rails-mcp-server-2-0-0-removing-execute-ruby",
          "title": "Why I Removed execute_ruby from Rails MCP Server",
          "collection": {
            "label": "posts",
            "name": "Blog"
          },
          "categories": "Announcements, Release, AI Tools",
          "tags": "",
          "url": "/blog/2026/08/rails-mcp-server-2-0-0-removing-execute-ruby/",
          "content": "I built the first version of Rails MCP Server before Claude Code, Codex, and tools like them were the popular way to work with AI on a codebase. Back then, Claude Desktop could talk to me and read whatever I pasted into the chat. Nothing else. If I wanted it to see a model’s associations, check a route, or run a quick query against my own data, I copied and pasted. Every question meant leaving the conversation, going to the terminal, and bringing the answer back by hand.\n\nThat’s the itch Rails MCP Server scratched: let Claude Desktop introspect a Rails project directly (routes, schema, models) without me being the copy-paste layer between every question and its answer. A companion tool, nvim-mcp-server, closed the other half of the loop, letting Claude Desktop write code changes straight into my Neovim buffers instead of me pasting a diff and applying it by hand.\n\nexecute_ruby made sense inside that setup. Claude Desktop had no way to touch my filesystem or run anything on its own. The MCP server was its only hands. Giving it a tool that could run real Ruby inside my running Rails app meant one call could answer what would otherwise take a dozen round trips: an ad-hoc scope, or a quick Model.find to check a hunch. It was the sharpest tool in the server, and it earned its place.\n\nWhat changed\n\nThat gap doesn’t exist for most people building with AI anymore. Claude Code, Codex, and the agentic coding tools that followed sit directly on top of the codebase. They already have a shell and the same Ruby your app runs on. If an agent wants to check User.where(inactive: true).count, it doesn’t need a special MCP tool for that. It can just run it, the way I would from my own terminal.\n\nThat’s most of the reasoning that justified execute_ruby, gone. The tool wasn’t wrong when I built it. It answered a real constraint, and that constraint has largely gone away.\n\nHardening a tool I was already rethinking\n\nI didn’t arrive here in one step. Yesterday’s 1.6.0 release closed several real bypasses in the execute_ruby sandbox: file reads that slipped past validation and symlinks that walked out of the project, on top of database writes that weren’t actually being rolled back. Today’s 1.6.1 went further: a researcher at Pluto Security responsibly disclosed a command-execution path through require \"pty\", and the fix restricted require to a small allowlist of data-only libraries, cutting off that path along with a few adjacent ones.\n\nBoth releases were worth shipping on their own. But writing 1.6.1 is what made the actual problem visible to me: I was patching individual escapes out of a tool whose entire premise is running caller-supplied Ruby with the privileges of the server process. A static scan and some runtime overrides can raise the floor, but they were never going to be a hard boundary. Path validation, transaction rollback, the confirm_risky gate: every fence I’ve put around execute_ruby has been best-effort, and 1.6.0 said so plainly. Best-effort is a reasonable trade when a tool is filling a real gap. It stops paying off once that’s no longer true.\n\nWhat 2.0.0 does\n\nVersion 2.0.0 removes execute_ruby outright, rather than hardening it again. The server is introspection-only now, and its dedicated analyzers already cover the ground execute_ruby was built for:\n\n\n  \n    \n      Instead of execute_ruby for…\n      Use\n    \n  \n  \n    \n      Reading a file\n      get_file\n    \n    \n      Finding files\n      list_files\n    \n    \n      Routes, schema, models, controllers, env, structure\n      get_routes, get_schema, analyze_models, analyze_controller_views, analyze_environment_config, project_info\n    \n  \n\n\nAd-hoc data queries (a custom scope, a one-off count) are the one thing this doesn’t replace, on purpose. That was always the part of execute_ruby doing the most work and carrying the most risk, and it’s exactly the part an agent sitting on top of your code no longer needs a server tool for.\n\nBootstrap tools drop from four to three: switch_project, search_tools, execute_tool. The internal analyzers haven’t changed. They’re still discovered through search_tools and invoked through execute_tool, same as before.\n\nIf you’re on a client without direct code execution and still want execute_ruby, the 1.6.x line keeps it, now with the 1.6.1 hardening. It isn’t disappearing; it’s just not where the project is headed.\n\nUpgrading\n\ngem update rails-mcp-server\n\n\nIf you have execute_ruby wired into a client’s tool config, remove it from there. Replace file reads with get_file and globs with list_files. Everything else keeps working the way it did.\n\nFor new installations:\n\ngem install rails-mcp-server\nrails-mcp-config\n\n\nWhere this leaves the project\n\nA tool that runs arbitrary code is always going to be the most interesting line in a security report, no matter how many layers sit around it. I’d rather ship a Rails MCP Server that doesn’t have that line at all than one that keeps explaining, release after release, why the latest patch finally closes the gap. What’s gone is the one tool whose risk had stopped being worth what it saved me.\n\nThanks again to Pluto Security for the responsible disclosure that shaped both 1.6.1 and this release. If you find a gap, the issue tracker is open.\n\nLinks\n\n\n  GitHub Repository\n  RubyGems\n  2.0.0 Release Notes\n  1.6.1 Release Notes\n  Security Policy"
        },
        {
          "id": "blog-2026-08-rails-mcp-server-1-6-0-sandbox-hardening",
          "title": "Rails MCP Server 1.6.0: Making \"Read-Only\" Actually Mean Read-Only",
          "collection": {
            "label": "posts",
            "name": "Blog"
          },
          "categories": "Announcements, Release, AI Tools",
          "tags": "",
          "url": "/blog/2026/08/rails-mcp-server-1-6-0-sandbox-hardening/",
          "content": "The execute_ruby tool lets an AI model run Ruby in the context of your Rails application. That is genuinely useful. One call can answer a question that would otherwise take a dozen tool round-trips, and it is also the single most dangerous thing this server does. The tool advertises itself as read-only. Version 1.6.0 is about making that promise closer to true.\n\nThis release started with an uncomfortable review of the sandbox. What I found is that “read-only” had holes you could walk through without trying. So 1.6.0 closes them, adds several layers of defense behind them, and, importantly, is honest about what the sandbox is and isn’t.\n\nWhat the sandbox actually is\n\nWorth stating plainly, because it frames everything below: execute_ruby runs your code through bin/rails runner. That means real Ruby, with your full application loaded and a live database connection. The sandbox is a combination of static analysis (a scan for dangerous patterns) and runtime overrides of File, IO, Dir, and Kernel.\n\nThat is defense-in-depth, not a locked box. It raises the floor; it is not an isolation boundary. Keeping that distinction visible is part of what 1.6.0 fixes. The previous framing implied more safety than the implementation delivered.\n\nThe holes that were closed\n\nFile reads leaked through the siblings. The sandbox overrode File.read and File.open, but not File.readlines, File.binread, File.foreach, or any of the IO equivalents. Those are separate entry points, and they read arbitrary paths:\n\n# Blocked before 1.6.0:\nFile.read(\"/etc/passwd\")     # =&gt; PATH ERROR\n\n# Wide open before 1.6.0:\nIO.read(\"/etc/passwd\")       # read anything\nFile.readlines(\"/etc/passwd\")\n\n\nEvery read entry point now routes through the same path validation.\n\nRaw readers, still exposed: the overrides kept the originals around as public aliases like File.original_read, so user code could just call the un-sandboxed method directly. Those are gone from the public surface now; the native handles live in private constants the sandbox uses internally.\n\nSymlinks could point out of the project. Path validation expanded paths but never resolved symlinks, so a link inside the project that pointed at /etc/passwd passed the check and then read the target. Validation now resolves the real path first, and the allowlist for system data (the timezone directories Rails needs for Time.zone) is matched against canonical locations so it keeps working on macOS, where /usr/share/zoneinfo is itself a symlink.\n\nHalf-blocked ENV: the scan rejected ENV[...] and ENV.fetch, but ENV.to_h, ENV.values_at, and ENV.each walked straight past it and handed over every secret in the process environment. The scan now rejects ENV access broadly, while still leaving Rails.env alone.\n\nThe layers added behind them\n\nClosing bypasses is necessary but not sufficient. A blocklist can always be out-metaprogrammed. So 1.6.0 adds controls that don’t depend on catching every trick.\n\nDatabase writes are rolled back. This is the big one, because “read-only” was never true for data. Nothing stopped User.delete_all or a raw DELETE. Now your code runs inside a transaction that is always rolled back:\n\n# This executes, returns a count, and then is undone:\nUser.where(inactive: true).delete_all\n# =&gt; 42   (and the 42 rows are still there)\n\n\nIt is harm reduction, not a guarantee (DDL auto-commits on some adapters like MySQL, and after_commit callbacks don’t fire), but the common accident is now a no-op instead of data loss.\n\nThe timeout actually stops the code. The previous timeout wrapped the subprocess call and, when it fired, stopped waiting, while the runaway rails runner kept going, orphaned. The command now runs in its own process group, and a timeout kills the whole group. A runaway query or an infinite loop is terminated, not abandoned.\n\nDual-use constructs ask first. Some things are legitimately useful and also the classic sandbox-escape tools: send, public_send, const_get, Kernel#open. Rather than silently allowing them or hard-blocking them, the tool now stops and explains:\n\nCONFIRMATION REQUIRED: This code uses constructs that can bypass\nthe sandbox's static safety checks:\n  - `send`: dynamic dispatch can invoke methods the static scan\n    cannot see, e.g. reaching blocked system/file APIs indirectly.\n\nAsk the user to review the code and confirm. If they approve,\nre-invoke execute_ruby with confirm_risky: true.\n\n\nThe model can’t wave this through on its own. A human reviews the code, and only then does the call run with confirm_risky: true. It puts a person in the loop exactly where the static analysis runs out of road.\n\nBeing honest about the boundary\n\nI want to be direct about the limits, because a security tool that oversells itself is worse than one that doesn’t. These controls are layered defense, not hard isolation. The tool still executes real Ruby with full application access, so a determined bypass is possible; DDL and writes on non-default connections can escape the rollback; there are no per-process CPU or memory caps beyond the timeout.\n\nIf you need stronger guarantees, the right moves are outside the Ruby layer: run the server against a database user with read-only grants, and/or run it inside an OS-level sandbox (a container, sandbox-exec, seccomp). The SECURITY.md in the repository now documents both the controls and these limitations so you can make an informed call.\n\nManager-agnostic Ruby resolution\n\nSeparate from the sandbox, 1.6.0 fixes a long-standing annoyance for anyone whose Ruby isn’t the system default. Tools that shell out to bin/rails (execute_ruby, get_schema, and the introspection halves of analyze_models and analyze_controller_views) were exporting the rbenv-only RBENV_VERSION and running a login shell. On macOS, path_helper then reshuffled PATH so bin/rails booted under system Ruby and failed.\n\nThe runner now prepends the active version manager’s shims directory to PATH (mise, asdf, or rbenv, honoring MISE_DATA_DIR, XDG_DATA_HOME, ASDF_DATA_DIR, and RBENV_ROOT) and uses a non-login shell so the project’s Ruby survives. rvm, which has no shims, is sourced when present. The version comes from your project’s .ruby-version, .tool-versions, or .mise.toml, so different projects can use different Rubies with no extra configuration.\n\nWhile I was in there, the analyzer path stopped swallowing errors with 2&gt;/dev/null, so a Rails boot failure now surfaces the real message instead of a blank “Error executing Rails command.”\n\nNamespaced models resolve properly\n\nanalyze_models could report a module-namespaced model as “not found” depending on how you referred to it. It now resolves from every input form: Namespace::Model, the path namespace/model, the flattened NamespaceModel, and the bare leaf Model. That holds independent of your app’s custom inflections. The introspection runner also derives the constant from the resolved file instead of interpolating raw input, which removes an injection surface in the generated scripts along the way.\n\nBreaking change: Ruby 3.2 is dropped\n\nThe minimum supported Ruby is now 3.3 (required_ruby_version &gt;= 3.3.0), and CI tests 3.3 and 3.4. The dependency updates in this release pull in transitive gems that require 3.3, so this was forced rather than chosen. If you’re on 3.2, upgrade your Ruby before updating the gem.\n\nSecurity and dependencies\n\nThe dependency bump is also a security update. Upgrading to Puma 8.0.2 clears CVE-2026-47736 and CVE-2026-47737 (both HIGH, covering PROXY Protocol v1 remote memory exhaustion and repeated-header handling), and the lockfile refresh clears a concurrent-ruby advisory. bundler-audit reports clean.\n\nUpgrading\n\ngem update rails-mcp-server\n\n\nMake sure you’re on Ruby 3.3 or newer first. If you use Claude Desktop, restart it to pick up the new version; the binary path in your configuration doesn’t change.\n\nFor new installations:\n\ngem install rails-mcp-server\nrails-mcp-config\n\n\nWhat’s Next\n\nGiving a model a Ruby runtime inside your app is a sharp tool. This release makes the edges more honest: fewer accidental cuts, and a clear label on what the guard does and doesn’t cover. The durable next step is real isolation: read-only database roles and OS-level sandboxing as first-class, documented setups rather than footnotes.\n\nIf you find a gap, the issue tracker is open, and security reports have a private channel through the repository’s Security tab. Pull requests are welcome.\n\nLinks\n\n\n  GitHub Repository\n  RubyGems\n  Documentation\n  Security Policy\n  AI Agent Guide"
        },
        {
          "id": "blog-2026-07-maquina-components-0-6-0-themeable-beyond-color",
          "title": "Maquina Components 0.6.0: Themeable Beyond Color",
          "collection": {
            "label": "posts",
            "name": "Blog"
          },
          "categories": "Announcements, Release",
          "tags": "",
          "url": "/blog/2026/07/maquina-components-0-6-0-themeable-beyond-color/",
          "content": "Maquina Components, the server-rendered UI component library for Rails and Tailwind, is out in 0.6.0. It adds a token layer for radius, elevation, focus rings and font weight, moves all engine CSS into @layer components, and fixes a focus ring that had been dead on six of seven button variants. The release is deliberately breaking: seven changes, and the first one fails silently in every existing application.\n\nUntil now the library was themeable in color and in nothing else. Radius, elevation, focus rings, font weight and hover states were written directly into the stylesheets, so changing any of them meant overriding selectors rather than declaring values. Theming one real application against the 0.5 releases took roughly 1,700 lines of override CSS, and most of that was not expressing a design. It was reaching past the cascade.\n\nWhy tokens alone would not have fixed it\n\nEvery engine rule was unlayered and carried specificity from its variant and state qualifiers, so appearance and structure shared one flat cascade. A theme could not reach the appearance without also being able to break the structure, and the engine defended against that by being hard to override at all. Adding tokens on top of that arrangement would have changed nothing.\n\nThree things changed together. There is now a token layer: --control-radius, --surface-radius, --focus-ring-width, --elevation-raised, --label-weight and the rest, declared in @theme and read from every rule that used to hardcode a value. All twenty stylesheets moved into @layer components, which is what lets a caller’s Tailwind utilities apply. And specificity is flat now: every rule sits at 0,1,0, with variants and states in :where(), so a theme’s [data-component=\"button\"] means every button, which it previously did not.\n\nWhich gives a contract worth stating plainly: a theme changes values, not selectors. Reach for a selector only when you want to change a component’s shape, like a different padding rhythm or a variant the engine does not ship.\n\nA flat theme is six lines:\n\n:root {\n  --elevation-control: none;\n  --elevation-raised: none;\n  --elevation-overlay: none;\n  --control-radius: 0.25rem;\n  --surface-radius: 0.25rem;\n}\n\n\nThe theming guide has the full token reference. Every component demo on the documentation site now carries a shape toggle in its chrome; flip it to brutal or soft and the whole library changes shape from token declarations alone.\n\nThe focus ring was dead\n\n[data-component=\"button\"]:focus-visible was declared before the variant rules, at the same specificity. Each variant then re-declared box-shadow for its own elevation, and later-at-equal-specificity wins. The focus ring was silently overwritten on every variant that set a shadow.\n\nOn the demo page, two of the sixteen buttons showed a ring, and both were destructive — the only variant that happened to re-declare its own focus rule after its variant rule. Primary, secondary, outline, ghost and link had no visible keyboard focus at all. That is a WCAG 2.4.7 failure. It shipped, and it was invisible in code review because every rule involved looked correct on its own.\n\nFocus is now an outline rather than a box-shadow. Outlines do not participate in box-shadow, so a variant’s elevation can no longer overwrite a ring. They survive forced-colors mode, and they are not clipped by overflow: hidden ancestors, which had been quietly cutting rings off inside the sidebar and drawer. Every focusable button rings now, and a test asserts that state rules follow variant rules in every stylesheet, so the ordering that caused this cannot come back.\n\nYour utility classes now win\n\ncss_classes: is the documented way to adjust one instance of a component, and it has been partly a lie. Because engine rules were unlayered, they beat any Tailwind utility passed through them:\n\n\n  [data-component=\"input\"] set w-full, so any w-* you passed was dead.\n  [data-form-part=\"actions\"] set display: flex, so sm:hidden did nothing.\n  [data-component=\"form\"] set display: grid. Pass sm:flex-row and it silently stopped being a row.\n\n\nThese are layout failures, not restyles, and they failed quietly, which is why the workaround was always a wrapper element. With the engine in @layer components, utilities win. Measured: an input with a width utility goes from 448px to 137px.\n\nWorth searching your views for css_classes: after upgrading. Anything you passed as decoration and never saw is about to take effect.\n\nBefore you upgrade: run the scanner\n\nThe release ships a scanner. Run it inside your application:\n\nbin/rails maquina:doctor\n\n\nIt reads your CSS, views and JavaScript and prints file:line for every pattern this release changes, grouped by severity: the unlayered * rule, component overrides the token layer makes redundant, restated SVG data URIs, [data-active] presence selectors, .dark twins. It never edits anything and always exits 0.\n\nBreaking changes\n\nSeven. The first affects every existing application and fails silently.\n\n\n  \n    The preflight shim in your theme.css now outranks the engine. Your installed theme.css carries this rule:\n\n    * {\n  border-color: var(--color-border);\n}\n    \n\n    Unlayered CSS outranks every layer at any specificity. Now that engine rules live in @layer components, that one universal rule wins over the tinted borders on all alert and toast variants: a destructive alert renders with a plain grey border where 0.5.1 painted a red one. The generator template is fixed, but the rule lives in your file. Wrap it:\n\n    @layer base {\n  * {\n    border-color: var(--color-border);\n  }\n}\n    \n  \n  Utility classes now win. Anything passed through css_classes: that was previously overridden by an engine rule will take effect.\n  Radius and elevation defaults normalize. Card goes 12px to 8px, popovers 6px to 8px, and four shadow-lg sites collapse to --elevation-overlay.\n  Focus rings become outlines, and form fields stop ringing on mouse click.\n  merge_component_data precedence narrows to identity keys.\n  Surfaces that sit above the page stop painting --background.\n  Tinted badges lose a stray hairline the shim had been forcing onto them.\n\n\nEvery one of them is a value, so the upgrading guide closes with an appendix that restores the 0.5.1 look with a single token block.\n\nUpgrading\n\nbundle update maquina-components\n\n\nThen re-run the installer to append the new shape and state tokens to your theme. It is idempotent and will not touch your palette:\n\nbin/rails generate maquina_components:install\n\n\nThen read the upgrading guide.\n\nAlso in this release\n\nNew:\n\n\n  Drawer gained title, description, section and separator partials. The first two had been styled by CSS since the beginning with nothing to emit them, so the documentation told you to hand-write &lt;h2 class=\"text-lg font-semibold\"&gt;.\n  Sidebar gained menu badges, menu actions and group actions on the same footing.\n  There is a label partial now, which makes the required-field indicator reachable.\n\n\nFixed:\n\n\n  dropdown_menu_simple raised NoMethodError and combobox_simple rendered an empty popover. Both had zero call sites in the repository, which is exactly why they shipped broken.\n  Two components were building correct data attributes and then discarding them, so [data-variant=\"bordered\"] on a table was unreachable.\n\n\nWhat this reinforced\n\nEvery bug in this release looked correct in the stylesheet. The focus ring rule was right there in the file, and the dead table variant was right there in the partial. What caught them was asserting on compiled output and computed styles: does this token reach the browser, does this rule come after that one, does this element actually have a ring. The tests that came out of the audit assert those things.\n\nThe quieter finding was a set of styled hooks that turned out to be emitted by nothing at all — CSS that read as supported API and matched no markup. That is worse than a missing feature, because it looks finished. Six became real partials here and two were deleted.\n\nDocumentation\n\n\n  Theming\n  Upgrading\n  Component documentation\n\n\nSource\n\n\n  Maquina Components\n  Full Changelog\n  Gem on RubyGems"
        },
        {
          "id": "blog-2026-07-introducing-nexo",
          "title": "Introducing Nexo: the harness for Ruby agents",
          "collection": {
            "label": "posts",
            "name": "Blog"
          },
          "categories": "Announcements, Product, AI Tools",
          "tags": "",
          "url": "/blog/2026/07/introducing-nexo/",
          "content": "Ruby has most of the pieces you need to build an AI agent already, and they’re good.\nruby_llm gives you a provider-neutral chat loop and tool calling that’s cleaner than\nmost language SDKs out there, one API across a dozen providers instead of a new SDK\nper vendor. ruby_llm-skills gives you SKILL.md loading. ruby_llm-mcp gives you MCP\nservers. ruby_llm-schema gives you structured output. What none of them give you is\na front door, one place where those pieces snap together with defaults you don’t have\nto think about twice.\n\nBuilding an agent on top of these gems means wiring the same sandbox and permissions\nboilerplate into every new project, by hand, every time. Nexo exists to stop that.\nIt doesn’t rebuild the tool-call loop, skill loading, MCP, or structured output; those\nalready exist, they’re well built, and duplicating them would just make Nexo worse at\nthe things ruby_llm and its ecosystem already do well. Nexo sits on top of them\ninstead. Two things were missing\nfrom the ecosystem, and those are the two things Nexo adds: a sandbox and permissions\nseam, and a real job primitive called WorkflowRun.\n\nAgent = Model + Harness\n\nA model on its own forgets everything the moment it finishes a response. Turning “a\nmodel that replies” into “an agent that does work” takes tools, a place for those\ntools to act, a policy for what they’re allowed to do, and a way to track a job from\nstart to finish. That collection of things is the harness. Ruby has all of it, but\nscattered across gems with different DSLs, and wiring them by hand is the first thing\nevery new agent project reinvents.\n\nNexo is the nexus: the connective tissue between a model and everything else an agent\nneeds, and between the fragmented RubyLLM-ecosystem gems and one coherent whole.\n\nrequire \"nexo\"\n\nclass CodeReviewer &lt; Nexo::Agent\n  model       ENV.fetch(\"NEXO_MODEL\")   # any ruby_llm model, no vendor default\n  sandbox     :local\n  permissions :read_only\n\n  instructions \"You are a careful code reviewer. Read files and report issues. Do not write files.\"\nend\n\nCodeReviewer.new(cwd: \"/path/to/repo\").prompt(\"Review the auth module\")\n\n\nWhat Nexo adds: a sandbox and permissions seam\n\nAn agent’s tools have to act somewhere, and something has to decide what they’re\nallowed to do there. Nexo splits that into two questions, where (the sandbox) and\nwhat (the permission mode), and answers both safely by default: :virtual sandbox,\n:read_only permissions. An untrusted model gets zero host access until you opt into\nmore, explicitly, in your own code.\n\n\n  Virtual runs in memory with zero host access. #shell raises\nNotImplementedError on purpose, that’s the safety property, not a bug in waiting.\n  Local gives you the host filesystem and shell for trusted dev and CI work.\nEvery path is guarded to stay inside cwd, and the shell only sees PATH, HOME,\nand LANG. It never sees your full environment.\n  Container runs tools inside a throwaway Docker or Apple container sandbox,\nhardened by default (no network, dropped capabilities, read-only rootfs), with every\nhardening an explicit opt-out. Full flag reference is in the\nsandboxes doc.\n  Remote is a four-method contract, read, write, exec, close, that you\nadapt to whatever provider you’re already using. Nexo ships zero vendor code here;\nswap the injected object and you’ve swapped providers.\n\n\nA write under :read_only, a shell call on Virtual, any denied action comes back\nas { error: ... } and the loop keeps running. The model can read that and adjust; a\npermission denial isn’t a reason to crash the process. A path that tries to escape its\nsandbox is a different kind of failure, and it raises SecurityError instead.\n\nWhat Nexo adds: WorkflowRun\n\nAn agent accumulates context, it’s a conversation that keeps going. A lot of real\nwork isn’t that. It’s a finite task that starts, does something, and finishes with a\nresult you can check on later, sometimes from a different process entirely. Nothing\nin the ecosystem covered that cleanly, so Nexo adds Workflow.\n\nA workflow can drive an agent too, so the two primitives Nexo owns fold into one\nrecipe: stage inputs into the run’s sandbox, run the agent against them, capture the\noutput. The agent class macro names the agent, and run_agent runs it bound to the\nrun’s own sandbox:\n\nclass Summarizer &lt; Nexo::Agent\n  model ENV.fetch(\"NEXO_MODEL\")   # any ruby_llm model, no vendor default\n\n  instructions \"Summarize the given text in two plain sentences. No commentary.\"\nend\n\nclass SummarizeDocument &lt; Nexo::Workflow\n  agent Summarizer\n\n  def call(payload)\n    emit(:started, doc_id: payload[:doc_id])\n    response = run_agent(\"Summarize this document:\\n\\n#{payload[:text]}\")\n    emit(:summarized, length: response.content.length)\n    { summary: response.content }\n  end\nend\n\nrun = SummarizeDocument.run(doc_id: 123, text: \"Long text...\")\nrun.id      # =&gt; \"0191d6b2-...\"  (UUID v7, time-ordered)\nrun.status  # =&gt; \"done\"\nrun.result  # =&gt; { \"summary\" =&gt; \"The document covers ...\" }\n\n\nEvery run gets a stable id, a status, a payload, a result, and an ordered event log\nyou can replay with Nexo::Workflow.logs(run.id) or rake nexo:logs[id]. The\nagent’s tool calls and its final response land in that same log alongside the\nworkflow’s own :started/:summarized events, so a driven run reads as one story\ninstead of two logs stitched together by hand. Outside Rails it all records to memory\nand just works offline, no database required; inside Rails, install the migration and\nthe same code persists to a nexo_workflow_runs table instead, with no other change.\n\nA workflow that raises is recorded as failed and the exception still propagates to\nyour caller, the opposite of a tool failure, which returns { error: ... } and never\ninterrupts the agent loop. Jobs that need to pause and pick back up later, waiting on\na human approval or a slow external process, get the same lifecycle extended with\ncheckpoints, suspend!, and resume, covered in the\ndurable workflows doc.\n\nProvider-neutral, on purpose\n\nThe only hard dependency is ruby_llm. No hardcoded default model, no vendor SDK\ncalled directly anywhere in the core loop; every example in the docs pulls the model\nfrom ENV.fetch(\"NEXO_MODEL\"), just as happy pointing at a local Ollama model as a\nhosted one. ruby_llm-skills, ruby_llm-mcp, and an optional Anthropic-oriented\nautonomous loop are all soft dependencies, required lazily, and they raise a clear\nNexo::MissingDependencyError with install instructions if you haven’t added them.\nYou only pay for what you use.\n\nWhere it stands today\n\nNexo is early and the API isn’t stable yet. The Apple container runtime parity\ntable\nin the docs says so plainly: Docker is the verified path today, and Apple’s CLI flags\nare encoded from the reference mapping but not yet confirmed against a live daemon.\nThe caveat is published rather than papered over.\n\nWhat’s shipped: the sandbox and permissions seam across Virtual, Local,\nContainer, and Remote; the Workflow/WorkflowRun lifecycle with staging,\nartifacts, and durable checkpoints; skills, MCP behind a fail-closed gate, a web\nfetch tool with an SSRF guard, sessions, and a Rails engine with generators for the\nconventional app/agents / app/workflows / app/skills layout.\n\ngem \"nexo_ai\"\n\n\nThe gem publishes as nexo_ai; everything in the code lives under Nexo::.\n\nNone of this exists without the RubyLLM ecosystem underneath it. Nexo was worth\nbuilding because that foundation, ruby_llm and\neverything built around it, is solid.\n\nThe Nexo documentation has the full index, sandboxes,\npermissions, tools, loop backends, workflows, durable workflows, skills, MCP, web,\nsessions, Rails, and concurrency. If you’re already building on ruby_llm and you’ve\nwritten this same sandbox-and-permissions setup more than once, this is for you.\n\nRepo: github.com/maquina-app/nexo"
        },
        {
          "id": "blog-2026-07-maquina-components-0-5-0-drawer-and-scaffold-templates",
          "title": "Maquina Components 0.5.0: Drawer and Scaffold Templates",
          "collection": {
            "label": "posts",
            "name": "Blog"
          },
          "categories": "Announcements, Release",
          "tags": "",
          "url": "/blog/2026/07/maquina-components-0-5-0-drawer-and-scaffold-templates/",
          "content": "This release adds a Turbo-aware Drawer component, a scaffold_templates generator that makes rails generate scaffold produce styled views, and engine helpers included in the generated helper module. It’s also a first for the project: most of it came from contributors.\n\nDrawer\n\nA slide-out panel with an overlay backdrop, built from sub-partials the same way Card and Sidebar are. A provider owns the state; header, content, and footer structure the panel; a trigger toggles it and a close dismisses it.\n\n&lt;%= render \"components/drawer/provider\", default_open: drawer_open? do %&gt;\n  &lt;%= render \"components/drawer\", state: drawer_state do %&gt;\n    &lt;%= render \"components/drawer/header\" do %&gt;\n      &lt;h2 class=\"text-lg font-semibold\"&gt;Filters&lt;/h2&gt;\n      &lt;%= render \"components/drawer/close\" %&gt;\n    &lt;% end %&gt;\n\n    &lt;%= render \"components/drawer/content\" do %&gt;\n      &lt;!-- Panel body --&gt;\n    &lt;% end %&gt;\n\n    &lt;%= render \"components/drawer/footer\" do %&gt;\n      &lt;!-- Apply / Reset --&gt;\n    &lt;% end %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\n\nDrop a trigger anywhere on the page as a plain toggle button:\n\n&lt;%= render \"components/drawer/trigger\" %&gt;\n\n\nThe drawer opens from the right by default; pass side: :left to open from the other edge.\n\nFeatures: compound structure (provider, header, content, footer, trigger, close), left or right side with an overlay backdrop, a configurable Cmd/Ctrl+D shortcut, cookie-based state persistence, and full Turbo Drive and Morph compatibility.\n\nSurviving Turbo\n\nThe reason a drawer is more than a CSS transition is the lifecycle around it. The controller handles the three places Turbo usually breaks a stateful component:\n\nCache teardown. Before Turbo snapshots the page for its cache, the drawer closes and hides its backdrop. A restored snapshot never comes back frozen mid-transition.\n\nMorph awareness. With turbo_refresh_method_tag :morph, the server re-renders the page in its default state—it doesn’t know the drawer was open. The controller re-reads its cookie on morph and reasserts the correct state—the same fix the sidebar got in 0.4.0.\n\nPersistence. State lives in a cookie, so the drawer holds across full page loads and Turbo navigations alike.\n\nThat state is exposed through three helpers, so your server-rendered markup and the client agree on the first paint:\n\ndrawer_state    # =&gt; :open or :closed\ndrawer_open?    # =&gt; true / false\ndrawer_closed?  # =&gt; true / false\n\n\nPassing default_open: drawer_open? into the provider and state: drawer_state into the drawer, as in the usage above, is what closes the loop between the cookie and the initial render.\n\nScaffold Templates\n\nHaving a component library is one thing; getting your generated code to use it is another. The new scaffold_templates generator closes that gap.\n\nbin/rails generate maquina_components:scaffold_templates\n\n\nIt copies a set of ERB scaffold templates—index, show, new, edit, _form, and the record partial—into lib/templates/erb/scaffold/. Rails has always let you override its generator templates from that path; what was missing was a set that renders with the component library. Now they ship with the gem.\n\nFrom then on, the standard scaffold generator produces styled views out of the box:\n\nbin/rails generate scaffold Post title:string body:text\n\n\nYou get tables, buttons, and form fields built with Maquina Components instead of Rails’ default markup—before writing any view code. Because the templates are copied into your app, they’re yours to edit afterward.\n\nEngine Helpers in the Generated Module\n\nA quieter change in the same direction. The generated MaquinaComponentsHelper now includes IconsHelper, SidebarHelper, and ToastHelper. Helpers like icon_for, sidebar_open?, and toast_flash_messages are available in host-app views without an extra include in ApplicationHelper. It’s a one-line diff in the template that removes a papercut every new install used to hit—reaching for icon_for and getting a NoMethodError because the module wasn’t wired up yet.\n\nIcon Class Handling\n\nThe one bug fix this release tightens apply_icon_options. It now guards against nil and non-string class values, HTML-escapes the class before it reaches the markup, and injects a class attribute onto &lt;svg&gt; elements that didn’t already have one. Small, but exactly the kind of edge case that only surfaces once icons are being passed around inside real templates.\n\nContributors\n\nThis release was built mostly by two people who aren’t me:\n\n\n  @GregorioNeto — the Drawer component (#21) and the icon class handling fix (#17)\n  @JuanVqz — the scaffold_templates generator (#20) and the engine helper modules in the generated helper (#19)\n\n\nThank you both.\n\nUpgrading\n\nbundle update maquina_components\n\n\nThen, when you want styled scaffolds, install the templates:\n\nbin/rails generate maquina_components:scaffold_templates\n\n\nThere are no breaking changes in this release.\n\nWhat This Reinforced\n\nThree of the four changes pull the same way: they make the gem lean on Rails instead of sitting next to it. The scaffold templates go through Rails’ own generator override path. Helpers are included the way any Rails helper is. And the Drawer keeps its state in a cookie and reacts to Turbo’s morph, the way the sidebar already does. Less to learn, fewer seams to trip over.\n\nThe other thing worth saying is that I reviewed this release more than I wrote it. Gregorio and Juan built the components; I merged them. That says more about where the project is than any one feature does.\n\nDocumentation\n\n\n  Drawer\n  Component documentation\n\n\nSource\n\n\n  Maquina Components\n  Full Changelog\n  Gem on RubyGems"
        },
        {
          "id": "blog-2026-07-introducing-fragua",
          "title": "Introducing Fragua: the harness already composed for Rails",
          "collection": {
            "label": "posts",
            "name": "Blog"
          },
          "categories": "Announcements, Product",
          "tags": "",
          "url": "/blog/2026/07/introducing-fragua/",
          "content": "Fragua is live in private beta. It’s an AI agent orchestrator built specifically for Rails developers, and it’s the newest product under the Maquina umbrella.\n\nFragua is Spanish for forge — the place where raw material becomes a finished tool under heat and pressure. That’s the intent: take a project from a one-line idea to a shipped pull request, running through research, planning, spec-driven development, and execution.\n\nWhy we built it\n\nCoding agents are good at writing code and bad at remembering why. Every new session tends to start from zero — the spec gets re-pasted, the data model gets re-explained, and the agent has no memory of a decision it helped make yesterday.\n\nWhat Fragua actually sells isn’t an AI that writes code. It’s durable context and a disciplined workflow. Each phase — brief, research, plan, spec — produces a structured artifact that persists in the workspace and becomes context for the next agent. By the time the execution agent runs, it’s not guessing; it’s reading the plan, the technical guide, and the spec that already exist.\n\nThe shape of it\n\nFoundation → Spec → Execution → Pull Request\n\n\nOnly Foundation (a working copy of your codebase) and an accepted Spec are required. Everything else — Product Brief, Research, MVP Plan, Brand Guide, Technical Guide — is optional enrichment that makes the execution agent sharper without ever gating it. Execution itself starts on its own: a sweep every ~2 minutes picks up accepted specs and approved issue fixes, so there’s no button for “start the build.”\n\nUnder the hood, Fragua splits the work across two planes — a web app that plans and directs, and your own machine that actually runs the agent and pushes the branch. We walk through that split, plus a live tour of the web app, in the video below.\n\nRails-first, not Rails-only\n\nEvery new app Fragua scaffolds is Rails 8.1 with Hotwire — Stimulus controllers, Minitest fixtures, Turbo Streams, the full set of conventions the agents already know how to read and write. That part is deliberate and non-negotiable: it’s what lets Fragua write code that looks like it belongs in your app instead of generic output.\n\nWhere you’re pointing Fragua at a codebase that already exists, the door is wider. Foundation and the Spec and Issue agents read your repo rather than dictate its stack, so brownfield projects outside Rails are something we’re genuinely open to exploring — if that’s your situation, say so in your access request and tell us what you’re working in.\n\nWhat’s actually different day to day\n\n\n  Durable context — nothing evaporates between sessions; later phases build on everything earlier ones produced.\n  BYOK — tokens bill straight to your own Anthropic account. Fragua never proxies the call, never stores the key, can’t see your bill.\n  Full observability — a live run timeline, per-turn cost broken down by phase and rolled up by workspace and month, and a durable audit trail. Metering, not gating — there are no spend caps to trip.\n  Institutional memory — the Knowledge Base, every phase artifact, and the full run history live in the workspace, not in one person’s head. When someone hands off or leaves, the context stays.\n  Your host, your code — agents run on your machine, against your repo, with your own git and GitHub credentials. Nothing you own leaves it.\n  Claude Code today, Codex coming — the agent runtime is Claude Code right now, with Codex support coming shortly.\n  Sandboxed if you want it — run the agent directly on your host, or isolate it inside Docker or a macOS Container instead. Setup is in the CLI guide.\n\n\nWatch the two-plane split in action\n\n\n\n\n\n\n\n\n\nWhere things stand\n\nFragua is in private beta, by invitation. A human reads every request before a seat opens — there’s no schedule, seats open based on fit rather than a calendar.\n\n\n  \n    \n      Plan\n      Price\n      Includes\n    \n  \n  \n    \n      Solo\n      $19/month\n      1 user, unlimited workspaces, full cost dashboard, per-feature worktrees, BYOK\n    \n    \n      Team\n      $69/month, flat\n      2–10 users, shared workspaces, admin &amp; member roles, audit trail across every run, priority support, BYOK\n    \n    \n      Enterprise\n      Contact for quote\n      11+ users, unlimited workspaces, on-premises deployment, dedicated support, BYOK\n    \n  \n\n\nAll plans are free for the duration of the beta. Pricing begins 30 days after public launch, with email notice ahead of time. A read-only viewer role is on the roadmap — not shipped yet.\n\nNeed more than 10 seats, or want it on-premises? Email mario@fragua.app and we’ll work out the details directly.\n\nIf you’re building on Rails 8.x and Hotwire — solo, freelance, a small consultancy, or a growing product team — request access at fragua.app/#access. Working in something else on an existing codebase? Tell us in the request; we’d like to hear about it.\n\nThe full docs live at fragua.app, including dedicated pages on observability, teams, and trust.\n\nAgents that respect the craft. Your host, your keys, your repo.\n\n\n\nFragua is built by Maquina."
        },
        {
          "id": "blog-2026-06-recuerd0-mcp-and-redesign",
          "title": "recuerd0 Now Has an MCP Server — and a Better Look",
          "collection": {
            "label": "posts",
            "name": "Blog"
          },
          "categories": "Announcements, Product",
          "tags": "",
          "url": "/blog/2026/06/recuerd0-mcp-and-redesign/",
          "content": "recuerd0 has always had multiple ways to access your knowledge: a REST API, a CLI for terminal workflows, and an agents guide for hooking coding agents directly into your memories. The access model has been there from the start — Bearer token, call the endpoints, get your context back.\n\nMCP adds another path. Instead of configuring the connection upfront, any MCP client can reach your memories on demand — mid-conversation, mid-task, whenever the context is needed — without a token to copy or a config file to edit. Building that properly meant adding an OAuth 2.1 authorization server, a consent screen, and connected application management — surface area that needed to feel right in the browser.\n\nWhile that work was underway, the editing experience got the same attention. The textarea that had always been good enough stopped feeling good enough when you’re managing a knowledge base you actually rely on. The fix was already sitting in plain sight: 37signals shipped House MD with Writebook, and it fit recuerd0’s requirements almost exactly.\n\nBoth changes landed at the same time, alongside a UI pass that tightened the information hierarchy across the workspace and memory views.\n\n\n\nMCP support\n\nrecuerd0 runs a remote MCP server at POST /mcp — Streamable HTTP, JSON-RPC 2.0, compatible with any client that speaks the protocol.\n\nThe connection is consent-based. The client registers itself with Dynamic Client Registration, you approve a short permission screen in the browser, and that’s it. No token to generate, no config to edit. Approve once per client; disconnect any time from your profile.\n\n\n\nThe authorization server is built directly into the Rails app — no external OAuth service, no added dependencies. PKCE instead of shared secrets. Access tokens last one hour; refresh tokens rotate silently, so a connected client stays connected without interrupting you.\n\nSix tools\n\nThe server exposes six tools across three read and three write operations, all scoped to your account’s workspaces:\n\n\n  \n    \n      Tool\n      Type\n      What it does\n    \n  \n  \n    \n      list_workspaces\n      read\n      List the workspaces in your account\n    \n    \n      list_memories\n      read\n      List memories; optional FTS5 query and category filter\n    \n    \n      read_memory\n      read\n      Read the full content of one memory\n    \n    \n      create_memory\n      write\n      Create a memory, with optional tags\n    \n    \n      update_memory\n      write\n      Update title, content, category, or tags\n    \n    \n      create_version\n      write\n      Append an immutable version, preserving history\n    \n  \n\n\nRead tools require the memories:read scope. Write tools — including create_version — require memories:write. Scopes map to the existing read_only / full_access permission tiers, so there’s no parallel auth system to reason about.\n\ncreate_version is the one worth pausing on. Every memory in recuerd0 already has a versioning model — you can snapshot a decision or convention as it evolves and keep the full history. The MCP tool exposes that directly. If a client updates a memory and you want to preserve what was there before, it can append a version rather than overwrite.\n\nConnect it\n\nThe MCP endpoint is at your instance URL:\n\nhttps://recuerd0.ai/mcp\n\n\nFor self-hosted instances, replace the domain with your own. Any MCP client that supports custom connectors — Claude Desktop, Claude.ai, Cursor, and others — can connect by pointing at that URL. The OAuth handshake runs itself.\n\nFull documentation is at recuerd0.ai/mcp.\n\n\n\nThe editor\n\nrecuerd0 stores knowledge as markdown. The editing experience should match that — not fight it. A plain textarea works, but it asks you to hold the syntax in your head while you write. After using it long enough, the friction starts to matter.\n\n\n\nThe new editor is based on House MD — the markdown editor 37signals built for Writebook. Minimal toolbar: bold, italic, quote, code, link, bullet list, numbered list. It writes markdown, not HTML. No mode switching, no preview tab — what you see while editing stays close to what the rendered output looks like.\n\nOne thing worth being explicit about: everything that reads your memories — the API, the CLI, MCP tools — still gets raw markdown. The editor doesn’t change that contract.\n\n\n\nThe UI\n\nrecuerd0 is built on Maquina Components — a Rails component library with cards, badges, buttons, dialogs, and form elements. The component foundation was already there; this pass refined how those pieces are composed and extended them with patterns specific to what recuerd0 actually is.\n\n\n\nThe workspace and memory views got a more deliberate information hierarchy — metadata where you need it, actions reachable on touch and keyboard, pinned items in their own labeled section rather than blended into the list. The color system was tightened so the app reads consistently across every page, and view preferences (list vs grid, cards vs compact) now persist between sessions.\n\nThe workspace index and workspace show pages have been updated. Memory show, the editor page, and settings are next.\n\n\n\nThe MCP server is live. Full documentation at recuerd0.ai/mcp.\n\nrecuerd0 is free to self-host. Managed hosting is $15/month.\n\nRelated reading\n\n\n  Recuerd0 Now Reads Like a Filesystem — grep, glob, and ranged read on your memories.\n  Announcing Recuerd0 — the original launch and the problem we set out to solve.\n  Recuerd0 source code is now available — how to self-host under OSAASY."
        },
        {
          "id": "blog-2026-04-recuerd0-api-release",
          "title": "Recuerd0 Now Reads Like a Filesystem",
          "collection": {
            "label": "posts",
            "name": "Blog"
          },
          "categories": "Announcements, Product",
          "tags": "",
          "url": "/blog/2026/04/recuerd0-api-release/",
          "content": "The new Recuerd0 API release teaches the memory store to behave like a filesystem — so AI agents already fluent in grep, glob, and read need no new vocabulary.\n\n\n\nRecuerd0 is the persistent memory store for AI coding agents built by Maquina, and this release reshapes how agents read from it. When an AI agent reaches into Recuerd0 for context, the bottleneck has never been storage. It has been how much the agent has to pull back to get to the one fact it needs. A 2,000-line transcript should not have to fit within the agent’s context window to answer “did we decide to use Postgres?” The new release fixes that — and a handful of other long-standing rough edges — by giving the API the same primitives every coding agent already knows: glob, grep, and ranged read.\n\nHere is what is new.\n\nFile-tool API: glob, grep, and ranged read on memories\n\nThe biggest shift in this release is conceptual. Memories are no longer monolithic blobs that you fetch whole. They are addressable like files.\n\nGlob. The browse and list endpoints accept a title glob pattern. * matches any sequence of characters, ? matches a single character. Combined with tags, source, category, and workspace_id, the agent can narrow a thousand memories down to the dozen worth looking at without reading any bodies.\n\nGET /memories.json?title=Meeting*&amp;tags=design,api&amp;category=decision\n\n\nRanged read. GET /workspaces/:id/memories/:id.json now accepts line_start and line_end (1-based, inclusive). The response always echoes total_lines, so the client knows how much memory is available and can compute a tail window in a single follow-up call.\n\nGET /workspaces/1/memories/42.json?line_start=40&amp;line_end=55\n\n\nThere is no head= or tail= parameter — and that is deliberate. line_start=1&amp;line_end=20 is “head 20”; line_start=(total_lines - 19)&amp;line_end=total_lines is “tail 20”. One verb covers both, and the client never has to learn a parallel vocabulary for the same operation.\n\nGrep with line numbers. ?mode=grep&amp;q=&lt;query&gt; switches the same endpoint into a grep response. Instead of returning the body, it returns an array of matches:\n\n{\n  \"content\": {\n    \"total_lines\": 2174,\n    \"matches\": [\n      {\n        \"line_number\": 1247,\n        \"line\": \"Decided: Postgres for the analytics warehouse, SQLite for everything else.\",\n        \"context_before\": [\"## Database choice\"],\n        \"context_after\": [\"Reason: ops simplicity outweighs the JOIN ceiling for our scale.\"]\n      }\n    ]\n  }\n}\n\n\nOptional context, before, and after parameters control how many surrounding lines to return — capped at 10 each, like grep -C, -B, and -A. The full-text search endpoint (/search.json) supports the same grep mode for cross-memory queries.\n\nThe two-step recipe the agent should reach for: first, use grep to locate the line numbers; then issue a follow-up line_start/line_end call to fetch only the surrounding window. A 2,000-line memory becomes a 20-line answer.\n\nMemory categories\n\nEvery memory now carries a category: decision, discovery, preference, or general (the default). It is a small thing, but it changes how an agent reasons about what it is reading. A decision is load-bearing — something the team chose and is sticking with. A discovery is a fact about the world. A preference is taste. The agent does not have to infer the difference from prose; it is right there in the metadata, filterable from any list endpoint.\n\nGET /memories.json?category=decision&amp;sort=updated_at\n\n\nCross-workspace memory links\n\nMemories can now reference each other across workspaces with first-class “see also” links. The Rails decision in your Backend workspace can point to the deployment write-up in Infrastructure without copying anything. Each memory’s response includes a links_count so the agent knows there is more context one hop away, and dedicated endpoints under /memories/:id/links let it list and traverse them.\n\nThis is the connective tissue for context that lives in more than one place — which, in practice, is most context worth keeping.\n\nWorkspace wake-up endpoint\n\nA new endpoint, GET /workspaces/:id/context.json, returns a compact “wake-up” payload for an agent starting a fresh session: workspace metadata, recent memory titles, and the highlights an agent should know about before it does anything else. It is the answer to “you are picking up where you left off, here is the room you just walked into.”\n\nPair it with a Claude Code session-start hook and a new conversation begins with the right context already loaded — no manual recuerd0 memory list dance, no asking the user to repeat themselves.\n\nHTTP caching across the API\n\nAll read endpoints now emit ETag and Last-Modified headers and respect conditional requests. A client that sends If-None-Match for a memory it already has receives a 304 Not Modified response with an empty body. For agents that re-fetch the same workspace several times in a session, this is a meaningful drop in tokens shipped over the wire — and a meaningful drop in load on the database.\n\nGrep and ranged-read responses are correctly bypassed by the cache, since they are derived from query parameters that change with each call.\n\nCLI: recuerd0 memory read\n\nThe recuerd0-cli gains a memory read command group that wraps the new endpoints so a human (or a terminal-bound agent) can use them without hand-crafting URLs:\n\nrecuerd0 memory read head 42 --lines 20\nrecuerd0 memory read tail 42 --lines 20\nrecuerd0 memory read lines 42 --start 100 --end 140\nrecuerd0 memory read grep 42 \"Postgres\" --context 2 --pretty\n\n\nIn --pretty mode, the grep subcommand emits a breadcrumb for each hit, suggesting the exact memory read lines, followed by a call to fetch a window around it. The two-step pattern is right there in the output — no thinking required.\n\nAgent guidance baked in\n\nThe Claude Code recuerd0 agent skill now ships guidance for when to use the new primitives, not just how. The dedup-before-write protocol prefers memory read grep over memory show for large candidates. The workflow guidelines tell the agent: when total_lines &gt; ~200, grep first and fetch a window — reserve full reads for memories you genuinely need in their entirety.\n\nThe point of teaching these patterns to the agent is the same as the point of adding them to the API in the first place: make the cheap thing the obvious thing.\n\nDocumentation\n\nEvery endpoint above is documented in the public API reference, and the CLI reference on recuerd0.ai has been updated to match. The grep→fetch-window workflow is called out as a recipe in both places, with worked examples.\n\nWhy this release matters\n\nCoding agents are getting fluent. They already know how to use glob, grep, and read — those primitives are how they navigate filesystems every day. Recuerd0’s job is not to invent a new vocabulary for context retrieval; it is to look enough like a filesystem that agents do not have to learn one.\n\nThis release is that bet, made concrete. A memory is now something you can grep. A workspace is now something you can wake up in. A long transcript no longer has to fit entirely within a context window just so the agent can quote one line from it.\n\nGet the update\n\n\n  SaaS users on recuerd0.ai: the new endpoints are live now. No action needed.\n  Self-hosters: pull the latest recuerd0 image (or git pull and redeploy with Kamal). Run migrations to pick up the new category column and the memory_links table.\n  CLI users: you must update to the latest version to get the new memory read commands — brew upgrade recuerd0-cli (or grab the latest binary from recuerd0-cli releases). Older CLI versions will not expose the new functionality.\n  Claude Code users: update the recuerd0 plugin from the Claude Code marketplace to pick up the new agent guidance and command reference. Without the plugin update, the agent will keep using the old memory show flow instead of the new grep-first patterns.\n\n\nFrequently asked questions\n\nHow do I grep a Recuerd0 memory?\nSend GET /workspaces/:id/memories/:id.json?mode=grep&amp;q=&lt;query&gt;. The response returns line numbers and surrounding context instead of the full body. From the CLI: recuerd0 memory read grep &lt;id&gt; \"&lt;query&gt;\" --context 2.\n\nWhat is the difference between ranged read and grep mode?\nGrep mode finds where a string appears (returns matching line numbers with context). Ranged read fetches what is at known line numbers via line_start and line_end. The recommended workflow is grep first to locate, then ranged read to fetch a window.\n\nDo I have to update the CLI and Claude Code plugin?\nYes. The new memory read commands ship in the latest recuerd0-cli, and the grep-first agent guidance ships in the updated recuerd0 plugin in the Claude Code marketplace. Older versions will keep working but won’t expose the new endpoints.\n\nWhat are memory categories used for?\nEach memory is tagged as decision, discovery, preference, or general. Agents (and humans) can filter by category to find load-bearing decisions without sifting through general notes.\n\nDoes HTTP caching apply to grep queries?\nNo. ETag/Last-Modified caching applies to whole-memory and list reads. Grep and ranged-read responses are derived from query parameters and bypass the cache by design.\n\nRelated reading\n\n\n  Announcing Recuerd0 — the original launch and the problem we set out to solve.\n  Recuerd0 source code is now available — how to self-host under OSASSY.\n  Maquina open-source projects — the rest of the Rails and AI tooling we maintain.\n\n\n\n\nRecuerd0 is built by Maquina. Source available under OSASSY license."
        },
        {
          "id": "blog-2026-03-mvp-creator-from-idea-to-documents",
          "title": "MVP Creator: From Idea to Documents in Three Prompts",
          "collection": {
            "label": "posts",
            "name": "Blog"
          },
          "categories": "Announcements, Tools",
          "tags": "",
          "url": "/blog/2026/03/mvp-creator-from-idea-to-documents/",
          "content": "Before writing a single line of code, I need to understand what I’m building. Not abstractly — concretely: who the users are, what the real problem is, what the app is called, what voice it has, what technical decisions I’m making from the start. For a long time, that work happened informally — in scattered notes, in my head, or spread across different roles on a team: product knowledge in one conversation, brand direction in another, architecture in some document nobody kept updated. Now I formalize it with an agent called MVP Creator.\n\nIn this video — the first in a series about my personal process with AI — I show how I use MVP Creator to generate the complete set of foundation documents for a new project: research report, business plan, brand guide, and technical guide. All of it with three prompts, from an initial idea to documentation ready to hand off to Claude Code.\n\n\n\n\n\n\n\nThe Three Prompts\n\nThe example in the video is a photo delivery platform for professional photographers. These are the exact prompts I use, in order.\n\n\n\nPrompt 1 — The Idea and Context\n\nHelp me create an MVP for a photo delivery platform for professional photographers.\nThink of it as a private gallery where photographers deliver finished work to clients.\n\nThe core concept: a photographer creates a Project (for a client or personal work),\norganizes photos into Collections within that project, and shares the gallery via\nsingle-use expirable links. Invited clients can view, comment, like, and download\nphotos in their preferred quality.\n\nKey features:\n- Projects with collections and high-resolution photo uploads\n- Active Storage for thumbnail + quality variants (low/medium/high)\n- Reorderable photos within collections, cover photo per collection\n- Shareable links: single-use, expire in 7 days, create read-only sessions\n- Download: single photo or multi-select as zip, with quality choice\n\nTarget users: freelance and studio photographers in Latin America\nLanguage: Spanish-first, English secondary\nApp name: I'm thinking \"Liminal\" — open to suggestions\n\nResearch these competitors: https://www.pic-time.com and\nhttps://www.picdrop.com/web — also look at how Google Drive handles\nshared folder UX as a reference point.\n\nUse the MVP Creator skill to generate the full documentation set.\n\n\nWith this prompt the agent launches competitor research, runs through the discovery questions, and generates the four foundation documents: research report, business plan, brand guide, and technical guide.\n\n\n\nPrompt 2 — Brand Voice\n\nBased on everything we've defined about Liminal — the LATAM market, photographers\ndelivering work to clients, the quiet confidence of the name itself — write a brand\nvoice document.\n\nThe voice should feel like a photographer who has found their style and doesn't need\nto announce it. Not austere, but economical. Someone who chooses words the way they\nchoose light — deliberately, with care for what gets left out as much as what stays in.\n\nProfessionalism here means craft, not corporate. The app handles something personal\n— a photographer's finished work, a client's important memories. The voice should\nhonor that weight without becoming precious about it.\n\nInfluences: the way Magnum Photos writes about their work. The directness of a good\nphoto caption. Not the breathless enthusiasm of a SaaS landing page.\n\nThe document should include:\n- Core personality traits (3–4, with explanation)\n- Tone spectrum (when to be warmer vs. more spare)\n- Vocabulary: words we use, words we avoid\n- UI microcopy examples (button labels, empty states, error messages)\n- Both Spanish and English examples side by side\n\n\nThis second prompt goes straight to the character of the app. A brand voice guide is a document that rarely gets produced in an MVP phase — and it’s one of the most useful when the time comes to write microcopy or define how the app speaks to its users.\n\n\n\nPrompt 3 — UI Mocks\n\nUsing the frontend-design skill, create UI mocks for Liminal's critical screens.\nPull from the brand guide already established and the brand voice: quiet craft,\ndeliberate, editorial — not SaaS.\n\nPrioritize these screens in order:\n\nClient-facing (unauthenticated, via share link):\n1. Gallery landing — the first thing a client sees when they open their link.\n2. Collection view — browsing photos within a collection, with like, comment,\n   and download interactions visible.\n3. Download selection — choosing photos and quality before downloading as zip.\n\nPhotographer-facing (authenticated):\n4. Project dashboard — list of projects with status at a glance.\n5. Collection editor — uploading photos, reordering, setting cover photo.\n6. Share link manager — creating and tracking links, seeing which have been used.\n\nFor each screen:\n- Design for desktop first, note mobile considerations\n- Show real placeholder content — no Lorem Ipsum\n- Embed a short design rationale note explaining the key decision made for that screen\n\nAesthetic direction: editorial photography magazine meets quiet utility. The UI\nshould feel like it was designed by someone who photographs, not someone who ships\ndashboards.\n\n\nThe third prompt uses the frontend-design skill together with Maquina Components to generate HTML mocks of the critical screens. The result isn’t a Figma file — it’s a functional visual reference, coherent with the brand guide, before opening the editor.\n\n\n\nThe Result\n\nThree prompts. Six documents. Mocks of the main screens. All the context needed to hand off to Claude Code and start generating code with direction.\n\nIt’s the same process I used to build Resto, a personal finance app based on the Japanese Kakeibo method.\n\nThe video runs 40 minutes. It’s not an accelerated demo — it’s the real process, iterations and corrections included.\n\n\n\nInstallation\n\nAll plugins are available in the maquina-app/rails-claude-code repository. Full documentation at MVP Creator — Documentation.\n\nTo install MVP Creator in Claude Code:\n\n# Add the marketplace\n/plugin marketplace add maquina-app/rails-claude-code\n\n# Install the plugin\n/plugin install mvp-creator@maquina\n\n\nTo install the full set of plugins used in this series:\n\n/plugin install rails-simplifier@maquina\n/plugin install rails-upgrade-assistant@maquina\n/plugin install maquina-ui-standards@maquina\n/plugin install mvp-creator@maquina\n/plugin install better-stimulus@maquina\n/plugin install spec-driven-development@maquina\n\n\nFor the Claude graphical interface, download the repository as a zip, extract the mvp-creator folder, rename the extension to .skill, and drag it into the Claude window to install it."
        },
        {
          "id": "blog-2026-03-maquina-generators-production-ready-rails-setup",
          "title": "Maquina Generators: From rails new to Production-Ready",
          "collection": {
            "label": "posts",
            "name": "Blog"
          },
          "categories": "Announcements, Release",
          "tags": "",
          "url": "/blog/2026/03/maquina-generators-production-ready-rails-setup/",
          "content": "Every Rails project starts the same way. You run rails new, you get a clean app with sensible defaults, and then you do the setup work before you can write application code. Authentication with signup and multi-tenancy. Rate limiting. Background jobs with a dashboard. Error tracking. Mailer templates. Security headers. It’s repetitive, sure, but it’s the work that gets your app to the point where you can build the thing you actually sat down to build.\n\nI’ve done this enough times to know exactly what’s coming. The order changes, the names of the models drift slightly, but the shape of the work is identical. It’s not that Rails is missing anything — it’s that the space between rails new and “ready to build features” is full of choices that are mostly already made. You just have to type them out each time.\n\nMaquina Generators automate that setup. One command after rails new, and you have authentication, multi-tenancy, roles, job processing, error tracking, request protection, and ops dashboards. All generated into your app as plain Rails code. No runtime dependency.\n\nWhat Maquina Generators Do\n\nThe gem lives in your development group. It generates standalone application code — models, controllers, views, migrations, initializers, mailers — and then you can delete the gem. Nothing it produces requires the gem at runtime. No engine mounts, no middleware injection, no monkey patches. Just files in your app that you own completely.\n\nThe workflow is five commands:\n\nrails new myapp --css tailwind\nbundle add maquina-generators --group development\nrails generate maquina:app --auth clave\nbin/rails db:migrate\nbin/dev\n\n\nThat’s it. Auth with email verification codes, an Account model with roles, Rack Attack blocking scanners and throttling logins, Solid Queue with a Procfile, Solid Errors catching exceptions, Mission Control monitoring your jobs — all wired up, all running.\n\n\n\nSeven generators handle the pieces:\n\n\n  \n    \n      Generator\n      Purpose\n    \n  \n  \n    \n      App\n      Full application setup — orchestrates everything below\n    \n    \n      Clave\n      Passwordless email-code authentication\n    \n    \n      Registration\n      Password-based auth with accounts and roles\n    \n    \n      Rack Attack\n      Request protection and IP throttling\n    \n    \n      Solid Queue\n      Background job processing with separate database\n    \n    \n      Solid Errors\n      Error tracking dashboard\n    \n    \n      Mission Control\n      Job queue monitoring dashboard\n    \n  \n\n\nThe App generator is the orchestrator. It runs whichever auth generator you choose, then all the infrastructure generators in sequence. You can also run each generator independently if you only need part of the stack.\n\nThe full documentation covers every generator, option, and generated file in detail.\n\nTwo Authentication Options\n\nRails 8’s built-in rails generate authentication gives you login. It doesn’t give you signup. It doesn’t give you accounts, roles, or multi-tenancy. For most applications, login alone isn’t enough.\n\nMaquina Generators offer two complete authentication systems that pick up where Rails leaves off.\n\nClave: Passwordless\n\nClave implements passwordless authentication using email verification codes. The user enters their email, receives a 6-digit hexadecimal code, enters the code, and they’re in. No passwords to store, no password resets to build, no complexity requirements to argue about.\n\nUser enters email → receives 6-digit code → enters code → signed in\n\n\nCodes expire in 15 minutes. There’s a 15-minute cooldown before a resend. Login attempts are rate-limited to 10 per 3 minutes. Sessions last 30 days by default. Plus characters are blocked in email addresses to prevent alias attacks.\n\nBeyond sign-in, Clave generates a full multi-tenancy layer. Every user belongs to an Account. The first user who creates an account becomes its admin. A role enum — admin or member — handles authorization from there.\n\nCurrent.user          # The signed-in user\nCurrent.account       # The user's account\nCurrent.user.admin?   # Check role\n\n\nYou scope queries through the account, and cross-tenant access is prevented at the model level:\n\n@projects = Current.account.projects\n\n\nClave generates models, controllers, a mailer with HTML and text templates, a daily cleanup job for expired sessions and codes, a test helper with sign_in_as(user), and full i18n support in English and Spanish.\n\nRegistration: Password-Based\n\nIf you prefer passwords, the Registration generator builds on Rails 8’s authentication. It runs rails generate authentication first, then adds what’s missing: an Account model, belongs_to :account on User, the role enum, a RegistrationsController that creates an Account and User in a single transaction, and Tailwind-styled views.\n\nclass RegistrationsController &lt; ApplicationController\n  allow_unauthenticated_access\n  rate_limit to: 10, within: 3.minutes, only: :create\n\n  def create\n    ActiveRecord::Base.transaction do\n      account = Account.create!(name: params[:account_name])\n      user = account.users.create!(\n        name: params[:name],\n        email_address: params[:email_address],\n        password: params[:password],\n        role: :admin\n      )\n    end\n    start_new_session_for user\n    redirect_to root_path\n  end\nend\n\n\n\n\nSame Current.user, Current.account, and role-based authorization as Clave. The multi-tenancy pattern is identical — only the sign-in mechanism differs.\n\nThe generators documentation covers every option, model, and controller for both auth systems.\n\nThe Ops Layer\n\nAuthentication is the most visible piece, but the App generator does more than auth. It sets up a complete operational layer that most Rails apps need but few have on day one.\n\nRack Attack gets configured with real-world defaults. PHP file requests, WordPress scanning paths, .env and .git probes — all blocked immediately. Sensitive paths like /cgi-bin, /phpmyadmin, and /actuator return 403. General traffic is throttled to 300 requests per 5 minutes per IP, with asset paths exempted. Login endpoints get tighter limits: 5 attempts per 20 seconds.\n\nSolid Queue is set up as the Active Job backend with its own SQLite database, a Procfile entry for the worker process, and a recurring schedule that runs the authentication cleanup job daily at 3am. The configuration lives in config/solid_queue.yml — three worker threads, half-second polling, standard dispatching.\n\nSolid Errors and Mission Control Jobs get mounted as dashboards with custom Tailwind views. Mission Control alone has 41 view files — job listings, queue status, worker monitoring, recurring task management — all styled to match your application instead of looking like a default engine mount.\n\nBoth dashboards share the same HTTP basic auth credentials:\n\n# bin/rails credentials:edit\nbackstage:\n  username: admin\n  password: your_secure_password\n\n\nOne set of credentials, stored in Rails credentials. Environment variable fallbacks if you prefer. After running the generators, you have /admin/solid_errors and /admin/mission_control_jobs working from the first bin/dev.\n\n\n\nThe App generator also sets up multi-database configuration — separate SQLite databases for the queue, cache, cable, and errors — installs Active Storage and Action Text, configures Turbo morphing, adds brakeman and Standard for code quality, and creates a HomeController with a root route. It’s the full post-rails new checklist, automated.\n\nOwn the Code\n\nThis is the part that matters most. Maquina Generators is a development-only gem. It generates code into your application and then it’s done. You can — and should — delete it from your Gemfile once you’ve run the generators.\n\n# Gemfile — remove after generating\ngroup :development do\n  gem \"maquina-generators\"\nend\n\n\nEvery file it produces is a standard Rails file in a standard location. Models in app/models, controllers in app/controllers, views in app/views, initializers in config/initializers. No engine, no namespace, no gem dependency at runtime. If you want to change how sessions expire, you edit app/controllers/concerns/authentication.rb. If you want different Rack Attack rules, you edit config/initializers/rack_attack.rb. If you want to add a third role beyond admin and member, you update the enum on User.\n\nThere’s no DSL to learn, no configuration file to maintain, no version upgrades to track. The generated code follows Rails conventions because it is Rails code. You can read every line, understand every decision, and change anything that doesn’t fit your project.\n\nThis connects to the broader Maquina ecosystem. The generators set up the foundation — auth, security, ops tooling. Maquina Components handles the UI layer with ViewComponent-based partials that the App generator installs automatically. When you start building features on top of this foundation, Rails Simplifier keeps AI-generated code idiomatic, and the MCP Server gives AI tools visibility into your codebase structure.\n\nEach tool is independent. Use one, use all, use none. No lock-in at any layer.\n\nGet Started\n\nInstall the gem and run the app generator:\n\nrails new myapp --css tailwind\ncd myapp\nbundle add maquina-generators --group development\nrails generate maquina:app --auth clave\nbin/rails db:migrate\nbin/dev\n\n\nChoose --auth clave for passwordless, --auth registration for passwords, or --auth none if you want the infrastructure without authentication.\n\nFull documentation is at maquina.app/documentation/generators. Source code is on GitHub."
        },
        {
          "id": "blog-2026-02-recuerd0-source-code-now-available",
          "title": "Recuerd0 Source Code Is Now Available",
          "collection": {
            "label": "posts",
            "name": "Blog"
          },
          "categories": "Announcements, Product",
          "tags": "",
          "url": "/blog/2026/02/recuerd0-source-code-now-available/",
          "content": "The self-hosted promise is fulfilled — Recuerd0’s source code is on GitHub.\n\n\n\nWhen we announced Recuerd0, we said the self-hosted version would be available pretty soon. Today it is. The full source code is on GitHub under the OSASSY license.\n\nThis is not a stripped-down edition. It’s the same codebase that runs recuerd0.ai — every feature, every endpoint, every migration.\n\nUnder the hood\n\nRecuerd0 is a Rails 8.1 application running on Ruby 4.0. The entire stack leans into the One Person Framework philosophy: minimize infrastructure, eliminate external dependencies, ship with confidence.\n\nSQLite for everything. Data, cache, queue, and cable — all backed by SQLite. No Postgres. No Redis. Solid Queue handles background jobs, Solid Cache handles caching, and Solid Cable handles WebSocket connections. One database engine, zero extra services.\n\nNo Node.js. The frontend uses Propshaft for asset delivery and Importmaps for JavaScript modules. Hotwire (Turbo + Stimulus) handles interactivity. Tailwind CSS 4 handles styling. The entire frontend pipeline runs without a JS build step.\n\nFull-text search with FTS5. Search is powered by SQLite’s FTS5 extension — no vector database, no embeddings, no RAG pipeline. The index updates on every write, returns results in milliseconds, and is fully deterministic. The agent decides what to search for; the database does the rest.\n\nMemory versioning. Every memory supports a flat branching model — create new versions from any point in history. Soft deletion with 30-day retention means nothing disappears by accident.\n\nMulti-tenancy. The Account model supports multiple tenants. In single-tenant mode (the default for self-hosted), public registration is disabled — you control who has access.\n\nUI components. The interface is built with the maquina-components gem, the same component library used across all Maquina projects.\n\nGetting started\n\nTwo paths to self-host:\n\nDocker image. Pull the ready-to-use Docker image and deploy. Configure your environment variables and you’re running.\n\nFrom source. Clone the repository, configure Kamal 2.x, and deploy to your server. The included Dockerfile and Kamal configuration handle the rest. Thruster sits in front of Puma, and SOLID_QUEUE_IN_PUMA=true runs background jobs in-process — one container, one process, everything included.\n\nSingle-tenant mode is the default. No public registration, no setup wizard. Deploy, create your account, start curating context.\n\nLicense\n\nRecuerd0 is released under the OSASSY license. It’s essentially MIT with one restriction: you can’t take the code and offer it as a competing hosted service. The same model 37signals uses. Deploy it on your infrastructure, modify it, use it internally — free forever.\n\nNot interested in self-hosting?\n\nRecuerd0 SaaS is $15/month for up to 10 users — managed hosting, automatic backups, and updates. Read the full product announcement for the complete story.\n\nThe source is on GitHub. Do what you want with it.\n\nView the repository →\n\n\n\nRecuerd0 is built by Maquina. Source available under OSASSY license."
        },
        {
          "id": "blog-2026-02-announcing-recuerd0",
          "title": "Announcing Recuerd0: A Knowledge Base for AI Tool Context",
          "collection": {
            "label": "posts",
            "name": "Blog"
          },
          "categories": "Announcements, Product",
          "tags": "",
          "url": "/blog/2026/02/announcing-recuerd0/",
          "content": "Organize, version, and serve project context to any LLM — from Claude Code to Cursor to ChatGPT.\n\n\n\nEvery AI coding tool starts each session with amnesia. Your architecture decisions, naming conventions, and deployment quirks — none of it carries over. You re-explain the same context with the same tools every single day.\n\nThe common workarounds are CLAUDE.md files, .cursorrules, AGENTS.md — each tool with its own configuration format. You end up duplicating knowledge across multiple places. They drift apart. Your Claude Code config says one thing; your Cursor rules say another.\n\nRecuerd0 is a dedicated knowledge base for managing the context your AI tools consume. You curate project knowledge once and serve it to every tool via REST API.\n\n\n\nHow it works\n\nWorkspaces group knowledge by project or domain. Backend conventions in one workspace, frontend patterns in another, org-wide standards in a shared workspace.\n\n\n\nMemories are versioned markdown documents with titles, tags, and full history. When conventions evolve, you create a new version — like Git for context. Branch from any version, track how decisions changed, and never lose the rationale.\n\n\n\nAccess is through a REST API with Bearer token authentication. Any tool that can make an HTTP request reads from the same source. There’s also a CLI for terminal workflows and a Claude Code plugin for tighter integration.\n\nSearch uses the database’s full-text search with millisecond performance. No embeddings, no vector database, no RAG pipeline. The agent decides what to search for and how to refine the search. The index updates on every write, is deterministic, and requires zero infrastructure beyond the database.\n\nArchitecture decisions\n\nHuman-curated, not auto-captured. Automatic knowledge capture sounds appealing, but it produces noisy results — context-specific fixes that don’t generalize, contradictory items as conventions evolve. The human decides what’s worth persisting. The team reviews and evolves it.\n\nTool-agnostic by design. We built an API, not a plugin for one tool. Your knowledge base survives any tool change. Claude Code, Cursor, ChatGPT, Windsurf, custom scripts, CI/CD pipelines — same context, same source.\n\nSmall and focused. Recuerd0 is designed for a small set of focused memories per workspace philosophy, not thousands of files. The constraint forces curation. When the workspace is focused, the right answer is obvious without sophisticated search algorithms.\n\nPricing\n\nRecuerd0 SaaS is $15/month for up to 10 users. Managed hosting, automatic backups, updates, and email support. Create an account and start in minutes at recuerd0.ai.\n\nTeams of 6 or more can contact us for custom plans.\n\nSelf-hosted is available under the OSASSY license — the same model 37signals uses for Fizzy. It’s essentially MIT with one addition: you can’t take the code and offer it as a competing hosted service. Deploy on your server, modify the code, use it internally — free forever.\n\nThe self-hosted version is not available at launch, but it will be available pretty soon.\n\nGet started\n\nThe API documentation covers every endpoint. The CLI reference has installation and commands. The agent workflows guide shows how to integrate with Claude Code, Cursor, and other tools.\n\nContext engineering has become a core developer skill. It deserves a dedicated tool.\n\nStart with Recuerd0 SaaS →\n\n\n\nRecuerd0 is built by Maquina. Source available under OSASSY license."
        },
        {
          "id": "blog-2026-02-maquina-components-0-4-0-turbo-compatibility",
          "title": "Maquina Components 0.4.0: Taming Turbo",
          "collection": {
            "label": "posts",
            "name": "Blog"
          },
          "categories": "Announcements, Release",
          "tags": "",
          "url": "/blog/2026/02/maquina-components-0-4-0-turbo-compatibility/",
          "content": "I was working on a Rails application—standard CRUD with a sidebar and a few interactive menus. Everything worked on first load. Then I navigated away and came back. The sidebar was gone. I opened a dropdown, clicked a Turbo link, hit the back button. The dropdown was still open, sitting there on top of a page that had already moved on.\n\nIf you’ve built anything with Turbo and Stimulus beyond basic forms, you’ve likely seen this. Components work fine on full page loads, but Turbo introduces a different lifecycle. Pages get cached mid-state, morphs overwrite client-side changes with stale server HTML, and your UI ends up stuck in states it should have left behind.\n\nFixing this in Maquina Components is what version 0.4.0 is about.\n\nThe Teardown Pattern\n\nThe core problem is described well by Better Stimulus. When Turbo navigates away from a page, it takes a snapshot of the DOM before leaving. When the user returns, Turbo shows that snapshot first. Any DOM changes your Stimulus controllers made—open menus, expanded panels, loading classes—get frozen into the cache.\n\nThe standard Stimulus disconnect callback handles general cleanup, but it doesn’t distinguish between “the element was removed from the DOM” and “Turbo is about to cache this page.” You need both.\n\nThe Teardown pattern adds a teardown method to controllers, triggered by Turbo’s turbo:before-cache event. Every controller that manipulates the DOM can opt in, resetting its visual state before Turbo takes the snapshot. This keeps disconnect clean for general lifecycle concerns and gives Turbo-specific rollback its own dedicated path.\n\nThis release applies that pattern across the interactive components in the library.\n\nSidebar: Three Problems at Once\n\nThe sidebar was the hardest to get right. It had three separate issues interacting with each other.\n\nRandom IDs broke morphing. The sidebar generated IDs like sidebar-a3f9b2 on every render. Turbo’s idiomorph algorithm matches elements by ID—when the ID changes every time, idiomorph can’t find the element and treats it as new. Every morph was destroying and recreating the sidebar from scratch. The fix: deterministic IDs. sidebar-left and sidebar-right, consistent across renders. The sidebar provider also gets a stable ID (sidebar-provider by default).\n\nMorphs overwrote client state. The sidebar stores its open/closed state in a cookie so it persists across page loads. During a Turbo morph, the server sends back HTML with the default state—it doesn’t know about the cookie. Idiomorph applies the server HTML, and the sidebar collapses even though the user had it open.\n\nThe fix adds a turbo:before-morph-element listener with a _morphing guard flag. When a morph happens, the controller reads the cookie (the source of truth on the client), reasserts the correct state, and strips the sidebar-loading class that the server HTML reintroduces.\n\nLayout shift on desktop. When Stimulus initialized and switched the sidebar from its mobile offcanvas mode to the desktop collapsible mode, there was a visible jump. The transition happened after the browser had already painted. This release smooths that handoff so the mode switch doesn’t cause a flash.\n\nThe Yield Trap\n\nThe second category of fixes has nothing to do with Turbo. It’s a Rails rendering behavior that caught me off guard.\n\nNine partials in the library used the standard block pattern:\n\n&lt;%= render \"components/card/description\" do %&gt;\n  &lt;p&gt;Custom HTML&lt;/p&gt;\n&lt;% end %&gt;\n\n\nThis works when you always pass a block. But render the partial without a block and yield inside it doesn’t return nothing—it renders the entire page’s content into the partial. Rails treats the missing block as a signal to yield the page-level content instead.\n\nThe result: components rendering the full page body inside a card title or a toast message. It only shows up in specific usage patterns, and when it does, the output looks completely wrong with no obvious cause.\n\nThe fix replaces yield with an explicit content: parameter in all nine affected partials:\n\n\n  card/title, card/description\n  alert/title, alert/description\n  toast/title, toast/description\n  combobox/label, toast (main), toaster\n\n\nThe five toast helper methods no longer accept blocks either.\n\nBreaking Changes\n\nThis is a minor version bump with breaking changes:\n\n\n  Block syntax removed for the 9 partials listed above. Use content: capture { ... } instead of do ... end.\n  Toast helpers no longer accept blocks. Use the content: parameter.\n  Sidebar IDs changed from sidebar-&lt;random_hex&gt; to sidebar-left / sidebar-right.\n  Sidebar provider now has a stable id attribute (sidebar-provider by default).\n\n\nMigration\n\nThe content parameter change is mechanical. Find every block-style call to the affected partials and wrap the content with capture:\n\n&lt;%= render \"components/card/description\" do %&gt;\n  &lt;p&gt;Custom HTML&lt;/p&gt;\n&lt;% end %&gt;\n\n&lt;%= render \"components/card/description\",\n      content: capture { %&gt;\n  &lt;p&gt;Custom HTML&lt;/p&gt;\n&lt;% } %&gt;\n\n\nFor sidebar IDs, if you reference specific sidebar element IDs in JavaScript or tests, update them to sidebar-left or sidebar-right.\n\nUpgrading\n\nbundle update maquina_components\n\n\nWhat This Reinforced\n\nTurbo is not a transparent layer over page loads. It’s a different execution model. Any Stimulus controller that touches the DOM needs to account for caching, morphing, and the gap between what the server renders and what the client has changed since. The Teardown pattern should be the default starting point for any controller that does more than read values.\n\nThe yield behavior in Rails partials was a genuine surprise. It’s documented, but it’s a quiet trap when you have optional block content. Explicit parameters are safer.\n\nDocumentation\n\n\n  Component Documentation\n  Sidebar\n  Card\n  Alert\n  Toast\n  Combobox\n\n\nSource\n\n\n  Maquina Components\n  Full Changelog\n  Better Stimulus: Teardown Pattern"
        },
        {
          "id": "blog-2026-01-maquina-0-3-1-calendar-date-picker-claude-skills",
          "title": "Maquina 0.3.1: Calendar, Date Picker & Claude Code Skills",
          "collection": {
            "label": "posts",
            "name": "Blog"
          },
          "categories": "Announcements, Release",
          "tags": "",
          "url": "/blog/2026/01/maquina-0-3-1-calendar-date-picker-claude-skills/",
          "content": "This month brings updates across the Maquina ecosystem: new Calendar and Date Picker components for Rails, two Claude Code skills for AI-assisted development, and live interactive previews for all components in the documentation.\n\nMaquina Components 0.3.1\n\nBuilding on version 0.3.0, this release adds two components for date selection: Calendar and Date Picker.\n\nCalendar\n\nAn inline calendar for single date or range selection. Useful when you need the full calendar visible—booking flows, availability displays, or any context where date proximity matters.\n\n&lt;%= render \"components/calendar\",\n      mode: :range,\n      selected: Date.today,\n      selected_end: Date.today + 5 %&gt;\n\n\nFor form integration, the calendar generates hidden inputs automatically:\n\n&lt;%= form_with model: @booking do |f| %&gt;\n  &lt;%= render \"components/calendar\",\n        mode: :range,\n        input_name: \"booking[check_in]\",\n        input_name_end: \"booking[check_out]\" %&gt;\n&lt;% end %&gt;\n\n\nFeatures: Single or range selection, min/max date constraints, disabled dates, week start configuration, and direct form integration with hidden inputs.\n\nDate Picker\n\nA button that opens a calendar in a popover. Better for forms where space is limited and you don’t need the calendar always visible.\n\n&lt;%= render \"components/date_picker\",\n      mode: :single,\n      placeholder: \"Select a date\",\n      input_name: \"event[date]\" %&gt;\n\n\nRange selection works the same way:\n\n&lt;%= render \"components/date_picker\",\n      mode: :range,\n      placeholder: \"Select date range\",\n      input_name: \"start_date\",\n      input_name_end: \"end_date\" %&gt;\n\n\nFeatures: Single or range selection, pre-selected date display, min/max boundaries, disabled state, and customizable placeholders.\n\nWhen to Use Which\n\n\n  \n    \n      Use Case\n      Component\n    \n  \n  \n    \n      Booking calendar with visible availability\n      Calendar\n    \n    \n      Date field in a form\n      Date Picker\n    \n    \n      Date range with context (prices, events)\n      Calendar\n    \n    \n      Quick date selection in limited space\n      Date Picker\n    \n  \n\n\nLive Previews\n\n\n\nThe documentation site now includes live, interactive previews for all components. Visit any component page in the documentation to see working examples in light and dark themes, multiple color variations, and code ready to copy.\n\nFor a complete showcase, the live demo application shows all components working together with sample data.\n\nUpgrading\n\nbundle update maquina_components\n\n\nNo generator changes required for existing installations.\n\nClaude Code Skills\n\nTwo new skills for AI-assisted Rails development.\n\nMaquina UI Standards\n\nTeaches Claude how to build UIs with maquina_components. Without guidance, Claude generates generic Rails patterns—plain divs, inline styles, inconsistent markup. With this skill, Claude generates code using your actual component library.\n\nIncludes: Component catalog with 20+ components, form patterns, layout patterns, Turbo integration, and accessibility guidelines.\n\n/plugin marketplace add maquina-app/rails-claude-code\n/plugin install maquina-ui-standards@maquina\n\n\nAsk Claude “Create a users index view with a table” and get:\n\n&lt;%= render \"components/card\" do %&gt;\n  &lt;%= render \"components/card/header\" do %&gt;\n    &lt;%= render \"components/card/title\", text: \"Users\" %&gt;\n  &lt;% end %&gt;\n  &lt;%= render \"components/card/content\" do %&gt;\n    &lt;%= render \"components/table\" do |t| %&gt;\n      &lt;% t.header do %&gt;\n        &lt;% t.head_cell \"Name\" %&gt;\n        &lt;% t.head_cell \"Email\" %&gt;\n      &lt;% end %&gt;\n    &lt;% end %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\n\nDocumentation: Maquina UI Standards\n\nRails Simplifier\n\nRefines Rails code following 37signals patterns and the One Person Framework philosophy.\n\nWhat it does: Converts service objects to model methods, transforms custom actions to CRUD resources, moves logic from controllers to models, detects N+1 queries, and applies Rails conventions like I18n and Time.current.\n\n/plugin marketplace add maquina-app/rails-claude-code\n/plugin install rails-simplifier@maquina\n\n\nExample prompts:\n\n&gt; Review recent changes using the rails-simplifier agent\n&gt; Use rails-simplifier to review the bookings controller\n\n\nDocumentation: Rails Simplifier\n\nSource\n\nAll projects are MIT licensed:\n\n\n  Maquina Components\n  Rails Claude Code Skills"
        },
        {
          "id": "blog-2026-01-claude-skill-for-maquina-components",
          "title": "Claude Skill for Maquina Components",
          "collection": {
            "label": "posts",
            "name": "Blog"
          },
          "categories": "Announcements, Tools",
          "tags": "",
          "url": "/blog/2026/01/claude-skill-for-maquina-components/",
          "content": "When I started extracting and standardizing maquina_components from real production applications, I was also experimenting with AI-assisted development. The two efforts ran in parallel—building a consistent component library while trying to get Claude to help me use it.\n\nThe results were mixed. Every time I asked Claude to build a view or implement a form, it was back and forth. “Use the card partial, not a div.” “The input needs a data attribute.” “That’s not how the combobox works.” I spent as much time correcting the AI as I would have spent writing the code myself.\n\nThe same friction appeared when writing specs. I’d describe a feature and Claude would suggest generic Rails patterns instead of the components I had available. It didn’t know about the library. How could it?\n\nThe Skill Experiment\n\nWhen Anthropic released the Skills functionality, I wondered if it was the right tool for this problem. Skills let you teach Claude project-specific knowledge—conventions, patterns, APIs. Exactly what was missing.\n\nI created a first version: a structured reference with component examples, form patterns, layout conventions, and Turbo integration guides. Added it to my projects and started using it.\n\nIt worked. Claude started generating code that matched my conventions. The combobox had proper keyboard navigation. Forms used the right data attributes. Turbo Streams updated components correctly. The back-and-forth dropped significantly.\n\nI kept the skill private. It was tied to my workflow, my projects. Not ready for others.\n\nMaking It Public\n\nYesterday I published Maquina Components 0.3.0 with Combobox and Toast. Shortly after, someone asked if I had an MCP server for the components.\n\nI replied that I had something better—a Claude Skill that I’d been using for while now. It was working great with the gem.\n\nSo I decided to open source it.\n\nWhat the Skill Provides\n\nA complete reference for building UIs with maquina_components:\n\n\n  \n    \n      Reference\n      Purpose\n    \n  \n  \n    \n      Component catalog\n      All 15+ components with ERB examples\n    \n    \n      Form patterns\n      Validation, error handling, inline layouts\n    \n    \n      Layout patterns\n      Sidebar navigation, page structure\n    \n    \n      Turbo integration\n      Frames, Streams, component updates\n    \n    \n      Spec checklist\n      Review criteria for UI quality\n    \n  \n\n\nInstallation\n\nCreate a skills directory in your Rails project:\n\ncd your-rails-app\nmkdir -p .claude/skills\n\n\nDownload the skill from the maquina_components repository and copy it to .claude/skills/maquina-ui-standards/.\n\nThen update your CLAUDE.md to reference it:\n\n## UI Components\n\nThis project uses maquina_components for UI. Before implementing views,\nforms, or interactive components, read the UI standards skill:\n\n.claude/skills/maquina-ui-standards/SKILL.md\n\nAlways consult the skill when:\n- Creating or modifying views\n- Implementing forms\n- Adding interactive components\n- Building layouts with sidebar/header patterns\n- Working with Turbo Streams that update UI\n\n\nUsage\n\nOnce installed, ask Claude naturally:\n\nCreate the users index view with a table showing name, email, and status.\n\n\nImplement the project form with name, description, and a framework combobox.\n\n\nReview this view against the maquina UI standards and suggest improvements.\n\n\nThe generated code matches what you’d write manually—just faster, and without the back-and-forth.\n\nSource\n\nThe skill is included in the maquina_components repository under MIT license. Updates follow gem releases."
        },
        {
          "id": "blog-2026-01-maquina-components-0.3.0-combobox-and-toast",
          "title": "Maquina Components 0.3.0: Combobox and Toast",
          "collection": {
            "label": "posts",
            "name": "Blog"
          },
          "categories": "Announcements, Release",
          "tags": "",
          "url": "/blog/2026/01/maquina-components-0.3.0-combobox-and-toast/",
          "content": "Version 0.3.0 of Maquina Components adds two frequently requested interactive components: Combobox and Toast.\n\nBoth components follow the same philosophy as the rest of the library—ERB partials, Tailwind CSS, and Stimulus controllers only where necessary.\n\nCombobox\n\n\n\nAn autocomplete input with a searchable dropdown list. Useful when selecting from many options—countries, users, tags, or any list that benefits from filtering.\n\n&lt;%= combobox placeholder: \"Select framework...\" do |cb| %&gt;\n  &lt;% cb.trigger %&gt;\n  &lt;% cb.content do %&gt;\n    &lt;% cb.input placeholder: \"Search...\" %&gt;\n    &lt;% cb.list do %&gt;\n      &lt;% cb.option value: \"rails\" do %&gt;Ruby on Rails&lt;% end %&gt;\n      &lt;% cb.option value: \"django\" do %&gt;Django&lt;% end %&gt;\n      &lt;% cb.option value: \"phoenix\" do %&gt;Phoenix&lt;% end %&gt;\n    &lt;% end %&gt;\n    &lt;% cb.empty %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\n\nFor simpler use cases, the data-driven helper builds the entire structure from an array:\n\n&lt;%= combobox_simple placeholder: \"Select country...\",\n                     name: \"user[country]\",\n                     options: Country.all.map { |c| { value: c.code, label: c.name } } %&gt;\n\n\nFeatures\n\n\n  Keyboard navigation (arrows, Home, End, Escape)\n  Real-time filtering as you type\n  Grouped options with labels and separators\n  Multiple width and alignment options\n  Full ARIA support (role=\"combobox\", role=\"listbox\")\n\n\nRequirements\n\nThe Combobox uses the HTML5 Popover API for light-dismiss behavior. Most modern browsers support it natively:\n\n\n  \n    \n      Browser\n      Version\n    \n  \n  \n    \n      Chrome\n      114+\n    \n    \n      Edge\n      114+\n    \n    \n      Safari\n      17+\n    \n    \n      Firefox\n      125+\n    \n  \n\n\nFor older browsers, add the popover polyfill:\n\nnpm install @oddbird/popover-polyfill\n\n\n// app/javascript/application.js\nimport \"@oddbird/popover-polyfill\"\n\n\nToast\n\n\n\nNon-intrusive notifications that appear temporarily and dismiss automatically. Ideal for form submission feedback, background task completion, or any transient message.\n\nServer-Side with Flash Messages\n\nThe most common pattern—render Rails flash messages as toasts:\n\n&lt;%= render \"components/toaster\", position: :bottom_right do %&gt;\n  &lt;%= toast_flash_messages %&gt;\n&lt;% end %&gt;\n\n\n# In your controller\nflash[:success] = \"Profile updated successfully!\"\nredirect_to @user\n\n\nFlash types map automatically to toast variants: :success, :error, :warning, :info.\n\nJavaScript API\n\nFor dynamic notifications without a page reload:\n\nToast.success(\"Changes saved!\")\n\nToast.error(\"Connection lost\", {\n  description: \"Please check your internet connection.\"\n})\n\nToast.warning(\"Session expiring\", { duration: 10000 })\n\n// Dismiss programmatically\nconst id = Toast.info(\"Processing...\")\nToast.dismiss(id)\n\n\nWith Turbo Streams\n\nAppend toasts to the container in Turbo Stream responses:\n\n&lt;%= turbo_stream.append \"toaster\" do %&gt;\n  &lt;%= toast :success, \"Post published!\" %&gt;\n&lt;% end %&gt;\n\n\nFeatures\n\n\n  Five variants: default, success, info, warning, error\n  Auto-dismiss with configurable duration (pauses on hover)\n  Six positioning options (corners and center edges)\n  Optional action buttons for undo/view operations\n  Full keyboard accessibility\n\n\nRequirements\n\nToast requires Stimulus for the auto-dismiss timer and JavaScript API. Add the controller to your Stimulus application:\n\n// app/javascript/application.js\nimport { Application } from \"@hotwired/stimulus\"\nimport { eagerLoadControllersFrom } from \"@hotwired/stimulus-loading\"\n\nconst application = Application.start()\neagerLoadControllersFrom(\"controllers\", application)\n\n\nUpgrading\n\nbundle update maquina_components\n\n\nNo generator changes are required. Both components use the existing theme variables.\n\nSee Them in Action\n\nTo explore Combobox, Toast, and all other components with demo data, clone the repository and run the dummy application:\n\ngit clone https://github.com/maquina-app/maquina_components.git\ncd maquina_components/test/dummy\nbin/dev\n\n\nThen visit http://localhost:5300 to interact with the full component showcase.\n\nDocumentation\n\n\n  Combobox documentation\n  Toast documentation\n  Full component list\n\n\nSource\n\nThe gem is MIT licensed. Source and issues on GitHub."
        },
        {
          "id": "blog-2025-12-rails-mcp-server-1-5-0-security-hardening",
          "title": "Rails MCP Server 1.5.0: Security Hardening and Sandboxed Environment Support",
          "collection": {
            "label": "posts",
            "name": "Blog"
          },
          "categories": "Announcements, Release, AI Tools",
          "tags": "",
          "url": "/blog/2025/12/rails-mcp-server-1-5-0-security-hardening/",
          "content": "Open source projects get better when people contribute back. Rails MCP Server 1.5.0 is a direct result of that—a release shaped significantly by a community contribution that I didn’t write.\n\nThe most important change in this version is a comprehensive security overhaul contributed by GitHub user hellvinz through PR #25. It’s the kind of work that doesn’t get enough recognition.\n\nThe Security Contribution\n\nWhen you give an AI model access to your codebase through MCP tools, security matters. The execute_ruby sandbox already restricted dangerous operations, but the file-accessing analyzers needed more rigorous input validation.\n\nPathValidator Module\n\nA centralized validation layer now protects all file-accessing analyzers. Path traversal attempts are blocked. Sensitive files are filtered automatically. The implementation is clean:\n\n# Path traversal attempts are blocked\nget_file(path: \"../../../etc/passwd\")\n# =&gt; \"Access denied: Path is outside the project directory\"\n\n# Sensitive files are filtered\nlist_files(pattern: \"config/*.key\")\n# =&gt; master.key, credentials.yml.enc excluded from results\n\n\nThe validator catches:\n\n  Path traversal attacks (../ sequences)\n  Absolute path access outside the project\n  Attempts to read sensitive files (master.key, credentials.yml.enc, .env)\n\n\nInjection Prevention\n\nShell commands now use IO.popen with array arguments instead of string interpolation. Table names in schema queries are validated against a strict pattern. These changes close potential injection vectors that existed in earlier versions.\n\nCI Infrastructure\n\nBeyond the code changes, hellvinz added security infrastructure I should have set up from the start:\n\n  Dependabot for dependency updates\n  CodeQL for static analysis\n  OpenSSF Scorecard integration\n  A proper SECURITY.md for vulnerability reporting\n\n\nThis kind of contribution takes real effort. Reviewing an unfamiliar codebase, identifying gaps, implementing fixes that don’t break existing functionality, unglamorous work that makes the project better for everyone who uses it.\n\nI’m grateful for the contribution.\n\nSandboxed Environment Support\n\nAI coding agents increasingly run in sandboxed environments—containers or restricted shells where they can only access the current project directory. GitHub Copilot Agent and Claude Code Agent both work this way.\n\nPrevious versions of Rails MCP Server assumed access to a user home directory for configuration files. That doesn’t work in a sandbox.\n\nThe --single-project flag solves this. It tells the server to use the current working directory as the only project, skipping configuration files entirely:\n\nrails-mcp-server --single-project\n\n\nGitHub Copilot Agent configuration goes in .vscode/mcp.json:\n\n{\n  \"servers\": {\n    \"rails-mcp\": {\n      \"command\": \"rails-mcp-server\",\n      \"args\": [\"--single-project\"]\n    }\n  }\n}\n\n\nClaude Code Agent can use the same flag. The server detects it’s running in a Rails directory and works immediately—no setup required.\n\nThis also simplifies CI/CD pipelines and any environment where you want the server to just work with the current directory.\n\nThe Copilot Agent documentation covers the setup in detail.\n\nSimplified Project Configuration\n\nPrevious versions required manual configuration in ~/.config/rails-mcp/projects.yml. That still works, but 1.5.0 adds flexibility:\n\n\n  \n    \n      Method\n      Use Case\n    \n  \n  \n    \n      --single-project flag\n      Sandboxed agents (Copilot, Claude Code), CI/CD\n    \n    \n      RAILS_MCP_PROJECT_PATH env var\n      Explicit path control\n    \n    \n      Auto-detection\n      Finds Rails apps from Gemfile, engines from gemspec\n    \n    \n      projects.yml\n      Multiple projects with named references\n    \n  \n\n\nThe server now auto-detects Rails applications by checking for a Gemfile with the rails gem, and Rails engines by looking for gemspec files with Rails dependencies. When only one project is available, it switches automatically.\n\nRails 8.1 Compatibility\n\nRails 8.1 changed the internal callback API. The analyze_controller_views tool was calling callback.options to extract :only and :except conditions, but that method no longer exists.\n\nThe fix maintains backward compatibility:\n\ncallbacks: controller._process_action_callbacks.map { |cb|\n  h = { kind: cb.kind.to_s, filter: cb.filter.to_s }\n  if cb.respond_to?(:options)\n    h[:only] = Array(cb.options[:only]).map(&amp;:to_s)\n    h[:except] = Array(cb.options[:except]).map(&amp;:to_s)\n  end\n  h\n}\n\n\nThis works with Rails 6.0 through 8.1. The callback conditions are extracted when available, omitted when not.\n\nOther Changes\n\nError messages now include hints. When you ask for a model named users instead of User, the error explains the naming convention. Small things that reduce friction.\n\nParameter passing in execute_tool is fixed. The params schema now generates correctly for MCP clients, so tools like analyze_models can actually receive their parameters. This was a real bug that made the tool harder to use than it should have been.\n\nInput validation for load_guide prevents path traversal in guide names. Another gap that hellvinz’s security review prompted me to address.\n\nBreaking Change\n\nThe load_guide analyzer renamed its parameter from guides to library:\n\n# Before (1.4.x)\nexecute_tool(\"load_guide\", { guides: \"rails\", guide: \"active_record\" })\n\n# After (1.5.0)\nexecute_tool(\"load_guide\", { library: \"rails\", guide: \"active_record\" })\n\n\nThe change clarifies that you’re selecting a documentation library (rails, turbo, stimulus, kamal, custom), not multiple guides. It’s a small breaking change, but the naming is more accurate.\n\nUpgrading\n\ngem update rails-mcp-server\n\n\nIf you’re using Claude Desktop, restart it to pick up the new version. The server binary path in your configuration doesn’t change.\n\nFor new installations:\n\ngem install rails-mcp-server\nrails-mcp-config\n\n\nThe interactive configuration tool handles Claude Desktop setup, project registration, and guide downloads.\n\nWhat’s Next\n\nThe MCP specification continues to evolve. As more AI tools adopt the protocol, Rails MCP Server will adapt to support them.\n\nIf you find issues or have ideas, the issue tracker is open. Pull requests are welcome. As this release shows, community contributions make a real difference—sometimes more than you might expect.\n\nLinks\n\n\n  GitHub Repository\n  RubyGems\n  Documentation\n  AI Agent Guide\n  Copilot Agent Setup"
        },
        {
          "id": "blog-2025-12-announcing-maquina-components-opinionated-ul-for-rails-applications",
          "title": "Announcing Maquina Components: Opinionated Ul for Rails Applications",
          "collection": {
            "label": "posts",
            "name": "Blog"
          },
          "categories": "Announcements, Release",
          "tags": "",
          "url": "/blog/2025/12/announcing-maquina-components-opinionated-ul-for-rails-applications/",
          "content": "Rails has opinions about most things. Database migrations, routing, asset handling, background jobs. But when it comes to building user interfaces, you’re on your own.\n\nThe framework gives you excellent primitives: importmaps, Stimulus, Turbo. But no default components. No standard way to build a button, a card, or a data table. Every Rails developer reinvents these from scratch.\n\nMaquina Components is my attempt to fill this gap—not the definitive solution, but one practical approach that works for how I build applications.\n\nWhy This Exists\n\nI started building components inspired by shadcn/ui for production Rails applications—dashboards, admin interfaces, internal tools. Over time, these components spread across multiple projects and became inconsistent: different APIs, different styling approaches, different levels of completeness.\n\nIt was time to extract the elements I use most and give them a cohesive API and consistent styling.\n\nThe Technical Choices\n\nI chose ERB partials with Tailwind CSS and Stimulus controllers for interactive elements. For static components like form inputs, pure CSS with data attributes is enough.\n\n&lt;%= render \"components/card\" do %&gt;\n  &lt;%= render \"components/card/header\" do %&gt;\n    &lt;%= render \"components/card/title\", text: \"Projects\" %&gt;\n  &lt;% end %&gt;\n  &lt;%= render \"components/card/content\" do %&gt;\n    &lt;%= render \"components/table\", collection: @projects %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\n\nI’m aware of alternatives like ViewComponent and Phlex. The projects I extracted these components from didn’t use them. I see the benefits of using a Ruby class to render UI, but bringing either library into a project is a commitment—not all teams are ready to make it.\n\nThe reason isn’t technical. It’s the perception of moving away from “the Rails way.” ERB partials are what Rails developers learn first. They’re simple, they work, and everyone understands them immediately.\n\nWhat’s Included\n\nTwelve components extracted from production applications:\n\n\n  \n    \n      Category\n      Components\n    \n  \n  \n    \n      Layout\n      Sidebar, Header\n    \n    \n      Content\n      Card, Alert, Badge, Table, Empty State\n    \n    \n      Navigation\n      Breadcrumbs, Dropdown Menu, Pagination\n    \n    \n      Interactive\n      Toggle Group\n    \n    \n      Forms\n      Input, Select, Checkbox, Button (via data attributes)\n    \n  \n\n\nEach component follows the shadcn/ui theming convention with CSS variables. Light and dark mode work out of the box.\n\nComposability Over Convenience\n\nThese components are intentionally small. A card is five partials: wrapper, header, title, description, content, footer. That’s more code to write than a single &lt;%= card(...) %&gt; helper.\n\nBut composition is the point. You take these partials and build larger, application-specific components. A ProjectCard that combines Card + Badge + Button. A UserTable that extends Table with custom columns. There are no limits because you own the abstraction layer.\n\nWhat I Didn’t Build\n\nI didn’t port shadcn/ui one-to-one. I extracted only the components I actually use. This is a practical toolkit, not a complete design system.\n\nIf you need modals, tooltips, date pickers, or complex form builders—those aren’t here yet. They might come later if I need them in my own projects.\n\nThe Rails Frontend Landscape\n\nThere’s no single UI kit that dominates Rails development. The community has fragmented across different approaches:\n\n\n  ViewComponent and Phlex for Ruby-based component abstractions\n  Inertia.js for React/Vue integration\n  Various shadcn/ui ports with different philosophies\n\n\nEvil Martians has written extensively about modern frontend in Rails. Their work with ViewComponent and Inertia.js is excellent, but those approaches add dependencies I prefer to avoid.\n\nMaquina Components takes a different path: standard ERB, standard Tailwind, minimal JavaScript. If you’re building server-rendered Rails applications and want components that don’t require learning a new paradigm, this might work for you.\n\nAlternatives\n\nIf this approach doesn’t resonate, here are alternatives worth exploring:\n\n\n  RailsUI — Premium UI templates and components\n  RailsBlocks — Copy-paste components for Rails\n  shadcn-rails — Another shadcn/ui port\n  Inertia Rails Starter — React/Vue with Inertia\n\n\nGetting Started\n\nbundle add maquina_components\nrails generate maquina_components:install\n\n\nThe generator adds the engine CSS, theme variables, and a helper file for icon customization.\n\nBrowse the documentation for examples and API details. The test/dummy application in the repository shows all components with demo data.\n\nOpen Source\n\nMaquina Components is MIT licensed. The source is on GitHub.\n\nIf you try it and have feedback, I’d like to hear it. If this isn’t for you, that’s okay too. Rails is big enough for many approaches."
        },
        {
          "id": "404",
          "title": "Page Not Found - Maquina",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/404",
          "content": "Recuerd0\n          \n          \n            Documentation\n          \n          \n            Open Source\n          \n          \n            Blog\n          \n      \n\n      \n        mobile-nav#toggle\">\n          \n            \n          \n        \n      \n    \n  \n\n\n\n      \n        404\n        \n          Page not found\n        \n        \n          Sorry, we couldn't find the page you're looking for.\n        \n        \n          \n            Go home\n          \n        \n      \n    \n  \n\n  \n  \n    \n\n    \n      \n        \n          \n              \n                \n                  \n                    \n  \n\n                  \n                \n              \n\n              \n                  \n                    Products\n                    \n                        \n                          \n                            Recuerd0 \n                          \n                        \n                    \n                  \n                  \n                    Open Source\n                    \n                        \n                          \n                            Documentation \n                          \n                        \n                        \n                          \n                            Generators \n                          \n                        \n                        \n                          \n                            Components \n                          \n                        \n                        \n                          \n                            All Projects \n                          \n                        \n                    \n                  \n                  \n                    Company\n                    \n                        \n                          \n                            Blog \n                          \n                        \n                    \n                  \n                  \n                    Resources\n                    \n                        \n                          \n                            GitHub \n                          \n                        \n                        \n                          \n                            RubyGems \n                          \n                        \n                    \n                  \n              \n            \n          \n\n          \n            \n              \n              \n              \n              \n            \n\n            \n              \n                \n                  &copy; 2026 Maquina. Mario Alberto Chávez Cárdenas"
        },
        {
          "id": "500",
          "title": "Server Error - Maquina",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/500",
          "content": "Recuerd0\n          \n          \n            Documentation\n          \n          \n            Open Source\n          \n          \n            Blog\n          \n      \n\n      \n        mobile-nav#toggle\">\n          \n            \n          \n        \n      \n    \n  \n\n\n\n      \n        500\n        \n          Something went wrong\n        \n        \n          We're experiencing technical difficulties. Please try again later.\n        \n        \n          \n            Go home\n          \n        \n      \n    \n  \n\n  \n  \n    \n\n    \n      \n        \n          \n              \n                \n                  \n                    \n  \n\n                  \n                \n              \n\n              \n                  \n                    Products\n                    \n                        \n                          \n                            Recuerd0 \n                          \n                        \n                    \n                  \n                  \n                    Open Source\n                    \n                        \n                          \n                            Documentation \n                          \n                        \n                        \n                          \n                            Generators \n                          \n                        \n                        \n                          \n                            Components \n                          \n                        \n                        \n                          \n                            All Projects \n                          \n                        \n                    \n                  \n                  \n                    Company\n                    \n                        \n                          \n                            Blog \n                          \n                        \n                    \n                  \n                  \n                    Resources\n                    \n                        \n                          \n                            GitHub \n                          \n                        \n                        \n                          \n                            RubyGems \n                          \n                        \n                    \n                  \n              \n            \n          \n\n          \n            \n              \n              \n              \n              \n            \n\n            \n              \n                \n                  &copy; 2026 Maquina. Mario Alberto Chávez Cárdenas"
        },
        {
          "id": "blog",
          "title": "Blog",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/blog/",
          "content": "Featured\n          \n\n          \n              \n                  \n\n                \n                  \n                    Monday, August 10, 2026\n                  \n\n                  \n                    \n                      \n                      equipr: Cross-Agent Skill and MCP Server Manager\n                    \n                  \n\n                  \n                    equipr is out: one Go binary that installs skills, commands, and MCP servers into Claude Code, Codex, OpenCode, and Pi, with no plugin registration.\n                  \n\n                    \n                        \n\n                      \n                        Mario Alberto Chávez Cárdenas\n                      \n                    \n                \n              \n              \n                  \n\n                \n                  \n                    Tuesday, August 4, 2026\n                  \n\n                  \n                    \n                      \n                      Why I Removed execute_ruby from Rails MCP Server\n                    \n                  \n\n                  \n                    Rails MCP Server 2.0.0 removes the execute_ruby tool entirely, because the reasoning that justified it in 2025 stopped holding once agentic coding tools could run Ruby themselves.\n                  \n\n                    \n                        \n\n                      \n                        Mario Alberto Chávez Cárdenas\n                      \n                    \n                \n              \n              \n                  \n\n                \n                  \n                    Monday, August 3, 2026\n                  \n\n                  \n                    \n                      \n                      Rails MCP Server 1.6.0: Making &quot;Read-Only&quot; Actually Mean Read-Only\n                    \n                  \n\n                  \n                    Version 1.6.0 hardens the execute_ruby sandbox by closing real file-read bypasses, rolling back database writes, killing runaway processes, and asking before running dual-use code, plus manager-agnostic Ruby resolution and namespaced model fixes.\n                  \n\n                    \n                        \n\n                      \n                        Mario Alberto Chávez Cárdenas\n                      \n                    \n                \n              \n          \n        \n      \n    \n\n  \n    \n      \n          \n            blog-filter#select\"\n              aria-current=\"true\"\n              class=\"\n                rounded-full px-3 py-1 text-sm font-medium text-gray-600 ring-1\n                ring-inset ring-gray-200 transition-colors hover:bg-gray-50\n                aria-[current=true]:bg-gray-900 aria-[current=true]:text-white\n                aria-[current=true]:ring-gray-900\n              \"\n            >\n              All\n            \n              blog-filter#select\"\n                aria-current=\"false\"\n                class=\"\n                  rounded-full px-3 py-1 text-sm font-medium text-gray-600 ring-1\n                  ring-inset ring-gray-200 transition-colors hover:bg-gray-50\n                  aria-[current=true]:bg-gray-900 aria-[current=true]:text-white\n                  aria-[current=true]:ring-gray-900\n                \"\n              >\n                Announcements\n              \n              blog-filter#select\"\n                aria-current=\"false\"\n                class=\"\n                  rounded-full px-3 py-1 text-sm font-medium text-gray-600 ring-1\n                  ring-inset ring-gray-200 transition-colors hover:bg-gray-50\n                  aria-[current=true]:bg-gray-900 aria-[current=true]:text-white\n                  aria-[current=true]:ring-gray-900\n                \"\n              >\n                Release\n              \n              blog-filter#select\"\n                aria-current=\"false\"\n                class=\"\n                  rounded-full px-3 py-1 text-sm font-medium text-gray-600 ring-1\n                  ring-inset ring-gray-200 transition-colors hover:bg-gray-50\n                  aria-[current=true]:bg-gray-900 aria-[current=true]:text-white\n                  aria-[current=true]:ring-gray-900\n                \"\n              >\n                Product\n              \n              blog-filter#select\"\n                aria-current=\"false\"\n                class=\"\n                  rounded-full px-3 py-1 text-sm font-medium text-gray-600 ring-1\n                  ring-inset ring-gray-200 transition-colors hover:bg-gray-50\n                  aria-[current=true]:bg-gray-900 aria-[current=true]:text-white\n                  aria-[current=true]:ring-gray-900\n                \"\n              >\n                AI Tools\n              \n              blog-filter#select\"\n                aria-current=\"false\"\n                class=\"\n                  rounded-full px-3 py-1 text-sm font-medium text-gray-600 ring-1\n                  ring-inset ring-gray-200 transition-colors hover:bg-gray-50\n                  aria-[current=true]:bg-gray-900 aria-[current=true]:text-white\n                  aria-[current=true]:ring-gray-900\n                \"\n              >\n                Tools\n              \n          \n\n        \n          \n            \n\n            \n          \n          RSS Feed\n        \n      \n\n      \n          \n            \n              \n                Monday, August 10, 2026\n              \n\n                \n                    \n\n                  \n                    Mario Alberto Chávez Cárdenas\n                  \n                \n            \n\n            \n              \n                equipr: Cross-Agent Skill and MCP Server Manager\n              \n\n              \n                equipr is out: one Go binary that installs skills, commands, and MCP servers into Claude Code, Codex, OpenCode, and Pi, with no plugin registration.\n              \n\n              \n                \n                  \n                  Read more\n                  \n                    \n                  \n                \n              \n            \n          \n          \n            \n              \n                Tuesday, August 4, 2026\n              \n\n                \n                    \n\n                  \n                    Mario Alberto Chávez Cárdenas\n                  \n                \n            \n\n            \n              \n                Why I Removed execute_ruby from Rails MCP Server\n              \n\n              \n                Rails MCP Server 2.0.0 removes the execute_ruby tool entirely, because the reasoning that justified it in 2025 stopped holding once agentic coding tools could run Ruby themselves.\n              \n\n              \n                \n                  \n                  Read more\n                  \n                    \n                  \n                \n              \n            \n          \n          \n            \n              \n                Monday, August 3, 2026\n              \n\n                \n                    \n\n                  \n                    Mario Alberto Chávez Cárdenas\n                  \n                \n            \n\n            \n              \n                Rails MCP Server 1.6.0: Making &quot;Read-Only&quot; Actually Mean Read-Only\n              \n\n              \n                Version 1.6.0 hardens the execute_ruby sandbox by closing real file-read bypasses, rolling back database writes, killing runaway processes, and asking before running dual-use code, plus manager-agnostic Ruby resolution and namespaced model fixes.\n              \n\n              \n                \n                  \n                  Read more\n                  \n                    \n                  \n                \n              \n            \n          \n          \n            \n              \n                Monday, July 27, 2026\n              \n\n                \n                    \n\n                  \n                    Mario Alberto Chávez Cárdenas\n                  \n                \n            \n\n            \n              \n                Maquina Components 0.6.0: Themeable Beyond Color\n              \n\n              \n                Radius, elevation and focus rings become design tokens in this Rails + Tailwind component library, engine CSS moves into @layer components, and utilities win.\n              \n\n              \n                \n                  \n                  Read more\n                  \n                    \n                  \n                \n              \n            \n          \n          \n            \n              \n                Monday, July 20, 2026\n              \n\n                \n                    \n\n                  \n                    Mario Alberto Chávez Cárdenas\n                  \n                \n            \n\n            \n              \n                Introducing Nexo: the harness for Ruby agents\n              \n\n              \n                Nexo gives the RubyLLM ecosystem one front door: safe sandboxing, explicit permissions, and a real job primitive for agent runs. What it is, and why it exists.\n              \n\n              \n                \n                  \n                  Read more\n                  \n                    \n                  \n                \n              \n            \n          \n          \n            \n              \n                Sunday, July 12, 2026\n              \n\n                \n                    \n\n                  \n                    Mario Alberto Chávez Cárdenas\n                  \n                \n            \n\n            \n              \n                Maquina Components 0.5.0: Drawer and Scaffold Templates\n              \n\n              \n                A Turbo-aware Drawer component and a scaffold_templates generator that styles rails g scaffold output — Maquina Components 0.5.0, built mostly by contributors.\n              \n\n              \n                \n                  \n                  Read more\n                  \n                    \n                  \n                \n              \n            \n          \n          \n            \n              \n                Monday, July 6, 2026\n              \n\n                \n                    \n\n                  \n                    Mario Alberto Chávez Cárdenas\n                  \n                \n            \n\n            \n              \n                Introducing Fragua: the harness already composed for Rails\n              \n\n              \n                Fragua, an AI agent orchestrator for Rails — turn an idea into a shipped app through research, plan, spec, and ship, with agents that respect the craft. Now taking private beta requests.\n              \n\n              \n                \n                  \n                  Read more\n                  \n                    \n                  \n                \n              \n            \n          \n          \n            \n              \n                Thursday, June 11, 2026\n              \n\n                \n                    \n\n                  \n                    Mario Alberto Chávez Cárdenas\n                  \n                \n            \n\n            \n              \n                recuerd0 Now Has an MCP Server — and a Better Look\n              \n\n              \n                recuerd0 now has a remote MCP server with OAuth 2.1, a markdown editor based on 37signals&#39; House MD, and a refined UI built on Maquina Components.\n              \n\n              \n                \n                  \n                  Read more\n                  \n                    \n                  \n                \n              \n            \n          \n          \n            \n              \n                Tuesday, April 7, 2026\n              \n\n                \n                    \n\n                  \n                    Mario Alberto Chávez Cárdenas\n                  \n                \n            \n\n            \n              \n                Recuerd0 Now Reads Like a Filesystem\n              \n\n              \n                Recuerd0&#39;s API now lets AI agents grep, glob, and read memories in line ranges — the same primitives Claude Code already uses. Plus categories, links, and caching.\n              \n\n              \n                \n                  \n                  Read more\n                  \n                    \n                  \n                \n              \n            \n          \n          \n            \n              \n                Monday, March 23, 2026\n              \n\n                \n                    \n\n                  \n                    Mario Alberto Chávez Cárdenas\n                  \n                \n            \n\n            \n              \n                MVP Creator: From Idea to Documents in Three Prompts\n              \n\n              \n                Use MVP Creator, a Claude Code plugin, to generate research reports, business plans, brand guides, and technical specs for new Rails projects. Three prompts, six documents.\n              \n\n              \n                \n                  \n                  Read more\n                  \n                    \n                  \n                \n              \n            \n          \n          \n            \n              \n                Friday, March 13, 2026\n              \n\n                \n                    \n\n                  \n                    Mario Alberto Chávez Cárdenas\n                  \n                \n            \n\n            \n              \n                Maquina Generators: From rails new to Production-Ready\n              \n\n              \n                Rails generators for authentication, job queues, error tracking, and security. No runtime dependency — generate once, own the code forever.\n              \n\n              \n                \n                  \n                  Read more\n                  \n                    \n                  \n                \n              \n            \n          \n          \n            \n              \n                Saturday, February 21, 2026\n              \n\n                \n                    \n\n                  \n                    Mario Alberto Chávez Cárdenas\n                  \n                \n            \n\n            \n              \n                Recuerd0 Source Code Is Now Available\n              \n\n              \n                The Recuerd0 source code is now on GitHub. Built with Rails 8, SQLite, and Hotwire — here&#39;s a look under the hood.\n              \n\n              \n                \n                  \n                  Read more\n                  \n                    \n                  \n                \n              \n            \n          \n          \n            \n              \n                Sunday, February 15, 2026\n              \n\n                \n                    \n\n                  \n                    Mario Alberto Chávez Cárdenas\n                  \n                \n            \n\n            \n              \n                Announcing Recuerd0: A Knowledge Base for AI Tool Context\n              \n\n              \n                Versioned knowledge base for AI coding tools. Curate project context once, serve it via REST API to Claude Code, Cursor, and ChatGPT. SaaS or self-hosted.\n              \n\n              \n                \n                  \n                  Read more\n                  \n                    \n                  \n                \n              \n            \n          \n          \n            \n              \n                Friday, February 13, 2026\n              \n\n                \n                    \n\n                  \n                    Mario Alberto Chávez Cárdenas\n                  \n                \n            \n\n            \n              \n                Maquina Components 0.4.0: Taming Turbo\n              \n\n              \n                Turbo Drive and Morph compatibility fixes for sidebars, plus a Rails partial rendering fix for block content in 9 components.\n              \n\n              \n                \n                  \n                  Read more\n                  \n                    \n                  \n                \n              \n            \n          \n          \n            \n              \n                Friday, January 23, 2026\n              \n\n                \n                    \n\n                  \n                    Mario Alberto Chávez Cárdenas\n                  \n                \n            \n\n            \n              \n                Maquina 0.3.1: Calendar, Date Picker &amp; Claude Code Skills\n              \n\n              \n                Maquina Components 0.3.1 adds Calendar and Date Picker for Rails. Plus Claude Code skills for consistent UI generation and Rails code simplification.\n              \n\n              \n                \n                  \n                  Read more\n                  \n                    \n                  \n                \n              \n            \n          \n          \n            \n              \n                Thursday, January 8, 2026\n              \n\n                \n                    \n\n                  \n                    Mario Alberto Chávez Cárdenas\n                  \n                \n            \n\n            \n              \n                Claude Skill for Maquina Components\n              \n\n              \n                Teach Claude how to build consistent UIs in Rails applications using maquina_components. A skill for AI-assisted development.\n              \n\n              \n                \n                  \n                  Read more\n                  \n                    \n                  \n                \n              \n            \n          \n          \n            \n              \n                Wednesday, January 7, 2026\n              \n\n                \n                    \n\n                  \n                    Mario Alberto Chávez Cárdenas\n                  \n                \n            \n\n            \n              \n                Maquina Components 0.3.0: Combobox and Toast\n              \n\n              \n                Two new interactive components for Rails applications. Searchable dropdowns and non-intrusive notifications.\n              \n\n              \n                \n                  \n                  Read more\n                  \n                    \n                  \n                \n              \n            \n          \n          \n            \n              \n                Monday, December 29, 2025\n              \n\n                \n                    \n\n                  \n                    Mario Alberto Chávez Cárdenas\n                  \n                \n            \n\n            \n              \n                Rails MCP Server 1.5.0: Security Hardening and Sandboxed Environment Support\n              \n\n              \n                Version 1.5.0 brings comprehensive security improvements from community contributor hellvinz, plus support for sandboxed AI agents like GitHub Copilot and Claude Code.\n              \n\n              \n                \n                  \n                  Read more\n                  \n                    \n                  \n                \n              \n            \n          \n          \n            \n              \n                Tuesday, December 16, 2025\n              \n\n                \n                    \n\n                  \n                    Mario Alberto Chávez Cárdenas\n                  \n                \n            \n\n            \n              \n                Announcing Maquina Components: Opinionated Ul for Rails Applications\n              \n\n              \n                Production-ready ERB partials styled with Tailwind CSS 4.0. Extracted from real applications.\n              \n\n              \n                \n                  \n                  Read more\n                  \n                    \n                  \n                \n              \n            \n          \n      \n\n      \n        No posts in this category yet.\n        blog-filter#select\"\n          class=\"font-medium text-gray-950 underline underline-offset-4 hover:text-gray-700\"\n        >\n          View all posts\n        ."
        },
        {
          "id": "company",
          "title": "About Maquina — Open Source Rails Tools & Philosophy",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/company/",
          "content": "Recuerd0\n              \n              \n                Documentation\n              \n              \n                Open Source\n              \n              \n                Blog\n              \n          \n\n          \n            mobile-nav#toggle\"\n            >\n              \n                \n              \n            \n          \n        \n      \n    \n  \n\n\n    \n  \n    \n      \n        \n          Empowering Rails developers everywhere.\n        \n        \n          We're building the tools that make multi-tenant Rails applications accessible to every developer.\n        \n\n        \n          \n            Our mission\n            \n              At Maquina, we believe that building multi-tenant applications shouldn't require months of boilerplate code or expensive SaaS subscriptions. Every Rails developer should have access to professional-grade tools for building modern applications.\n            \n            \n              Maquina was born from years of experience building production Rails applications. We've distilled the best patterns and practices into a single, cohesive framework that gets out of your way and lets you focus on what makes your application unique.\n            \n          \n\n          \n            \n              \n                \n                  \n                \n              \n              \n                \n                  \n                \n              \n              \n                \n                  \n                \n              \n              \n                \n                  \n                \n              \n            \n          \n\n          \n            The Numbers\n            \n            \n              \n                License\n                MIT\n              \n              \n                Open Source\n                100%\n              \n              \n                Rails Version\n                7+\n              \n              \n                Ruby Version\n                3.2+\n              \n            \n          \n        \n      \n    \n\n    \n      \n        Our Story\n        \n          Built by developers, for developers.\n        \n        \n          Maquina is the result of years of building Rails applications and learning what patterns work best.\n        \n\n        \n          \n            \n              After building dozens of multi-tenant Rails applications, we noticed the same patterns emerging over and over: authentication flows, organization management, role-based access control, and UI components that needed to be rebuilt for every project.\n            \n            \n              We decided to distill these patterns into a single, cohesive framework. Maquina follows Rails conventions, embraces Hotwire for modern interactivity without JavaScript complexity, and provides beautiful UI components built with ViewComponent and Tailwind CSS.\n            \n            \n              \n                Read the docs\n              \n            \n          \n          \n            \n              \n                \n                  \n                \n                Open Source on GitHub\n              \n            \n          \n        \n      \n    \n\n    \n      \n        Our Values\n        \n          Principles that guide us.\n        \n        \n          Everything we build is guided by these core principles.\n        \n\n        Core Principles\n        \n\n        \n          \n            \n              \n                \n              \n            \n            Convention over configuration\n            \n              Follow Rails conventions whenever possible. Sensible defaults mean less code to write and maintain. We believe the best code is the code you don't have to write.\n            \n          \n          \n            \n              \n                \n              \n            \n            Security first\n            \n              Security is not an afterthought. Every feature is designed with security best practices from the start. Authentication, authorization, and data isolation are core to the framework.\n            \n          \n          \n            \n              \n                \n              \n            \n            Documentation matters\n            \n              Great software deserves great documentation. We invest heavily in guides, examples, and API references. If it's not documented, it doesn't exist.\n            \n          \n          \n            \n              \n                \n              \n            \n            Community driven\n            \n              Built by the community, for the community. Every contribution matters and every voice is heard. We're committed to building in the open with transparency.\n            \n          \n        \n      \n    \n\n    \n      \n        Get Involved\n        \n          Join our open source community.\n        \n        \n          We welcome contributions of all kinds. Whether it's code, documentation, or feedback, every contribution helps make Maquina better for everyone.\n        \n\n        \n          \n            Ways to contribute\n            \n              \n                \n                  \n                  \n                \n                \n                  \n                    Contribution type\n                    Link\n                  \n                \n                \n                  \n                    \n                      \n                        Code\n                      \n                    \n                  \n                  \n                    Submit a pull request\n                    \n                      \n                        View\n                      \n                    \n                  \n                  \n                    Report a bug\n                    \n                      \n                        View\n                      \n                    \n                  \n                  \n                    \n                      \n                        Community\n                      \n                    \n                  \n                  \n                    Join the discussion\n                    \n                      \n                        View\n                      \n                    \n                  \n                  \n                    Star on GitHub\n                    \n                      \n                        View\n                      \n                    \n                  \n                \n              \n            \n          \n\n          \n            \n            \n              \n                \n                  Open source is not just about code. It's about building a community of developers who share knowledge and help each other grow.\n                \n              \n              \n                The Maquina Team\n                \n                  \n                    Open Source Contributors\n                  \n                \n              \n            \n          \n        \n      \n    \n  \n  \n    \n\n    \n      \n        \n          \n              \n                \n                  \n                    \n  \n\n                  \n                \n              \n\n              \n                  \n                    Products\n                    \n                        \n                          \n                            Recuerd0 \n                          \n                        \n                    \n                  \n                  \n                    Open Source\n                    \n                        \n                          \n                            Documentation \n                          \n                        \n                        \n                          \n                            Generators \n                          \n                        \n                        \n                          \n                            Components \n                          \n                        \n                        \n                          \n                            All Projects \n                          \n                        \n                    \n                  \n                  \n                    Company\n                    \n                        \n                          \n                            Blog \n                          \n                        \n                    \n                  \n                  \n                    Resources\n                    \n                        \n                          \n                            GitHub \n                          \n                        \n                        \n                          \n                            RubyGems \n                          \n                        \n                    \n                  \n              \n            \n          \n\n          \n            \n              \n              \n              \n              \n            \n\n            \n              \n                \n                  &copy; 2026 Maquina. Mario Alberto Chávez Cárdenas"
        },
        {
          "id": "documentation-ai-tools-better-stimulus",
          "title": "Better Stimulus",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/ai-tools/better-stimulus/",
          "content": "A Claude Code plugin that applies opinionated StimulusJS best practices sourced from betterstimulus.com. Use it whenever writing, reviewing, debugging, or refactoring Stimulus controllers.\n\n\n\nWhat Is This?\n\nA Claude Code skill that:\n\n\n  Writes Stimulus controllers that follow the Values API, Classes API, and Outlets patterns\n  Reviews existing controllers against a concrete best-practices checklist\n  Refactors anti-patterns such as hardcoded selectors, instance-variable state, and manual addEventListener calls\n  Applies SOLID principles adapted to Stimulus (Single Responsibility, Open-Closed, Dependency Inversion)\n  Integrates with Turbo — teardown before caching, restoring DOM state, form interception\n\n\nThe skill is triggered by mentions of data-controller, data-action, data-target, data-values, outlets, lifecycle callbacks, Hotwire patterns, or Turbo-and-Stimulus integration.\n\n\n\nQuick Start\n\n1. Add the Marketplace\n\n/plugin marketplace add maquina-app/rails-claude-code\n\n\n2. Install the Plugin\n\n/plugin install better-stimulus@maquina\n\n\n3. Ask for a Review or Refactor\n\n&gt; Review my dropdown_controller.js against Stimulus best practices\n&gt; Refactor this controller to use the Values API instead of instance variables\n&gt; Write a Stimulus controller that toggles a panel with late binding\n\n\n\n\nWhat It Enforces\n\n\n  \n    \n      Area\n      Best Practice\n    \n  \n  \n    \n      State\n      Use the Values API as the single source of truth, not instance variables\n    \n    \n      CSS classes\n      Store in static classes, never hardcode strings\n    \n    \n      Events\n      Declare in data-action markup, not addEventListener in connect()\n    \n    \n      Dependencies\n      Late binding via Values / Classes / dataset — no hardcoded selectors\n    \n    \n      Composition\n      Prefer mixins for roles, inheritance only for specializations\n    \n    \n      Inter-controller\n      Outlets for direct calls, custom events for broadcast\n    \n    \n      Third-party libs\n      Initialize in connect(), destroy in disconnect()\n    \n    \n      Turbo\n      Implement teardown() wired to turbo:before-cache when mutating DOM\n    \n    \n      SRP\n      Split controllers that act on both this.element and targets\n    \n  \n\n\nA full pre-commit checklist ships with the plugin so Claude can verify any new controller before handing it back.\n\n\n\nUsage Examples\n\nReview an Existing Controller\n\n&gt; Review app/javascript/controllers/modal_controller.js\n\n\nThe skill checks for state in instance variables, hardcoded CSS classes, manual event listeners, mixed element/target responsibilities, and missing Turbo teardown.\n\nRefactor Toward the Values API\n\n&gt; Refactor this controller so state lives in values, not this.open\n\n\nThe skill moves state into static values, adds a *ValueChanged callback, and updates markup to include the new data attributes.\n\nWrite a New Controller with Late Binding\n\n&gt; Write a toggle controller that uses data-toggle-active-class for styling\n\n\nThe skill produces a controller that reads its active class via the Classes API so the same code works anywhere the markup declares a class.\n\nApply Hotwire Patterns\n\n&gt; This controller mutates the DOM — add a teardown for Turbo caching\n\n\nThe skill adds a teardown() method and wires it to turbo:before-cache at the application level.\n\n\n\nReference Material\n\nThe plugin ships with two reference files the skill consults on demand:\n\n\n  \n    \n      Reference\n      Purpose\n    \n  \n  \n    \n      references/cookbook.md\n      Copy-paste-ready controllers: faceted search, refresh-when-visible, auto sort, dark mode, radio dropdown\n    \n    \n      references/solid.md\n      SOLID principles adapted to Stimulus with examples and rationale\n    \n  \n\n\n\n\nPackage Contents\n\nbetter-stimulus/\n└── skills/better-stimulus/\n    ├── SKILL.md                    # Main skill\n    └── references/\n        ├── cookbook.md             # Ready-to-use controller patterns\n        ├── inter-controller.md     # Outlets, callbacks, custom events\n        ├── error-handling.md       # Global error handler\n        └── solid.md                # SOLID principles for Stimulus\n\n\n\n\nTeam Installation\n\nAdd to your project’s .claude/settings.json:\n\n{\n  \"extraKnownMarketplaces\": {\n    \"maquina\": {\n      \"source\": {\n        \"source\": \"github\",\n        \"repo\": \"maquina-app/rails-claude-code\"\n      }\n    }\n  },\n  \"enabledPlugins\": [\n    \"better-stimulus@maquina\"\n  ]\n}\n\n\n\n\nNext Steps\n\n\n  \n    \n      GitHub Repository\n    \n    \n      View source code and contribute.\n    \n  \n\n  \n    \n      betterstimulus.com\n    \n    \n      The original collection of Stimulus best practices.\n    \n  \n\n  \n    \n      Maquina UI Standards\n    \n    \n      Pair with component-level UI guidance."
        },
        {
          "id": "documentation-ai-tools-hotwire-patterns",
          "title": "Hotwire Patterns",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/ai-tools/hotwire-patterns/",
          "content": "A Claude Code skill that gives Claude internals-informed mental models for building and debugging Hotwire applications. Core philosophy: enhance the browser, don’t reinvent it — start by imagining a JS-free, plain-HTML version of every feature, then compose the pages into an integrated UI with Turbo. HTML is the source of truth for state, everywhere.\n\nIt complements Better Stimulus (the authority for writing controllers) and Rails Hotwire Driver (which exercises a running Hotwire app from the terminal). This skill is the knowledge layer — how Turbo and Stimulus actually work under the hood.\n\n\n\nWhat Is This?\n\nA Claude Code skill that helps Claude reason about:\n\n\n  Turbo Drive, Frames, and Streams — how each observer scopes an update, and the classic frame-id mismatch that produces “Content missing”\n  Morphing — the idiomorph algorithm, exactly when a morph runs, and how to exclude elements\n  Turbo Cache — snapshot mechanics, preview flashing, turbo-permanent, and cache-control\n  Broadcasting — ActionCable stream sources, the ~0.5s debounce, and request-id dedup\n  Stimulus design — callbacks over connect, composition via events vs outlets\n  Hotwire Native — Path Configuration, Bridge Components, and the native-adapter mental model\n  Testing &amp; debugging — system-test flakiness, collaborative tests, legacy migration, and internals-informed debugging\n\n\nIt is delivered as a skill (a knowledge module plus focused reference files), not an autonomous agent. Claude reads SKILL.md for the decision frameworks and pulls in a reference only when a specific branch needs it.\n\n\n\nThe Escalation Ladder\n\nThe skill’s central idea: Hotwire is a cost/benefit dial, not a single approach. Choose the cheapest tool that works, and escalate only when the previous rung stops being a good tradeoff:\n\n\n  Turbo Drive + Morphing refreshes — re-render everything server-side; fastest to build.\n  Turbo Frames — decompose the page; localize updates without touching the rest.\n  Turbo Stream actions — surgical DOM updates; more precise, more maintenance cost.\n  Stimulus — small client-side behavior where a server round-trip makes no sense.\n  Island of a reactive framework or API calls — only for genuinely high-interactivity widgets (maps, editors).\n\n\nDifferent parts of one app can sit on different rungs; it all composes.\n\n\n\nQuick Start\n\n1. Add the Marketplace\n\n/plugin marketplace add maquina-app/rails-claude-code\n\n\n2. Install the Plugin\n\n/plugin install hotwire-patterns@maquina\n\n\n3. Ask About Hotwire\n\nThe skill triggers on Hotwire design decisions and symptoms:\n\n&gt; Why does morphing wipe my form?\n&gt; My Turbo Stream broadcast isn't arriving\n&gt; This system test is flaky\n&gt; How do I add Turbo to a legacy app?\n&gt; Wrap my app with Hotwire Native\n\n\n\n\nWhat It Covers\n\nThe main SKILL.md holds the decision frameworks and per-topic essentials; each deep dive lives in a reference file Claude loads on demand.\n\n\n  \n    \n      Topic\n      Reference\n      Highlights\n    \n  \n  \n    \n      Turbo internals\n      (in SKILL.md)\n      Drive/Frames/Streams observers, _top escaping, lazy frames, the frame-id mismatch\n    \n    \n      Morphing\n      morphing.md\n      idiomorph algorithm, when morph actually runs, scoped exclusion via turbo:before-morph-element\n    \n    \n      Turbo Cache\n      (in SKILL.md)\n      snapshot cloneNode, preview flashing, data-turbo-temporary, cache-control meta\n    \n    \n      Broadcasting\n      (in SKILL.md)\n      signed stream names, the background-job debounce, originating-client dedup\n    \n    \n      Stimulus design\n      stimulus.md\n      callbacks over connect, events vs outlets, the dynamic-forms server-render pattern\n    \n    \n      Hotwire Native\n      hotwire-native.md\n      native adapter, Path Configuration, Bridge Components, publishing\n    \n    \n      Testing\n      testing-and-legacy.md\n      flakiness (assert stable state), multi-session broadcast tests, gradual Turbo adoption\n    \n    \n      Debugging\n      debugging.md\n      unminify Turbo, DOM break-on breakpoints, source landmarks, ActionCable filtering\n    \n  \n\n\n\n\nCustom Stream Actions\n\nA recurring theme: the correct UI update is only known server-side after processing. Custom Turbo Stream actions are the sanctioned way to run backend-driven browser behavior with a constrained, maintainable vocabulary — and they keep you CSP-compatible (no unsafe-inline):\n\nTurbo.StreamActions.log = function () {\n  console.log(this.getAttribute(\"message\"))\n}\n\n\nPair with a Ruby helper module included into Turbo::Streams::TagBuilder. Prefer small, app-specific actions over dropping in large libraries.\n\n\n\nPackage Contents\n\nhotwire-patterns/\n└── skills/hotwire-patterns/\n    ├── SKILL.md                    # Overview + decision frameworks + per-topic essentials\n    └── references/\n        ├── morphing.md             # idiomorph algorithm and gotchas\n        ├── stimulus.md             # reusable, composable controller design\n        ├── hotwire-native.md       # iOS/Android wrapping\n        ├── testing-and-legacy.md   # system tests + gradual Turbo adoption\n        └── debugging.md            # internals-informed debugging\n\n\n\n\nTeam Installation\n\nAdd to your project’s .claude/settings.json:\n\n{\n  \"extraKnownMarketplaces\": {\n    \"maquina\": {\n      \"source\": {\n        \"source\": \"github\",\n        \"repo\": \"maquina-app/rails-claude-code\"\n      }\n    }\n  },\n  \"enabledPlugins\": [\n    \"hotwire-patterns@maquina\"\n  ]\n}\n\n\n\n\nNext Steps\n\n\n  \n    \n      GitHub Repository\n    \n    \n      View source code and contribute.\n    \n  \n\n  \n    \n      Better Stimulus\n    \n    \n      Write the Stimulus controllers behind your Hotwire UI.\n    \n  \n\n  \n    \n      Rails Hotwire Driver\n    \n    \n      Exercise a running Hotwire app from the terminal."
        },
        {
          "id": "documentation-ai-tools",
          "title": "AI Tools",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/ai-tools/",
          "content": "MCP servers, Claude Code plugins, and AI integrations that connect LLMs to your Rails development workflow. Analyze code, access documentation, and coordinate changes across tools.\n\n\n\nAvailable Tools\n\nMCP Servers\n\n\n  \n    \n      Rails MCP Server\n    \n    \n      Let LLMs introspect models, routes, schemas, and files through dedicated analyzers.\n    \n  \n\n  \n    \n      Neovim MCP Server\n    \n    \n      Read and update Neovim buffers from AI assistants.\n    \n  \n\n\nClaude Code Plugins\n\n\n  \n    \n      Rails Simplifier\n    \n    \n      Code simplification following 37signals patterns and One Person Framework.\n    \n  \n\n  \n    \n      Rails Upgrade Assistant\n    \n    \n      Generate upgrade guides for Rails 6.0 through 8.1.\n    \n  \n\n  \n    \n      Maquina UI Standards\n    \n    \n      Build consistent UIs with maquina_components.\n    \n  \n\n  \n    \n      MVP Creator\n    \n    \n      Research, plan, and document MVPs for Rails applications.\n    \n  \n\n  \n    \n      Better Stimulus\n    \n    \n      Opinionated StimulusJS best practices from betterstimulus.com.\n    \n  \n\n  \n    \n      Spec-Driven Development\n    \n    \n      Shape features into specs and hand off to Claude Code.\n    \n  \n\n  \n    \n      Rails Security Auditor\n    \n    \n      Severity-grouped security audit reports for Rails 8.0–8.2.\n    \n  \n\n  \n    \n      Rails Hotwire Driver\n    \n    \n      Drive a running local Rails dev server from the terminal.\n    \n  \n\n  \n    \n      Hotwire Patterns\n    \n    \n      Deep Hotwire internals, decision frameworks, and debugging.\n    \n  \n\n\n\n\nInstalling These\n\nEvery tool on this page is installed by hand: a gem, a marketplace command, a config file edit. equipr does it mechanically instead. Point it at a marketplace or an Agent Plugins source and it places each skill, command, and MCP server where each agent expects it, across Claude Code, Codex CLI, OpenCode, and Pi.\n\nequipr add https://github.com/coreyhaines31/marketingskills\nequipr install marketingskills/marketing-skills:seo-audit\n\n\nView equipr Documentation\n\n\n\nWhat is MCP?\n\nThe Model Context Protocol (MCP) is a standardized way for AI models to interact with their environment. It defines how models request and use tools, access resources, and maintain context.\n\nMCP servers expose tools that AI assistants can call:\n\n# Example: AI assistant analyzes a Rails model\nexecute_tool(tool_name: \"analyze_models\", params: { model_name: \"User\" })\n\n\nSupported Clients\n\n\n  Claude Desktop\n  Any MCP-compatible client\n  Custom integrations via HTTP/SSE mode\n\n\n\n\nWhat are Claude Code Plugins?\n\nClaude Code plugins extend Claude’s capabilities within your development environment. They can be installed from marketplaces and provide:\n\n\n  Agents — Specialized AI assistants for specific tasks\n  Skills — Knowledge modules that teach Claude project-specific patterns\n  Commands — Custom slash commands for workflows\n\n\nInstalling Plugins\n\n# Add the marketplace\n/plugin marketplace add maquina-app/rails-claude-code\n\n# Install a plugin\n/plugin install rails-simplifier@maquina\n\n\n\n\nGetting Started\n\nWith MCP Servers\n\n1. Install an MCP Server\n\ngem install rails-mcp-server\n\n\n2. Configure Your Client\n\nFor Claude Desktop, add to claude_desktop_config.json:\n\n{\n  \"mcpServers\": {\n    \"railsMcpServer\": {\n      \"command\": \"rails-mcp-server\"\n    }\n  }\n}\n\n\n3. Start Using Tools\n\nIn Claude Desktop, the MCP server tools become available automatically. Ask Claude to:\n\n\n  “Analyze the User model in my Rails project”\n  “Show me the routes for the orders controller”\n  “What’s the database schema for the products table?”\n\n\nWith Claude Code Plugins\n\n1. Add the Marketplace\n\n/plugin marketplace add maquina-app/rails-claude-code\n\n\n2. Install Plugins\n\n/plugin install rails-simplifier@maquina\n/plugin install rails-upgrade-assistant@maquina\n/plugin install maquina-ui-standards@maquina\n/plugin install mvp-creator@maquina\n/plugin install better-stimulus@maquina\n/plugin install spec-driven-development@maquina\n/plugin install rails-security-auditor@maquina\n/plugin install rails-hotwire-driver@maquina\n/plugin install hotwire-patterns@maquina\n\n\n3. Use the Plugins\n\n&gt; Simplify the recent changes to the bookings controller\n&gt; Upgrade my Rails app to 8.1\n&gt; Create a users index view with maquina components\n\n\n\n\nArchitecture\n\nMCP Communication\n\nMCP servers communicate via JSON-RPC 2.0:\n\n\n  \n    \n      Mode\n      Use Case\n    \n  \n  \n    \n      STDIO\n      Direct integration with Claude Desktop\n    \n    \n      HTTP/SSE\n      Web-based clients, remote access\n    \n  \n\n\nPlugin Structure\n\nClaude Code plugins follow a standard structure:\n\nplugin-name/\n├── agents/           # AI agent definitions\n│   └── agent.md\n├── skills/           # Knowledge modules\n│   └── SKILL.md\n├── commands/         # Custom slash commands\n│   └── command.md\n└── references/       # Documentation\n    └── *.md\n\n\n\n\nTool Reference\n\n\n  \n    \n      Tool\n      Type\n      Purpose\n    \n  \n  \n    \n      Rails MCP Server\n      MCP Server\n      Code analysis and Ruby execution\n    \n    \n      Neovim MCP Server\n      MCP Server\n      Editor buffer coordination\n    \n    \n      Rails Simplifier\n      Plugin\n      Code simplification with 37signals patterns\n    \n    \n      Rails Upgrade Assistant\n      Plugin\n      Rails 6.0–8.1 upgrade planning\n    \n    \n      Maquina UI Standards\n      Plugin\n      UI component generation\n    \n    \n      MVP Creator\n      Plugin\n      MVP research, planning, and documentation\n    \n    \n      Better Stimulus\n      Plugin\n      StimulusJS best practices and refactoring\n    \n    \n      Spec-Driven Development\n      Plugin\n      Feature specs, task breakdown, and progress tracking\n    \n    \n      Rails Security Auditor\n      Plugin\n      Rails 8.0–8.2 security audits with severity grouping\n    \n    \n      Rails Hotwire Driver\n      Skill\n      Drive a running local Rails dev server from the terminal\n    \n    \n      Hotwire Patterns\n      Skill\n      Deep Hotwire internals, decision frameworks, and debugging\n    \n  \n\n\n\n\nTeam Installation\n\nFor consistent tooling across your team, add to .claude/settings.json:\n\n{\n  \"extraKnownMarketplaces\": {\n    \"maquina\": {\n      \"source\": {\n        \"source\": \"github\",\n        \"repo\": \"maquina-app/rails-claude-code\"\n      }\n    }\n  },\n  \"enabledPlugins\": [\n    \"rails-simplifier@maquina\",\n    \"rails-upgrade-assistant@maquina\",\n    \"maquina-ui-standards@maquina\",\n    \"mvp-creator@maquina\",\n    \"better-stimulus@maquina\",\n    \"spec-driven-development@maquina\",\n    \"rails-security-auditor@maquina\",\n    \"rails-hotwire-driver@maquina\",\n    \"hotwire-patterns@maquina\"\n  ]\n}\n\n\nCommit this file to your repository. Team members get the same plugins automatically."
        },
        {
          "id": "documentation-ai-tools-maquina-ui-standards",
          "title": "Maquina UI Standards",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/ai-tools/maquina-ui-standards/",
          "content": "A Claude Code plugin that teaches Claude how to build UIs with maquina_components — ERB partials styled with Tailwind CSS 4 and data attributes, inspired by shadcn/ui.\n\n\n\nWhat Is This?\n\nA Claude Code skill that provides:\n\n\n  Component catalog — All 15+ components with ERB examples\n  Form patterns — Validation, error handling, inline layouts\n  Layout patterns — Sidebar navigation, page structure\n  Turbo integration — Frames, Streams, component updates\n  Spec checklist — Review criteria for UI quality\n\n\nWhen installed, Claude generates code that matches your component conventions without back-and-forth corrections.\n\n\n\nThe Problem It Solves\n\nWithout the skill, asking Claude to build a view results in generic Rails patterns:\n\n&lt;div class=\"card\"&gt;\n  &lt;h2&gt;&lt;%= @user.name %&gt;&lt;/h2&gt;\n&lt;/div&gt;\n\n\nWith the skill, Claude uses your actual components:\n\n&lt;%= render \"components/card\" do %&gt;\n  &lt;%= render \"components/card/header\" do %&gt;\n    &lt;%= render \"components/card/title\", text: @user.name %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\n\nThe skill eliminates the “use the card partial, not a div” corrections that slow down AI-assisted development.\n\n\n\nQuick Start\n\n1. Add the Marketplace\n\n/plugin marketplace add maquina-app/rails-claude-code\n\n\n2. Install the Plugin\n\n/plugin install maquina-ui-standards@maquina\n\n\n3. Start Building\n\n&gt; Create the users index view with a table showing name, email, and status\n\n\n\n\nRequirements\n\nThe maquina_components gem must be installed in your Rails application:\n\nbundle add maquina_components\nrails generate maquina_components:install\n\n\n\n\nWhat It Provides\n\n\n  \n    \n      Reference\n      Purpose\n    \n  \n  \n    \n      Component catalog\n      All available components with ERB examples\n    \n    \n      Form patterns\n      Validation states, error handling, inline layouts\n    \n    \n      Layout patterns\n      Sidebar navigation, page headers, content areas\n    \n    \n      Turbo integration\n      Frames, Streams, and component updates\n    \n    \n      Spec checklist\n      Accessibility and consistency review criteria\n    \n    \n      Helpers reference\n      Ruby helpers provided by maquina_components\n    \n    \n      Stimulus controllers\n      Controllers shipped with the component library\n    \n    \n      Installation guide\n      Setup for existing and new Rails applications\n    \n  \n\n\nAs of v0.5.0 the plugin ships as a model-invoked skill instead of a subagent: Claude loads the standards into the same conversation where your feature is being built — with your models, controllers, and spec in context — rather than delegating view work to an isolated agent. The trigger works the same way; the skill activates when UI work starts. This release also syncs the references with maquina_components 0.5.x: the Drawer component, the Stats grid, vocabulary aliases (:destructive/:error, size :default), text:/content: leaf parameters, and the ability to attach your own Stimulus controllers to any component (data-controller values concatenate instead of being replaced).\n\n\n  On maquina_components 0.7.0. The bundled references are verified against 0.7.0: the token layer, engine CSS in @layer components, the drawer and sidebar parts, and the fact that utility classes passed through css_classes: now win where they used to be swallowed. They also carry the 0.7.0 changes — the collapse_after deprecation on responsive_breadcrumbs, the icon-override boundary, sidebar inert, and the rule never to transition outline-color in a component of your own. Theming and Upgrading remain the canonical source if the two ever disagree.\n\n\n\n\nUsage Examples\n\nCreate a View\n\n&gt; Create the users index view with a table showing name, email, and status\n\n\nClaude generates:\n\n&lt;%= render \"components/page_header\", title: \"Users\", description: \"Manage team members\" %&gt;\n\n&lt;%= render \"components/card\" do %&gt;\n  &lt;%= render \"components/card/content\" do %&gt;\n    &lt;table data-component=\"table\"&gt;\n      &lt;thead&gt;\n        &lt;tr&gt;\n          &lt;th&gt;Name&lt;/th&gt;\n          &lt;th&gt;Email&lt;/th&gt;\n          &lt;th&gt;Status&lt;/th&gt;\n        &lt;/tr&gt;\n      &lt;/thead&gt;\n      &lt;tbody&gt;\n        &lt;% @users.each do |user| %&gt;\n          &lt;tr&gt;\n            &lt;td&gt;&lt;%= user.name %&gt;&lt;/td&gt;\n            &lt;td&gt;&lt;%= user.email %&gt;&lt;/td&gt;\n            &lt;td&gt;\n              &lt;%= render \"components/badge\",\n                variant: user.active? ? :success : :secondary do %&gt;\n                &lt;%= user.active? ? \"Active\" : \"Inactive\" %&gt;\n              &lt;% end %&gt;\n            &lt;/td&gt;\n          &lt;/tr&gt;\n        &lt;% end %&gt;\n      &lt;/tbody&gt;\n    &lt;/table&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\n\nImplement a Form\n\n&gt; Implement the project form with name, description, and a framework combobox\n\n\nClaude generates:\n\n&lt;%= form_with model: @project do |form| %&gt;\n  &lt;div class=\"space-y-4\"&gt;\n    &lt;div&gt;\n      &lt;%= form.label :name %&gt;\n      &lt;%= form.text_field :name, data: { component: \"input\" } %&gt;\n    &lt;/div&gt;\n\n    &lt;div&gt;\n      &lt;%= form.label :description %&gt;\n      &lt;%= form.text_area :description, data: { component: \"textarea\" }, rows: 4 %&gt;\n    &lt;/div&gt;\n\n    &lt;div&gt;\n      &lt;%= form.label :framework_id %&gt;\n      &lt;%= combobox_simple name: \"project[framework_id]\",\n            value: @project.framework_id,\n            options: @frameworks.map { |f| { value: f.id, label: f.name } },\n            placeholder: \"Select a framework\" %&gt;\n    &lt;/div&gt;\n\n    &lt;div class=\"flex justify-end gap-2\"&gt;\n      &lt;%= link_to \"Cancel\", projects_path,\n        data: { component: \"button\", variant: \"outline\" } %&gt;\n      &lt;%= form.submit \"Save\",\n        data: { component: \"button\", variant: \"primary\" } %&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n&lt;% end %&gt;\n\n\nReview Existing Code\n\n&gt; Review this view against the maquina UI standards and suggest improvements\n\n\nClaude checks for:\n\n  Proper component usage instead of raw HTML\n  Correct data attributes on form fields\n  Accessibility attributes\n  Consistent spacing and layout patterns\n  Turbo Frame and Stream integration\n\n\n\n\nComponent Patterns\n\nPartial Components\n\nComponents rendered as partials with strict locals:\n\n&lt;%# Card with header and content %&gt;\n&lt;%= render \"components/card\" do %&gt;\n  &lt;%= render \"components/card/header\" do %&gt;\n    &lt;%= render \"components/card/title\", text: \"Appointments\" %&gt;\n    &lt;%= render \"components/card/description\", text: \"Manage your schedule\" %&gt;\n  &lt;% end %&gt;\n  &lt;%= render \"components/card/content\" do %&gt;\n    &lt;!-- Content here --&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\n\nData Attribute Components\n\nForm elements and buttons use data attributes for styling:\n\n&lt;%# Text input %&gt;\n&lt;%= form.text_field :name, data: { component: \"input\" } %&gt;\n\n&lt;%# Button %&gt;\n&lt;%= link_to \"Edit\", edit_path,\n  data: { component: \"button\", variant: \"outline\", size: \"sm\" } %&gt;\n\n&lt;%# Badge %&gt;\n&lt;%= render \"components/badge\", variant: :success do %&gt;\n  Active\n&lt;% end %&gt;\n\n\nLayout Patterns\n\n&lt;%# Sidebar layout %&gt;\n&lt;%= render \"components/sidebar/provider\", state: sidebar_state do %&gt;\n  &lt;%= render \"components/sidebar\" do %&gt;\n    &lt;%= render \"components/sidebar/header\" do %&gt;\n      &lt;!-- Logo --&gt;\n    &lt;% end %&gt;\n    &lt;%= render \"components/sidebar/content\" do %&gt;\n      &lt;%= render \"components/sidebar/group\", title: \"Navigation\" do %&gt;\n        &lt;%= render \"components/sidebar/menu\" do %&gt;\n          &lt;%= render \"components/sidebar/menu_item\" do %&gt;\n            &lt;%= render \"components/sidebar/menu_button\",\n              url: dashboard_path,\n              icon_name: :home,\n              title: \"Dashboard\",\n              active: current_page?(dashboard_path) %&gt;\n          &lt;% end %&gt;\n        &lt;% end %&gt;\n      &lt;% end %&gt;\n    &lt;% end %&gt;\n  &lt;% end %&gt;\n\n  &lt;%= render \"components/sidebar/inset\" do %&gt;\n    &lt;%= yield %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\n\n\n\nPackage Contents\n\nmaquina-ui-standards/\n├── skills/ui/SKILL.md                 # Model-invoked skill\n├── QUICKSTART.md                      # Quick reference for humans\n└── references/\n    ├── component-catalog.md           # All available components\n    ├── form-patterns.md               # Validation, error handling\n    ├── layout-patterns.md             # Pages, dashboards\n    ├── turbo-integration.md           # Frames, streams\n    ├── spec-checklist.md              # Accessibility, consistency\n    ├── helpers-reference.md           # Ruby helpers provided by maquina_components\n    ├── stimulus-controllers.md        # Controllers shipped with the library\n    └── installation-guide.md          # Setup for existing and new Rails apps\n\n\n\n\nTeam Installation\n\nAdd to your project’s .claude/settings.json:\n\n{\n  \"extraKnownMarketplaces\": {\n    \"maquina\": {\n      \"source\": {\n        \"source\": \"github\",\n        \"repo\": \"maquina-app/rails-claude-code\"\n      }\n    }\n  },\n  \"enabledPlugins\": [\n    \"maquina-ui-standards@maquina\"\n  ]\n}\n\n\n\n\nAlternative: Claude Skill Installation\n\nIf you prefer using Claude Skills instead of the plugin system, copy the skill to your project:\n\nmkdir -p .claude/skills\n# Copy from the rails-claude-code repository\ncp -r maquina-ui-standards/skills/ui .claude/skills/maquina-ui-standards\ncp -r maquina-ui-standards/references .claude/skills/\n\n\nThen reference it in your CLAUDE.md:\n\n## UI Components\n\nThis project uses maquina_components for UI. Before implementing views,\nforms, or interactive components, read the UI standards skill:\n\n.claude/skills/maquina-ui-standards/SKILL.md\n\nAlways consult the skill when:\n- Creating or modifying views\n- Implementing forms\n- Adding interactive components\n- Building layouts with sidebar/header patterns\n- Working with Turbo Streams that update UI\n\n\n\n\nNext Steps\n\n\n  \n    \n      GitHub Repository\n    \n    \n      View source code and contribute.\n    \n  \n\n  \n    \n      Component Documentation\n    \n    \n      Browse all maquina_components.\n    \n  \n\n  \n    \n      Announcement Post\n    \n    \n      Read about the skill's development.\n    \n  \n\n  \n    \n      maquina_components Gem\n    \n    \n      Install the component library."
        },
        {
          "id": "documentation-ai-tools-mvp-creator",
          "title": "MVP Creator",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/ai-tools/mvp-creator/",
          "content": "A Claude Code plugin that creates comprehensive MVP documentation for Rails applications through guided research and discovery. Go from idea to implementation-ready deliverables.\n\n\n\nWhat Is This?\n\nA Claude Code skill that:\n\n\n  Researches your topic using web search and competitive analysis\n  Guides you through discovery questions to refine the MVP scope\n  Generates a Research Report with market context and competitor landscape\n  Produces an MVP Business Plan with feature priorities and user stories\n  Creates a Brand Guide with visual identity and tone of voice\n  Builds a Technical Guide with architecture decisions and Rails conventions\n  Configures Claude Setup (CLAUDE.md, .mcp.json, commands) for development handoff\n\n\n\n\nThe Workflow\n\n\n  \n    \n      Step\n      What Happens\n    \n  \n  \n    \n      1. Topic/Idea\n      You describe your app concept or business idea\n    \n    \n      2. Research\n      Skill researches competitors, market, and technology landscape\n    \n    \n      3. Discovery Questions\n      Interactive Q&amp;A to refine scope, audience, and priorities\n    \n    \n      4. Generate Deliverables\n      Five documents produced in sequence\n    \n    \n      5. Handoff\n      Ready for Spec-Driven Development to begin implementation\n    \n  \n\n\nThe skill walks you through each step interactively. You provide context and make decisions — the skill handles research, structure, and writing.\n\n\n\nQuick Start\n\n1. Add the Marketplace\n\n/plugin marketplace add maquina-app/rails-claude-code\n\n\n2. Install the Plugin\n\n/plugin install mvp-creator@maquina\n\n\n3. Start Creating\n\n&gt; I have an idea for a project management app\n\n\n\n\nDeliverables\n\nEvery MVP session produces five documents:\n\n\n  \n    \n      Deliverable\n      Description\n    \n  \n  \n    \n      Research Report\n      Competitor analysis, market overview, feature comparison\n    \n    \n      MVP Business Plan\n      Vision, feature priorities, user flows, success metrics\n    \n    \n      Brand Guide\n      Logo direction, colors, typography, components, voice\n    \n    \n      Technical Guide\n      Architecture, patterns, data models, code style\n    \n    \n      Claude Setup\n      CLAUDE.md, .mcp.json, and commands for Claude Desktop/Code\n    \n  \n\n\nResearch Report\n\nThe skill searches the web for competitors, analyzes their features, pricing, and positioning. You get a structured comparison that informs every subsequent deliverable.\n\nMVP Business Plan\n\nDefines what to build first. Includes prioritized features, user stories, and success metrics. Scoped to what a single developer can ship.\n\nBrand Guide\n\nVisual identity decisions: color palette, typography, component styling, and tone of voice. Ready to apply when building the UI.\n\nTechnical Guide\n\nRails-specific architecture: models, associations, authentication approach, API patterns, and testing strategy. Follows 37signals conventions.\n\nClaude Setup\n\nPre-configured CLAUDE.md with project context, .mcp.json for MCP server integration, and custom commands. Drop these into your new Rails project and start building with full AI context.\n\n\n\nUsage Examples\n\nStart from an Idea\n\n&gt; I have an idea for a project management app\n\n\nThe skill begins with research, then asks discovery questions to shape the MVP.\n\nPlan a SaaS Product\n\n&gt; Help me plan a SaaS for freelancers\n\n\nThe skill treats this as a full MVP session — research, discovery, and all five deliverables.\n\nResearch Competitors\n\n&gt; Research competitors for a booking system\n\n\nThe skill focuses on the research phase and produces a detailed competitor analysis.\n\nCreate a Business Plan\n\n&gt; Create a business plan for my app idea\n\n\nSkips research if you already know the market. Goes straight to discovery and deliverables.\n\nDesign a Brand\n\n&gt; Design a brand for my Rails project\n\n\nGenerates the Brand Guide deliverable with color palette, typography, and voice guidelines.\n\n\n\nPackage Contents\n\nmvp-creator/\n├── QUICKSTART.md                        # Quick reference\n└── skills/mvp-creator/\n    ├── SKILL.md                         # Main skill\n    ├── scripts/\n    │   └── init.sh                      # Project initialization\n    └── references/\n    ├── rails-philosophy.md              # Rails conventions and principles\n    ├── rails-ui-patterns.md             # UI design patterns\n    ├── rails-api-patterns.md            # API design patterns\n    ├── rails-implementation-patterns.md # Implementation guidelines\n    └── deliverable-templates/           # Templates for all 5 deliverables\n        ├── research-report.md\n        ├── mvp-business-plan.md\n        ├── brand-guide.md\n        ├── technical-guide.md\n        └── claude-setup.md\n\n\n\n\nTeam Installation\n\nAdd to your project’s .claude/settings.json:\n\n{\n  \"extraKnownMarketplaces\": {\n    \"maquina\": {\n      \"source\": {\n        \"source\": \"github\",\n        \"repo\": \"maquina-app/rails-claude-code\"\n      }\n    }\n  },\n  \"enabledPlugins\": [\n    \"mvp-creator@maquina\"\n  ]\n}\n\n\n\n\nNext Steps\n\n\n  \n    \n      GitHub Repository\n    \n    \n      View source code and contribute.\n    \n  \n\n  \n    \n      Spec-Driven Development\n    \n    \n      Continue from MVP to implementation with SDD.\n    \n  \n\n  \n    \n      Announcement Post\n    \n    \n      Watch the full 40-minute walkthrough video."
        },
        {
          "id": "documentation-ai-tools-nvim-mcp-server",
          "title": "Neovim MCP Server",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/ai-tools/nvim-mcp-server/",
          "content": "A Ruby implementation of a Model Context Protocol (MCP) server for Neovim integration. Coordinate file changes between your editor and AI assistants by reading and updating Neovim buffers directly.\n\n\n\nWhat is MCP?\n\nThe Model Context Protocol (MCP) is a standardized way for AI models to interact with their environment. It defines how models request and use tools, access resources, and maintain context.\n\nThe Neovim MCP Server exposes your editor’s buffers to AI assistants, enabling them to read file contents and push changes directly into your editing session.\n\n\n\nFeatures\n\n\n  Read buffer contents from Neovim\n  Update buffer contents with new code\n  Coordinate changes between AI assistants and your editor\n  Works with Claude Desktop and other MCP clients\n  STDIO and HTTP server modes\n\n\n\n\nQuick Start\n\n1. Install the Gem\n\ngem install nvim-mcp-server\n\n\n2. Configure Neovim\n\nAdd to your init.lua to start the RPC server:\n\n-- Start the Neovim RPC server on a socket\nvim.fn.serverstart('/tmp/nvim-mcp.sock')\n\n\nOr start Neovim with a socket:\n\nnvim --listen /tmp/nvim-mcp.sock\n\n\n3. Configure Claude Desktop\n\nAdd to claude_desktop_config.json:\n\n{\n  \"mcpServers\": {\n    \"nvimMcpServer\": {\n      \"command\": \"nvim-mcp-server\",\n      \"args\": [\"--socket\", \"/tmp/nvim-mcp.sock\"]\n    }\n  }\n}\n\n\n\n\nAvailable Tools\n\nThe server provides 2 tools for buffer management.\n\n\n  \n    \n      Tool\n      Description\n    \n  \n  \n    \n      get_project_buffers\n      Get contents of all open buffers\n    \n    \n      update_buffer\n      Update a buffer with new content\n    \n  \n\n\nGet Project Buffers\n\nReturns the contents of all buffers currently open in Neovim:\n\nget_project_buffers()\n\n\nResponse includes file paths and their contents, allowing AI assistants to understand your current working context.\n\nUpdate Buffer\n\nUpdates a specific buffer with new content:\n\nupdate_buffer(file_path: \"/path/to/file.rb\", content: \"new content here\")\n\n\nThe changes appear immediately in Neovim, ready for you to review, modify, or save.\n\n\n\nServer Modes\n\nSTDIO Mode (Default)\n\nFor direct integration with Claude Desktop:\n\nnvim-mcp-server --socket /tmp/nvim-mcp.sock\n\n\nHTTP Mode\n\nFor HTTP endpoints with JSON-RPC and SSE:\n\nnvim-mcp-server --mode http --socket /tmp/nvim-mcp.sock\nnvim-mcp-server --mode http --socket /tmp/nvim-mcp.sock -p 8080\n\n\nEndpoints:\n\n  JSON-RPC: http://localhost:6030/mcp/messages\n  SSE: http://localhost:6030/mcp/sse\n\n\n\n\nNeovim Configuration\n\nSocket Setup\n\nThe MCP server communicates with Neovim via RPC over a Unix socket. Configure Neovim to listen:\n\nOption 1: In init.lua (recommended)\n\n-- Always start the socket server\nvim.fn.serverstart('/tmp/nvim-mcp.sock')\n\n\nOption 2: Shell alias\n\nalias nvim='nvim --listen /tmp/nvim-mcp.sock'\n\n\nOption 3: Per-session\n\nnvim --listen /tmp/nvim-mcp.sock\n\n\nMultiple Neovim Instances\n\nFor multiple Neovim instances, use unique socket paths:\n\n-- In init.lua\nlocal socket_path = '/tmp/nvim-mcp-' .. vim.fn.getpid() .. '.sock'\nvim.fn.serverstart(socket_path)\nprint('Neovim socket: ' .. socket_path)\n\n\nThen specify the socket when starting the MCP server:\n\nnvim-mcp-server --socket /tmp/nvim-mcp-12345.sock\n\n\n\n\nRuby Version Manager Users\n\nClaude Desktop bypasses version manager initialization. Use the Ruby shim path:\n\n{\n  \"mcpServers\": {\n    \"nvimMcpServer\": {\n      \"command\": \"/home/your_user/.rbenv/shims/ruby\",\n      \"args\": [\n        \"/path/to/nvim-mcp-server/exe/nvim-mcp-server\",\n        \"--socket\",\n        \"/tmp/nvim-mcp.sock\"\n      ]\n    }\n  }\n}\n\n\n\n\nTesting and Debugging\n\nUse MCP Inspector to test the server:\n\nnpm -g install @modelcontextprotocol/inspector\nnpx @modelcontextprotocol/inspector nvim-mcp-server --socket /tmp/nvim-mcp.sock\n\n\nThe Inspector UI lets you:\n\n  See available tools\n  Execute tool calls interactively\n  View request and response details\n  Debug issues in real-time\n\n\nVerify Neovim Socket\n\nCheck that Neovim is listening:\n\n# Should show the socket file\nls -la /tmp/nvim-mcp.sock\n\n\nFrom within Neovim, verify the server address:\n\n:echo v:servername\n\n\n\n\nUse Cases\n\nCode Review Workflow\n\n\n  Open files in Neovim\n  Ask Claude to review the open buffers\n  Claude reads via get_project_buffers\n  Claude suggests changes via update_buffer\n  Review changes in Neovim before saving\n\n\nAI-Assisted Editing\n\nCombine with other MCP servers for powerful workflows:\n\n\n  Use Rails MCP Server to understand your codebase\n  Use Neovim MCP Server to apply changes directly to your editor\n  Review and refine changes before committing\n\n\n\n\nNext Steps\n\n\n  \n    \n      GitHub Repository\n    \n    \n      Source code, issues, and contribution guidelines.\n    \n  \n\n  \n    \n      Rails MCP Server\n    \n    \n      Analyze models, routes, and schemas in your Rails projects."
        },
        {
          "id": "documentation-ai-tools-rails-hotwire-driver",
          "title": "Rails Hotwire Driver",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/ai-tools/rails-hotwire-driver/",
          "content": "A Claude Code skill that drives a running local Rails dev server from the shell — no browser required. Log in (including OTP/magic-link codes read straight from the log), submit ERB forms with the correct CSRF token, inspect Turbo Stream responses, and trace any request through the development log by its request id.\n\nIt is the runtime complement to the Rails MCP Server, which only reads code statically. This skill adds live interaction with a real, running app.\n\n\n\nWhat Is This?\n\nA Claude Code skill that lets Claude:\n\n\n  Authenticate — submit login forms with the right CSRF token, and read OTP/verification codes that Rails prints to the dev log in development\n  Submit forms — GET the page, read hidden inputs (including authenticity_token), merge your fields, and POST/PUT/PATCH/DELETE through ERB forms\n  Inspect Turbo Streams — fire a request and read back the parsed action #target pairs the server returned\n  Read the log safely — tail, grep, pull OTP patterns, or slice the exact lines for one X-Request-Id\n  Bridge to Playwright — convert the curl session to/from Playwright storageState so you log in once and share the authenticated session between curl and a real browser\n\n\nIt is delivered as a skill (knowledge module plus shell scripts), not an autonomous agent. Claude reads SKILL.md and runs the scripts in scripts/ against your local app.\n\n\n\nWhen It Fits (and When It Doesn’t)\n\nGood fit: ERB + Hotwire apps with minimal JavaScript. The server renders HTML and text/vnd.turbo-stream.html; you are verifying that server-rendered contract.\n\nIt does not execute JavaScript. No Stimulus controllers run, no DOM morphing, no requestSubmit, no ActionCable-broadcast rendering. You can see a broadcast happen in the log (via request-id correlation), but not its DOM effect. For those cases, pair it with a browser-driving tool like the Playwright MCP — the session bridge means you only log in once.\n\n\n\nQuick Start\n\n1. Add the Marketplace\n\n/plugin marketplace add maquina-app/rails-claude-code\n\n\n2. Install the Plugin\n\n/plugin install rails-hotwire-driver@maquina\n\n\n3. Drive Your App\n\nWith your Rails app running locally (e.g. bin/rails s), just ask:\n\n&gt; Log in as me@example.com and open the dashboard\n&gt; Submit the new post form and show me which turbo-streams came back\n&gt; Read the OTP code from the log and finish the login\n&gt; Trace request abc-123 through the development log\n\n\n\n\nPrerequisites\n\nConfirm these before driving:\n\n\n  The app is running locally in development, and you know its port. Set BASE_URL (default http://localhost:3000). The scripts refuse any non-local host — allowed: localhost, loopback IPs, and any *.localhost name.\n  Nokogiri is available — it ships with essentially every Rails bundle. Run the Ruby scripts via the project bundle (bundle exec ruby ...).\n  Recommended: request-id tagging for best log correlation. In config/environments/development.rb:\n    config.log_tags = [ :request_id ]\n    \n    Without it, readlog.sh request falls back to a context window instead of an exact filter — still useful, just noisier.\n  \n\n\nThese scripts only ever talk to a local server and only read the development log. Reading secrets like OTP codes out of a log is a development-only affordance — readlog.sh refuses any path containing production.\n\n\n\nThe Scripts\n\nAll live in scripts/. A shared cookie jar at ./.hotwire/cookies.txt carries the session across calls.\n\n\n  \n    \n      Script\n      Purpose\n    \n  \n  \n    \n      req.sh\n      One HTTP request with cookies persisted. Prints response headers (with X-Request-Id, Set-Cookie redacted) and the body.\n    \n    \n      submit_form.rb\n      Submit a form with the correct CSRF token. GETs the page, reads hidden inputs including authenticity_token, merges your fields, honors Rails’ _method field.\n    \n    \n      readlog.sh\n      Read the dev log safely — tail, grep, request &lt;id&gt;, or otp.\n    \n    \n      flow.sh\n      Full login → OTP → action in one command, all sharing the cookie jar.\n    \n    \n      jar_to_storage.rb / storage_to_jar.rb\n      Bridge the curl session to/from Playwright storageState.\n    \n  \n\n\nreq.sh — one request, cookies persisted\n\nreq.sh GET  /products\nreq.sh GET  /cart turbo            # Accept: text/vnd.turbo-stream.html\nreq.sh GET  /messages frame:inbox  # Turbo-Frame: inbox (load a lazy frame)\nreq.sh POST /cart/add 'product_id=1&amp;qty=2'\n\n\nsubmit_form.rb — the right CSRF token, every time\n\nThis is the tool for any POST/PUT/PATCH/DELETE through an ERB form. It eliminates the single most common hand-driving failure — a missing or stale CSRF token.\n\nbundle exec ruby scripts/submit_form.rb /session/new \"email=me@x.com\" \"password=secret\"\nbundle exec ruby scripts/submit_form.rb /posts/new \"form#new_post\" \"post[title]=Hi\"\n\n\nIt reports status, X-Request-Id, any redirect Location, and — for turbo-stream responses — a parsed list of action #target pairs.\n\nreadlog.sh — read the dev log safely\n\nreadlog.sh tail 200\nreadlog.sh grep 'SQL|SELECT' 500\nreadlog.sh request &lt;x-request-id&gt;   # exact lines for one request (needs log_tags)\nreadlog.sh otp                      # grep common OTP / magic-link / token patterns\n\n\nflow.sh — login → OTP → action in one command\n\nOrchestrates the other three: submits the login form (CSRF handled), reads the OTP from the log scoped to the login’s request id (not a blind grep), submits the OTP, then optionally performs one authenticated action.\n\n# OTP / magic-link login, then hit an authenticated page:\nflow.sh --email me@x.com --password secret \\\n        --login-path /session/new \\\n        --otp-path /session/otp --otp-field code \\\n        --then-path /dashboard --then-method GET\n\n# Password-only (omit --otp-path to skip the OTP steps):\nflow.sh --email me@x.com --password secret --then-path /account\n\n# Authenticated POST through a form (CSRF auto-handled):\nflow.sh --email me@x.com --otp-path /session/otp \\\n        --then-path /posts/new --then-method POST --then-fields 'post[title]=Hi'\n\n\n\n\nCore Workflows\n\nOTP / magic-link login (the log trick)\n\nIn development, the mailer/notifier writes the code to the log rather than sending real email. flow.sh does this in one command; manually the steps are:\n\n\n  Trigger it: submit_form.rb /session/new \"email=...\".\n  Read the code: take the X-Request-Id from step 1, run readlog.sh request &lt;id&gt;, and extract the code.\n  Submit it: submit_form.rb /otp \"code=123456\".\n\n\nVerify a Turbo Stream\n\n\n  req.sh POST /cart/add 'product_id=1' turbo (or submit_form.rb for CSRF forms).\n  Read the parsed action #target list to confirm the server returned the streams you expected (e.g. replace #cart_summary, append #flash).\n  Correlate render details with readlog.sh request &lt;X-Request-Id&gt; — which partials rendered, what SQL ran.\n\n\nTrace one request end to end\n\nAny req.sh/submit_form.rb call prints X-Request-Id. Feed it to readlog.sh request &lt;id&gt; for a clean, single-request slice of the log — the most reliable way to see params, SQL, partial renders, and errors without log noise.\n\n\n\nPairing with Playwright\n\nThis skill verifies the server’s contract (turbo-stream actions, SQL, logs, the raw HTML before JS runs). Playwright verifies client behavior (did Stimulus wire up, did the stream actually mutate the DOM, did a lazy frame load). They’re complementary — the session bridge means you log in only once.\n\ncurl → Playwright (the common case)\n\nAuthenticate fast with the OTP-from-log trick, then hand the logged-in session to a real browser.\n\nflow.sh --email me@x.com --otp-path /session/otp --then-path /\nruby jar_to_storage.rb --origin http://fragua.localhost &gt; state.json\n# then: npx @playwright/mcp@latest --storage-state state.json\n\n\nPlaywright → curl (reverse)\n\nIf a login is too JS-heavy for curl to replay (OAuth popup, Stimulus-driven form), let Playwright do it through the real UI, export its session, and drop back to the fast curl + log tools.\n\n# in Playwright: await context.storageState({ path: 'state.json' })\nruby storage_to_jar.rb --in state.json     # writes ./.hotwire/cookies.txt\nreq.sh GET /dashboard                       # now authenticated\n\n\nThe bridge scripts emit the standard storageState format, so they work with the Playwright MCP, the Node test runner, or playwright-ruby-client.\n\n\n\nkamal-proxy and *.localhost Hosts\n\nIf you front your apps with kamal-proxy and reach them at names like http://fragua.localhost, set BASE_URL=http://fragua.localhost (with the port if not 80). The proxy routes by the Host header, which curl and Net::HTTP send automatically.\n\n*.localhost resolves to loopback on macOS and most browsers, but not always on Linux. Force resolution with RESOLVE:\n\nRESOLVE=1 BASE_URL=http://fragua.localhost:80 req.sh GET /\n# connects to 127.0.0.1 but still sends Host: fragua.localhost\n\n\nRESOLVE works for both req.sh and submit_form.rb; the Host header is preserved for routing either way. Point LOG_FILE at the specific app’s log/development.log, since each app under the proxy has its own log.\n\n\n\nConfiguration\n\nSet via environment variables:\n\n\n  \n    \n      Variable\n      Default\n      Purpose\n    \n  \n  \n    \n      BASE_URL\n      http://localhost:3000\n      Target server. For kamal-proxy use the routed name.\n    \n    \n      RESOLVE\n      (off)\n      Force the host to resolve to an IP. RESOLVE=1 → 127.0.0.1; RESOLVE=&lt;ip&gt; → that IP.\n    \n    \n      JAR\n      ./.hotwire/cookies.txt\n      Cookie jar path.\n    \n    \n      LOG_FILE\n      ./log/development.log\n      Log to read (point at the specific app’s log).\n    \n    \n      MAX_BYTES\n      100000\n      Response body cap for req.sh.\n    \n  \n\n\n\n\nGuardrails\n\nThese are deliberate — don’t weaken them:\n\n\n  Local only. Both shell scripts reject non-localhost hosts.\n  No production logs. readlog.sh refuses paths containing production.\n  Don’t echo cookies. req.sh redacts Set-Cookie; report auth state, not the cookie value.\n  Keep these separate from rails-mcp-server — that server is introspection-only (it does not execute arbitrary Ruby or make network requests). These scripts are a deliberately separate, narrowly-scoped affordance.\n\n\n\n\nPackage Contents\n\nrails-hotwire-driver/\n└── skills/\n    └── rails-hotwire-driver/\n        ├── SKILL.md                 # Skill knowledge module\n        └── scripts/\n            ├── req.sh               # One HTTP request, cookies persisted\n            ├── submit_form.rb       # CSRF-correct form submit\n            ├── readlog.sh           # Safe dev-log reader\n            ├── flow.sh              # login → OTP → action\n            ├── jar_to_storage.rb    # curl jar → Playwright storageState\n            └── storage_to_jar.rb    # Playwright storageState → curl jar\n\n\n\n\nTeam Installation\n\nAdd to your project’s .claude/settings.json:\n\n{\n  \"extraKnownMarketplaces\": {\n    \"maquina\": {\n      \"source\": {\n        \"source\": \"github\",\n        \"repo\": \"maquina-app/rails-claude-code\"\n      }\n    }\n  },\n  \"enabledPlugins\": [\n    \"rails-hotwire-driver@maquina\"\n  ]\n}\n\n\n\n\nNext Steps\n\n\n  \n    \n      GitHub Repository\n    \n    \n      View source code and contribute.\n    \n  \n\n  \n    \n      Rails MCP Server\n    \n    \n      Pair static code analysis with live runtime interaction.\n    \n  \n\n  \n    \n      Better Stimulus\n    \n    \n      Write the Stimulus controllers behind your Hotwire UI."
        },
        {
          "id": "documentation-ai-tools-rails-mcp-server",
          "title": "Rails MCP Server",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/ai-tools/rails-mcp-server/",
          "content": "A Ruby implementation of a Model Context Protocol (MCP) server for Rails projects. Let LLMs interact with your Rails codebase through code analysis, exploration, and development assistance.\n\nCurrent Version: 2.0.0\n\n\n\nWhat is MCP?\n\nThe Model Context Protocol (MCP) is a standardized way for AI models to interact with their environment. It defines a structured method for models to request and use tools, access resources, and maintain context during interactions.\n\nRails MCP Server implements the MCP specification to give AI models access to Rails projects for code analysis, exploration, and assistance.\n\n\n\nFeatures\n\n\n  Manage multiple Rails projects with auto-detection\n  Browse project files and structures\n  View Rails routes with filtering\n  Inspect model information and relationships (Prism static analysis)\n  Get database schema information\n  Analyze controller-view relationships\n  Analyze environment configurations\n  Read and glob project files through dedicated tools\n  Access Rails, Turbo, Stimulus, and Kamal documentation\n  Context-efficient architecture with progressive tool discovery\n  GitHub Copilot Agent support (v1.5.0+)\n  Rails 8.1+ compatibility (v1.5.0+)\n\n\n\n\nQuick Start\n\n1. Install the Gem\n\ngem install rails-mcp-server\n\n\n2. Configure Projects\n\nOption A: Interactive configuration\n\nrails-mcp-config\n\n\nThis provides a TUI for managing projects, downloading guides, and configuring Claude Desktop.\n\nOption B: Single-project mode (v1.5.0+)\n\nFor quick usage with the current directory:\n\ncd /path/to/your/rails/app\nrails-mcp-server --single-project\n\n\nOption C: Environment variable (v1.5.0+)\n\nexport RAILS_MCP_PROJECT_PATH=/path/to/your/rails/app\nrails-mcp-server\n\n\n3. Configure Your AI Client\n\nClaude Desktop\n\nSelect “Claude Desktop integration” in the configuration tool, or manually add to claude_desktop_config.json:\n\n{\n  \"mcpServers\": {\n    \"railsMcpServer\": {\n      \"command\": \"ruby\",\n      \"args\": [\"/path/to/rails-mcp-server/exe/rails-mcp-server\"]\n    }\n  }\n}\n\n\nGitHub Copilot Agent (v1.5.0+)\n\nSee the Copilot Agent Setup Guide for detailed instructions.\n\n\n\nProject Detection (v1.5.0+)\n\nThe server uses priority-based project detection:\n\n\n  \n    \n      Priority\n      Method\n      Description\n    \n  \n  \n    \n      1 (Highest)\n      RAILS_MCP_PROJECT_PATH env var\n      Explicit path to project\n    \n    \n      2\n      --single-project flag\n      Uses current working directory\n    \n    \n      3\n      Auto-detection\n      Detects Rails apps (Gemfile) or engines (gemspec)\n    \n    \n      4 (Lowest)\n      projects.yml\n      Traditional multi-project configuration\n    \n  \n\n\nWhen only one project is configured, the server auto-switches to it.\n\n\n\nAvailable Tools\n\nThe server provides 3 registered tools plus internal analyzers accessible via execute_tool.\n\n\n  Removed in v2.0.0: the execute_ruby tool has been removed. The server is now introspection-only — use the dedicated analyzers below (e.g. get_file, list_files, get_routes, get_schema). See Migrating from execute_ruby.\n\n\nRegistered Tools\n\n\n  \n    \n      Tool\n      Description\n    \n  \n  \n    \n      switch_project\n      Change the active Rails project\n    \n    \n      search_tools\n      Discover available tools by category or keyword\n    \n    \n      execute_tool\n      Invoke internal analyzers by name\n    \n  \n\n\nInternal Analyzers\n\n\n  \n    \n      Analyzer\n      Description\n    \n  \n  \n    \n      project_info\n      Project information, Rails version, directory structure\n    \n    \n      list_files\n      List files matching a pattern\n    \n    \n      get_file\n      Retrieve file content\n    \n    \n      get_routes\n      Rails routes with filtering\n    \n    \n      analyze_models\n      Active Record models with associations and validations\n    \n    \n      get_schema\n      Database schema information\n    \n    \n      analyze_controller_views\n      Controller-view relationships\n    \n    \n      analyze_environment_config\n      Environment configuration analysis\n    \n    \n      load_guide\n      Load documentation guides\n    \n  \n\n\n\n\nUsage Examples\n\nSwitch Project\n\nswitch_project(project_name: \"my_rails_app\")\n\n\nGet Routes\n\nexecute_tool(tool_name: \"get_routes\")\nexecute_tool(tool_name: \"get_routes\", params: { controller: \"users\" })\nexecute_tool(tool_name: \"get_routes\", params: { verb: \"POST\" })\n\n\nAnalyze Models\n\nexecute_tool(tool_name: \"analyze_models\")\nexecute_tool(tool_name: \"analyze_models\", params: { model_name: \"User\" })\nexecute_tool(tool_name: \"analyze_models\", params: { model_name: \"User\", analysis_type: \"full\" })\n\n\nTips:\n\n  Use CamelCase singular: User, BlogPost, OrderItem\n  Use analysis_type: \"full\" to include Prism static analysis (callbacks, scopes, methods)\n\n\nGet Schema\n\nexecute_tool(tool_name: \"get_schema\")\nexecute_tool(tool_name: \"get_schema\", params: { table_name: \"users\" })\nexecute_tool(tool_name: \"get_schema\", params: { detail_level: \"tables\" })\n\n\nTips:\n\n  Use snake_case plural: users, blog_posts, order_items\n  Use detail_level: \"tables\" for a quick table list\n\n\nRead a File\n\nexecute_tool(tool_name: \"get_file\", params: { path: \"Gemfile\" })\nexecute_tool(tool_name: \"get_file\", params: { path: \"app/models/user.rb\" })\n\n\nPaths are relative to the project root. Reads are confined to the project directory, and sensitive files (.env, credentials, keys) are refused.\n\nFind Files\n\nexecute_tool(tool_name: \"list_files\", params: { pattern: \"app/models/**/*.rb\" })\nexecute_tool(tool_name: \"list_files\", params: { pattern: \"app/**/*user*\" })\n\n\nMigrating from execute_ruby\n\nThe execute_ruby tool was removed in v2.0.0. It ran caller-supplied Ruby via bin/rails runner, which made it an arbitrary-code-execution surface a pattern-based sandbox could not safely contain. The server is an introspection tool, and its dedicated analyzers cover what execute_ruby was used for:\n\n\n  \n    \n      Old execute_ruby usage\n      Use instead\n    \n  \n  \n    \n      read_file(path)\n      execute_tool(tool_name: \"get_file\", params: { path: … })\n    \n    \n      list_files(pattern)\n      execute_tool(tool_name: \"list_files\", params: { pattern: … })\n    \n    \n      file_exists? / project_root\n      list_files / execute_tool(tool_name: \"project_info\")\n    \n    \n      Routes / schema / models / controllers\n      get_routes, get_schema, analyze_models, analyze_controller_views\n    \n  \n\n\nAd-hoc live data queries (User.count, custom scopes) are intentionally no longer supported. If you rely on free-form execution, pin to the 1.6.x line, which retains the hardened execute_ruby.\n\n\n\nServer Modes\n\nSTDIO Mode (Default)\n\nFor direct integration with Claude Desktop:\n\nrails-mcp-server\n\n\nSingle-Project Mode (v1.5.0+)\n\nFor working with the current directory only:\n\ncd /path/to/rails/app\nrails-mcp-server --single-project\n\n\nHTTP Mode\n\nFor HTTP endpoints with JSON-RPC and SSE:\n\nrails-mcp-server --mode http\nrails-mcp-server --mode http -p 8080\nrails-mcp-server --mode http --bind-all  # Allow LAN access\n\n\nEndpoints:\n\n  JSON-RPC: http://localhost:6029/mcp/messages\n  SSE: http://localhost:6029/mcp/sse\n\n\n\n\nConfiguration\n\nEnvironment Variable (v1.5.0+)\n\nSet the project path explicitly:\n\nexport RAILS_MCP_PROJECT_PATH=~/projects/my-rails-app\nrails-mcp-server\n\n\nManual Project Configuration\n\nEdit ~/.config/rails-mcp/projects.yml:\n\nstore: \"~/projects/store\"\nblog: \"~/projects/rails-blog\"\necommerce: \"/full/path/to/ecommerce-app\"\n\n\nRuby Version Manager Users\n\nClaude Desktop bypasses version manager initialization. Use the Ruby shim path:\n\n{\n  \"mcpServers\": {\n    \"railsMcpServer\": {\n      \"command\": \"/home/your_user/.rbenv/shims/ruby\",\n      \"args\": [\"/path/to/rails-mcp-server/exe/rails-mcp-server\"]\n    }\n  }\n}\n\n\nThe rails-mcp-config tool detects this automatically.\n\n\n\nDocumentation Resources\n\nAccess comprehensive documentation through load_guide:\n\nexecute_tool(tool_name: \"load_guide\", params: { library: \"rails\" })\nexecute_tool(tool_name: \"load_guide\", params: { library: \"rails\", guide: \"getting_started\" })\nexecute_tool(tool_name: \"load_guide\", params: { library: \"rails\", guide: \"active_record_basics\" })\nexecute_tool(tool_name: \"load_guide\", params: { library: \"turbo\" })\nexecute_tool(tool_name: \"load_guide\", params: { library: \"stimulus\" })\nexecute_tool(tool_name: \"load_guide\", params: { library: \"kamal\" })\nexecute_tool(tool_name: \"load_guide\", params: { library: \"custom\" })\n\n\nAvailable libraries:\n\n\n  \n    \n      Library\n      Content\n    \n  \n  \n    \n      rails\n      Official Rails Guides\n    \n    \n      turbo\n      Hotwire Turbo handbook and reference\n    \n    \n      stimulus\n      Stimulus handbook and reference\n    \n    \n      kamal\n      Kamal deployment documentation\n    \n    \n      custom\n      User-added custom guides\n    \n  \n\n\nDownload guides using the configuration tool:\n\nrails-mcp-config\n# Select \"Download guides\"\n\n\n\n  Breaking Change in v1.5.0: The guides parameter was renamed to library.\n  \n    Old: params: { guides: \"rails\" }\n    New: params: { library: \"rails\" }\n  \n\n\n\n\nAnalyzer Parameter Reference\n\n\n  \n    \n      Analyzer\n      Required\n      Optional Parameters\n    \n  \n  \n    \n      project_info\n      -\n      max_depth, include_files, detail_level\n    \n    \n      list_files\n      -\n      directory, pattern\n    \n    \n      get_file\n      path\n      -\n    \n    \n      get_routes\n      -\n      controller, verb, path_contains, named_only, detail_level\n    \n    \n      analyze_models\n      -\n      model_name, model_names, detail_level, analysis_type\n    \n    \n      get_schema\n      -\n      table_name, table_names, detail_level\n    \n    \n      analyze_controller_views\n      -\n      controller_name, detail_level, analysis_type\n    \n    \n      analyze_environment_config\n      -\n      (none)\n    \n    \n      load_guide\n      library\n      guide\n    \n  \n\n\nCommon Parameter Values\n\ndetail_level:\n\n  names - Minimal output (just names/paths)\n  summary - Compact overview\n  full - Complete details (default)\n\n\nanalysis_type (for models and controllers):\n\n  introspection - Uses Rails runtime APIs (default)\n  static - Uses Prism AST parsing\n  full - Both introspection and static analysis\n\n\n\n\nUsing with MCP Proxy\n\nFor STDIO-only clients that need HTTP/SSE capabilities:\n\n# Start server in HTTP mode\nrails-mcp-server --mode http\n\n# Install and run MCP proxy\nnpm install -g mcp-remote\nnpx mcp-remote http://localhost:6029/mcp/sse\n\n\nConfigure Claude Desktop to use the proxy:\n\n{\n  \"mcpServers\": {\n    \"railsMcpServer\": {\n      \"command\": \"npx\",\n      \"args\": [\"mcp-remote\", \"http://localhost:6029/mcp/sse\"]\n    }\n  }\n}\n\n\n\n\nTesting and Debugging\n\nUse MCP Inspector to test the server:\n\nnpm -g install @modelcontextprotocol/inspector\nnpx @modelcontextprotocol/inspector /path/to/rails-mcp-server\n\n\nThe Inspector UI lets you:\n\n  See all available tools\n  Execute tool calls interactively\n  View request and response details\n  Debug issues in real-time\n\n\n\n\nSecurity\n\nIntrospection-only (v2.0.0+)\n\nThe server does not execute caller-supplied Ruby. It exposes a fixed set of introspection analyzers, so there is no arbitrary-code-execution surface. (The free-form execute_ruby tool was removed in v2.0.0.)\n\nThe tools that boot the app (get_schema, get_routes, and the introspection half of analyze_models / analyze_controller_views) run bin/rails runner with fixed, server-authored scripts; caller input is passed as validated parameters, never interpolated as code. Booting a project runs that project’s environment, so point the server only at Rails projects you trust.\n\nInput Validation\n\nAll file-accessing tools use centralized input validation (PathValidator):\n\n\n  Path traversal prevention - Blocks ../ and absolute paths that escape the project root\n  Sensitive file protection - Filters master.key, credentials.yml.enc, .env files\n  Shell injection prevention - Uses safe argument passing\n  SQL injection prevention - Validates table names in schema queries\n\n\n\n\nCompatibility\n\n\n  \n    \n      Component\n      Supported Versions\n    \n  \n  \n    \n      Ruby\n      3.3+ (Ruby 3.2 dropped in v1.6.0)\n    \n    \n      Rails (target projects)\n      6.0+\n    \n    \n      Rails 8.1.1+\n      Full support (v1.5.0+)\n    \n    \n      Claude Desktop\n      Supported\n    \n    \n      GitHub Copilot Agent\n      Supported (v1.5.0+)\n    \n    \n      Other MCP Clients\n      Via STDIO or HTTP mode\n    \n  \n\n\n\n\nChangelog Highlights\n\nv2.0.0\n\nBreaking Changes:\n\n  Removed the execute_ruby tool. The server is now introspection-only; use the dedicated analyzers (see Migrating from execute_ruby). Bootstrap tools reduced from 4 to 3.\n\n\nSecurity:\n\n  Removing execute_ruby eliminates the arbitrary-code-execution surface behind the v1.6.x hardening series.\n\n\nv1.6.x\n\nSecurity:\n\n  Hardened, then removed, the execute_ruby sandbox: blocked the PTY.spawn command-execution path and other stdlib escapes, hard-blocked dynamic dispatch to execution sinks, and fixed a ReDoS in the static scan (v1.6.1). Thanks to Pluto Security for the responsible disclosure.\n  Puma upgraded to 8.0.2, clearing CVE-2026-47736 / CVE-2026-47737.\n\n\nBreaking Changes:\n\n  Dropped Ruby 3.2 support (minimum is now Ruby 3.3).\n\n\nv1.5.0\n\nNew Features:\n\n  GitHub Copilot Agent support\n  --single-project flag for single-project mode\n  RAILS_MCP_PROJECT_PATH environment variable\n  Auto-detection of Rails apps and engines\n  Auto-switch when only one project configured\n\n\nSecurity:\n\n  Added PathValidator for centralized input sanitization\n  Added CI security infrastructure (Dependabot, CodeQL, OpenSSF Scorecard)\n\n\nBreaking Changes:\n\n  load_guide parameter renamed: guides → library\n\n\n\n\nNext Steps\n\n\n  \n    \n      GitHub Repository\n    \n    \n      Source code, issues, and contribution guidelines.\n    \n  \n\n  \n    \n      AI Agent Guide\n    \n    \n      Comprehensive guide for AI agents using this server.\n    \n  \n\n  \n    \n      GitHub Copilot Setup\n    \n    \n      Configure Rails MCP Server with GitHub Copilot Agent.\n    \n  \n\n  \n    \n      RubyGems\n    \n    \n      Install the latest version from RubyGems."
        },
        {
          "id": "documentation-ai-tools-rails-security-auditor",
          "title": "Rails Security Auditor",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/ai-tools/rails-security-auditor/",
          "content": "A Claude Code plugin that audits a Rails application’s security configuration and produces a severity-grouped, actionable report grounded in Rails 8.0–8.2 defaults. Optionally applies fixes for any finding.\n\n\n\nWhat Is This?\n\nA Claude Code agent that:\n\n\n  Scans your Rails config: production environment, initializers, controllers, Gemfile, and CI workflows\n  Detects your Rails version from Gemfile.lock and adjusts expectations accordingly\n  Runs 10 categories of security checks against current Rails defaults\n  Groups findings by severity (Critical, High, Medium, Informational)\n  Explains each finding in plain language — what it is, why it matters, how to fix it\n  Applies fixes on request — single finding, all Critical, or all findings\n\n\nThe agent runs autonomously: it reads the files it needs, produces the full report, and pauses to offer fixes.\n\n\n\nQuick Start\n\n1. Add the Marketplace\n\n/plugin marketplace add maquina-app/rails-claude-code\n\n\n2. Install the Plugin\n\n/plugin install rails-security-auditor@maquina\n\n\n3. Run the Audit\n\n&gt; /audit-security\n\n\nOr simply ask:\n\n&gt; Audit my Rails app's security configuration\n&gt; Check if my CSRF setup is correct\n&gt; Am I missing any security headers?\n\n\n\n\nWhat It Checks\n\nTen check categories, drawn from the full catalog in references/checks.md:\n\n\n  \n    \n      Category\n      Scope\n    \n  \n  \n    \n      PROD\n      force_ssl, assume_ssl, log level, filter_parameters, trusted proxies\n    \n    \n      CSRF\n      protect_from_forgery, strategy, per-form tokens\n    \n    \n      HDR\n      X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy\n    \n    \n      CSP\n      Content Security Policy initializer and directives\n    \n    \n      SESS\n      Session cookie SameSite, expire_after, httponly\n    \n    \n      RATE\n      rate_limit macro, Rack::Attack throttles and safelists\n    \n    \n      AUTH\n      Authorization gem, tenant scoping, allow_unauthenticated_access\n    \n    \n      GEM\n      Brakeman, bundler-audit, Rails CVEs\n    \n    \n      CI\n      Security scanning in CI pipeline\n    \n    \n      FWKD\n      Rails 8.2 framework defaults (CSRF header strategy, transaction-aware jobs)\n    \n  \n\n\nPlus: column-level encryption (encrypts) and hardcoded secrets scans.\n\n\n\nHow Findings Are Reported\n\nEach finding follows a consistent structure:\n\n### [PROD-01] force_ssl missing in production\nFile: config/environments/production.rb\nFound: config.force_ssl is not set\n\nWhy this matters:\nWithout force_ssl, users on HTTP don't get redirected to HTTPS.\nCookies and sessions can travel in the clear on public networks.\n\nHow to fix it:\nconfig.force_ssl = true\n\nOffer: Would you like me to apply this fix?\n\n\nSeverity counts go at the top of the report:\n\n\n  \n    \n      Severity\n      Meaning\n    \n  \n  \n    \n      ❌ Critical\n      Active vulnerability — fix immediately\n    \n    \n      ⚠️ High\n      Important gap — fix soon\n    \n    \n      🔶 Medium\n      Hardening — recommended\n    \n    \n      ℹ️ Informational\n      Optional improvement\n    \n    \n      ✅ Passed\n      Already configured correctly\n    \n  \n\n\n\n\nAudit Principles\n\nThe auditor is designed to minimize noise:\n\n\n  Intentional configuration isn’t flagged. If assume_ssl is set alongside force_ssl, the agent recognizes it as a Cloudflare/Kamal setup and doesn’t mark force_ssl redirect as missing.\n  Version-aware. A Rails 7.1 app isn’t expected to have 8.2 defaults; checks adapt to the detected version.\n  Missing files are findings. No rack_attack.rb → finding. No content_security_policy.rb → finding.\n  Ambiguous intent surfaces as “Verify:” rather than an assertion of wrongness.\n  Severity is guidance, not a verdict. The agent adjusts when the app’s context makes a finding materially more or less risky.\n\n\n\n\nApplying Fixes\n\nAfter the report, you can fix findings one at a time, all Critical findings at once, or everything:\n\n&gt; Fix all Critical findings\n&gt; Apply the CSP fix only\n&gt; Create the missing rack_attack.rb initializer\n\n\nFor each fix the agent reads the current file, applies the minimal change needed, shows a before/after diff, and confirms the write. When a fix requires creating a new initializer from scratch, it generates the full file.\n\n\n\nPackage Contents\n\nrails-security-auditor/\n├── agents/rails-security-auditor.md    # Main agent\n└── references/\n    └── checks.md                       # Full check catalog with fixes\n\n\n\n\nTeam Installation\n\nAdd to your project’s .claude/settings.json:\n\n{\n  \"extraKnownMarketplaces\": {\n    \"maquina\": {\n      \"source\": {\n        \"source\": \"github\",\n        \"repo\": \"maquina-app/rails-claude-code\"\n      }\n    }\n  },\n  \"enabledPlugins\": [\n    \"rails-security-auditor@maquina\"\n  ]\n}\n\n\nWire /audit-security into your release checklist so every major deploy starts with a clean report.\n\n\n\nNext Steps\n\n\n  \n    \n      GitHub Repository\n    \n    \n      View source code and contribute.\n    \n  \n\n  \n    \n      Rails Upgrade Assistant\n    \n    \n      Pair with upgrades to pick up new security defaults.\n    \n  \n\n  \n    \n      Rails Simplifier\n    \n    \n      Clean up code patterns discovered during an audit."
        },
        {
          "id": "documentation-ai-tools-rails-simplifier",
          "title": "Rails Simplifier",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/ai-tools/rails-simplifier/",
          "content": "A Claude Code plugin that refines Ruby on Rails code following 37signals patterns and the One Person Framework philosophy. Transform complex code into clean, maintainable Rails conventions.\n\n\n\nWhat Is This?\n\nA Claude Code skill that:\n\n\n  Simplifies service objects into rich model methods and concerns\n  Converts custom controller actions to CRUD resources\n  Transforms boolean state columns into state records\n  Optimizes fat controllers into thin controllers with model methods\n  Applies Rails best practices like I18n, Time.current, and eager loading\n  Detects N+1 queries and suggests fixes\n\n\n\n\nPhilosophy\n\nThe One Person Framework\n\nFrom DHH (December 2021):\n\n\n  “A toolkit so powerful that it allows a single individual to create modern applications upon which they might build a competitive business.”\n\n\nConceptual Compression\n\nFrom RailsConf 2018:\n\n\n  “Like a video codec that throws away irrelevant details such that you might download the film in real-time.”\n\n\nVanilla Rails is Plenty\n\nFrom Jorge Manrubia at 37signals:\n\n\n  “If you have the luxury of starting a new Rails app today, go vanilla.”\n\n\n\n\nQuick Start\n\n1. Add the Marketplace\n\n/plugin marketplace add maquina-app/rails-claude-code\n\n\n2. Install the Plugin\n\n/plugin install rails-simplifier@maquina\n\n\n3. Start Simplifying\n\n&gt; Review recent changes using the rails-simplifier skill\n\n\n\n\nWhat It Simplifies\n\n\n  \n    \n      Pattern\n      Simplification\n    \n  \n  \n    \n      Service objects\n      Rich model methods + concerns\n    \n    \n      Custom controller actions\n      CRUD resources\n    \n    \n      Boolean state columns\n      State records (has_one :closure)\n    \n    \n      Fat controllers\n      Thin controllers, model methods\n    \n    \n      Time.now\n      Time.current\n    \n    \n      Hardcoded strings\n      I18n keys\n    \n    \n      N+1 queries\n      includes / preload\n    \n    \n      Date tests without travel_to\n      Freeze time to fixture\n    \n  \n\n\n\n\nUsage Examples\n\nReview Recent Changes\n\n&gt; Review recent changes using the rails-simplifier skill\n\n\nThe skill analyzes your recent commits and suggests simplifications based on 37signals patterns.\n\nReview a Specific Controller\n\n&gt; Use rails-simplifier to review the bookings controller\n\n\nReview a Model\n\n&gt; Use rails-simplifier to review the Order model\n\n\nFull Project Review\n\n&gt; Run rails-simplifier on the app directory\n\n\n\n\nSimplification Patterns\n\nService Objects to Model Methods\n\nBefore:\n\n# app/services/order_processor.rb\nclass OrderProcessor\n  def initialize(order)\n    @order = order\n  end\n\n  def process\n    @order.update(processed_at: Time.current)\n    @order.line_items.each(&amp;:fulfill)\n    OrderMailer.confirmation(@order).deliver_later\n  end\nend\n\n# In controller\nOrderProcessor.new(@order).process\n\n\nAfter:\n\n# app/models/order.rb\nclass Order &lt; ApplicationRecord\n  def process!\n    update(processed_at: Time.current)\n    line_items.each(&amp;:fulfill)\n    OrderMailer.confirmation(self).deliver_later\n  end\nend\n\n# In controller\n@order.process!\n\n\nBoolean States to State Records\n\nBefore:\n\nclass Post &lt; ApplicationRecord\n  scope :published, -&gt; { where(published: true) }\n  scope :draft, -&gt; { where(published: false) }\nend\n\n\nAfter:\n\nclass Post &lt; ApplicationRecord\n  has_one :publication\n\n  scope :published, -&gt; { joins(:publication) }\n  scope :draft, -&gt; { where.missing(:publication) }\n\n  def publish!\n    create_publication!\n  end\n\n  def unpublish!\n    publication&amp;.destroy\n  end\nend\n\n\nCustom Actions to CRUD\n\nBefore:\n\n# config/routes.rb\nresources :posts do\n  member do\n    post :publish\n    post :unpublish\n    post :archive\n  end\nend\n\n# app/controllers/posts_controller.rb\ndef publish\n  @post.update(published: true)\n  redirect_to @post\nend\n\n\nAfter:\n\n# config/routes.rb\nresources :posts do\n  resource :publication, only: [:create, :destroy]\n  resource :archival, only: [:create, :destroy]\nend\n\n# app/controllers/publications_controller.rb\nclass PublicationsController &lt; ApplicationController\n  def create\n    @post = Post.find(params[:post_id])\n    @post.create_publication!\n    redirect_to @post\n  end\n\n  def destroy\n    @post = Post.find(params[:post_id])\n    @post.publication.destroy\n    redirect_to @post\n  end\nend\n\n\nN+1 Query Detection\n\nBefore:\n\ndef index\n  @posts = Post.all\nend\n\n# In view: @posts.each { |post| post.author.name }\n\n\nAfter:\n\ndef index\n  @posts = Post.includes(:author)\nend\n\n\n\n\nTeam Installation\n\nAdd to your project’s .claude/settings.json:\n\n{\n  \"extraKnownMarketplaces\": {\n    \"maquina\": {\n      \"source\": {\n        \"source\": \"github\",\n        \"repo\": \"maquina-app/rails-claude-code\"\n      }\n    }\n  },\n  \"enabledPlugins\": [\n    \"rails-simplifier@maquina\"\n  ]\n}\n\n\n\n\nResources\n\n\n  37signals Rails Patterns — Collection of patterns from 37signals\n  Jorge Manrubia’s Blog — Rails architecture insights\n  Rails Doctrine — The philosophy behind Rails\n\n\n\n\nNext Steps\n\n\n  \n    \n      GitHub Repository\n    \n    \n      View source code and contribute.\n    \n  \n\n  \n    \n      Rails MCP Server\n    \n    \n      Enhance analysis with MCP tools."
        },
        {
          "id": "documentation-ai-tools-rails-upgrade-skill",
          "title": "Rails Upgrade Skill",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/ai-tools/rails-upgrade-skill/",
          "content": "A comprehensive Claude skill that helps you upgrade Ruby on Rails applications through any version from 6.0 to 8.1.1. Built on official Rails CHANGELOGs. It analyzes your project with Claude Code’s built-in file tools — no external services required.\n\n\n\nWhat Is This?\n\nA Claude skill that:\n\n\n  Analyzes your Rails project automatically by reading its files\n  Detects your current version and target version\n  Plans single-hop or multi-hop upgrade paths\n  Identifies breaking changes specific to your code\n  Preserves custom configurations with warnings\n  Generates comprehensive upgrade reports\n  Applies the fixes for you, editing the files directly\n  Based on official Rails CHANGELOGs from GitHub\n\n\n\n\nQuick Start\n\n1. Install the Plugin\n\n/plugin marketplace add maquina-app/rails-claude-code\n/plugin install rails-upgrade-assistant@maquina\n\n\n2. Start Upgrading\n\nSay to Claude:\n\n\"Upgrade my Rails app to 8.1\"\n\n\nClaude will:\n\n\n  Detect your current version from Gemfile.lock and plan the path (single or multi-hop)\n  Generate a breaking-changes detection script and run it\n  Evaluate the findings against your actual code\n  Produce a comprehensive upgrade report with OLD → NEW examples\n  Offer to apply the fixes directly\n\n\n\n\nSupported Upgrade Paths\n\n\n  \n    \n      From\n      To\n      Hops\n      Breaking Changes\n      Difficulty\n    \n  \n  \n    \n      8.0.x\n      8.1.1\n      1\n      8 changes\n      Easy\n    \n    \n      7.2.x\n      8.0.4\n      1\n      13 changes\n      Hard\n    \n    \n      7.1.x\n      7.2.3\n      1\n      38 changes\n      Medium\n    \n    \n      7.0.x\n      7.1.6\n      1\n      12 changes\n      Medium\n    \n    \n      6.1.x\n      7.0.0\n      1\n      17 changes\n      Hard\n    \n    \n      6.0.x\n      6.1.0\n      1\n      18 changes\n      Medium\n    \n    \n      6.0.x\n      8.1.1\n      6\n      106 changes\n      Very Hard\n    \n  \n\n\nSequential Upgrades Required\n\nRails upgrades must be sequential:\n\nCorrect: 6.0 → 6.1 → 7.0 → 7.1 → 7.2 → 8.0 → 8.1\nWrong:   6.0 → 7.0 (skips 6.1)\n\n\nFor multi-hop upgrades, Claude will:\n\n\n  Explain the sequential requirement\n  Plan all intermediate hops\n  Generate separate reports for each hop\n  Guide you through completing each hop before moving to next\n\n\n\n\nHow It Works\n\nFull upgrade (the default)\n\nBest for understanding what needs to change before making edits.\n\n\"Upgrade my Rails app from 7.2 to 8.0\"\n\n\nClaude will:\n\n\n  Read Gemfile.lock to detect the current version\n  Load appropriate version guide(s)\n  Analyze your project files for custom code\n  Identify breaking changes affecting your code\n  Generate comprehensive upgrade report\n\n\nYou remain in control and apply changes manually.\n\nQuery-specific\n\nBest for specific questions about changes.\n\n\"What ActiveRecord changes are in Rails 8.0?\"\n\"How do I handle the SSL configuration change?\"\n\"What breaking changes affect my models?\"\n\"Will my Redis cache work after upgrading to 8.0?\"\n\n\n\n\nKey Breaking Changes by Version\n\nRails 8.0 → 8.1\n\nHigh impact:\n\n\n  SSL configuration now commented out (affects non-Kamal deploys)\n  Database pool: renamed to max_connections:\n  bundler-audit script required\n\n\nRails 7.2 → 8.0\n\nHigh impact:\n\n\n  Asset pipeline: Sprockets → Propshaft\n  Solid gems: New defaults for cache/queue/cable\n  Multi-database config required for Solid gems\n\n\nRails 7.1 → 7.2\n\nHigh impact:\n\n\n  Transaction-aware job enqueuing (behavior change)\n  ActiveRecord::Base.connection deprecated\n  show_exceptions changed from boolean to symbol\n  Rails.application.secrets removed\n\n\nRails 7.0 → 7.1\n\nHigh impact:\n\n\n  cache_classes → enable_reloading (inverted logic)\n  Force SSL now default in production\n  SQLite database moved to storage/\n\n\nRails 6.1 → 7.0\n\nHigh impact:\n\n\n  Zeitwerk autoloader required (Classic removed)\n  rails command replaces rake for most tasks\n  Spring removed from default Gemfile\n  ActiveSupport::Dependencies autoloading deprecated\n\n\nRails 6.0 → 6.1\n\nHigh impact:\n\n\n  Per-database connection handling changes\n  ActiveRecord::Base#connection pool behavior updated\n  Hotwire (Turbo + Stimulus) introduced as default frontend\n  rails db:prepare added as preferred setup command\n\n\n\n\nCustom Code Detection\n\nThe skill automatically detects and warns about customizations:\n\nDatabase Configuration\n\n# Custom SQLite path detected in config/database.yml\n# Current: database: db/development.sqlite3\n# Rails 7.1+: database: storage/development.sqlite3\n# Action: Review and update path\n\n\nSSL Middleware\n\n# Custom SSL middleware detected in config/application.rb\n# Line 23: middleware.use CustomSSLMiddleware\n# Rails 7.1+: May conflict with config.force_ssl = true\n# Action: Review compatibility\n\n\nAutoload Paths\n\n# Custom autoload_paths in config/application.rb\n# Line 15: config.autoload_paths &lt;&lt; Rails.root.join('lib')\n# Rails 7.1+: lib/ autoloaded by default (config.autoload_lib)\n# Action: Remove manual path to avoid conflicts\n\n\nAsset Pipeline\n\n# Custom Sprockets processors detected\n# Files: lib/assets/processors/custom_minifier.rb\n# Rails 8.0+: Propshaft doesn't support processors\n# Action: Migrate to different approach or keep Sprockets\n\n\n\n\nWhat You Get\n\nEvery upgrade request generates a detailed report:\n\n1. Executive Summary\n\n\n  Current and target versions\n  Number of breaking changes\n  Estimated time and risk assessment\n\n\n2. Project Analysis\n\n\n  Your Rails version and structure\n  Files that need updating\n  Custom configurations detected\n\n\n3. Breaking Changes (Prioritized)\n\n\n  HIGH Priority: Will cause app to fail\n  MEDIUM Priority: Should address soon\n  LOW Priority: Optional improvements\n\n\n4. Code Examples (OLD vs NEW)\n\n# OLD (Rails 7.2)\nconfig.action_dispatch.show_exceptions = true\n\n# NEW (Rails 7.2+)\nconfig.action_dispatch.show_exceptions = :all\n\n\n5. Step-by-Step Migration Guide\n\n\n  Phase-by-phase breakdown\n  Time estimates per phase\n  Testing checkpoints\n\n\n6. Testing Checklist\n\n\n  Unit test guidance\n  Integration test scenarios\n  Manual testing checklist\n\n\n\n\nPre-Upgrade Checklist\n\nBefore starting any upgrade:\n\nCritical:\n\n\n  All tests currently passing\n  Database backed up\n  Application under version control\n  Staging environment available\n  Rollback plan documented\n\n\nImportant:\n\n\n  Current version confirmed\n  Dependencies reviewed for compatibility\n  Custom code documented\n\n\n\n\nPackage Contents\n\nrails-upgrade-assistant/\n├── agents/rails-upgrade-assistant.md   Main agent\n├── workflows/                  How to generate deliverables\n├── examples/                   Real usage scenarios\n├── reference/                  Quick reference\n├── version-guides/             Rails version details\n├── templates/                  Report templates\n└── detection-scripts/          Pattern definitions\n\n\nVersion Guides\n\n\n  upgrade-6.0-to-6.1.md - 18 breaking changes\n  upgrade-6.1-to-7.0.md - 17 breaking changes\n  upgrade-7.0-to-7.1.md - 12 breaking changes\n  upgrade-7.1-to-7.2.md - 38 breaking changes\n  upgrade-7.2-to-8.0.md - 13 breaking changes\n  upgrade-8.0-to-8.1.md - 8 breaking changes\n\n\n\n\nUsage Examples\n\nSimple Upgrade\n\n\"Upgrade my Rails app to 8.1\"\n\n\nWith Specific Details\n\n\"Upgrade my Rails app from 7.2 to 8.0\"\n\n\nRisk Assessment Only\n\n\"Assess upgrade impact from 7.2 to 8.0\"\n\n\nComponent-Specific Questions\n\n\"What ActiveRecord changes are in Rails 8.0?\"\n\"Show me all configuration file changes for 7.2\"\n\n\n\n\nNext Steps\n\n\n  \n    \n      GitHub Repository\n    \n    \n      View source code and contribute.\n    \n  \n\n  \n    \n      Rails MCP Server\n    \n    \n      Optional — static code analysis to complement upgrades."
        },
        {
          "id": "documentation-ai-tools-spec-driven-development",
          "title": "Spec-Driven Development",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/ai-tools/spec-driven-development/",
          "content": "A Claude Code plugin for Rails-focused spec-driven development. Shape a feature into a structured spec, break it into tasks, and hand it off to Claude Code for implementation — with a progress file that survives across sessions.\n\n\n\nWhat Is This?\n\nA Claude Code skill that turns rough feature ideas into implementation-ready specs:\n\n\n  Plans your product from existing MVP documentation (mission, roadmap, tech stack)\n  Shapes feature specs through targeted discovery questions\n  Writes formal specifications with user stories, acceptance criteria, and scope boundaries\n  Breaks each spec into ordered, self-contained tasks\n  Discovers coding standards and tribal knowledge already present in the codebase\n  Tracks progress in a YAML file so any session can resume where the last one stopped\n\n\nThe goal: features built to spec instead of code thrown together, with a paper trail you can point Claude at later.\n\n\n\nThe Workflow\n\n\n  \n    \n      Step\n      Command\n      Output\n    \n  \n  \n    \n      1. Initialize\n      /sdd-init\n      sdd/ directory + progress.yml\n    \n    \n      2. Plan the product\n      /sdd-plan\n      mission.md, roadmap.md, tech-stack.md\n    \n    \n      3. Shape a feature\n      /sdd-shape\n      planning/requirements.md + formal spec.md\n    \n    \n      4. Break into tasks\n      /sdd-tasks\n      tasks.md with ordered task groups\n    \n    \n      5. Discover standards\n      /sdd-discover-standards\n      Extracted patterns in standards/\n    \n    \n      6. Check progress\n      /sdd-status\n      Current phase, completed items, next step\n    \n  \n\n\nSteps 1–2 are one-time setup. Steps 3–6 repeat per feature. You can also drive the workflow with natural language — the skill responds to “shape a spec for comments” or “what’s next?” without requiring the slash commands.\n\n\n\nQuick Start\n\n1. Add the Marketplace\n\n/plugin marketplace add maquina-app/rails-claude-code\n\n\n2. Install the Plugin\n\n/plugin install spec-driven-development@maquina\n\n\n3. Initialize in Your Project\n\n&gt; /sdd-init\n\n\nThe skill creates the sdd/ directory, a progress file, and the folder structure the rest of the workflow depends on.\n\n4. Plan the Product\n\n&gt; /sdd-plan\n\n\nIf MVP Creator documentation already exists in the project, the skill reuses it. Otherwise it asks you the product-planning questions directly.\n\n5. Shape Your First Feature\n\n&gt; /sdd-shape user authentication\n\n\nThe skill asks the clarifying questions, writes requirements, and produces a spec ready for task breakdown.\n\n\n\nSlash Commands\n\n\n  \n    \n      Command\n      What It Does\n    \n  \n  \n    \n      /sdd-init\n      Bootstrap the sdd/ directory and progress.yml for a project\n    \n    \n      /sdd-plan\n      Create mission, roadmap, and tech-stack documents from MVP docs or discovery\n    \n    \n      /sdd-shape\n      Shape a feature into requirements and a formal spec\n    \n    \n      /sdd-tasks\n      Generate an ordered task breakdown from a spec\n    \n    \n      /sdd-status\n      Show current phase, completed steps, and next action\n    \n    \n      /sdd-discover-standards\n      Extract coding patterns and tribal knowledge from the codebase\n    \n  \n\n\n\n\nWhat You’ll Get\n\nEach feature lives in its own folder under sdd/specs/:\n\nsdd/\n├── progress.yml                  # Workflow state across sessions\n├── product/\n│   ├── mission.md                # Product vision\n│   ├── roadmap.md                # Feature priorities\n│   └── tech-stack.md             # Technology choices\n├── standards/                    # Coding standards discovered in your code\n│   ├── global/\n│   ├── backend/\n│   └── frontend/\n└── specs/\n    └── 2026-04-16-user-auth/\n        ├── planning/\n        │   ├── requirements.md   # Gathered requirements\n        │   └── visuals/          # Mockups, wireframes (optional)\n        ├── spec.md               # Formal specification\n        └── tasks.md              # Ordered task breakdown\n\n\nSpecs are self-contained: once shaped, Claude can execute them without needing to replay the conversation that produced them.\n\n\n\nUsage Examples\n\nStart From an Existing MVP\n\n&gt; /sdd-plan\n\n\nIf you ran MVP Creator earlier, the skill pulls directly from business-plan.md and technical-guide.md instead of asking the same questions again.\n\nShape a Feature From an Idea\n\n&gt; I want to add comments to posts\n\n\nThe skill asks for scope, user roles, moderation rules, and related code — then writes requirements and a spec.\n\nBreak a Spec Into Tasks\n\n&gt; /sdd-tasks for the user-auth spec\n\n\nProduces grouped tasks (database layer, models, controllers, views, tests) with acceptance criteria.\n\nResume After a Break\n\n&gt; /sdd-status\n\n\nReads progress.yml and reports exactly where you left off — no context-replay needed.\n\nCapture Tribal Knowledge\n\n&gt; /sdd-discover-standards\n\n\nThe skill scans your codebase for recurring patterns (naming conventions, controller structure, test style) and writes them into sdd/standards/ so future specs align with your existing code.\n\n\n\nPackage Contents\n\nspec-driven-development/\n├── README.md                            # Full documentation\n├── QUICKSTART.md                        # Quick reference\n├── commands/                            # 6 slash commands\n│   ├── sdd-init.md\n│   ├── sdd-plan.md\n│   ├── sdd-shape.md\n│   ├── sdd-tasks.md\n│   ├── sdd-status.md\n│   └── sdd-discover-standards.md\n├── scripts/\n│   ├── init_sdd.sh                      # Bootstrap SDD structure\n│   ├── new_spec.sh                      # Create a new spec folder\n│   └── status.sh                        # Show progress\n├── templates/\n│   ├── standard-template.md             # Spec template\n│   └── progress.yml                     # Progress-tracking file\n└── skills/spec-driven-development/\n    ├── SKILL.md                         # Main skill (routes to the slash commands)\n    └── references/\n        ├── rails-standards.md           # Rails conventions\n        ├── hotwire-patterns.md          # Turbo/Stimulus patterns\n        └── document-templates.md        # Spec templates\n\n\n\n\nTeam Installation\n\nAdd to your project’s .claude/settings.json:\n\n{\n  \"extraKnownMarketplaces\": {\n    \"maquina\": {\n      \"source\": {\n        \"source\": \"github\",\n        \"repo\": \"maquina-app/rails-claude-code\"\n      }\n    }\n  },\n  \"enabledPlugins\": [\n    \"spec-driven-development@maquina\"\n  ]\n}\n\n\nCommit sdd/ alongside your code. Every teammate — human or AI — picks up the same plan, specs, and standards.\n\n\n\nNext Steps\n\n\n  \n    \n      GitHub Repository\n    \n    \n      View source code and contribute.\n    \n  \n\n  \n    \n      MVP Creator\n    \n    \n      Upstream handoff: go from idea to MVP documentation.\n    \n  \n\n  \n    \n      Maquina UI Standards\n    \n    \n      Build the views described in your specs with consistent components."
        },
        {
          "id": "documentation-components-alert",
          "title": "Alert",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/alert/",
          "content": "Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\nUsage\n\n&lt;%= render &quot;components/alert&quot;, icon: :info do %&gt;\n  &lt;%= render &quot;components/alert/title&quot;, text: &quot;Heads up!&quot; %&gt;\n  &lt;%= render &quot;components/alert/description&quot;, text: &quot;You can add components using the CLI.&quot; %&gt;\n&lt;% end %&gt;\n\nExamples\n\nDestructive\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/alert&quot;, variant: :destructive, icon: :triangle_alert do %&gt;\n  &lt;%= render &quot;components/alert/title&quot;, text: &quot;Error&quot; %&gt;\n  &lt;%= render &quot;components/alert/description&quot;, text: &quot;Your session has expired.&quot; %&gt;\n&lt;% end %&gt;\n\nSuccess\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/alert&quot;, variant: :success, icon: :check_circle do %&gt;\n  &lt;%= render &quot;components/alert/title&quot;, text: &quot;Success&quot; %&gt;\n  &lt;%= render &quot;components/alert/description&quot;, text: &quot;Your changes have been saved.&quot; %&gt;\n&lt;% end %&gt;\n\nInfo\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/alert&quot;, variant: :info, icon: :info do %&gt;\n  &lt;%= render &quot;components/alert/title&quot;, text: &quot;Heads up&quot; %&gt;\n  &lt;%= render &quot;components/alert/description&quot;, text: &quot;This release normalizes the default radius.&quot; %&gt;\n&lt;% end %&gt;\n\nWarning\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/alert&quot;, variant: :warning, icon: :triangle_alert do %&gt;\n  &lt;%= render &quot;components/alert/title&quot;, text: &quot;Warning&quot; %&gt;\n  &lt;%= render &quot;components/alert/description&quot;, text: &quot;This action cannot be undone.&quot; %&gt;\n&lt;% end %&gt;\n\nAPI Reference\n\nAlert\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      variant\n      Symbol\n      :default\n      :default, :destructive, :success, :warning, :info; :error is accepted as an alias of :destructive\n    \n    \n      icon\n      Symbol\n      nil\n      Icon name to display\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\nCustom icon markup\n\nicon: renders a built-in glyph as the alert's first child, which is what the variant icon colors key off. If you need your own markup instead — an inline SVG, an icon font, an &lt;img&gt;, or an icon that is not the first child — mark it with data-alert-part=&quot;icon&quot; and it picks up the same sizing and per-variant color:\n\n&lt;%= render &quot;components/alert&quot;, variant: :success do %&gt;\n  &lt;span data-alert-part=&quot;icon&quot;&gt;&lt;%= image_tag &quot;check.svg&quot; %&gt;&lt;/span&gt;\n  &lt;%= render &quot;components/alert/title&quot;, text: &quot;Saved&quot; %&gt;\n&lt;% end %&gt;\n\nPass data: { has_icon: true } alongside it so the alert reserves the left padding it normally adds for icon:.\n\n\nAlert Title\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      text\n      String\n      nil\n      Title text\n    \n    \n      content\n      String\n      nil\n      HTML content via capture\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nAlert Description\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      text\n      String\n      nil\n      Description text\n    \n    \n      content\n      String\n      nil\n      HTML content via capture\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes"
        },
        {
          "id": "documentation-components-badge",
          "title": "Badge",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/badge/",
          "content": "Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\nUsage\n\n&lt;%= render &quot;components/badge&quot; do %&gt;\n  Badge\n&lt;% end %&gt;\n\nExamples\n\nVariants\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/badge&quot;, variant: :primary do %&gt;Primary&lt;% end %&gt;\n&lt;%= render &quot;components/badge&quot;, variant: :secondary do %&gt;Secondary&lt;% end %&gt;\n&lt;%= render &quot;components/badge&quot;, variant: :destructive do %&gt;Destructive&lt;% end %&gt;\n&lt;%= render &quot;components/badge&quot;, variant: :success do %&gt;Success&lt;% end %&gt;\n&lt;%= render &quot;components/badge&quot;, variant: :warning do %&gt;Warning&lt;% end %&gt;\n&lt;%= render &quot;components/badge&quot;, variant: :info do %&gt;Info&lt;% end %&gt;\n&lt;%= render &quot;components/badge&quot;, variant: :outline do %&gt;Outline&lt;% end %&gt;\n\nSizes\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/badge&quot;, size: :sm do %&gt;Small&lt;% end %&gt;\n&lt;%= render &quot;components/badge&quot;, size: :md do %&gt;Medium&lt;% end %&gt;\n&lt;%= render &quot;components/badge&quot;, size: :lg do %&gt;Large&lt;% end %&gt;\n\nWith Icons\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/badge&quot;, variant: :success do %&gt;\n  &lt;%= icon_for :check, class: &quot;size-3&quot; %&gt;\n  Verified\n&lt;% end %&gt;\n\n&lt;%= render &quot;components/badge&quot;, variant: :warning do %&gt;\n  &lt;%= icon_for :clock, class: &quot;size-3&quot; %&gt;\n  Pending\n&lt;% end %&gt;\n\nAPI Reference\n\nBadge\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      variant\n      Symbol\n      :default\n      :default, :primary, :secondary, :destructive, :success, :warning, :info, :outline; :error is accepted as an alias of :destructive\n    \n    \n      size\n      Symbol\n      :md\n      :sm, :md, :lg; :default is accepted as an alias of :md\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes"
        },
        {
          "id": "documentation-components-breadcrumbs",
          "title": "Breadcrumbs",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/breadcrumbs/",
          "content": "Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\nUsage\n\n&lt;%= render &quot;components/breadcrumbs&quot; do %&gt;\n  &lt;%= render &quot;components/breadcrumbs/list&quot; do %&gt;\n    &lt;%= render &quot;components/breadcrumbs/item&quot; do %&gt;\n      &lt;%= render &quot;components/breadcrumbs/link&quot;, href: &quot;/&quot; do %&gt;Home&lt;% end %&gt;\n    &lt;% end %&gt;\n    &lt;%= render &quot;components/breadcrumbs/separator&quot; %&gt;\n    &lt;%= render &quot;components/breadcrumbs/item&quot; do %&gt;\n      &lt;%= render &quot;components/breadcrumbs/link&quot;, href: &quot;/components&quot; do %&gt;Components&lt;% end %&gt;\n    &lt;% end %&gt;\n    &lt;%= render &quot;components/breadcrumbs/separator&quot; %&gt;\n    &lt;%= render &quot;components/breadcrumbs/item&quot; do %&gt;\n      &lt;%= render &quot;components/breadcrumbs/page&quot; do %&gt;Breadcrumbs&lt;% end %&gt;\n    &lt;% end %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nUsing Helper\n\n&lt;%= breadcrumbs({&quot;Home&quot; =&gt; root_path, &quot;Users&quot; =&gt; users_path}, &quot;John Doe&quot;) %&gt;\n\nExamples\n\nWith Icons\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/breadcrumbs/link&quot;, href: &quot;/&quot; do %&gt;\n  &lt;%= icon_for(:home, class: &quot;size-4&quot;) %&gt;\n  Home\n&lt;% end %&gt;\n\nCustom Separators\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/breadcrumbs/separator&quot;, icon: :slash %&gt;\n&lt;%= render &quot;components/breadcrumbs/separator&quot;, icon: :arrow_right %&gt;\n\nWith Ellipsis\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/breadcrumbs/item&quot; do %&gt;\n  &lt;%= render &quot;components/breadcrumbs/ellipsis&quot; %&gt;\n&lt;% end %&gt;\n\nResponsive\n\n&lt;%= responsive_breadcrumbs(\n  {&quot;Home&quot; =&gt; &quot;/&quot;, &quot;Docs&quot; =&gt; &quot;/docs&quot;, &quot;Components&quot; =&gt; &quot;/components&quot;},\n  &quot;Breadcrumbs&quot;\n) %&gt;\n\nItems collapse only when they do not fit, and come back when they do. The controller measures the trail against its container and hides middle items one at a time, from the first one inward, until the row fits — so the ellipsis always stands for the items directly behind it. Widen the container and the hidden items return; there is no one-way collapse and no item-count threshold.\n\nThe container is what is measured, not the window, so a breadcrumb inside a collapsing sidebar or a resizing panel re-fits when that panel moves. If a single current-page title is too long to help by collapsing anything, it truncates with an ellipsis as a last resort.\n\nThe ellipsis dropdown\n\nWhen items are collapsed, the … becomes a button. Clicking it opens a menu listing the hidden items as links, so nothing in the trail becomes unreachable. It renders in the top layer as a popover — light dismiss and Escape work natively — and needs no markup from you beyond responsive_breadcrumbs.\n\nAPI Reference\n\nBreadcrumbs\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      responsive\n      Boolean\n      false\n      Collapse middle items when the trail does not fit its container, and restore them when it does\n    \n    \n      collapse_after\n      Integer\n      —\n      Deprecated, ignored. Removed in 0.8.0. It collapsed on item count without consulting available width, which also collapsed a trail with room to spare; space-based collapsing works now\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nBreadcrumbs List\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nBreadcrumbs Item\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nBreadcrumbs Link\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      href\n      String\n      required\n      Link destination\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nBreadcrumbs Page\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nBreadcrumbs Separator\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      icon\n      Symbol\n      :chevron_right\n      Icon name, or :custom to use block\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nBreadcrumbs Ellipsis\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes"
        },
        {
          "id": "documentation-components-calendar",
          "title": "Calendar",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/calendar/",
          "content": "Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\nUsage\n\n&lt;%= render &quot;components/calendar&quot; %&gt;\n\nWith Selected Date\n\n&lt;%= render &quot;components/calendar&quot;, selected: Date.today %&gt;\n\nExamples\n\nSingle Selection\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/calendar&quot;,\n      mode: :single,\n      selected: Date.today %&gt;\n\nRange Selection\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/calendar&quot;,\n      mode: :range,\n      selected: Date.today,\n      selected_end: Date.today + 5 %&gt;\n\nWith Date Constraints\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/calendar&quot;,\n      min_date: Date.today,\n      max_date: Date.today + 14 %&gt;\n\nWeek Starting Monday\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/calendar&quot;,\n      week_starts_on: :monday %&gt;\n\nForm Integration\n\n&lt;%= form_with model: @event do |f| %&gt;\n  &lt;%= render &quot;components/calendar&quot;,\n        selected: @event.date,\n        input_name: &quot;event[date]&quot; %&gt;\n&lt;% end %&gt;\n\nRange Form Integration\n\n&lt;%= render &quot;components/calendar&quot;,\n      mode: :range,\n      input_name: &quot;booking[check_in]&quot;,\n      input_name_end: &quot;booking[check_out]&quot; %&gt;\n\nAPI Reference\n\nCalendar\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      selected\n      Date, String\n      nil\n      Selected start date\n    \n    \n      selected_end\n      Date, String\n      nil\n      Selected end date (range mode)\n    \n    \n      month\n      Integer\n      nil\n      Display month (1-12)\n    \n    \n      year\n      Integer\n      nil\n      Display year\n    \n    \n      mode\n      Symbol\n      :single\n      :single or :range\n    \n    \n      min_date\n      Date, String\n      nil\n      Minimum selectable date\n    \n    \n      max_date\n      Date, String\n      nil\n      Maximum selectable date\n    \n    \n      disabled_dates\n      Array\n      []\n      Dates to disable\n    \n    \n      show_outside_days\n      Boolean\n      true\n      Show days from adjacent months\n    \n    \n      week_starts_on\n      Symbol\n      :sunday\n      :sunday or :monday\n    \n    \n      cell_size\n      String\n      nil\n      Custom cell size CSS value\n    \n    \n      input_name\n      String\n      nil\n      Hidden input name for forms\n    \n    \n      input_name_end\n      String\n      nil\n      End date hidden input name\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nCalendar Header\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      month\n      Integer\n      required\n      Display month\n    \n    \n      year\n      Integer\n      required\n      Display year\n    \n    \n      month_name\n      String\n      required\n      Formatted month name\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes"
        },
        {
          "id": "documentation-components-card",
          "title": "Card",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/card/",
          "content": "Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\nUsage\n\n&lt;%= render &quot;components/card&quot; do %&gt;\n  &lt;%= render &quot;components/card/header&quot; do %&gt;\n    &lt;%= render &quot;components/card/title&quot;, text: &quot;Card Title&quot; %&gt;\n    &lt;%= render &quot;components/card/description&quot;, text: &quot;Card description.&quot; %&gt;\n  &lt;% end %&gt;\n  &lt;%= render &quot;components/card/content&quot; do %&gt;\n    &lt;p&gt;Card content goes here.&lt;/p&gt;\n  &lt;% end %&gt;\n  &lt;%= render &quot;components/card/footer&quot; do %&gt;\n    &lt;button data-component=&quot;button&quot; data-variant=&quot;primary&quot;&gt;Save&lt;/button&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nExamples\n\nSimple Card\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/card&quot; do %&gt;\n  &lt;%= render &quot;components/card/content&quot;, spacing: :full do %&gt;\n    &lt;p&gt;A simple card with just content.&lt;/p&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nWith Header Action\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/card&quot; do %&gt;\n  &lt;%= render &quot;components/card/header&quot;, layout: :row do %&gt;\n    &lt;div&gt;\n      &lt;%= render &quot;components/card/title&quot;, text: &quot;Team Members&quot; %&gt;\n      &lt;%= render &quot;components/card/description&quot;, text: &quot;Manage your team.&quot; %&gt;\n    &lt;/div&gt;\n    &lt;%= render &quot;components/card/action&quot; do %&gt;\n      &lt;button data-component=&quot;button&quot; data-variant=&quot;primary&quot; data-size=&quot;sm&quot;&gt;Add&lt;/button&gt;\n    &lt;% end %&gt;\n  &lt;% end %&gt;\n  &lt;%= render &quot;components/card/content&quot; do %&gt;\n    &lt;p class=&quot;text-sm text-muted-foreground&quot;&gt;No members yet.&lt;/p&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nWith Footer\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/card&quot; do %&gt;\n  &lt;%= render &quot;components/card/header&quot; do %&gt;\n    &lt;%= render &quot;components/card/title&quot;, text: &quot;Settings&quot; %&gt;\n  &lt;% end %&gt;\n  &lt;%= render &quot;components/card/content&quot; do %&gt;\n    &lt;p&gt;Configure your preferences.&lt;/p&gt;\n  &lt;% end %&gt;\n  &lt;%= render &quot;components/card/footer&quot;, align: :end do %&gt;\n    &lt;button data-component=&quot;button&quot; data-variant=&quot;outline&quot;&gt;Cancel&lt;/button&gt;\n    &lt;button data-component=&quot;button&quot; data-variant=&quot;primary&quot;&gt;Save&lt;/button&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nAPI Reference\n\nCard\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nCard Header\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      layout\n      Symbol\n      :column\n      :column or :row\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nCard Title\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      text\n      String\n      nil\n      Title text\n    \n    \n      content\n      String\n      nil\n      HTML content via capture\n    \n    \n      size\n      Symbol\n      :default\n      :default or :sm\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nCard Description\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      text\n      String\n      nil\n      Description text\n    \n    \n      content\n      String\n      nil\n      HTML content via capture\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nCard Action\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nCard Content\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      spacing\n      Symbol\n      :default\n      :default or :full (when no header)\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nCard Footer\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      align\n      Symbol\n      :start\n      :start, :center, :end, :between\n    \n    \n      spacing\n      Symbol\n      :default\n      :default or :full (when no content)\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes"
        },
        {
          "id": "documentation-components-combobox",
          "title": "Combobox",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/combobox/",
          "content": "Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\nThe trigger renders its own up/down chevron, and the search field inside the popover carries a focus ring like any other form control.\n\nUsage\n\n&lt;%= render &quot;components/combobox&quot;, placeholder: &quot;Select...&quot; do |combobox_id| %&gt;\n  &lt;%= render &quot;components/combobox/trigger&quot;, for_id: combobox_id, placeholder: &quot;Select...&quot; %&gt;\n\n  &lt;%= render &quot;components/combobox/content&quot;, id: combobox_id do %&gt;\n    &lt;%= render &quot;components/combobox/input&quot;, placeholder: &quot;Search...&quot; %&gt;\n\n    &lt;%= render &quot;components/combobox/list&quot; do %&gt;\n      &lt;%= render &quot;components/combobox/option&quot;, value: &quot;one&quot; do %&gt;Option One&lt;% end %&gt;\n      &lt;%= render &quot;components/combobox/option&quot;, value: &quot;two&quot; do %&gt;Option Two&lt;% end %&gt;\n    &lt;% end %&gt;\n\n    &lt;%= render &quot;components/combobox/empty&quot; %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nExamples\n\nWith Selection\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/combobox/option&quot;, value: &quot;active&quot;, selected: true do %&gt;Active&lt;% end %&gt;\n&lt;%= render &quot;components/combobox/option&quot;, value: &quot;archived&quot;, disabled: true do %&gt;Archived&lt;% end %&gt;\n\nWith Groups\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/combobox/list&quot; do %&gt;\n  &lt;%= render &quot;components/combobox/group&quot; do %&gt;\n    &lt;%= render &quot;components/combobox/label&quot;, text: &quot;Backend&quot; %&gt;\n    &lt;%= render &quot;components/combobox/option&quot;, value: &quot;ruby&quot; do %&gt;Ruby&lt;% end %&gt;\n  &lt;% end %&gt;\n\n  &lt;%= render &quot;components/combobox/separator&quot; %&gt;\n\n  &lt;%= render &quot;components/combobox/group&quot; do %&gt;\n    &lt;%= render &quot;components/combobox/label&quot;, text: &quot;Frontend&quot; %&gt;\n    &lt;%= render &quot;components/combobox/option&quot;, value: &quot;js&quot; do %&gt;JavaScript&lt;% end %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nAPI Reference\n\nCombobox\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      id\n      String\n      nil\n      Custom ID; defaults to a deterministic id derived from the input name\n    \n    \n      name\n      String\n      nil\n      Form input name\n    \n    \n      value\n      String\n      nil\n      Pre-selected value\n    \n    \n      placeholder\n      String\n      \"Select...\"\n      Placeholder text\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nCombobox Trigger\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      for_id\n      String\n      required\n      ID of content popover\n    \n    \n      placeholder\n      String\n      \"Select...\"\n      Placeholder text\n    \n    \n      variant\n      Symbol\n      :outline\n      Button variant\n    \n    \n      size\n      Symbol\n      :default\n      Button size\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nCombobox Content\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      id\n      String\n      required\n      Popover ID\n    \n    \n      align\n      Symbol\n      :start\n      :start, :center, :end\n    \n    \n      width\n      Symbol\n      :default\n      :sm, :default, :md, :lg, :full\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nCombobox Input\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      placeholder\n      String\n      \"Search...\"\n      Search placeholder\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nCombobox Option\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      value\n      String\n      required\n      Option value\n    \n    \n      selected\n      Boolean\n      false\n      Whether selected\n    \n    \n      disabled\n      Boolean\n      false\n      Whether disabled\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nCombobox Empty\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      text\n      String\n      \"No results found.\"\n      Empty state message\n    \n    \n      content\n      String\n      nil\n      Captured HTML via capture, or use block\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nCombobox Group\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nCombobox Label\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      text\n      String\n      nil\n      Label text\n    \n    \n      content\n      String\n      nil\n      HTML content via capture\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nCombobox List\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\nBuilder Helper\n\nThe combobox helper wires the trigger and content ids together for you:\n\n&lt;%= combobox placeholder: &quot;Select framework...&quot;, name: &quot;framework&quot; do |cb| %&gt;\n  &lt;% cb.trigger %&gt;\n  &lt;% cb.content do %&gt;\n    &lt;% cb.input placeholder: &quot;Search...&quot; %&gt;\n    &lt;% cb.list do %&gt;\n      &lt;% cb.group do %&gt;\n        &lt;% cb.label &quot;Frontend&quot; %&gt;\n        &lt;% cb.option value: &quot;react&quot; do %&gt;React&lt;% end %&gt;\n        &lt;% cb.option value: &quot;vue&quot; do %&gt;Vue&lt;% end %&gt;\n      &lt;% end %&gt;\n      &lt;% cb.separator %&gt;\n      &lt;% cb.option value: &quot;rails&quot; do %&gt;Rails&lt;% end %&gt;\n    &lt;% end %&gt;\n    &lt;% cb.empty %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nFor flat option lists, combobox_simple renders everything from data:\n\n&lt;%= combobox_simple placeholder: &quot;Select framework...&quot;,\n      name: &quot;framework&quot;,\n      options: [\n        { value: &quot;nextjs&quot;, label: &quot;Next.js&quot; },\n        { value: &quot;remix&quot;, label: &quot;Remix&quot; }\n      ] %&gt;"
        },
        {
          "id": "documentation-components-date-picker",
          "title": "Date Picker",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/date-picker/",
          "content": "Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\nUsage\n\n&lt;%= render &quot;components/date_picker&quot;,\n      mode: :single,\n      placeholder: &quot;Select a date&quot;,\n      input_name: &quot;event_date&quot; %&gt;\n\nExamples\n\nRange Selection\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/date_picker&quot;,\n      mode: :range,\n      placeholder: &quot;Select date range&quot;,\n      input_name: &quot;start_date&quot;,\n      input_name_end: &quot;end_date&quot; %&gt;\n\nWith Pre-selected Date\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/date_picker&quot;,\n      mode: :single,\n      selected: Date.today,\n      input_name: &quot;event_date&quot; %&gt;\n\nWith Date Constraints\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/date_picker&quot;,\n      min_date: Date.today,\n      max_date: Date.today + 30,\n      placeholder: &quot;Select within 30 days&quot; %&gt;\n\nDisabled\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/date_picker&quot;,\n      selected: Date.today,\n      disabled: true %&gt;\n\nAPI Reference\n\nDate Picker\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      selected\n      Date, String\n      nil\n      Pre-selected date\n    \n    \n      selected_end\n      Date, String\n      nil\n      End date for range mode\n    \n    \n      mode\n      Symbol\n      :single\n      :single or :range\n    \n    \n      min_date\n      Date, String\n      nil\n      Minimum selectable date\n    \n    \n      max_date\n      Date, String\n      nil\n      Maximum selectable date\n    \n    \n      disabled_dates\n      Array\n      []\n      Array of dates to disable\n    \n    \n      show_outside_days\n      Boolean\n      true\n      Show days from adjacent months\n    \n    \n      week_starts_on\n      Symbol\n      :sunday\n      :sunday or :monday\n    \n    \n      placeholder\n      String\n      nil\n      Placeholder text\n    \n    \n      input_name\n      String\n      nil\n      Name for hidden form input\n    \n    \n      input_name_end\n      String\n      nil\n      End date input name (range mode)\n    \n    \n      id\n      String\n      nil\n      Custom ID; defaults to a deterministic id derived from the input name\n    \n    \n      disabled\n      Boolean\n      false\n      Whether disabled\n    \n    \n      required\n      Boolean\n      false\n      Mark input as required\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nTurbo Drive\n\nThe date picker controller automatically closes the popover before Turbo caches the page. No configuration is needed — pressing the browser back button will always show the date picker in its closed state."
        },
        {
          "id": "documentation-components-drawer",
          "title": "Drawer",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/drawer/",
          "content": "Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\nUsage\n\n&lt;%= render &quot;components/drawer/provider&quot;, default_open: drawer_open? do %&gt;\n  &lt;%= render &quot;components/drawer&quot;, state: drawer_state do %&gt;\n    &lt;%= render &quot;components/drawer/header&quot; do %&gt;\n      &lt;%= render &quot;components/drawer/title&quot;, text: &quot;Drawer Title&quot; %&gt;\n    &lt;% end %&gt;\n\n    &lt;%= render &quot;components/drawer/content&quot; do %&gt;\n      Drawer content\n    &lt;% end %&gt;\n\n    &lt;%= render &quot;components/drawer/footer&quot; do %&gt;\n      Footer actions\n    &lt;% end %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nExamples\n\nWith Trigger\n\nThe trigger can live anywhere on the page — it finds the drawer through a Stimulus outlet and mirrors its state with aria-expanded.\n\n&lt;%= render &quot;components/drawer/trigger&quot; do %&gt;Open Drawer&lt;% end %&gt;\n\n&lt;%= render &quot;components/drawer/provider&quot; do %&gt;\n  &lt;%= render &quot;components/drawer&quot; do %&gt;\n    &lt;!-- content --&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nSections and Separators\n\nGroup the drawer body into stacked sections, divided by a separator.\n\n&lt;%= render &quot;components/drawer/content&quot; do %&gt;\n  &lt;%= render &quot;components/drawer/section&quot; do %&gt;\n    &lt;%= render &quot;components/drawer/title&quot;, text: &quot;Filters&quot;, tag: :h3 %&gt;\n  &lt;% end %&gt;\n\n  &lt;%= render &quot;components/drawer/separator&quot; %&gt;\n\n  &lt;%= render &quot;components/drawer/section&quot; do %&gt;\n    &lt;%# More rows %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\ndrawer/separator renders the separator primitive, so it keeps the primitive's 1px track while the drawer part re-spaces it for the panel.\n\nLeft Side Drawer\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/drawer&quot;, side: :left do %&gt;\n  &lt;!-- content --&gt;\n&lt;% end %&gt;\n\nKeyboard &amp; Accessibility\n\n\n  Cmd/Ctrl + D toggles the drawer (configurable via keyboard_shortcut).\n  Escape closes it; clicking the backdrop closes it.\n  The panel is a role=&quot;dialog&quot; with aria-modal and a configurable aria_label. Focus moves into the panel on open and returns to the previously focused element on close.\n  While closed, the panel is aria-hidden and inert, so the off-screen content is invisible to assistive technology and unreachable by keyboard.\n\n\nAPI Reference\n\nProvider\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      id\n      String\n      \"drawer-provider\"\n      Element ID for stable morph matching\n    \n    \n      default_open\n      Boolean\n      false\n      Initial open state\n    \n    \n      cookie_name\n      String\n      \"drawer_state\"\n      Cookie for persistence\n    \n    \n      keyboard_shortcut\n      String\n      \"d\"\n      Toggle shortcut (Cmd/Ctrl + key)\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\nDrawer\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      id\n      String\n      auto\n      Element ID (deterministic, derived from side)\n    \n    \n      state\n      Symbol\n      :closed\n      :open or :closed\n    \n    \n      side\n      Symbol\n      :right\n      :left or :right\n    \n    \n      aria_label\n      String\n      \"Drawer\"\n      Accessible name for the dialog panel\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\nTrigger\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      icon_name\n      Symbol\n      nil\n      Optional icon for the toggle button\n    \n    \n      variant\n      Symbol\n      :default\n      Button variant\n    \n    \n      size\n      Symbol\n      :default\n      Button size\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\nOther Parts\n\n\n  \n    \n      Partial\n      Description\n    \n  \n  \n    \n      drawer/header\n      Top section with title and built-in close button\n    \n    \n      drawer/title\n      Heading inside the header. text: / content:, tag: (default :h2)\n    \n    \n      drawer/description\n      Supporting line under the title. text: / content:, tag: (default :p)\n    \n    \n      drawer/content\n      Scrollable middle section\n    \n    \n      drawer/footer\n      Bottom section for actions\n    \n    \n      drawer/section\n      Groups related rows inside the content area. Container — pass a block\n    \n    \n      drawer/separator\n      Divider between sections. orientation: (default :horizontal)\n    \n    \n      drawer/close\n      Close button (X icon)\n    \n  \n\n\nHelper Methods\n\n\n  \n    \n      Method\n      Description\n    \n  \n  \n    \n      drawer_state(cookie_name)\n      Returns :open or :closed from the cookie\n    \n    \n      drawer_open?(cookie_name)\n      Returns true if open\n    \n    \n      drawer_closed?(cookie_name)\n      Returns true if closed\n    \n  \n\n\nTurbo Drive\n\nThe drawer controller integrates with Turbo Drive to keep state correct across navigations:\n\n\n  Cache teardown: the drawer closes and the backdrop hides before Turbo caches the page.\n  Morph awareness: with turbo_refresh_method_tag :morph, the drawer re-reads its cookie so client state survives the morph.\n  Persistence: open/closed state lives in a cookie and survives full page loads."
        },
        {
          "id": "documentation-components-dropdown-menu",
          "title": "Dropdown Menu",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/dropdown-menu/",
          "content": "Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\nUsage\n\n&lt;%= render &quot;components/dropdown_menu&quot; do %&gt;\n  &lt;%= render &quot;components/dropdown_menu/trigger&quot; do %&gt;Open Menu&lt;% end %&gt;\n\n  &lt;%= render &quot;components/dropdown_menu/content&quot; do %&gt;\n    &lt;%= render &quot;components/dropdown_menu/item&quot;, href: &quot;#&quot; do %&gt;Profile&lt;% end %&gt;\n    &lt;%= render &quot;components/dropdown_menu/item&quot;, href: &quot;#&quot; do %&gt;Settings&lt;% end %&gt;\n    &lt;%= render &quot;components/dropdown_menu/separator&quot; %&gt;\n    &lt;%= render &quot;components/dropdown_menu/item&quot;, href: &quot;#&quot; do %&gt;Logout&lt;% end %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nExamples\n\nWith Icons\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/dropdown_menu/item&quot;, href: &quot;#&quot; do %&gt;\n  &lt;%= icon_for :user, class: &quot;size-4&quot; %&gt;\n  Profile\n&lt;% end %&gt;\n&lt;%= render &quot;components/dropdown_menu/item&quot;, href: &quot;#&quot;, variant: :destructive do %&gt;\n  &lt;%= icon_for :log_out, class: &quot;size-4&quot; %&gt;\n  Logout\n&lt;% end %&gt;\n\nWith Shortcuts\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/dropdown_menu/item&quot;, href: &quot;#&quot; do %&gt;\n  Undo\n  &lt;%= render &quot;components/dropdown_menu/shortcut&quot; do %&gt;⌘Z&lt;% end %&gt;\n&lt;% end %&gt;\n\nIcon Trigger\n\nThe default trigger renders its own chevron, which rotates 180° while the menu is open. Reach for as_child when you need different content — an icon-only button, an sr-only label — not merely to get an affordance. Note that as_child hands you the whole button: data-dropdown-menu-target=\"trigger\", data-action=\"dropdown-menu#toggle\", aria-haspopup and aria-expanded are all yours to write. The controller updates aria-expanded at runtime, but only if the attribute is there to begin with.\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/dropdown_menu/trigger&quot;, as_child: true do %&gt;\n  &lt;button type=&quot;button&quot;\n          data-component=&quot;button&quot;\n          data-variant=&quot;ghost&quot;\n          data-size=&quot;icon&quot;\n          data-dropdown-menu-target=&quot;trigger&quot;\n          data-action=&quot;dropdown-menu#toggle&quot;\n          aria-haspopup=&quot;menu&quot;\n          aria-expanded=&quot;false&quot;&gt;\n    &lt;%= icon_for :more_horizontal, class: &quot;size-4&quot; %&gt;\n  &lt;/button&gt;\n&lt;% end %&gt;\n\nAPI Reference\n\nDropdown Menu\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nDropdown Menu Trigger\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      variant\n      Symbol\n      :outline\n      Button variant when as_child is false\n    \n    \n      size\n      Symbol\n      :default\n      Button size when as_child is false\n    \n    \n      as_child\n      Boolean\n      false\n      Use custom trigger markup\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nDropdown Menu Content\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      align\n      Symbol\n      :start\n      :start, :center, :end\n    \n    \n      side\n      Symbol\n      :bottom\n      :top, :bottom, :left, :right\n    \n    \n      width\n      Symbol\n      :default\n      :default, :sm, :md, :lg\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nDropdown Menu Item\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      href\n      String\n      nil\n      URL, renders link if provided\n    \n    \n      method\n      Symbol\n      nil\n      HTTP method (:delete, :post, etc.)\n    \n    \n      variant\n      Symbol\n      :default\n      :default or :destructive\n    \n    \n      disabled\n      Boolean\n      false\n      Whether disabled\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nDropdown Menu Label\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      text\n      String\n      nil\n      Label text\n    \n    \n      content\n      String\n      nil\n      Captured HTML via capture, or use block\n    \n    \n      inset\n      Boolean\n      false\n      Align with icon items\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nDropdown Menu Separator\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nDropdown Menu Group\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nDropdown Menu Shortcut\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      text\n      String\n      nil\n      Shortcut text\n    \n    \n      content\n      String\n      nil\n      Captured HTML via capture, or use block\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\nBuilder Helper\n\nThe dropdown_menu helper builds the whole menu — trigger, content, items, separators, and shortcuts — without composing partials by hand:\n\n&lt;%= dropdown_menu do |menu| %&gt;\n  &lt;% menu.trigger do %&gt;\n    &lt;%= icon_for :more_horizontal %&gt;\n  &lt;% end %&gt;\n  &lt;% menu.content align: :end, width: :md do %&gt;\n    &lt;% menu.label &quot;Actions&quot; %&gt;\n    &lt;% menu.item &quot;Edit&quot;, href: edit_path, icon: :pencil do |item| %&gt;\n      &lt;% item.shortcut &quot;⌘E&quot; %&gt;\n    &lt;% end %&gt;\n    &lt;% menu.separator %&gt;\n    &lt;% menu.item &quot;Delete&quot;, href: delete_path, method: :delete, variant: :destructive, icon: :trash %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nFor data-driven menus, dropdown_menu_simple renders trigger and items from a list:\n\n&lt;%= dropdown_menu_simple &quot;Actions&quot;, items: [\n  { label: &quot;Edit&quot;, href: edit_path, icon: :pencil },\n  { label: &quot;Delete&quot;, href: delete_path, method: :delete, destructive: true }\n] %&gt;\n\n\n  \n    \n      Builder Method\n      Description\n    \n  \n  \n    \n      menu.trigger(variant:, size:, as_child:, &amp;block)\n      Renders the trigger button\n    \n    \n      menu.content(align:, side:, width:, &amp;block)\n      Positioned menu container\n    \n    \n      menu.item(label, href:, method:, icon:, variant:, disabled:, &amp;block)\n      Menu item; yields an item builder for shortcut(text)\n    \n    \n      menu.label(text, inset:)\n      Section heading\n    \n    \n      menu.separator / menu.group(&amp;block)\n      Divider / logical grouping"
        },
        {
          "id": "documentation-components-empty",
          "title": "Empty",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/empty/",
          "content": "Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\nUsage\n\n&lt;%= render &quot;components/empty&quot; do %&gt;\n  &lt;%= render &quot;components/empty/header&quot; do %&gt;\n    &lt;%= render &quot;components/empty/media&quot;, icon: :inbox %&gt;\n    &lt;%= render &quot;components/empty/title&quot;, text: &quot;No messages&quot; %&gt;\n    &lt;%= render &quot;components/empty/description&quot;, text: &quot;Messages you receive will appear here.&quot; %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nExamples\n\nWith Action\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/empty&quot; do %&gt;\n  &lt;%= render &quot;components/empty/header&quot; do %&gt;\n    &lt;%= render &quot;components/empty/media&quot;, icon: :folder %&gt;\n    &lt;%= render &quot;components/empty/title&quot;, text: &quot;No projects yet&quot; %&gt;\n    &lt;%= render &quot;components/empty/description&quot;, text: &quot;Get started by creating your first project.&quot; %&gt;\n  &lt;% end %&gt;\n  &lt;%= render &quot;components/empty/content&quot; do %&gt;\n    &lt;button data-component=&quot;button&quot; data-variant=&quot;primary&quot;&gt;Create project&lt;/button&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nOutline Variant\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/empty&quot;, variant: :outline do %&gt;\n  &lt;%= render &quot;components/empty/header&quot; do %&gt;\n    &lt;%= render &quot;components/empty/media&quot;, icon: :upload %&gt;\n    &lt;%= render &quot;components/empty/title&quot;, text: &quot;Drop files here&quot; %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nCompact Size\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/empty&quot;, size: :compact do %&gt;\n  &lt;%= render &quot;components/empty/header&quot; do %&gt;\n    &lt;%= render &quot;components/empty/media&quot;, icon: :search %&gt;\n    &lt;%= render &quot;components/empty/title&quot;, text: &quot;No results found&quot; %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nAPI Reference\n\nEmpty\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      variant\n      Symbol\n      :default\n      :default or :outline\n    \n    \n      size\n      Symbol\n      :default\n      :default or :compact\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nEmpty Header\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nEmpty Media\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      icon\n      Symbol\n      nil\n      Icon name\n    \n    \n      content\n      String\n      nil\n      Captured HTML via capture, or use block\n    \n    \n      variant\n      Symbol\n      :icon\n      :icon or :avatar\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nEmpty Title\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      text\n      String\n      nil\n      Title text\n    \n    \n      content\n      String\n      nil\n      Captured HTML via capture, or use block\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nEmpty Description\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      text\n      String\n      nil\n      Description text\n    \n    \n      content\n      String\n      nil\n      Captured HTML via capture, or use block\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nEmpty Content\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\nHelper Methods\n\nThree helpers cover the common empty-state patterns without composing partials:\n\n&lt;%= empty_state title: &quot;No documents&quot;, description: &quot;Create your first document.&quot;, icon: :folder do %&gt;\n  &lt;%= link_to &quot;New document&quot;, new_document_path, data: { component: &quot;button&quot;, variant: &quot;primary&quot; } %&gt;\n&lt;% end %&gt;\n\n&lt;%= empty_search_state query: params[:q], reset_path: documents_path %&gt;\n\n&lt;%= empty_list_state resource_name: &quot;project&quot;, new_path: new_project_path %&gt;\n\n\n  \n    \n      Method\n      Description\n    \n  \n  \n    \n      empty_state(title:, description:, icon:, variant:, size:, &amp;block)\n      General empty state; block renders action content\n    \n    \n      empty_search_state(query:, reset_path:, size:)\n      No-results state for searches, with optional reset link\n    \n    \n      empty_list_state(resource_name:, new_path:, icon:, size:)\n      First-run state for empty collections, with optional create link"
        },
        {
          "id": "documentation-components-form",
          "title": "Form",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/form/",
          "content": "Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\nUsage\n\n&lt;%= form_with model: @user, data: { component: &quot;form&quot; } do |f| %&gt;\n  &lt;div data-form-part=&quot;group&quot;&gt;\n    &lt;%= f.label :email, data: { component: &quot;label&quot; } %&gt;\n    &lt;%= f.email_field :email, data: { component: &quot;input&quot; }, placeholder: &quot;you@example.com&quot; %&gt;\n  &lt;/div&gt;\n\n  &lt;%= f.submit &quot;Sign in&quot;, data: { component: &quot;button&quot;, variant: &quot;primary&quot; } %&gt;\n&lt;% end %&gt;\n\nExamples\n\nInput\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= f.text_field :name, data: { component: &quot;input&quot; }, placeholder: &quot;Full name&quot; %&gt;\n&lt;%= f.text_field :name, data: { component: &quot;input&quot;, size: &quot;sm&quot; } %&gt;\n&lt;%= f.text_field :name, data: { component: &quot;input&quot;, size: &quot;lg&quot; } %&gt;\n\nTextarea\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= f.text_area :bio, data: { component: &quot;textarea&quot; }, rows: 4 %&gt;\n\nSelect\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= f.select :country, options, {}, data: { component: &quot;select&quot; } %&gt;\n\nCheckbox\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;label class=&quot;flex items-center gap-2&quot;&gt;\n  &lt;%= f.check_box :terms, data: { component: &quot;checkbox&quot; } %&gt;\n  &lt;span class=&quot;text-sm&quot;&gt;Accept terms&lt;/span&gt;\n&lt;/label&gt;\n\nRadio\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;label class=&quot;flex items-center gap-2&quot;&gt;\n  &lt;%= f.radio_button :plan, &quot;pro&quot;, data: { component: &quot;radio&quot; } %&gt;\n  &lt;span class=&quot;text-sm&quot;&gt;Pro&lt;/span&gt;\n&lt;/label&gt;\n\nSwitch\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;label class=&quot;flex items-center gap-3&quot;&gt;\n  &lt;%= f.check_box :notifications, data: { component: &quot;switch&quot; } %&gt;\n  &lt;span class=&quot;text-sm&quot;&gt;Enable notifications&lt;/span&gt;\n&lt;/label&gt;\n\nButton\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;button data-component=&quot;button&quot; data-variant=&quot;primary&quot;&gt;Primary&lt;/button&gt;\n&lt;button data-component=&quot;button&quot; data-variant=&quot;secondary&quot;&gt;Secondary&lt;/button&gt;\n&lt;button data-component=&quot;button&quot; data-variant=&quot;destructive&quot;&gt;Destructive&lt;/button&gt;\n&lt;button data-component=&quot;button&quot; data-variant=&quot;outline&quot;&gt;Outline&lt;/button&gt;\n&lt;button data-component=&quot;button&quot; data-variant=&quot;ghost&quot;&gt;Ghost&lt;/button&gt;\n&lt;button data-component=&quot;button&quot; data-variant=&quot;link&quot;&gt;Link&lt;/button&gt;\n\nAPI Reference\n\nForm Container\n\n\n  \n    \n      Attribute\n      Description\n    \n  \n  \n    \n      data-component=\"form\"\n      Grid layout with gap\n    \n    \n      data-form-part=\"group\"\n      Field group container\n    \n    \n      data-form-part=\"description\"\n      Help text styling\n    \n    \n      data-form-part=\"error\"\n      Error message styling\n    \n    \n      data-form-part=\"actions\"\n      Submit area container\n    \n  \n\n\n\nInput\n\n\n  \n    \n      Attribute\n      Values\n      Description\n    \n  \n  \n    \n      data-component\n      input\n      Text input styling\n    \n    \n      data-size\n      sm, lg\n      Size variant\n    \n  \n\n\n\nTextarea\n\n\n  \n    \n      Attribute\n      Values\n      Description\n    \n  \n  \n    \n      data-component\n      textarea\n      Textarea styling\n    \n  \n\n\n\nSelect\n\n\n  \n    \n      Attribute\n      Values\n      Description\n    \n  \n  \n    \n      data-component\n      select\n      Native select styling\n    \n  \n\n\n\nCheckbox\n\n\n  \n    \n      Attribute\n      Values\n      Description\n    \n  \n  \n    \n      data-component\n      checkbox\n      Checkbox styling\n    \n  \n\n\n\nRadio\n\n\n  \n    \n      Attribute\n      Values\n      Description\n    \n  \n  \n    \n      data-component\n      radio\n      Radio button styling\n    \n  \n\n\n\nSwitch\n\n\n  \n    \n      Attribute\n      Values\n      Description\n    \n  \n  \n    \n      data-component\n      switch\n      Toggle switch styling\n    \n  \n\n\n\nLabel\n\n\n  \n    \n      Attribute\n      Values\n      Description\n    \n  \n  \n    \n      data-component\n      label\n      Label styling\n    \n    \n      data-required\n      (presence)\n      Shows required indicator\n    \n  \n\n\n\nButton\n\n\n  \n    \n      Attribute\n      Values\n      Description\n    \n  \n  \n    \n      data-component\n      button\n      Button styling\n    \n    \n      data-variant\n      primary, secondary, destructive, outline, ghost, link\n      Visual style\n    \n    \n      data-size\n      sm, lg, icon, icon-sm, icon-lg\n      Size variant"
        },
        {
          "id": "documentation-components-header",
          "title": "Header",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/header/",
          "content": "Quick Reference\n\nParameters\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      **html_options\n      Hash\n      {}\n      HTML attributes (id:, data:, etc.)\n    \n  \n\n\nData Attributes\n\nComponent Identifier\n\n\n  \n    \n      Attribute\n      Element\n      Description\n    \n  \n  \n    \n      data-component=\"header\"\n      &lt;header&gt;\n      Main component identifier\n    \n  \n\n\n\n\nBasic Usage\n\n&lt;%= render \"components/header\" do %&gt;\n  &lt;%= render \"components/sidebar/trigger\" %&gt;\n  &lt;%= render \"components/separator\", orientation: :vertical %&gt;\n  &lt;%= breadcrumbs({\"Dashboard\" =&gt; dashboard_path}, @page_title) %&gt;\n&lt;% end %&gt;\n\n\n\n\nExamples\n\nWith Breadcrumbs\n\n&lt;%= render \"components/header\" do %&gt;\n  &lt;%= render \"components/sidebar/trigger\" %&gt;\n  &lt;%= render \"components/separator\", orientation: :vertical %&gt;\n  &lt;%= breadcrumbs(\n    {\"Dashboard\" =&gt; dashboard_path, \"Users\" =&gt; users_path},\n    \"John Doe\"\n  ) %&gt;\n&lt;% end %&gt;\n\n\nWith Actions\n\n&lt;%= render \"components/header\" do %&gt;\n  &lt;%= render \"components/sidebar/trigger\" %&gt;\n  &lt;%= render \"components/separator\", orientation: :vertical %&gt;\n  &lt;%= breadcrumbs({\"Projects\" =&gt; projects_path}, @project.name) %&gt;\n  \n  &lt;div class=\"ml-auto flex items-center gap-2\"&gt;\n    &lt;%= link_to \"Edit\", edit_project_path(@project), data: { component: \"button\", variant: \"outline\", size: \"sm\" } %&gt;\n    &lt;%= link_to \"Delete\", project_path(@project), data: { component: \"button\", variant: \"destructive\", size: \"sm\" }, method: :delete %&gt;\n  &lt;/div&gt;\n&lt;% end %&gt;\n\n\nWith Search\n\n&lt;%= render \"components/header\" do %&gt;\n  &lt;%= render \"components/sidebar/trigger\" %&gt;\n  &lt;%= render \"components/separator\", orientation: :vertical %&gt;\n  \n  &lt;div class=\"flex-1 max-w-md\"&gt;\n    &lt;%= form_with url: search_path, method: :get, class: \"relative\" do |f| %&gt;\n      &lt;%= icon_for :search, class: \"absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground\" %&gt;\n      &lt;%= f.search_field :q, data: { component: \"input\" }, class: \"pl-10 h-8\", placeholder: \"Search...\" %&gt;\n    &lt;% end %&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=\"ml-auto flex items-center gap-2\"&gt;\n    &lt;%= render \"components/dropdown_menu\" do %&gt;\n    &lt;% end %&gt;\n  &lt;/div&gt;\n&lt;% end %&gt;\n\n\nSimple Page Title\n\n&lt;%= render \"components/header\" do %&gt;\n  &lt;%= render \"components/sidebar/trigger\" %&gt;\n  &lt;%= render \"components/separator\", orientation: :vertical %&gt;\n  &lt;h1 class=\"text-sm font-medium\"&gt;Dashboard&lt;/h1&gt;\n&lt;% end %&gt;\n\n\n\n\nReal-World Patterns\n\nStandard App Header\n\n&lt;%= render \"components/header\" do %&gt;\n  &lt;%= render \"components/sidebar/trigger\" %&gt;\n  &lt;%= render \"components/separator\", orientation: :vertical %&gt;\n  \n  &lt;%= responsive_breadcrumbs(@breadcrumb_links, @breadcrumb_current) %&gt;\n  \n  &lt;div class=\"ml-auto flex items-center gap-3\"&gt;\n    &lt;button type=\"button\" data-component=\"button\" data-variant=\"ghost\" data-size=\"icon-sm\" class=\"relative\"&gt;\n      &lt;%= icon_for :bell, class: \"size-4\" %&gt;\n      &lt;span class=\"absolute -top-1 -right-1 size-4 rounded-full bg-destructive text-destructive-foreground text-xs flex items-center justify-center\"&gt;3&lt;/span&gt;\n    &lt;/button&gt;\n    \n    &lt;%= dropdown_menu do |menu| %&gt;\n      &lt;% menu.trigger variant: :ghost, size: :sm do %&gt;\n        &lt;%= image_tag current_user.avatar, class: \"size-6 rounded-full\" %&gt;\n      &lt;% end %&gt;\n      &lt;% menu.content align: :end do %&gt;\n        &lt;% menu.label { current_user.name } %&gt;\n        &lt;% menu.separator %&gt;\n        &lt;% menu.item \"Profile\", href: profile_path, icon: :user %&gt;\n        &lt;% menu.item \"Settings\", href: settings_path, icon: :settings %&gt;\n        &lt;% menu.separator %&gt;\n        &lt;% menu.item \"Logout\", href: logout_path, method: :delete, icon: :log_out %&gt;\n      &lt;% end %&gt;\n    &lt;% end %&gt;\n  &lt;/div&gt;\n&lt;% end %&gt;\n\n\nWith Tabs\n\n&lt;%= render \"components/header\" do %&gt;\n  &lt;%= render \"components/sidebar/trigger\" %&gt;\n  &lt;%= render \"components/separator\", orientation: :vertical %&gt;\n  \n  &lt;nav class=\"flex items-center gap-1\"&gt;\n    &lt;%= link_to \"Overview\", project_path(@project), \n      class: \"px-3 py-1.5 text-sm rounded-md #{'bg-accent text-accent-foreground' if current_page?(project_path(@project))}\" %&gt;\n    &lt;%= link_to \"Tasks\", project_tasks_path(@project),\n      class: \"px-3 py-1.5 text-sm rounded-md #{'bg-accent text-accent-foreground' if current_page?(project_tasks_path(@project))}\" %&gt;\n    &lt;%= link_to \"Settings\", edit_project_path(@project),\n      class: \"px-3 py-1.5 text-sm rounded-md #{'bg-accent text-accent-foreground' if current_page?(edit_project_path(@project))}\" %&gt;\n  &lt;/nav&gt;\n&lt;% end %&gt;\n\n\n\n\nTheme Variables\n\nvar(--background)\nvar(--border)\n\n\n\n\nCustomization\n\nFixed Height\n\nThe header has a fixed height for consistency with sidebar layouts:\n\n[data-component=\"header\"] {\n  @apply h-14;\n}\n\n\nSticky Header\n\n&lt;%= render \"components/header\", css_classes: \"sticky top-0 z-50\" do %&gt;\n&lt;% end %&gt;\n\n\n\n\nAccessibility\n\n\n  Uses semantic &lt;header&gt; element\n  Works with skip links for keyboard navigation\n  Provides consistent landmark for screen readers\n\n\n\n\nFile Structure\n\napp/views/components/\n└── _header.html.erb\n\napp/assets/stylesheets/header.css\ndocs/header.md"
        },
        {
          "id": "documentation-components",
          "title": "Components",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/",
          "content": "Production-ready UI components for Rails applications. Copy-paste ERB partials styled with Tailwind CSS 4.0 and optional Stimulus controllers.\n\nWhat you get:\n\n  20+ components — From layouts to forms, navigation to feedback\n  Zero dependencies — Just Tailwind CSS and optionally Stimulus\n  Token-driven theming — Familiar shadcn/ui CSS variables for color, plus tokens for shape, elevation, focus rings and weight\n  Rails conventions — ERB partials, data attributes, form helpers\n\n\n\n  \n  \n\n\n\n  Already on 0.5.1? 0.6.0 is a deliberately breaking release: engine CSS moved into @layer components, and radius, elevation, focus rings and weights became tokens. One change affects every existing app and fails silently — the unlayered * rule in your installed theme.css. Run the scanner, then read the guide:\n\n  bundle update maquina-components\nbin/rails maquina:doctor\n  \n\n  → Upgrading to 0.6.0 · Theming\n\n\n\n  Latest release: 0.7.0. An accessibility release, no breaking changes. Focus rings appear instantly instead of fading in from the control’s own text colour; the dropdown and combobox triggers get the chevrons they never had; a collapsed off-canvas sidebar leaves the tab order and stops reserving layout on phones; and breadcrumbs collapse on available space rather than on item count. collapse_after on responsive_breadcrumbs is deprecated.\n\n  → What changes in 0.7.0\n\n\n\n\nDemo Application\n\nView Live Demo →\n\nExplore all components in action without installing anything. The demo showcases light/dark themes, color themes, and responsive layouts.\n\nFor local development, clone the components repository:\n\ngit clone https://github.com/maquina-app/maquina_components.git\ncd maquina_components/test/dummy\nbin/rails server\n\n\nVisit http://localhost:3000 to explore the components locally.\n\n\n\nQuick Start\n\n1. Add the Gem\n\n# Gemfile\ngem \"maquina-components\"\n\n\nbundle install\n\n\n2. Run the Install Generator\n\nbin/rails generate maquina_components:install\n\n\nThis adds the engine CSS import, theme variables (shadcn/ui convention), a shape/state token block for radius, elevation, focus rings and weights, and a helper file for icon customization.\n\nRe-running the generator is safe: it is idempotent, appends each block only once, and never rewrites your palette.\n\n3. Start Using Components\n\n&lt;%= render \"components/card\" do %&gt;\n  &lt;%= render \"components/card/header\" do %&gt;\n    &lt;%= render \"components/card/title\", text: \"Welcome\" %&gt;\n  &lt;% end %&gt;\n  &lt;%= render \"components/card/content\" do %&gt;\n    &lt;p&gt;Your content here&lt;/p&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\n\nFor form elements, use data attributes:\n\n&lt;%= form_with model: @user do |f| %&gt;\n  &lt;%= f.text_field :email, data: { component: \"input\" } %&gt;\n  &lt;%= f.submit \"Save\", data: { component: \"button\", variant: \"primary\" } %&gt;\n&lt;% end %&gt;\n\n\n\n\nAI-Assisted Development\n\nUse the Maquina UI Standards Claude Code plugin to generate views that follow component conventions automatically.\n\nInstead of correcting AI-generated code (“use the card partial, not a div”), the plugin teaches Claude your component patterns:\n\n&gt; Create the users index view with a table showing name, email, and status\n\n\nClaude generates code using your actual components — proper partials, correct data attributes, and consistent patterns.\n\n\n\nAvailable Components\n\nLayout\n\n\n  \n    \n      Component\n      Description\n    \n  \n  \n    \n      Sidebar\n      Collapsible navigation with mobile support and keyboard shortcuts\n    \n    \n      Header\n      Page header for sidebar layouts with breadcrumbs and actions\n    \n    \n      Drawer\n      Slide-out panel with overlay, persistence, and keyboard shortcut\n    \n  \n\n\nContent\n\n\n  \n    \n      Component\n      Description\n    \n  \n  \n    \n      Card\n      Content containers with header, body, and footer sections\n    \n    \n      Alert\n      Callouts with 4 variants and icon support\n    \n    \n      Badge\n      Status indicators with 7 variants and 3 sizes\n    \n    \n      Table\n      Responsive data tables with striped and bordered variants, sticky headers, and a collection helper\n    \n    \n      Empty State\n      Placeholder for no-data scenarios with icons and actions\n    \n    \n      Separator\n      Horizontal or vertical divider\n    \n    \n      Stats\n      Metric cards in a responsive grid\n    \n  \n\n\nNavigation\n\n\n  \n    \n      Component\n      Description\n    \n  \n  \n    \n      Breadcrumbs\n      Navigation with responsive collapsing support\n    \n    \n      Dropdown Menu\n      Actions menu triggered by a button with keyboard navigation\n    \n    \n      Pagination\n      Navigation for paginated content with Pagy integration\n    \n  \n\n\nInteractive\n\n\n  \n    \n      Component\n      Description\n    \n  \n  \n    \n      Calendar\n      Date selection with single and range modes\n    \n    \n      Combobox\n      Searchable dropdown with keyboard navigation and filtering\n    \n    \n      Date Picker\n      Popover calendar triggered by a button for date selection\n    \n    \n      Toggle Group\n      Single or multiple selection button groups\n    \n  \n\n\nFeedback\n\n\n  \n    \n      Component\n      Description\n    \n  \n  \n    \n      Toast\n      Non-intrusive notifications with auto-dismiss and variants\n    \n  \n\n\nForms\n\n\n  \n    \n      Component\n      Description\n    \n  \n  \n    \n      Form Components\n      Inputs, selects, checkboxes styled with data attributes\n    \n  \n\n\n\n\nPrerequisites\n\nThe generator requires tailwindcss-rails:\n\nbundle add tailwindcss-rails\nbin/rails tailwindcss:install\n\n\n\n\nStimulus Setup\n\nInteractive components (Sidebar, Dropdown Menu, Toggle Group, Breadcrumbs, Combobox, Toast) require Stimulus. With importmaps:\n\n# config/importmap.rb\npin \"@hotwired/turbo-rails\", to: \"turbo.min.js\"\npin \"@hotwired/stimulus\", to: \"stimulus.min.js\"\npin \"@hotwired/stimulus-loading\", to: \"stimulus-loading.js\"\npin_all_from \"app/javascript/controllers\", under: \"controllers\"\n\n\n// app/javascript/application.js\nimport \"@hotwired/turbo-rails\"\nimport { Application } from \"@hotwired/stimulus\"\nimport { eagerLoadControllersFrom } from \"@hotwired/stimulus-loading\"\n\nconst application = Application.start()\napplication.debug = false\nwindow.Stimulus = application\n\neagerLoadControllersFrom(\"controllers\", application)\n\n\nStatic components (Badge, Card, Alert, Button, form elements) work without JavaScript.\n\nExtending Component Behavior\n\nEvery component merges your data: hash with its own data attributes. Identity keys (component, variant, size) always win, but controller and action concatenate — so you can attach your own Stimulus behavior to any component without losing the built-in one:\n\n&lt;%= render \"components/combobox\", name: \"country\",\n      data: { controller: \"analytics\", action: \"change-&gt;analytics#track\" } %&gt;\n&lt;%# renders data-controller=\"combobox analytics\" %&gt;\n\n\n\n\nIcons\n\nComponents render icons through the icon_for helper, which falls back to a built-in set of inline SVGs (check, chevrons, calendar, search, mail, trash, and more):\n\n&lt;%= icon_for :check, class: \"size-4\" %&gt;\n&lt;%= icon_for :trash, class: \"size-4\", stroke_width: 1.5 %&gt;\n\n\nTo use your own icon system (Heroicons, Lucide, inline SVG files), override main_icon_svg_for in the generated MaquinaComponentsHelper — icon_for consults it first and only falls back to the built-ins when it returns nil:\n\n# app/helpers/maquina_components_helper.rb\ndef main_icon_svg_for(name)\n  lucide_icon(name)\nend\n\n\nOr return the SVG yourself, a name at a time:\n\nmodule MaquinaComponentsHelper\n  def main_icon_svg_for(name)\n    case name\n    when :home\n      &lt;&lt;~SVG\n        &lt;svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"&gt;\n          &lt;path d=\"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8\"/&gt;\n          &lt;path d=\"M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\"/&gt;\n        &lt;/svg&gt;\n      SVG\n    end\n  end\nend\n\n\nIcons are sourced from Lucide. Copy SVG code directly from their website.\n\nWhat the override does and does not reach\n\nmain_icon_svg_for backs the public icon_for helper — every icon you render, plus the component parameters that take an icon name (alert, sidebar menu items, empty states, breadcrumb separators).\n\nIt deliberately does not reach the icons an engine component renders for itself: a dropdown trigger’s chevron, the toast close button, the calendar’s arrows. Those go through an internal builtin_icon_for that only ever reads the engine’s own set, so a component looks the same in every app regardless of how you have configured icons — and so a partial override cannot leave a control without its affordance.\n\nThe practical consequence: if an engine component’s own icon looks wrong or missing, defining that name in main_icon_svg_for will not change it. That is a bug in the engine, not something to fix in your app — please report it.\n\nCatching typos: strict_icons\n\nAn unknown icon name renders nothing at all, which is invisible in review and in production. MaquinaComponents.strict_icons raises UnknownIconError instead. It is on by default in development and test and off in production, so a typo fails loudly while you work and can never take a page down for a user.\n\n# config/initializers/maquina_components.rb\nMaquinaComponents.strict_icons = false   # opt out; unknown names render nothing\n\n\nThis covers both helpers. If it raises for a name you never wrote yourself, an engine component asked for an icon the engine does not ship — the message says so, and says that a main_icon_svg_for entry will not help.\n\n\n\nTheme Variables\n\nColors are CSS variables following the shadcn/ui theming convention.\n\nEach one is defined twice, which Tailwind CSS v4 requires: in :root for the value, and in @theme so it also becomes a utility (bg-primary, text-muted-foreground).\n\n:root {\n  --primary: oklch(0.488 0.243 264.376);\n  --primary-foreground: oklch(0.985 0 0);\n}\n\n@theme {\n  --color-primary: var(--primary);\n  --color-primary-foreground: var(--primary-foreground);\n}\n\n\nEdit the values in :root to match your brand — the generator installs neutral grays. Adding a semantic color works the same way: declare it in :root, then mirror it in @theme if you want the utility.\n\nEverything that is not a color — shape, focus rings, elevation, weights, control marks — is a token too, and a theme changes values, not selectors. See Theming for the full token table, ready-made themes, and how to pin a single component.\n\nGenerator Options\n\n# Skip theme variables (if you already have them)\nbin/rails generate maquina_components:install --skip-theme\n\n# Skip helper creation\nbin/rails generate maquina_components:install --skip-helper\n\n# Skip both\nbin/rails generate maquina_components:install --skip-theme --skip-helper\n\n\n\n\nFile Structure After Setup\n\napp/\n├── assets/tailwind/\n│   └── application.css               # Theme + engine import\n├── helpers/\n│   └── maquina_components_helper.rb  # Icon override\n├── javascript/\n│   └── application.js                # Stimulus init\n└── views/layouts/\n    └── application.html.erb          # Layout with components\n\n\n\n\nTroubleshooting\n\nGenerator Issues\n\n“tailwindcss-rails doesn’t appear to be installed”\n\nInstall it first:\n\nbundle add tailwindcss-rails\nbin/rails tailwindcss:install\n\n\nRuntime Issues\n\nSidebar trigger not working\n\n\n  Ensure Stimulus is initialized\n  Verify provider wraps both sidebar and content\n  Check browser console for errors\n\n\nStyles not applying\n\n\n  Verify engine CSS is imported after @import \"tailwindcss\";\n  Check that @theme block exists with color bindings\n  Restart dev server after CSS changes\n\n\nDark mode not working\n\n\n  Add .dark class to &lt;html&gt; element\n  Ensure .dark { } block has variable overrides\n\n\nIcons not rendering\n\n\n  Check the icon name matches your main_icon_svg_for cases\n  Verify the helper is included in ApplicationHelper\n  Turn on strict_icons in development so an unknown name raises instead of rendering nothing\n  If the missing icon belongs to a component rather than to your own markup (a trigger’s chevron, a close button), main_icon_svg_for cannot fix it — see Icons\n\n\n\n\nNext Steps\n\n\n  \n    \n      Sidebar\n    \n    \n      Build your application layout with collapsible navigation.\n    \n  \n\n  \n    \n      Card\n    \n    \n      Display content in containers with header, body, and footer.\n    \n  \n\n  \n    \n      Form Components\n    \n    \n      Style inputs, selects, and buttons with data attributes.\n    \n  \n\n  \n    \n      AI-Assisted Development\n    \n    \n      Use Claude Code to generate views with component conventions."
        },
        {
          "id": "documentation-components-pagination",
          "title": "Pagination",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/pagination/",
          "content": "Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\nUsage\n\n&lt;%= render &quot;components/pagination&quot; do %&gt;\n  &lt;%= render &quot;components/pagination/content&quot; do %&gt;\n    &lt;%= render &quot;components/pagination/item&quot; do %&gt;\n      &lt;%= render &quot;components/pagination/previous&quot;, href: &quot;/page/1&quot; %&gt;\n    &lt;% end %&gt;\n    &lt;%= render &quot;components/pagination/item&quot; do %&gt;\n      &lt;%= render &quot;components/pagination/link&quot;, href: &quot;/page/1&quot; do %&gt;1&lt;% end %&gt;\n    &lt;% end %&gt;\n    &lt;%= render &quot;components/pagination/item&quot; do %&gt;\n      &lt;%= render &quot;components/pagination/link&quot;, href: &quot;/page/2&quot;, active: true do %&gt;2&lt;% end %&gt;\n    &lt;% end %&gt;\n    &lt;%= render &quot;components/pagination/item&quot; do %&gt;\n      &lt;%= render &quot;components/pagination/link&quot;, href: &quot;/page/3&quot; do %&gt;3&lt;% end %&gt;\n    &lt;% end %&gt;\n    &lt;%= render &quot;components/pagination/item&quot; do %&gt;\n      &lt;%= render &quot;components/pagination/ellipsis&quot; %&gt;\n    &lt;% end %&gt;\n    &lt;%= render &quot;components/pagination/item&quot; do %&gt;\n      &lt;%= render &quot;components/pagination/next&quot;, href: &quot;/page/3&quot; %&gt;\n    &lt;% end %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\n\nPagy Integration\n\nFor Pagy-backed collections, the pagination_nav helper renders the full pagination from a Pagy object, with Turbo-aware links:\n\n&lt;%= pagination_nav(@pagy, :users_path) %&gt;\n\n&lt;%# Preserve query params and target a Turbo Frame %&gt;\n&lt;%= pagination_nav(@pagy, :search_users_path,\n      params: { q: params[:q] },\n      turbo: { action: :replace, frame: &quot;users&quot; }) %&gt;\n\npagination_simple renders the same navigation without page-number labels. Both return nothing when there is a single page.\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      pagy\n      Pagy\n      required\n      The Pagy pagination object\n    \n    \n      route_helper\n      Symbol\n      required\n      Route helper used to build page links\n    \n    \n      params\n      Hash\n      {}\n      Extra query params preserved across pages\n    \n    \n      turbo\n      Hash\n      { action: :replace }\n      Turbo data attributes for the links, e.g. frame:\n    \n    \n      show_labels\n      Boolean\n      true\n      Show Previous/Next text labels (pagination_nav only)\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\nAPI Reference\n\nPagination\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nPagination Content\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nPagination Item\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nPagination Link\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      href\n      String\n      required\n      URL for the page\n    \n    \n      active\n      Boolean\n      false\n      Whether current page\n    \n    \n      disabled\n      Boolean\n      false\n      Whether disabled\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nPagination Previous\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      href\n      String\n      nil\n      URL for previous page\n    \n    \n      label\n      String\n      \"Previous\"\n      Button label\n    \n    \n      disabled\n      Boolean\n      false\n      Whether disabled\n    \n    \n      show_label\n      Boolean\n      true\n      Show text label\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nPagination Next\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      href\n      String\n      nil\n      URL for next page\n    \n    \n      label\n      String\n      \"Next\"\n      Button label\n    \n    \n      disabled\n      Boolean\n      false\n      Whether disabled\n    \n    \n      show_label\n      Boolean\n      true\n      Show text label\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nPagination Ellipsis\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes"
        },
        {
          "id": "documentation-components-separator",
          "title": "Separator",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/separator/",
          "content": "Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\nUsage\n\n&lt;%= render &quot;components/separator&quot; %&gt;\n\nExamples\n\nVertical\n\nUse inside a flex row — for example between header actions, as the Header component does.\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;div class=&quot;flex h-8 items-center&quot;&gt;\n  &lt;span&gt;Docs&lt;/span&gt;\n  &lt;%= render &quot;components/separator&quot;, orientation: :vertical %&gt;\n  &lt;span&gt;Source&lt;/span&gt;\n&lt;/div&gt;\n\nAPI Reference\n\nSeparator\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      orientation\n      Symbol\n      :horizontal\n      :horizontal or :vertical\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes, including data"
        },
        {
          "id": "documentation-components-sidebar",
          "title": "Sidebar",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/sidebar/",
          "content": "Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\nUsage\n\n&lt;%= render &quot;components/sidebar/provider&quot;, default_open: sidebar_open? do %&gt;\n  &lt;%= render &quot;components/sidebar&quot;, state: sidebar_state do %&gt;\n    &lt;%= render &quot;components/sidebar/header&quot; do %&gt;\n      &lt;%# Logo/branding %&gt;\n    &lt;% end %&gt;\n\n    &lt;%= render &quot;components/sidebar/content&quot; do %&gt;\n      &lt;%= render &quot;components/sidebar/group&quot;, title: &quot;Navigation&quot; do %&gt;\n        &lt;%= render &quot;components/sidebar/menu&quot; do %&gt;\n          &lt;%= render &quot;components/sidebar/menu_item&quot; do %&gt;\n            &lt;%= render &quot;components/sidebar/menu_button&quot;,\n              title: &quot;Dashboard&quot;,\n              icon_name: :home,\n              url: root_path,\n              active: current_page?(root_path) %&gt;\n          &lt;% end %&gt;\n        &lt;% end %&gt;\n      &lt;% end %&gt;\n    &lt;% end %&gt;\n\n    &lt;%= render &quot;components/sidebar/footer&quot; do %&gt;\n      &lt;%# User menu %&gt;\n    &lt;% end %&gt;\n  &lt;% end %&gt;\n\n  &lt;%= render &quot;components/sidebar/inset&quot; do %&gt;\n    &lt;%= render &quot;components/header&quot; do %&gt;\n      &lt;%= render &quot;components/sidebar/trigger&quot; %&gt;\n    &lt;% end %&gt;\n    &lt;%= yield %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nExamples\n\nMenu Button\n\n&lt;%= render &quot;components/sidebar/menu_button&quot;,\n  title: &quot;Dashboard&quot;,\n  icon_name: :home,\n  url: root_path,\n  active: true %&gt;\n\nMenu Link (Avatar Style)\n\n&lt;%= render &quot;components/sidebar/menu_link&quot;,\n  url: profile_path,\n  text_icon: &quot;A&quot;,\n  title: &quot;ACME Corp&quot;,\n  subtitle: &quot;Workspace&quot; %&gt;\n\nMenu Badge and Menu Action\n\nBoth pin to the right edge of the menu item, so they must be rendered inside menu_item, as siblings of the menu_button (or menu_link). That nesting is load-bearing: the item is the positioning context, and a menu item containing an action automatically reserves right-hand padding on its button so the icon never sits on top of the label. The demo above shows both — the 24 count on Inbox and the options control on Calendar.\n\n&lt;%= render &quot;components/sidebar/menu_item&quot; do %&gt;\n  &lt;%= render &quot;components/sidebar/menu_button&quot;,\n    title: &quot;Inbox&quot;, icon_name: :inbox, url: inbox_path %&gt;\n  &lt;%= render &quot;components/sidebar/menu_badge&quot;, text: &quot;24&quot; %&gt;\n  &lt;%= render &quot;components/sidebar/menu_action&quot;,\n    label: &quot;Inbox options&quot;,\n    icon_name: :ellipsis,\n    show_on_hover: true %&gt;\n&lt;% end %&gt;\n\nmenu_action renders a &lt;button type=&quot;button&quot;&gt; by default and an &lt;a&gt; when you pass url:. It is icon-only, so label: is required — it becomes both the aria-label and screen-reader text. show_on_hover: true keeps the action invisible until the item is hovered or focused.\n\nGroup Action\n\n&lt;%= render &quot;components/sidebar/group&quot;, title: &quot;Projects&quot; do %&gt;\n  &lt;%= render &quot;components/sidebar/group_action&quot;,\n    label: &quot;Add project&quot;, url: new_project_path %&gt;\n\n  &lt;%= render &quot;components/sidebar/menu&quot; do %&gt;\n    &lt;%# ... %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nSeparator\n\n&lt;%= render &quot;components/sidebar/separator&quot; %&gt;\n\nRenders the separator primitive, so it keeps the primitive's 1px track while the sidebar part re-spaces it and swaps in the sidebar's own border token.\n\nAPI Reference\n\nProvider\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      id\n      String\n      \"sidebar-provider\"\n      Element ID for stable morph matching\n    \n    \n      default_open\n      Boolean\n      true\n      Initial open state\n    \n    \n      variant\n      Symbol\n      :inset\n      Visual variant\n    \n    \n      cookie_name\n      String\n      \"sidebar_state\"\n      Cookie for persistence\n    \n    \n      keyboard_shortcut\n      String\n      \"b\"\n      Toggle shortcut (Cmd/Ctrl+key)\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nSidebar\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      id\n      String\n      auto\n      Element ID\n    \n    \n      state\n      Symbol\n      :collapsed\n      :expanded or :collapsed\n    \n    \n      collapsible\n      Symbol\n      :offcanvas\n      :offcanvas, :icon, or :none\n    \n    \n      variant\n      Symbol\n      :inset\n      :sidebar, :floating, or :inset\n    \n    \n      side\n      Symbol\n      :left\n      :left or :right\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nMenu Button\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      title\n      String\n      required\n      Button text\n    \n    \n      url\n      String\n      required\n      Link URL\n    \n    \n      icon_name\n      Symbol\n      nil\n      Icon name\n    \n    \n      size\n      Symbol\n      :default\n      :default, :sm, or :lg\n    \n    \n      active\n      Boolean\n      false\n      Whether active\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nMenu Link\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      url\n      String\n      required\n      Link URL\n    \n    \n      title\n      String\n      required\n      Primary text\n    \n    \n      subtitle\n      String\n      nil\n      Secondary text\n    \n    \n      text_icon\n      String\n      nil\n      Text for avatar\n    \n    \n      icon\n      String\n      nil\n      Image URL for avatar\n    \n    \n      active\n      Boolean\n      false\n      Whether active\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nTrigger\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      icon_name\n      Symbol\n      :left_panel\n      Icon name for toggle button\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nOther Parts\n\n\n  \n    \n      Partial\n      Description\n    \n  \n  \n    \n      sidebar/header\n      Top section for logo/branding\n    \n    \n      sidebar/content\n      Scrollable middle section\n    \n    \n      sidebar/footer\n      Bottom section for user menu\n    \n    \n      sidebar/group\n      Groups menu items with optional title\n    \n    \n      sidebar/menu\n      List container for menu items\n    \n    \n      sidebar/menu_item\n      Individual menu item wrapper\n    \n    \n      sidebar/menu_badge\n      Count or short label pinned inside a menu item. text: / content:\n    \n    \n      sidebar/menu_action\n      Icon control pinned right inside a menu item. label: (required), url:, icon_name:, show_on_hover:\n    \n    \n      sidebar/group_action\n      Icon control pinned to the right of a group label. label: (required), url:, icon_name:\n    \n    \n      sidebar/separator\n      Divider between groups. orientation: (default :horizontal)\n    \n    \n      sidebar/trigger\n      Toggle button for sidebar\n    \n    \n      sidebar/inset\n      Main content area wrapper\n    \n  \n\n\n\nHelper Methods\n\n\n  \n    \n      Method\n      Description\n    \n  \n  \n    \n      sidebar_state(cookie_name)\n      Returns :expanded or :collapsed\n    \n    \n      sidebar_open?(cookie_name)\n      Returns true if expanded\n    \n    \n      sidebar_closed?(cookie_name)\n      Returns true if collapsed\n    \n  \n\n\n\nAccessibility\n\n\n  A collapsed off-canvas sidebar is out of the tab order. When it is parked off-screen, its container carries inert, so keyboard focus skips it entirely rather than walking through a screenful of destinations no pointer can reach. It is applied server-side as well as by the controller, so the invariant holds before Stimulus connects. A collapsible: :icon sidebar is a visible rail and stays reachable; an open sidebar obviously does too.\n  Below 768px the sidebar reserves no layout. The container is a fixed overlay with a backdrop at that width, so the gap that normally holds space for it collapses to zero — structurally, in CSS, whatever the state cookie says and before any JavaScript has run. A phone load carrying an expanded cookie gets a full-width content column in the first painted frame, with no settle.\n  Sidebar items expose aria-current=\"page\" when active, and the trigger keeps aria-expanded and aria-controls in sync with the sidebar it drives.\n\n\nTurbo Drive\n\nThe sidebar controller integrates with Turbo Drive to maintain correct state across navigations:\n\n\n  Cache teardown: On mobile, the sidebar closes and the backdrop is hidden before Turbo caches the page. Pressing back never shows a stale open sidebar or scroll-locked body.\n  Morph awareness: When using turbo_refresh_method_tag :morph, the sidebar re-reads its cookie to preserve the desktop toggle state and forces closed on mobile after a morph refresh.\n  Desktop persistence: The sidebar state is stored in a cookie, so it survives full page loads and Turbo navigations without extra configuration.\n\n\nStable IDs\n\nThe sidebar generates deterministic IDs based on its side: parameter (sidebar-left, sidebar-right) instead of random IDs. This allows idiomorph to match old and new elements across morph renders, preventing the sidebar from being destroyed and recreated.\n\nThe provider div also receives a stable ID (sidebar-provider) for the same reason.\n\nIf you render multiple sidebars on the same side, pass explicit id: parameters to avoid collisions:\n\n&lt;%= render &quot;components/sidebar/provider&quot;, id: &quot;sidebar-main&quot; do %&gt;\n  &lt;%= render &quot;components/sidebar&quot;, id: &quot;sidebar-nav&quot;, side: :left do %&gt;\n    ...\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nMorph Compatibility\n\nDuring a Turbo morph, the server-rendered data-sidebar-open-value may carry a stale value (e.g., from a broadcast where the server has no access to the browser cookie). The controller treats the browser cookie as the source of truth:\n\n\n  Before morph updates attributes, the controller sets an internal guard flag.\n  When idiomorph overwrites data-sidebar-open-value, the Stimulus value callback is skipped — preventing the stale server value from overwriting the cookie.\n  After morph completes, the controller reads the cookie, reasserts the correct state, and removes the sidebar-loading class that morph re-adds from server HTML.\n\n\nTurbo Frames\n\nThe sidebar works inside Turbo Frames because stable IDs enable clean Stimulus disconnect/reconnect cycles. On reconnection, initialize() re-reads the cookie, so the sidebar always reflects the latest client-side state."
        },
        {
          "id": "documentation-components-stats",
          "title": "Stats",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/stats/",
          "content": "Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\nUsage\n\n&lt;%= render &quot;components/stats/stats_grid&quot;, columns: 4, cards: [\n  { title: &quot;Total Revenue&quot;, value: &quot;$1,250.00&quot;, icon: :dollar, subtitle: &quot;Trending up this month&quot; },\n  { title: &quot;New Customers&quot;, value: &quot;1,234&quot;, icon: :users },\n  { title: &quot;Active Accounts&quot;, value: &quot;45,678&quot;, icon: :check_circle },\n  { title: &quot;Growth Rate&quot;, value: &quot;4.5%&quot;, icon: :chart_bar }\n] %&gt;\n\nExamples\n\nSingle Card\n\nCards render standalone too. Use value_classes and icon_classes for color accents — a utility class always wins over the theme default.\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/stats/stats_card&quot;,\n  title: &quot;Open Tickets&quot;,\n  value: &quot;12&quot;,\n  icon: :circle_alert,\n  icon_classes: &quot;text-amber-500&quot;,\n  subtitle: &quot;3 urgent&quot; %&gt;\n\nWith Action\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/stats/stats_grid&quot;,\n  columns: 3,\n  cards: cards,\n  action: link_to(&quot;View report&quot;, reports_path, data: { component: &quot;button&quot;, variant: &quot;outline&quot; }),\n  action_position: :end %&gt;\n\nAPI Reference\n\nStats Grid\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      cards\n      Array\n      []\n      Hashes of stats_card parameters\n    \n    \n      columns\n      Integer\n      3\n      Grid columns from the sm breakpoint up, 1-6\n    \n    \n      action\n      String\n      nil\n      Captured HTML rendered beside the grid\n    \n    \n      action_position\n      Symbol\n      :end\n      :start or :end\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes, including data\n    \n  \n\n\nStats Card\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      title\n      String\n      required\n      Metric label\n    \n    \n      value\n      String\n      required\n      Metric value\n    \n    \n      icon\n      Symbol\n      nil\n      Built-in icon name; custom HTML is also accepted\n    \n    \n      subtitle\n      String\n      nil\n      Secondary line under the value\n    \n    \n      icon_classes\n      String\n      \"\"\n      Classes for the icon area, e.g. a color utility\n    \n    \n      value_classes\n      String\n      \"\"\n      Classes for the value, e.g. a color utility\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes, including data"
        },
        {
          "id": "documentation-components-table",
          "title": "Table",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/table/",
          "content": "Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\nUsage\n\n&lt;%= render &quot;components/table&quot; do %&gt;\n  &lt;%= render &quot;components/table/header&quot; do %&gt;\n    &lt;%= render &quot;components/table/row&quot; do %&gt;\n      &lt;%= render &quot;components/table/head&quot; do %&gt;Name&lt;% end %&gt;\n      &lt;%= render &quot;components/table/head&quot; do %&gt;Email&lt;% end %&gt;\n      &lt;%= render &quot;components/table/head&quot;, css_classes: &quot;text-right&quot; do %&gt;Amount&lt;% end %&gt;\n    &lt;% end %&gt;\n  &lt;% end %&gt;\n\n  &lt;%= render &quot;components/table/body&quot; do %&gt;\n    &lt;% @users.each do |user| %&gt;\n      &lt;%= render &quot;components/table/row&quot; do %&gt;\n        &lt;%= render &quot;components/table/cell&quot; do %&gt;&lt;%= user.name %&gt;&lt;% end %&gt;\n        &lt;%= render &quot;components/table/cell&quot; do %&gt;&lt;%= user.email %&gt;&lt;% end %&gt;\n        &lt;%= render &quot;components/table/cell&quot;, css_classes: &quot;text-right&quot; do %&gt;&lt;%= user.amount %&gt;&lt;% end %&gt;\n      &lt;% end %&gt;\n    &lt;% end %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nExamples\n\nWith Footer\n\n&lt;%= render &quot;components/table&quot; do %&gt;\n  &lt;%= render &quot;components/table/header&quot; do %&gt;\n    &lt;%# ... %&gt;\n  &lt;% end %&gt;\n  &lt;%= render &quot;components/table/body&quot; do %&gt;\n    &lt;%# ... %&gt;\n  &lt;% end %&gt;\n  &lt;%= render &quot;components/table/footer&quot; do %&gt;\n    &lt;%= render &quot;components/table/row&quot; do %&gt;\n      &lt;%= render &quot;components/table/cell&quot;, colspan: 2 do %&gt;Total&lt;% end %&gt;\n      &lt;%= render &quot;components/table/cell&quot;, css_classes: &quot;text-right&quot; do %&gt;$750.00&lt;% end %&gt;\n    &lt;% end %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nSelected Row\n\n&lt;%= render &quot;components/table/row&quot;, selected: true do %&gt;\n  &lt;%= render &quot;components/table/cell&quot; do %&gt;Selected item&lt;% end %&gt;\n&lt;% end %&gt;\n\nBordered Variant\n\nDraws a border around the scroll container.\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/table&quot;, variant: :bordered do %&gt;\n  &lt;%# ... %&gt;\n&lt;% end %&gt;\n\nStriped Variant\n\nAlternates row backgrounds on the table itself.\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n&lt;%= render &quot;components/table&quot;, table_variant: :striped do %&gt;\n  &lt;%# ... %&gt;\n&lt;% end %&gt;\n\nSimple Table Helper\n\nFor collection-driven tables, the simple_table helper renders the whole structure from a column definition. Keys can be attribute names, hash keys, or procs.\n\n&lt;%= simple_table @invoices, caption: &quot;Recent invoices&quot;, columns: [\n  { key: :number, label: &quot;Invoice&quot; },\n  { key: :customer, label: &quot;Customer&quot; },\n  { key: -&gt;(i) { i.amount.format }, label: &quot;Amount&quot;, align: :right }\n], row_id: :id, table_variant: :striped %&gt;\n\nAPI Reference\n\nEvery table partial passes unknown keywords through as HTML attributes, so standard table attributes like colspan and rowspan work directly on cells, and id, aria, or data attributes work on any part.\n\nTable\n\nThe table renders two elements: a scrollable container div and the table element inside it. The variant parameter styles the container (that is why :bordered lives there), while table_variant styles the table element itself.\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      container\n      Boolean\n      true\n      Wrap in scrollable container\n    \n    \n      variant\n      Symbol\n      nil\n      Container variant, :bordered draws a border around the scroll container\n    \n    \n      table_variant\n      Symbol\n      nil\n      Table variant, :striped alternates row backgrounds\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nTable Header\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      sticky\n      Boolean\n      false\n      Sticky header on scroll\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nTable Row\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      selected\n      Boolean\n      false\n      Highlight as selected\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nTable Head\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      text\n      String\n      nil\n      Heading text\n    \n    \n      content\n      String\n      nil\n      Captured HTML via capture, or use block\n    \n    \n      scope\n      String\n      \"col\"\n      Scope attribute for accessibility\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nTable Cell\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      text\n      String\n      nil\n      Cell text\n    \n    \n      content\n      String\n      nil\n      Captured HTML via capture, or use block\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes, e.g. colspan, rowspan\n    \n  \n\n\n\nsimple_table Helper\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      collection\n      Enumerable\n      required\n      Objects or hashes to render\n    \n    \n      columns\n      Array\n      required\n      Hashes with key (attribute, hash key, or proc), label, and optional align (:center, :right; left by default)\n    \n    \n      caption\n      String\n      nil\n      Table caption\n    \n    \n      variant\n      Symbol\n      nil\n      Container variant, :bordered\n    \n    \n      table_variant\n      Symbol\n      nil\n      Table variant, :striped\n    \n    \n      empty_message\n      String\n      \"No data available\"\n      Shown when the collection is empty\n    \n    \n      row_id\n      Symbol\n      nil\n      Method used to build each row id, row-{value}\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes for the table\n    \n  \n\n\n\nTable Body / Footer / Caption\n\nCaption also accepts text and content like cell and head.\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes"
        },
        {
          "id": "documentation-components-theming",
          "title": "Theming",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/theming/",
          "content": "Reshape every component by declaring token values, not override CSS.\n\nColors have always been CSS variables. As of 0.6.0 so are shape, focus rings, elevation and weight — which is the whole of what used to require override CSS.\n\nThe contract: a theme changes values, not selectors. If a theme needs a selector, either you are changing one component’s shape on purpose, or the token layer is missing a token — open an issue.\n\nUpgrading from 0.5.1? Read Upgrading first — it leads with a one-line fix every existing app needs.\n\n\n\nRole Tokens\n\nTokens are named for the role a value plays, not for a size, so controls and surfaces can be shaped independently.\n\n\n  \n    \n      Token\n      Default\n      Applies to\n    \n  \n  \n    \n      --control-radius\n      0.375rem\n      Buttons, inputs, selects, textareas, badges, menu items, pagination links, calendar days, sidebar items\n    \n    \n      --surface-radius\n      0.5rem\n      Cards, alerts, popovers, toasts, tables, stats, empty, calendar, drawer, the sidebar inset\n    \n    \n      --mark-radius\n      4px\n      The checkbox box\n    \n    \n      --pill-radius\n      calc(infinity * 1px)\n      Radio, switch track\n    \n    \n      --focus-ring-width\n      3px\n      Every focus ring\n    \n    \n      --focus-ring-offset\n      0px\n      Every focus ring\n    \n    \n      --focus-ring-style\n      solid\n      Every focus ring\n    \n    \n      --focus-ring-color\n      see below\n      Every focus ring; invalid fields and destructive buttons override it with the destructive tint\n    \n    \n      --elevation-control\n      shadow-xs\n      Inputs, selects, textareas, checkbox, radio\n    \n    \n      --elevation-raised\n      shadow-sm\n      Cards, stats cards, floating sidebar, every filled button\n    \n    \n      --elevation-overlay\n      shadow-md\n      Dropdown and combobox popovers, the date-picker popover, toasts, the drawer panel\n    \n    \n      --elevation-none\n      none\n      Ghost and link buttons, the inset sidebar\n    \n    \n      --label-weight\n      500\n      Labels, buttons\n    \n    \n      --value-weight\n      700\n      Stat values\n    \n    \n      --control-fill\n      transparent\n      Field background; re-set under .dark\n    \n  \n\n\n--focus-ring-color has no single default\n\nThe other three focus tokens are declared once in the engine’s @theme block. --focus-ring-color is declared nowhere: each rule supplies its own default as the var() fallback, because the right resting colour differs by family.\n\n\n  \n    \n      Family\n      Default when you do not set the token\n    \n  \n  \n    \n      Buttons, cards, badges, toasts, drawer, pagination, calendar, toggle group, date picker\n      var(--ring)\n    \n    \n      Everything inside the sidebar, and the menu button\n      var(--sidebar-ring, var(--ring))\n    \n    \n      Form fields — input, textarea, select, checkbox, radio\n      color-mix(in oklch, var(--ring) 50%, transparent)\n    \n  \n\n\nSetting --focus-ring-color once at :root overrides all three at the same time, which is usually what you want — a declared token means no fallback ever fires. Set it in a narrower scope to keep the families apart.\n\nTwo states deliberately outrank a :root override, because a state must win: an aria-invalid field (and anything inside .field_with_errors) and a data-variant=\"destructive\" button declare --focus-ring-color on the element itself. An element’s own custom property beats an inherited one, so those rings stay on the destructive tint whatever :root says.\n\nNever transition outline-color\n\nIf you write your own component against these tokens, keep outline-color out of its transition — and that means not using Tailwind’s transition-colors, which includes outline-color in v4. A transitioned ring animates from its pre-focus value, which on a control that has never painted an outline is the initial currentColor: the control’s own text colour. On a filled variant that is a near-white ring for the first 150ms, which is no focus indicator at all on exactly the controls that matter most. It also makes getComputedStyle read the previous colour if you measure right after a Tab press, which is a reliable way to convince yourself a working ring is broken.\n\nName the properties instead:\n\ntransition-property: color, background-color, border-color, text-decoration-color;\n\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n  Try it on any demo on this site. Every preview panel now carries a shape button next to the dark-mode toggle. It cycles default → brutal → soft, which are nothing but different values for the tokens above — no component selector is involved. Flip it on any component page to watch the token layer move.\n\n\n\n\nFlat Theme in Six Lines\n\n:root {\n  --elevation-control: none;\n  --elevation-raised: none;\n  --elevation-overlay: none;\n  --elevation-none: none;\n  --control-radius: 0.25rem;\n  --surface-radius: 0.25rem;\n}\n\n\nEvery shadow in the library disappears and every box takes a 4px corner. The checkbox and the switch keep their own roles, which is the point of separating them.\n\nBrutalist Theme in Twelve Lines\n\n:root {\n  --control-radius: 0;\n  --surface-radius: 0;\n  --mark-radius: 0;\n  --pill-radius: 0;\n  --focus-ring-width: 4px;\n  --focus-ring-offset: 3px;\n  --focus-ring-color: var(--foreground);\n  --elevation-control: none;\n  --elevation-raised: none;\n  --elevation-overlay: none;\n  --label-weight: 700;\n  --value-weight: 900;\n}\n\n\nSquare everything, thicken the ring and push it off the edge, drop every shadow, and make labels and values shout. No component selector anywhere.\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n\nWhere the Declarations Go\n\nPut them in a plain, unlayered :root block in your theme.css — that is what the installer generates, and unlayered CSS wins over the engine’s @theme defaults whatever the import order.\n\nDo not wrap them in @theme: that emits into @layer theme alongside the engine’s own defaults, where source order becomes the only tie-breaker. Do not rename them into Tailwind’s namespaces (--radius-*, --shadow-*) either — a @theme { --radius-*: initial } in an app would wipe them.\n\n/* app/assets/tailwind/theme.css */\n:root {\n  --surface-radius: 1rem;\n}\n\n\n\n\nRecoloring Control Marks\n\nThe checkbox tick, the checkbox dash, the radio dot, the switch thumb and the select chevron are whole SVG data URIs rather than a color token, and that is forced by CSS, not a choice: var() cannot be interpolated into url(), a data URI is a separate SVG document so currentColor never resolves inside it, and mask-image would mask the whole element — box, border and shadow — along with the glyph. So each mark is exposed as its own property.\n\n\n  \n    \n      Token\n      Mark\n    \n  \n  \n    \n      --checkbox-mark-image\n      Checkbox tick\n    \n    \n      --checkbox-indeterminate-image\n      Checkbox dash\n    \n    \n      --radio-mark-image\n      Radio dot\n    \n    \n      --switch-thumb-image\n      Switch thumb\n    \n    \n      --select-chevron-image\n      Select chevron\n    \n  \n\n\nThey theme like every other token: set one in :root (or in any theme block) and every control picks it up. The engine keeps its own artwork in the use-site fallback rather than declaring it on the control, precisely so that a global declaration wins.\n\n:root {\n  --checkbox-mark-image: url(\"data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='%23ffffff' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M8 2l1.8 4.2L14 8l-4.2 1.8L8 14l-1.8-4.2L2 8l4.2-1.8z'/%3e%3c/svg%3e\");\n}\n\n\nPer-instance opt-in\n\nA light --primary makes the default white ink measure about 1.15:1 against the checked fill. If that is a one-off rather than a theme-wide decision, one attribute fixes it with no CSS at all:\n\n&lt;%= f.check_box :terms, data: { component: \"checkbox\", mark: \"dark\" } %&gt;\n\n\ndata-mark=\"dark\" works on the checkbox, radio, switch and select; data-mark=\"light\" is also available on the select. Both are declared on the control, so an explicit per-instance opt-in beats a global default — which is the right way round.\n\nThe select chevron carries a different default per color scheme, because gray-500 alone is low-contrast on a dark field. That default is inherited rather than declared on the control, so one :root line still retints it in both schemes. If you want a different ink per scheme, say so explicitly:\n\n:root { --select-chevron-image: url(\"…dark ink…\"); }\n.dark { --select-chevron-image: url(\"…light ink…\"); }\n\n\n\n\n  \n    \n      Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\n\nDark Mode\n\nDark-mode differences are token values too, so you rarely need a .dark twin of a component rule. Set the token inside your own .dark block:\n\n.dark {\n  --focus-ring-color: color-mix(in oklch, var(--ring) 70%, transparent);\n}\n\n\n--control-fill is the one to know about: the engine re-declares it under .dark on the fields themselves, so overriding the dark field background needs a selector that reaches the control.\n\n.dark [data-component=\"input\"],\n.dark [data-component=\"textarea\"],\n.dark [data-component=\"select\"] {\n  --control-fill: oklch(0.2 0.03 260);\n}\n\n\n\n\nPinning One Component\n\nEvery radius and elevation site also reads a component-level property that falls back to the role token, so you can pin one component without redefining a role. Role tokens are the public API; these exist for the one-off.\n\n:root {\n  --card-radius: 0.75rem;   /* cards only; everything else stays 0.5rem */\n  --toast-shadow: none;     /* toasts only */\n}\n\n\n\n  \n    \n      Property\n      Falls back to\n    \n  \n  \n    \n      --button-radius, --input-radius, --textarea-radius, --select-radius, --badge-radius, --pagination-radius, --toggle-group-radius, --date-picker-radius, --menu-button-radius, --sidebar-item-radius, --calendar-cell-radius, --combobox-item-radius, --dropdown-menu-item-radius, --toast-action-radius, --toast-close-radius, --drawer-close-radius\n      --control-radius\n    \n    \n      --card-radius, --alert-radius, --table-radius, --stats-radius, --empty-radius, --fieldset-radius, --calendar-radius, --combobox-radius, --dropdown-menu-radius, --menu-button-content-radius, --date-picker-popover-radius, --sidebar-radius, --inset-radius, --avatar-radius, --toast-radius\n      --surface-radius\n    \n    \n      --checkbox-radius\n      --mark-radius\n    \n    \n      --radio-radius, --switch-radius\n      --pill-radius\n    \n    \n      --card-shadow, --stats-shadow\n      --elevation-raised\n    \n    \n      --combobox-shadow, --dropdown-menu-shadow, --menu-button-shadow, --date-picker-popover-shadow, --toast-shadow, --toast-hover-shadow, --drawer-shadow\n      --elevation-overlay\n    \n  \n\n\n\n\nAuditing an Existing Theme\n\nmaquina:doctor scans an app’s CSS, views and JavaScript and prints every place that restates something the token layer now owns, plus the one pattern that breaks outright. It never edits anything.\n\nbin/rails maquina:doctor\n\n\nSee Upgrading for what changed in 0.6.0 and how to keep the 0.5.1 look.\n\n\n\nNext Steps\n\n\n  \n    \n      Upgrading to 0.6.0\n    \n    \n      The preflight shim fix, the scanner, and every breaking change.\n    \n  \n\n  \n    \n      Form Components\n    \n    \n      Where control radius, marks and focus rings show up first."
        },
        {
          "id": "documentation-components-toast",
          "title": "Toast",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/toast/",
          "content": "Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\nUsage\n\n&lt;%= render &quot;components/toast&quot;,\n  title: &quot;Scheduled: Catch up&quot;,\n  description: &quot;Friday, February 10, 2025 at 5:57 PM&quot; %&gt;\n\nExamples\n\nSuccess\n\n&lt;%= render &quot;components/toast&quot;,\n  variant: :success,\n  title: &quot;Success!&quot;,\n  description: &quot;Your changes have been saved.&quot; %&gt;\n\nError\n\n&lt;%= render &quot;components/toast&quot;,\n  variant: :error,\n  title: &quot;Error&quot;,\n  description: &quot;There was a problem with your request.&quot; %&gt;\n\nWarning\n\n&lt;%= render &quot;components/toast&quot;,\n  variant: :warning,\n  title: &quot;Warning&quot;,\n  description: &quot;Your session is about to expire.&quot; %&gt;\n\nWith Action\n\n&lt;%= render &quot;components/toast&quot;,\n  title: &quot;Event Created&quot;,\n  description: &quot;Your event has been scheduled.&quot;,\n  content: capture { %&gt;\n  &lt;%= render &quot;components/toast/action&quot;, label: &quot;Undo&quot;, href: &quot;#&quot; %&gt;\n&lt;% } %&gt;\n\nAPI Reference\n\nToast\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      variant\n      Symbol\n      :default\n      :default, :success, :info, :warning, :error; :destructive is accepted as an alias of :error\n    \n    \n      title\n      String\n      nil\n      Toast title text\n    \n    \n      description\n      String\n      nil\n      Toast description text\n    \n    \n      icon\n      Symbol\n      nil\n      Icon name (auto-selected by variant)\n    \n    \n      duration\n      Integer\n      5000\n      Auto-dismiss time in ms\n    \n    \n      dismissible\n      Boolean\n      true\n      Show close button\n    \n    \n      content\n      String\n      nil\n      HTML content via capture (e.g., action buttons)\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nToast Title\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      text\n      String\n      nil\n      Title text\n    \n    \n      content\n      String\n      nil\n      HTML content via capture\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nToast Description\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      text\n      String\n      nil\n      Description text\n    \n    \n      content\n      String\n      nil\n      HTML content via capture\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nToaster\n\nThe toaster is the container that holds and positions toast notifications. Place it once in your layout.\n\n&lt;%= render &quot;components/toaster&quot;, position: :bottom_right,\n      content: toast_flash_messages %&gt;\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      position\n      Symbol\n      :bottom_right\n      :top_left, :top_right, :bottom_left, :bottom_right\n    \n    \n      content\n      String\n      nil\n      Pre-rendered toasts (e.g., flash messages)\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nToast Action\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      label\n      String\n      required\n      Button/link text\n    \n    \n      href\n      String\n      nil\n      Link URL (renders button if nil)\n    \n    \n      method\n      Symbol\n      nil\n      HTTP method for Turbo\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nHelper Methods\n\n\n  \n    \n      Method\n      Description\n    \n  \n  \n    \n      toast_flash_messages(exclude: [])\n      Renders all flash messages as toasts\n    \n    \n      toast(variant, title, **options)\n      Renders a single toast\n    \n    \n      toast_success(title, **options)\n      Shorthand for success variant\n    \n    \n      toast_error(title, **options)\n      Shorthand for error variant\n    \n    \n      toast_warning(title, **options)\n      Shorthand for warning variant\n    \n    \n      toast_info(title, **options)\n      Shorthand for info variant\n    \n  \n\n\nJavaScript API\n\nThe toaster exposes a global Toast object for creating toasts from JavaScript:\n\nToast.success(\"Message saved!\")\nToast.error(\"Something went wrong\", { description: \"Please try again\" })\nToast.destructive(\"Record deleted\")   // alias of Toast.error\nToast.info(\"New update available\", { duration: 10000 })\nToast.warning(\"Session expiring soon\")\nToast.show(\"Custom message\", { variant: \"default\" })\nToast.dismiss(toastId)\nToast.dismissAll()"
        },
        {
          "id": "documentation-components-toggle-group",
          "title": "Toggle Group",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/toggle-group/",
          "content": "Preview\n    \n\n    \n      \n        \n          \n          \n          \n          \n          \n          \n          \n          \n          \n        \n        \n          \n        \n      \n\n      \n        \n          \n          \n          \n          \n        \n        default\n      \n\n    \n  \n\n  \n    \n\n    \n      \n    \n  \n\n\n\n\nUsage\n\n&lt;%= render &quot;components/toggle_group&quot;, type: :single, value: &quot;center&quot; do %&gt;\n  &lt;%= render &quot;components/toggle_group/item&quot;, value: &quot;left&quot;, aria_label: &quot;Align left&quot; do %&gt;\n    &lt;%= icon_for :align_left, class: &quot;size-4&quot; %&gt;\n  &lt;% end %&gt;\n  &lt;%= render &quot;components/toggle_group/item&quot;, value: &quot;center&quot;, aria_label: &quot;Align center&quot;, pressed: true do %&gt;\n    &lt;%= icon_for :align_center, class: &quot;size-4&quot; %&gt;\n  &lt;% end %&gt;\n  &lt;%= render &quot;components/toggle_group/item&quot;, value: &quot;right&quot;, aria_label: &quot;Align right&quot; do %&gt;\n    &lt;%= icon_for :align_right, class: &quot;size-4&quot; %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nExamples\n\nMultiple Selection\n\n&lt;%= render &quot;components/toggle_group&quot;, type: :multiple, value: [&quot;bold&quot;, &quot;italic&quot;] do %&gt;\n  &lt;%= render &quot;components/toggle_group/item&quot;, value: &quot;bold&quot;, aria_label: &quot;Bold&quot;, pressed: true do %&gt;\n    &lt;%= icon_for :bold, class: &quot;size-4&quot; %&gt;\n  &lt;% end %&gt;\n  &lt;%= render &quot;components/toggle_group/item&quot;, value: &quot;italic&quot;, aria_label: &quot;Italic&quot;, pressed: true do %&gt;\n    &lt;%= icon_for :italic, class: &quot;size-4&quot; %&gt;\n  &lt;% end %&gt;\n  &lt;%= render &quot;components/toggle_group/item&quot;, value: &quot;underline&quot;, aria_label: &quot;Underline&quot; do %&gt;\n    &lt;%= icon_for :underline, class: &quot;size-4&quot; %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nOutline Variant\n\n&lt;%= render &quot;components/toggle_group&quot;, type: :single, variant: :outline do %&gt;\n  &lt;%= render &quot;components/toggle_group/item&quot;, value: &quot;list&quot;, aria_label: &quot;List view&quot; do %&gt;\n    &lt;%= icon_for :list, class: &quot;size-4&quot; %&gt;\n  &lt;% end %&gt;\n  &lt;%= render &quot;components/toggle_group/item&quot;, value: &quot;grid&quot;, aria_label: &quot;Grid view&quot; do %&gt;\n    &lt;%= icon_for :grid, class: &quot;size-4&quot; %&gt;\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nWith Text Labels\n\n&lt;%= render &quot;components/toggle_group&quot;, type: :single, size: :lg do %&gt;\n  &lt;%= render &quot;components/toggle_group/item&quot;, value: &quot;day&quot;, pressed: true do %&gt;\n    Day\n  &lt;% end %&gt;\n  &lt;%= render &quot;components/toggle_group/item&quot;, value: &quot;week&quot; do %&gt;\n    Week\n  &lt;% end %&gt;\n  &lt;%= render &quot;components/toggle_group/item&quot;, value: &quot;month&quot; do %&gt;\n    Month\n  &lt;% end %&gt;\n&lt;% end %&gt;\n\nAPI Reference\n\nToggle Group\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      type\n      Symbol\n      :single\n      :single or :multiple selection\n    \n    \n      variant\n      Symbol\n      :default\n      :default or :outline\n    \n    \n      size\n      Symbol\n      :default\n      :sm, :default, :lg\n    \n    \n      value\n      String/Array\n      nil\n      Initial selected value(s)\n    \n    \n      disabled\n      Boolean\n      false\n      Disable all items\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\n\nToggle Group Item\n\n\n  \n    \n      Parameter\n      Type\n      Default\n      Description\n    \n  \n  \n    \n      value\n      String\n      required\n      Value when selected\n    \n    \n      pressed\n      Boolean\n      false\n      Initial pressed state\n    \n    \n      disabled\n      Boolean\n      false\n      Disable this item\n    \n    \n      aria_label\n      String\n      nil\n      Accessible label for icon-only items\n    \n    \n      css_classes\n      String\n      \"\"\n      Additional CSS classes\n    \n    \n      html_options\n      Hash\n      {}\n      Additional HTML attributes\n    \n  \n\n\nBuilder Helper\n\nThe toggle_group helper renders the group and its items in one call:\n\n&lt;%= toggle_group type: :multiple, variant: :outline do |group| %&gt;\n  &lt;% group.item value: &quot;bold&quot;, icon: :bold, aria_label: &quot;Toggle bold&quot; %&gt;\n  &lt;% group.item value: &quot;italic&quot;, icon: :italic, aria_label: &quot;Toggle italic&quot; %&gt;\n&lt;% end %&gt;\n\nOr fully data-driven with toggle_group_simple:\n\n&lt;%= toggle_group_simple type: :single, items: [\n  { value: &quot;left&quot;, icon: :align_left, aria_label: &quot;Align left&quot; },\n  { value: &quot;center&quot;, icon: :align_center, aria_label: &quot;Align center&quot; },\n  { value: &quot;right&quot;, icon: :align_right, aria_label: &quot;Align right&quot; }\n] %&gt;"
        },
        {
          "id": "documentation-components-upgrading",
          "title": "Upgrading",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/components/upgrading/",
          "content": "What breaks between releases, and what to do about it.\n\n\n\n0.6.1 → 0.7.0\n\nNo breaking changes and nothing to migrate — an accessibility release. One deprecation, and a good deal of host-side code you can now delete.\n\nbundle update maquina-components\n\n\nWhat changes on its own\n\n\n  Focus rings appear instantly. They used to fade in over 150ms from the control’s own text colour, because every component painting a token ring also carried transition-colors and Tailwind v4 folds outline-color into that utility. On a filled variant that meant a near-white ring for the first frames — no visible focus indicator on the highest-stakes controls in a page. If you built your own components against --focus-ring-*, they have the same latent bug; see Theming.\n  Two triggers gain the chevron they never had. The dropdown menu trigger and the combobox trigger both asked for icon names the engine did not ship, and rendered nothing at all. Any as_child trigger you wrote purely to supply a chevron can collapse back to the default path — but keep the ones carrying their own content.\n  A collapsed off-canvas sidebar leaves the tab order, and below 768px the sidebar reserves no layout. Both are structural now.\n  Breadcrumbs collapse on available space. The width measurement never actually fired before — the last item’s flex-shrink absorbed the overflow, so the row reported a perfect fit at every width.\n\n\nDeprecated: collapse_after\n\nresponsive_breadcrumbs(..., collapse_after: 3) still accepts the argument and now ignores it. It existed only to fake collapsing while the measurement was broken, and it collapsed on item count alone — so it also collapsed a trail with plenty of room. Delete it from your calls; it goes away in 0.8.0.\n\nWorkarounds you can delete\n\nSeveral apps carry host-side code for the bugs above. Deleting it is the right outcome, not keeping it:\n\n\n  a restated focus ring on buttons — especially a box-shadow one on a filled variant, which collides with the elevation each variant declares and is clipped by any overflow-hidden ancestor\n  a low-specificity :focus-visible baseline standing in for engine rings that “did not paint”, and any rule restoring the ring on breadcrumb links\n  a controller setting inert on the sidebar when it is off-canvas\n  an unlayered @media (width &lt; 768px) rule forcing the sidebar gap to 0\n\n\n\n\n0.5.1 → 0.6.0\n\nStart here, then run the scanner:\n\nbundle update maquina-components\nbin/rails maquina:doctor\n\n\nmaquina:doctor reads your CSS, views and JavaScript and prints file:line for every pattern this release changes, grouped BREAKING / REVIEW / CLEANUP. It never edits anything and never fails a build.\n\n\n\n1. Your theme.css Preflight Shim Now Flattens Alert and Toast Borders\n\nThis affects every existing app, and it fails silently. The theme.css shipped by earlier installers ends with an unlayered universal rule:\n\n/* 0.5.1 — as installed */\n* {\n  border-color: var(--color-border);\n}\n\n\nIn 0.6.0 the engine’s rules live in @layer components. Unlayered CSS outranks every layer at any specificity, so that one rule now wins over the tinted borders on all alert and toast variants: a destructive alert’s border measures oklch(0.928 0.006 264) — plain --border — where 0.5.1 painted oklch(0.92 0.05 25).\n\nThe generator template is fixed, but the rule lives in your file. Wrap it:\n\n/* 0.6.0 — one line of nesting */\n@layer base {\n  * {\n    border-color: var(--color-border);\n  }\n}\n\n\nmaquina:doctor reports this as breaking / unlayered-universal-rule. The same applies to any other unlayered * rule you have added.\n\n\n\n2. Utilities Passed Through css_classes Now Win\n\nEvery engine rule is flattened to specificity 0,1,0 and layered, so a Tailwind utility passed as css_classes: finally takes effect. It used to be silently swallowed — which means utilities you already pass may start applying.\n\n&lt;%= render \"components/form\", css_classes: \"flex\" do %&gt;\n\n\n\n  \n    \n      Site\n      0.5.1\n      0.6.0\n    \n  \n  \n    \n      Input with a width utility\n      448px\n      137px\n    \n    \n      Form actions with a hidden utility\n      display: flex\n      display: none\n    \n    \n      Form with a flex utility\n      display: grid\n      display: flex\n    \n  \n\n\nSearch your views for css_classes: before upgrading. Anything you passed as decoration and never saw is now live; delete what you did not mean.\n\n\n\n3. Radius and Elevation Defaults Normalize\n\nRadius now comes from four role tokens. Eight sites move:\n\n\n  \n    \n      Component / part\n      0.5.1\n      0.6.0\n    \n  \n  \n    \n      [data-component=\"card\"]\n      12px\n      8px\n    \n    \n      [data-sidebar-part=\"inset\"] (variant inset)\n      12px\n      8px\n    \n    \n      [data-sidebar-part=\"inset\"] [data-component=\"header\"] top corners\n      12px\n      8px\n    \n    \n      [data-combobox-part=\"content\"] popover\n      6px\n      8px\n    \n    \n      [data-dropdown-menu-part=\"content\"] popover\n      6px\n      8px\n    \n    \n      [data-combobox-part=\"option\"]\n      4px\n      6px\n    \n    \n      [data-dropdown-menu-part=\"item\"]\n      4px\n      6px\n    \n    \n      [data-toast-part=\"close\"]\n      4px\n      6px\n    \n  \n\n\nFour elevation sites collapse from shadow-lg to --elevation-overlay, which resolves to shadow-md: the toast, the toast on hover, the drawer panel and the date-picker popover.\n\nEach site keeps a component-level escape hatch, so any one of them can be pinned without redefining a role. See Theming, or take the whole block from the appendix below.\n\n\n\n4. Focus Rings Are Outlines, and Buttons Finally Have Them\n\nThree changes in one:\n\n/* 0.5.1 — a box-shadow ring, on :focus as well as :focus-visible */\n[data-component=\"input\"]:focus,\n[data-component=\"input\"]:focus-visible {\n  box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring);\n}\n\n/* 0.6.0 onward — an outline, keyboard focus only, from tokens.\n   Written as longhands since 0.7.0: the shorthand is invalid at\n   computed-value time as a unit, so one unresolvable var() took the\n   whole ring down and left outline-color: currentColor behind. */\n[data-component=\"input\"]:focus-visible {\n  outline-width: var(--focus-ring-width);\n  outline-style: var(--focus-ring-style);\n  outline-color: var(--focus-ring-color);\n  outline-offset: var(--focus-ring-offset);\n}\n\n\n\n  Form fields no longer ring on a mouse click. The bare :focus half of each :focus, :focus-visible pair is gone; keyboard focus still rings.\n  Rings are outline + outline-offset, uniformly 3px at offset 0. The sites that faked a backdrop band with 0 0 0 2px var(--background), 0 0 0 4px var(--ring) lose the band. An outline cannot be clipped by an ancestor’s overflow and never affects layout, which is why the drawer and the sidebar could not use a ring before.\n  Six button variants gain a ring they never had. :focus-visible used to be declared before the variant rules at equal specificity, so every variant that set a background overwrote it: 2 of 16 buttons on the specimen page actually ringed. If your app restated a ring on buttons to work around this, delete it.\n\n\nIf a custom component of yours keys off the engine’s ring, read the tokens instead: --focus-ring-width, --focus-ring-offset, --focus-ring-style, --focus-ring-color.\n\n\n\n5. merge_component_data Precedence Narrows\n\nThe component used to win every key it set. Now it wins only its identity keys: :component, :variant, :size, and any key ending in _part or -part. :controller and :action still concatenate — the component’s tokens first, then yours. Everything else the caller wins.\n\n&lt;%# 0.5.1: the toast's own state won, this did nothing %&gt;\n&lt;%# 0.6.0: renders data-state=\"exiting\" %&gt;\n&lt;%= render \"components/toast\", title: \"Saved\", data: { state: \"exiting\" } %&gt;\n\n\nThe merged hash is also .compacted, so a nil value emits no attribute at all where it used to emit an empty one. false still renders \"false\" — that is a value, not an absence.\n\nRelated, and also reported by the doctor as breaking: a sidebar item now omits data-active entirely when it is inactive, instead of writing data-active=\"false\". Presence selectors no longer match:\n\n/* before */ [data-sidebar-part=\"menu-button\"][data-active] { }\n/* after  */ [data-sidebar-part=\"menu-button\"][data-active=\"true\"] { }\n\n\n&lt;!-- before --&gt; &lt;a data-[active]:bg-accent&gt;\n&lt;!-- after  --&gt; &lt;a data-[active=true]:bg-accent&gt;\n\n\n\n\n6. Surfaces Above the Page Stop Painting the Page Color\n\nAn alert, a calendar and the date-picker popover painted --background — the page. Anything floating above the page is a surface, so they now paint --card or --popover.\n\nIf your theme sets those to the same value, nothing moves. That is exactly why this went unnoticed: in the default light theme all three are white. In the default dark theme they separate.\n\nalert, calendar background (dark)   oklch(0.13 0.028 261) → oklch(0.178 0.032 260)\n\n\nMeasured the old way, the calendar sat at ΔL 0.00 against the page — an invisible surface. Related: the outline and ghost buttons and the active pagination link now paint transparent instead of --background, so they work inside a card, which they previously did not.\n\nTo pin the old behavior, point the surface tokens at the page:\n\n:root {\n  --popover: var(--background);\n  --card: var(--background);\n}\n\n\n\n  Checking surface-against-surface contrast? Use ΔL on the CIE L* axis, not a WCAG ratio. WCAG contrast is a text metric; on two adjacent large surfaces it reads a misleading ~1.1 and tells you nothing.\n\n\n\n\n7. Tinted Badges Lose a Stray Hairline\n\nBadge’s success / warning / destructive variants have always set border-color: transparent. The unlayered * shim from step 1 was overriding it with --border, so those badges carried a grey 1px outline they were never meant to have. Once the shim is layered, the intended transparent border shows through.\n\nNothing to do — but if you had compensated for the hairline elsewhere, remove the compensation.\n\n\n\nAppendix: Keeping the 0.5.1 Look\n\nEverything above is a value, so a single token block reverts the visual changes. Drop this into your theme.css and delete the lines you do not want.\n\n:root {\n  /* Radius — the eight sites that moved */\n  --card-radius: 0.75rem;\n  --inset-radius: 0.75rem;\n  --combobox-radius: 0.375rem;\n  --dropdown-menu-radius: 0.375rem;\n  --combobox-item-radius: 0.25rem;\n  --dropdown-menu-item-radius: 0.25rem;\n  --toast-close-radius: 0.25rem;\n\n  /* Elevation — the four sites that collapsed shadow-lg → shadow-md */\n  --toast-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);\n  --toast-hover-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);\n  --drawer-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);\n  --date-picker-popover-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);\n\n  /* Focus ring — the closest outline equivalent of the old two-step ring */\n  --focus-ring-width: 2px;\n  --focus-ring-offset: 2px;\n}\n\n\nTwo things this block cannot bring back, because they are not values:\n\n\n  The backdrop band. The old ring drew --background under --ring inside a single box-shadow; an outline is one line. --focus-ring-offset: 2px leaves the same gap, showing whatever is actually behind the control.\n  The mouse-click ring on form fields, and the absent ring on five button variants. Both were :focus-visible bugs, and both are fixed on purpose.\n\n\nRunning bin/rails generate maquina_components:install again is safe: it is idempotent, appends the shape/state token block only once, and never rewrites your palette.\n\n\n\nNext Steps\n\n\n  \n    \n      Theming\n    \n    \n      The full token table, ready-made themes, and pinning a single component.\n    \n  \n\n  \n    \n      Components Overview\n    \n    \n      Installation, setup, and the full component index."
        },
        {
          "id": "documentation-engines",
          "title": "Engines",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/engines/",
          "content": "Mountable Rails engines that add complete features to your app. Mount under a backstage path, configure a few options, and get production-ready functionality themed with maquina_components.\n\n\n\nAvailable Engines\n\n\n  \n    \n      Maquina Newsletters\n    \n    \n      Draft, approve, schedule, and batch-send HTML newsletters from a backstage area.\n    \n  \n\n\n\n\nWhat Is a Mountable Engine?\n\nA Rails engine is a miniature application that plugs into a host app. You mount it at a path in config/routes.rb, run its installer, and it brings its own models, controllers, views, and background jobs — while reusing your app’s database, authentication, and configuration.\n\n# config/routes.rb\nmount MaquinaNewsletters::Engine =&gt; \"/backstage/newsletters\"\n\n\nMaquina engines keep authentication in the host app’s hands (they inherit from a base controller you configure) and theme their UI with maquina_components, so they look like a native part of your app."
        },
        {
          "id": "documentation-engines-maquina-newsletters",
          "title": "Maquina Newsletters",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/engines/maquina-newsletters/",
          "content": "A mountable Rails 8 engine for drafting, approving, scheduling, and batch-sending HTML newsletters. Compose with Action Text, gate sends behind an approval step, schedule deliberately, and let a background job deliver in batches — all from a backstage area inside your own app.\n\n\n\nWhat Is This?\n\nMaquina Newsletters is a Rails engine you mount under a backstage path (e.g. /backstage/newsletters). It gives you a complete newsletter lifecycle without bringing in an external service:\n\n\n  Draft and edit with the Action Text rich-text editor — Trix or Lexxy, your choice\n  Image attachments and embeds via Active Storage\n  Approval workflow — a newsletter can’t be sent straight from a draft\n  Deliberate scheduling — pick the date, time, and batch size in one explicit step\n  Batch sending — split delivery across days, or send to everyone at once\n  Test sends to any address, plus a send-now override\n  Per-issue exclusion list to drop specific recipients\n\n\nThe engine renders its own UI (themed with maquina_components) and resolves recipients from a model and scope you configure. Authentication stays in your hands — the engine inherits from a base controller you point it at.\n\n\n\nRequirements\n\n\n  Rails 8\n  Active Storage and Action Text configured in the host app\n  image_processing (~&gt; 2.0) plus an image processor — ruby-vips (recommended) or mini_magick\n  A system image library: libvips (recommended) or ImageMagick\n    \n      macOS: brew install vips (or brew install imagemagick)\n      Debian/Ubuntu: apt-get install libvips (or apt-get install imagemagick)\n    \n  \n  tailwindcss-rails (engine UI theming) and maquina_components (host theming)\n  lexxy — optional, for the Lexical-based editor (Rails 8.1+ can auto-configure it)\n\n\n\n\nQuick Start\n\n1. Add the Gem\n\n# Gemfile\ngem \"maquina_newsletters\", \"~&gt; 1.5\"\n\n\nbundle install\n\n\n2. Mount the Engine\n\n# config/routes.rb\nmount MaquinaNewsletters::Engine =&gt; \"/backstage/newsletters\"\n\n\n3. Run the Installer\n\nbin/rails generate maquina_newsletters:install\nbin/rails db:migrate\n\n\nThe installer sets up the engine’s migrations and, if they aren’t already present, runs active_storage:install and action_text:install for you.\n\n4. Add Image Processing\n\n# Gemfile\ngem \"image_processing\", \"~&gt; 2.0\"\ngem \"ruby-vips\" # or: gem \"mini_magick\"\n\n\nThen bundle install and install the system library (see Requirements).\n\n5. Wire Up Tailwind\n\n/* app/assets/tailwind/application.css */\n@import \"tailwindcss\";\n@import \"../builds/tailwind/maquina_newsletters\";\n\n\nbin/rails tailwindcss:build   # or tailwindcss:watch in development\n\n\nKeep app/assets/builds/* in .gitignore and rebuild on each machine.\n\n6. Set the Mailer Host\n\nSo image URLs in delivered emails are absolute:\n\n# config/environments/production.rb\nconfig.action_mailer.default_url_options = { host: \"newsletters.example.com\" }\n\n\n\n\nConfiguration\n\nCreate an initializer to tell the engine who receives newsletters and how it’s protected:\n\n# config/initializers/maquina_newsletters.rb\nMaquinaNewsletters.configure do |config|\n  # Recipient resolution — which records receive a newsletter.\n  config.recipient_model      = \"User\"          # constantized at use-time\n  config.recipient_scope      = :active         # a scope returning a relation\n  config.recipient_email_attr = :email_address  # the email column\n\n  # Base controller — see \"Authentication\" below.\n  config.base_controller_class = \"BackstageController\"\n\n  # Optional HTTP Basic Auth (off by default)\n  config.http_basic_auth_enabled  = true\n  config.http_basic_auth_user     = ENV[\"NEWSLETTERS_USER\"]\n  config.http_basic_auth_password = ENV[\"NEWSLETTERS_PASSWORD\"]\nend\n\n\nIf the initializer is absent, the defaults are:\n\n\n  \n    \n      Setting\n      Default\n    \n  \n  \n    \n      recipient_model\n      \"User\"\n    \n    \n      recipient_scope\n      :active\n    \n    \n      recipient_email_attr\n      :email_address\n    \n    \n      base_controller_class\n      \"ActionController::Base\"\n    \n    \n      HTTP Basic Auth\n      disabled\n    \n  \n\n\n\n\nAuthentication\n\nThe engine does not provide authentication — that’s the host app’s job. Every engine controller inherits from a base controller you configure by name:\n\nconfig.base_controller_class = \"BackstageController\"\n\n\nPoint it at an already-authenticated controller in your app (session checks, etc.) and every engine route is protected automatically.\n\nFor apps whose base controller doesn’t authenticate, the engine ships an optional HTTP Basic Auth fallback:\n\n\n  Enabled with credentials — challenges with HTTP Basic Auth\n  Enabled without credentials — fails closed (401 on every request)\n  Disabled — no built-in auth; relies on the base controller\n\n\nDon’t stack both methods — pick one.\n\n\n\nThe Newsletter Lifecycle\n\nA newsletter moves through four states:\n\n\n  \n    \n      State\n      What happens\n    \n  \n  \n    \n      Draft\n      Create and edit content (subject + Action Text body). Saving creates a draft; no send time is set.\n    \n    \n      Approved\n      Approve a draft when it’s ready. You can’t send from a draft.\n    \n    \n      Scheduled\n      On an approved issue, set the send timing and batch size.\n    \n    \n      Sending → Sent\n      A background job delivers. A sending/sent issue can’t be edited.\n    \n  \n\n\nYou can move backward too: Back to draft (from approved/scheduled/sent) and Unschedule (from scheduled back to approved).\n\nScheduling\n\nThe schedule form appears on an approved issue and takes three inputs:\n\n\n  Date — date picker, today onward (no past dates)\n  Time — 8:00 AM to 8:00 PM in 30-minute increments\n  Batch size — recipients per batch. 0 sends to everyone at once; a positive number splits the send across days, one batch per day.\n\n\nIf the chosen date/time has already passed, it auto-rolls forward to the next 30-minute slot and the confirmation says so. Once scheduled, a read-only summary shows Recipients / Scheduled at / Batch size / Sent at.\n\nSend Now &amp; Test Send\n\n\n  Send now — an overflow (⋮) action that delivers immediately to all recipients behind a confirmation, bypassing scheduling.\n  Test send — available while drafting/approving/scheduling. Sends exactly one email to any address you type, ignoring batch size and schedule, without changing the issue’s state. Ideal for preview validation.\n\n\nRecipients\n\nRecipients are resolved at send time from your configured model and scope (e.g. User.active), minus the per-issue exclusion list. The resulting addresses are downcased, de-duplicated, and sorted for stable batching.\n\n\n\nEditors\n\nThe host chooses the Action Text editor via config.action_text.editor:\n\n\n  :trix — Rails default, no extra setup\n  :lexxy — Lexical-based; install the lexxy gem and wire up its JS/CSS\n\n\nFor Lexxy with importmaps:\n\n# config/importmap.rb\npin \"lexxy\", to: \"lexxy.js\"\npin \"@rails/activestorage\", to: \"activestorage.esm.js\"\n\n\n// app/javascript/application.js\nimport * as ActiveStorage from \"@rails/activestorage\"\nimport \"lexxy\"\nActiveStorage.start()\n\n\n&lt;%# in your layout, after the CSS build %&gt;\n&lt;%= stylesheet_link_tag \"lexxy\" %&gt;\n\n\nOn Rails 8.1, installing the lexxy gem auto-sets config.action_text.editor = :lexxy; set it to :trix to override.\n\n\n\nNext Steps\n\n\n  \n    \n      GitHub Repository\n    \n    \n      Source code, issues, and contribution guidelines.\n    \n  \n\n  \n    \n      Maquina Components\n    \n    \n      The UI library that themes the engine's backstage views.\n    \n  \n\n  \n    \n      Maquina Generators\n    \n    \n      Generate the authentication that protects your backstage."
        },
        {
          "id": "documentation-generators",
          "title": "Maquina Generators",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/generators/",
          "content": "Rails generators that produce standalone application code. No runtime dependency — generate once, own the code forever. Delete the gem when you’re done.\n\n\n\nWhat Is This?\n\nAfter rails new, every developer follows the same steps: configure authentication, set up request throttling, wire up a job queue, add error tracking. These steps aren’t gaps in the framework — they’re workflow choices that are repetitive and time-consuming.\n\nMaquina Generators make the post-rails new setup as deterministic as the framework itself. The gem is development-only. Everything it produces lives in your app and is yours to modify.\n\nrails new myapp --css tailwind\nbundle add maquina-generators --group development\nrails generate maquina:app --auth clave\nbin/rails db:migrate\nbin/dev\n\n\nFive commands. Auth, multi-tenancy, roles, job queue, error tracking, request protection — all generated, all yours.\n\n\n\n\n\nQuick Start\n\n1. Create a Rails App\n\nrails new myapp --css tailwind\ncd myapp\n\n\n2. Add the Gem\n\nbundle add maquina-generators --group development\n\n\n3. Run the App Generator\n\nrails generate maquina:app --auth registration\n\n\n4. Finish Setup\n\nbin/rails db:migrate\nbin/rails credentials:edit\n# Add: backstage: { username: admin, password: your_password }\nbin/dev\n\n\n\n\nAvailable Generators\n\n\n  \n    \n      Generator\n      Command\n      Purpose\n    \n  \n  \n    \n      App\n      rails g maquina:app\n      Full application setup (orchestrator)\n    \n    \n      Clave\n      rails g maquina:clave\n      Passwordless email-code authentication\n    \n    \n      Registration\n      rails g maquina:registration\n      Password-based auth with accounts and roles\n    \n    \n      Rack Attack\n      rails g maquina:rack_attack\n      Request protection and IP throttling\n    \n    \n      Solid Queue\n      rails g maquina:solid_queue\n      Background job processing\n    \n    \n      Solid Errors\n      rails g maquina:solid_errors\n      Error tracking dashboard\n    \n    \n      Mission Control\n      rails g maquina:mission_control_jobs\n      Job queue monitoring dashboard\n    \n  \n\n\n\n\nThe App Generator\n\nThe orchestrator. Runs after rails new and configures a complete, production-ready application in a single command.\n\nrails g maquina:app --auth clave --prefix /admin --port 3000\n\n\nWhat It Does\n\n\n  Adds gems — brakeman, standard, rails-i18n, maquina-components, aws-sdk-s3\n  Creates configs — Procfile.dev, .rubocop.yml, .standard.yml\n  Configures environments — letter_opener for dev, APPLICATION_HOST for production\n  Installs Rails features — Action Text, Active Storage, Turbo morphing\n  Runs auth generator — your choice of clave, registration, or none\n  Runs sub-generators — rack_attack, solid_queue, mission_control_jobs, solid_errors\n  Installs Solid adapters — Solid Queue, Solid Cache, Solid Cable, Solid Errors\n  Installs Maquina Components — UI library ready to use\n  Creates HomeController — with root route\n  Sets up multi-database — primary, queue, cache, cable, errors\n\n\nOptions\n\n\n  \n    \n      Option\n      Default\n      Description\n    \n  \n  \n    \n      --auth\n      none\n      Authentication: none, clave, or registration\n    \n    \n      --prefix\n      /admin\n      URL prefix for ops dashboards\n    \n    \n      --port\n      3000\n      Development server port\n    \n  \n\n\nGenerated Database Configuration\n\ndevelopment:\n  primary:\n    database: storage/development.sqlite3\n  queue:\n    database: storage/development_queue.sqlite3\n  cache:\n    database: storage/development_cache.sqlite3\n  cable:\n    database: storage/development_cable.sqlite3\n  errors:\n    database: storage/development_errors.sqlite3\n\n\n\n\nAuthentication: Clave (Passwordless)\n\nComplete passwordless authentication using email verification codes. Users receive a 6-digit code via email to sign in — no passwords to manage, no password resets to build.\n\nrails g maquina:clave\n\n\nWhat You Get\n\nModels:\n\n  Account — multi-tenant container (has_many :users)\n  User — with role enum (member/admin), account association, blocking support\n  Session — browser session tracking with IP and user agent\n  EmailVerification — verification codes with expiry and attempt tracking\n  Current — ActiveSupport::CurrentAttributes with session, user, and account\n\n\nControllers:\n\n  SessionsController — email entry for sign-in\n  Session::VerificationsController — code verification\n  Session::VerificationResendsController — resend with 15-minute cooldown\n  RegistrationsController — account creation (optional)\n  Registration verification controllers\n\n\nAdditional:\n\n  VerificationMailer — HTML + text email templates\n  AuthenticationCleanupJob — daily cleanup of expired sessions and codes\n  SessionTestHelper — sign_in_as(user) and sign_out for tests\n  Full i18n support (English and Spanish)\n\n\nHow It Works\n\nUser enters email → receives 6-digit code → enters code → signed in\n\n\n\n  Codes expire in 15 minutes\n  15-minute cooldown before resend\n  Rate limited: 10 attempts per 3 minutes\n  Sessions last 30 days (configurable)\n  + characters blocked in emails to prevent alias attacks\n\n\nMulti-Tenancy\n\nEvery user belongs to an Account. The first user who creates an account becomes its admin.\n\n# Access anywhere in your app\nCurrent.user          # The signed-in user\nCurrent.account       # The user's account\nCurrent.user.admin?   # Check role\n\n\nScoping Queries\n\nclass ProjectsController &lt; ApplicationController\n  def index\n    @projects = Current.account.projects\n  end\n\n  def create\n    @project = Current.account.projects.build(project_params)\n    # ...\n  end\n\n  private\n\n  def set_project\n    @project = Current.account.projects.find(params[:id])\n  end\nend\n\n\nOptions\n\n\n  \n    \n      Option\n      Default\n      Description\n    \n  \n  \n    \n      --skip-views\n      false\n      Skip view templates\n    \n    \n      --skip-registration\n      false\n      Skip sign-up flow (sign-in only)\n    \n  \n\n\n\n\nAuthentication: Registration (Password-Based)\n\nPassword-based authentication that builds on Rails 8’s built-in rails generate authentication. Adds multi-tenancy with an Account model, user roles, and a registration flow.\n\nrails g maquina:registration\n\n\nWhat It Adds to Rails Auth\n\nRails 8’s authentication generator gives you login but no signup. Registration adds:\n\n\n  Account model with has_many :users\n  User gains belongs_to :account and role enum (admin/member)\n  Current.account delegation\n  RegistrationsController — creates Account + User in a single transaction\n  Tailwind-styled views\n  English and Spanish translations\n\n\nGenerated Models\n\nclass Account &lt; ApplicationRecord\n  has_many :users, dependent: :destroy\n  validates :name, presence: true\nend\n\nclass User &lt; ApplicationRecord\n  has_secure_password\n  has_many :sessions, dependent: :destroy\n  belongs_to :account\n  validates :name, presence: true\n  enum :role, { member: \"member\", admin: \"admin\" }, default: \"member\"\nend\n\nclass Current &lt; ActiveSupport::CurrentAttributes\n  attribute :session\n  delegate :user, to: :session, allow_nil: true\n  delegate :account, to: :user, allow_nil: true\nend\n\n\nRegistration Flow\n\nclass RegistrationsController &lt; ApplicationController\n  allow_unauthenticated_access\n  rate_limit to: 10, within: 3.minutes, only: :create\n\n  def create\n    ActiveRecord::Base.transaction do\n      account = Account.create!(name: params[:account_name])\n      user = account.users.create!(\n        name: params[:name],\n        email_address: params[:email_address],\n        password: params[:password],\n        role: :admin\n      )\n    end\n    start_new_session_for user\n    redirect_to root_path\n  end\nend\n\n\nOptions\n\n\n  \n    \n      Option\n      Default\n      Description\n    \n  \n  \n    \n      --skip-views\n      false\n      Skip view templates\n    \n  \n\n\n\n\nRack Attack\n\nRequest protection with sensible defaults. Blocks common attack vectors and throttles abusive requests.\n\nrails g maquina:rack_attack\n\n\nDefault Protections\n\nBlocklists:\n\n  PHP files and WordPress paths\n  Sensitive files (.env, .git, .aws, .ssh)\n  Scanner targets (/cgi-bin, /phpmyadmin, /actuator, /debug)\n\n\nThrottles:\n\n  General: 300 requests per 5 minutes per IP (assets exempt)\n  Login: 5 attempts per 20 seconds per IP\n\n\nSafelists:\n\n  Localhost (127.0.0.1, ::1)\n\n\nAll rules live in config/initializers/rack_attack.rb. Edit directly.\n\n\n\nSolid Queue\n\nSets up Solid Queue as your Active Job backend with a separate database and Procfile integration.\n\nrails g maquina:solid_queue --database sqlite3\n\n\nConfiguration\n\n# config/solid_queue.yml\ndefault: &amp;default\n  dispatchers:\n    - polling_interval: 1\n      batch_size: 500\n  workers:\n    - queues: \"*\"\n      threads: 3\n      polling_interval: 0.1\n  recurring:\n    authentication_cleanup:\n      class: AuthenticationCleanupJob\n      schedule: every day at 3am\n\n\nOptions\n\n\n  \n    \n      Option\n      Default\n      Description\n    \n  \n  \n    \n      --database\n      sqlite3\n      Database adapter (sqlite3 or postgresql)\n    \n  \n\n\n\n\nSolid Errors\n\nError tracking dashboard with custom Tailwind views and HTTP basic auth.\n\nrails g maquina:solid_errors --prefix /admin\n\n\nWhat You Get\n\n\n  Custom Tailwind-styled error views\n  HTTP basic auth (credentials-first, ENV fallback)\n  Severity badge helpers\n  Clipboard and backtrace filter Stimulus controllers\n  Shared admin navigation bar\n\n\nAuthentication\n\n# Checks in order:\n# 1. Rails.application.credentials.backstage.username / .password\n# 2. ENV[\"SOLID_ERRORS_USER\"] / ENV[\"SOLID_ERRORS_PASSWORD\"]\n\n\nSet up credentials:\n\nbin/rails credentials:edit\n\n\nbackstage:\n  username: admin\n  password: your_secure_password\n\n\nOptions\n\n\n  \n    \n      Option\n      Default\n      Description\n    \n  \n  \n    \n      --prefix\n      required\n      URL prefix (e.g., /admin)\n    \n    \n      --user-env-var\n      SOLID_ERRORS_USER\n      Custom env var for username\n    \n    \n      --password-env-var\n      SOLID_ERRORS_PASSWORD\n      Custom env var for password\n    \n    \n      --copy-views\n      true\n      Include custom Tailwind views\n    \n  \n\n\n\n\nMission Control Jobs\n\nJob queue monitoring dashboard with custom Tailwind views. 41 view files styled to match your application.\n\nrails g maquina:mission_control_jobs --prefix /admin\n\n\nWhat You Get\n\n\n  Full Tailwind-styled dashboard for Solid Queue\n  Job status badges, queue views, worker monitoring\n  Recurring task management\n  Shared admin navigation (links to Solid Errors)\n  HTTP basic auth (same credentials as Solid Errors)\n\n\nOptions\n\n\n  \n    \n      Option\n      Default\n      Description\n    \n  \n  \n    \n      --prefix\n      required\n      URL prefix (e.g., /admin)\n    \n    \n      --user-env-var\n      MISSION_CONTROL_JOBS_USER\n      Custom env var for username\n    \n    \n      --password-env-var\n      MISSION_CONTROL_JOBS_PASSWORD\n      Custom env var for password\n    \n    \n      --copy-views\n      true\n      Include custom Tailwind views\n    \n  \n\n\n\n\nArchitecture Overview\n\nAfter running maquina:app --auth clave, your project structure looks like this:\n\napp/\n  controllers/\n    concerns/\n      authentication.rb          # Session management\n    sessions_controller.rb       # Sign-in\n    registrations_controller.rb  # Sign-up\n    home_controller.rb           # Root page\n  models/\n    account.rb                   # Multi-tenant container\n    user.rb                      # Roles + auth\n    current.rb                   # Request context\n    session.rb                   # Browser sessions\n    email_verification.rb        # Verification codes\n  mailers/\n    verification_mailer.rb       # Email codes\n  jobs/\n    authentication_cleanup_job.rb  # Daily cleanup\n\nconfig/\n  initializers/\n    rack_attack.rb               # Request protection\n    solid_errors.rb              # Error tracking auth\n    mission_control.rb           # Job dashboard auth\n  solid_queue.yml                # Queue configuration\n\n\nSecurity Defaults\n\n\n  Rate limiting on registration and login\n  Rack Attack blocks scanners and bots\n  All controllers require authentication by default\n  Account scoping prevents cross-tenant data access\n\n\nOps Dashboards\n\n\n  /admin/solid_errors — error tracking\n  /admin/mission_control_jobs — job queue monitoring\n\n\nBoth protected with HTTP basic auth using shared backstage credentials.\n\n\n\nRole-Based Authorization\n\nUse the role enum to restrict actions:\n\nclass ProjectsController &lt; ApplicationController\n  before_action :require_admin, only: [:destroy]\n\n  private\n\n  def require_admin\n    unless Current.user.admin?\n      redirect_to projects_path, alert: t(\"flash.general.forbidden\")\n    end\n  end\nend\n\n\nRoles:\n\n  admin — first user created with account, full access\n  member — default role, restricted from destructive actions\n\n\n\n\nCustomization\n\nAll generated code lives in your app. Common customization points:\n\n\n  \n    \n      What\n      Where\n    \n  \n  \n    \n      Redirect after login\n      app/controllers/concerns/authentication.rb → after_authentication_url\n    \n    \n      Session duration\n      Change 30.days.from_now in authentication.rb\n    \n    \n      Code expiration\n      Change 15.minutes.from_now in verification controllers\n    \n    \n      Resend cooldown\n      EmailVerification::COOLDOWN_MINUTES (default: 15)\n    \n    \n      View styling\n      Edit view templates directly\n    \n    \n      Email sender\n      app/mailers/verification_mailer.rb\n    \n    \n      Translations\n      config/locales/clave.*.yml or registration.*.yml\n    \n    \n      Rack Attack rules\n      config/initializers/rack_attack.rb\n    \n    \n      Dashboard credentials\n      bin/rails credentials:edit → backstage:\n    \n    \n      Queue config\n      config/solid_queue.yml\n    \n  \n\n\n\n\nRequirements\n\n\n  Ruby &gt;= 3.2.0\n  Rails &gt;= 7.2\n  Tailwind CSS (for generated views)\n\n\nThe gem has zero runtime dependencies. Add it to your development group, generate your code, and remove it.\n\n# Gemfile\ngroup :development do\n  gem \"maquina-generators\"\nend\n\n\n\n\nNext Steps\n\n\n  \n    \n      GitHub Repository\n    \n    \n      View source code and contribute.\n    \n  \n\n  \n    \n      Maquina Components\n    \n    \n      UI components installed by the app generator.\n    \n  \n\n  \n    \n      Rails Simplifier\n    \n    \n      Keep generated code idiomatic with 37signals patterns.\n    \n  \n\n  \n    \n      Rails MCP Server\n    \n    \n      Give AI visibility into your generated codebase."
        },
        {
          "id": "documentation",
          "title": "Documentation",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/",
          "content": "Maquina is a growing collection of open-source tools extracted from production Rails applications. No complex build pipelines. No framework fatigue.\n\n\n\nWhy Maquina?\n\nRails developers who ship alone need tools that work together without adding complexity. Maquina provides:\n\n\n  Generators — Production-ready app scaffolding. Authentication, job queues, error tracking, and security in one command.\n  UI Components — ERB partials styled with Tailwind CSS 4.0. No React, no build step.\n  Engines — Mountable Rails engines that drop complete features into your app.\n  AI Tools — MCP servers and Claude Code plugins that understand your Rails codebase.\n  Developer Utilities — Menu bar apps and CLI tools for your local environment.\n\n\nAll projects are MIT licensed, extracted from production apps, and built for the Rails way.\n\n\n\nPhilosophy\n\nNoBuild\n\nNo complex JavaScript build pipelines. Ship CSS and JS directly with importmaps and Tailwind CSS. Every Maquina tool follows this principle.\n\nSingle Developer\n\nOne developer can build and maintain the entire application. Tools should reduce complexity, not add it.\n\nCRUD Excellence\n\nMost applications are CRUD at their core. Maquina tools make common patterns elegant and maintainable.\n\n\n\nProjects\n\nGenerators\n\nRails generators that produce standalone application code with no runtime dependency. Authentication (passwordless or password-based), multi-tenancy, Rack Attack, Solid Queue, error tracking, and job dashboards — all configured in a single command.\n\nbundle add maquina_generators --group development\nrails generate maquina:app --auth clave\n\n\nView Generators Documentation\n\nUI Components\n\nProduction-ready components for Rails applications. ERB partials with strict locals, Tailwind CSS 4.0 styling, and Stimulus controllers only where needed.\n\nbundle add maquina_components\nrails generate maquina_components:install\n\n\nBrowse Components\n\nEngines\n\nMountable Rails engines that add complete features to your app. Mount under a backstage path, run the installer, and get production-ready functionality themed with maquina_components.\n\n# config/routes.rb\nmount MaquinaNewsletters::Engine =&gt; \"/backstage/newsletters\"\n\n\nView Engines\n\nAI Tools\n\nMCP servers and Claude Code plugins that let AI assistants understand your Rails projects. Analyze models, routes, schemas, simplify code, and coordinate changes across your editor.\n\n\n  \n    \n      Tool\n      Type\n      Purpose\n    \n  \n  \n    \n      Rails MCP Server\n      MCP Server\n      Let LLMs analyze your Rails codebase\n    \n    \n      Neovim MCP Server\n      MCP Server\n      Coordinate buffer changes with AI assistants\n    \n    \n      Rails Simplifier\n      Plugin\n      Code simplification with 37signals patterns\n    \n    \n      Rails Upgrade Assistant\n      Plugin\n      Generate upgrade guides for Rails 7.0 through 8.1\n    \n    \n      Maquina UI Standards\n      Plugin\n      Build consistent UIs with maquina_components\n    \n  \n\n\nBrowse all AI Tools →\n\nDeveloper Tools\n\nMenu bar apps and CLI utilities for your local development environment.\n\n\n  \n    \n      Tool\n      Purpose\n    \n  \n  \n    \n      Redis Menu\n      Manage local Redis instances from your menu bar\n    \n    \n      Mongo Menu\n      Manage local MongoDB instances from your menu bar\n    \n    \n      Git Continuity\n      Transfer work-in-progress between machines\n    \n  \n\n\nBrowse all Developer Tools →\n\n\n\nCommunity\n\nAll projects are on GitHub under the maquina-app organization. Issues, pull requests, and contributions welcome."
        },
        {
          "id": "documentation-nexo-concurrency",
          "title": "Concurrency",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/nexo/concurrency/",
          "content": "Async is entirely optional. Nexo installs and runs synchronously with no async gem present, and only complains if you actually use a concurrency feature. The async gem is a soft dependency — add it yourself when you want fan-out:\n\ngem \"async\", \"~&gt; 2.0\"\n\n\nTwo facts make this cheap:\n\n\n  LLM calls are already async-compatible. ruby_llm speaks HTTP over Faraday’s net/http adapter, which yields on socket I/O under Ruby’s fiber scheduler. Loops::RubyLLM therefore runs unchanged inside a reactor — no API change, no rewrite. Wrapping a single agent.prompt in Async {} gains nothing; async only pays off under fan-out.\n  The value Nexo adds is rate-bounded fan-out. Nexo.concurrent bounds in-flight work so you don’t trip provider rate limits, and propagates the first error instead of swallowing it.\n\n\n\n\nNexo.concurrent — bounded fan-out\n\n# 100 docs, but never more than 8 provider calls in flight; results in doc order.\nresults = Nexo.concurrent(max_in_flight: 8) do |c|\n  Document.find_each { |d| c.add { SummarizeDocument.run(doc_id: d.id, text: d.body).result } }\nend\n\n\nEvery block added with c.add { … } runs inside one async reactor, capped at max_in_flight in flight (an Async::Semaphore) and coordinated by an Async::Barrier. Results come back as an Array in submission order (not completion order). On the first task that raises, that error is re-raised and the remaining in-flight tasks are stopped — errors are never swallowed. max_in_flight defaults to Nexo.config.max_in_flight (8) and is the single most important knob for staying under provider rate limits.\n\nUsing Nexo.concurrent with async not installed raises Nexo::MissingDependencyError with install guidance.\n\nInside a durable workflow, Workflow#checkpoint_all is the workflow-durability flavored sibling of Nexo.concurrent: it drives this same bounded fan-out but persists each step to the run’s state as it lands, so a resume only re-runs what never completed. See Parallel checkpoints in the durable-workflows guide.\n\n\n\nSandboxes::Local offload\n\nUnder a reactor, blocking file/subprocess I/O would stall every other fiber. Flip the switch and Sandboxes::Local offloads its read/write/glob/shell to a worker thread:\n\nNexo.configure { |c| c.concurrency = :async }   # default is :threaded\n\n\nThe decision is driven by config, not by scheduler detection: under :async the blocking block runs on a worker thread so the reactor keeps serving other fibers; under :threaded (the default) it runs inline with zero overhead — byte-for-byte the synchronous behavior. Offloading changes neither return values nor the security properties: the path-escape guard, narrowed ENV, and Timeout-wrapped subprocess are all preserved. (Sandboxes::Virtual is pure memory and Sandboxes::Remote is already HTTP/fiber-friendly — neither needs offload.)\n\n\n\nWorkflow buffered emit\n\nEach emit normally persists immediately. Under a reactor that per-event DB write blocks the whole loop, so Workflow.run takes a buffer_events: flag (default Nexo.config.buffer_workflow_events, false):\n\nrun = SummarizeDocument.run({doc_id: 1, text: body}, buffer_events: true)\n# events buffer in memory and flush to the store exactly once, on completion\n\n\nWith buffering on, events accumulate in memory and flush in a single save_events! at the end of the run (on both success and failure). The default (unbuffered) behavior is unchanged.\n\n\n\nRunning under Rails / a fiber server\n\nAsync DB work is the sharp edge. Under a fiber server such as Falcon, many concurrent queries can exhaust the ActiveRecord connection pool, so:\n\n\n  Raise DB_POOL (the connection-pool size) to cover your in-flight concurrency.\n  On Rails 7.1+, consider config.active_record.async_query_executor.\n  Prefer buffer_events: true for workflows so each run writes its event log once instead of per event.\n\n\nNote that DB work under a reactor is offloaded/pooled, not truly fiber-async — Nexo does not ship a fiber-native DB driver. For server setup (Falcon, the fiber scheduler), see the async guide.\n\n\n\nNext steps\n\n\n  \n    \n      Loops\n    \n    \n      The per-agent engine that concurrency fans out.\n    \n  \n\n  \n    \n      Workflows\n    \n    \n      Structure fan-out work into a finite, inspectable run."
        },
        {
          "id": "documentation-nexo-durable-workflows",
          "title": "Durable workflows",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/nexo/durable-workflows/",
          "content": "A long-running or human-in-the-loop workflow can pause durably and continue later — possibly in another process — without re-running completed, already-paid-for work. Three small primitives compose over the existing run persistence (no step-graph engine, no replay log, no scheduler):\n\n\n  checkpoint(name) { … } runs its block once and stores the json-serializable result under name in the run’s state. On a later run/resume of the same run, a present checkpoint returns the stored value without re-running the block. This is the tool that makes resume cheap and side-effect-safe.\n  suspend!(reason:, resume_key: nil) pauses the run: it marks the run \"suspended\" (a non-failure outcome, distinct from \"failed\") and returns it to the caller — Workflow.run does not raise. Call it outside a checkpoint.\n  Workflow.resume(run_id, input = {}) (sync) and Workflow.resume_later(run_id, input = {}) (enqueued) continue a suspended run, feeding input in as #resume_input.\n\n\n\n\nThe basic loop\n\nclass DocumentApproval &lt; Nexo::Workflow\n  def call(payload)\n    document = checkpoint(:fetch) { fetch_expensive(payload[:id]) } # paid for once\n\n    # `resume_input` is {} on the first pass, so we pause; on resume the host\n    # feeds { approved: true }, so we fall through and publish.\n    suspend!(reason: \"awaiting approval\") unless resume_input[:approved]\n\n    checkpoint(:publish) { publish!(document) }\n    { done: true }\n  end\nend\n\nrun = DocumentApproval.run(id: 42)   # reaches suspend!, returns\nrun.status                            # =&gt; \"suspended\"\nrun.suspend_reason                    # =&gt; \"awaiting approval\"  (AR store)\nrun.state[\"fetch\"]                    # =&gt; the fetched document (checkpoint persisted)\n\n# ...later, once a human approves — possibly in another process:\nresumed = DocumentApproval.resume(run.id, approved: true)\nresumed.status                        # =&gt; \"done\"  (the :fetch block did NOT re-run)\n\n\nA host UI lists paused runs with the suspended scope and inspects them with the readers (Nexo ships no controllers/views — the UI is your app’s job):\n\nNexo::WorkflowRun.suspended            # scope: all paused runs\nrun.suspended?                          # =&gt; true\nrun.suspend_reason                      # =&gt; \"awaiting approval\"\nrun.checkpoint_result(:fetch)           # =&gt; the stored :fetch value, or nil\n\n\nFor a durable, cross-process resume from a background job, enqueue it — the job carries the run id plus the (json-safe) resume input; the payload still lives on the run:\n\n# The resume input is a positional Hash (queue: is the only keyword), so pass it\n# as { approved: true } — bare approved: true would bind as an unknown keyword.\nDocumentApproval.resume_later(run.id, { approved: true }, queue: :nexo)\n\n\nLive example\n\nThe full offline approval flow is runnable in the repo:\n\nruby -Ilib examples/approval_workflow.rb\n\n\n\n  View examples/approval_workflow.rb on GitHub →\n\n\n\n\nParallel checkpoints — checkpoint_all\n\nWhen several checkpoints are independent (no step depends on another’s result), run them concurrently with checkpoint_all(name =&gt; callable, …) instead of a sequence of checkpoint calls. It fans the pending steps out through Nexo.concurrent — all in flight at once — and persists each step as it completes (not the batch as a whole), so a resume after a partial failure only re-runs the steps that never landed:\n\nclass BuildDashboard &lt; Nexo::Workflow\n  def call(payload)\n    data = checkpoint_all(\n      account: -&gt; { fetch_account(payload[:id]) },   # these two run\n      usage:   -&gt; { fetch_usage(payload[:id]) }      # concurrently\n    )\n    { report: render(data[:account], data[:usage]) }\n  end\nend\n\n\ncheckpoint_all returns a Hash keyed by the original names you passed (data[:account]), with values read back from state — the same shape whether a value came from this pass or a prior one. Each newly-completed step also surfaces a \"checkpoint\"-typed event on the run’s event log (data is the step name only, never the value — so a dashboard can show batch progress without the event log carrying large or sensitive results). Steps already present in state are skipped silently and emit nothing.\n\nBound the batch by how many keys you pass — there is no separate rate knob; every pending step goes in flight. Because it drives Nexo.concurrent, checkpoint_all needs the async gem only when something is actually pending — an all-persisted pass returns the prior values directly without touching concurrency. The same restrictions as checkpoint apply: values must be json-serializable, a step must not be named after a reserved state key (__suspend__/__approval__/__buffer_events__ — raises Nexo::Error before any step runs), and do not call suspend! inside a step (undefined — unsupported).\n\n\n  Known trade-off: per-step persistence, not an atomic batch. checkpoint_all is not transactional. If step B raises after step A persisted, A stays in state, B is absent, the run goes \"failed\", and the exception propagates through the workflow’s normal failure path (Nexo.concurrent’s “first failure re-raises, the rest stop” — it is not rescued away). A subsequent execute of the same run re-submits only the still-missing names — A is skipped, B re-runs. Do not treat a batch as all-or-nothing.\n\n\n\n\nDurable agent approval — :approve\n\nThe example above suspends at an explicit suspend! the workflow author placed. The :approve mode adds the durable, cross-process sibling of :ask for the case where a run_agent-driven agent hits a permission gate mid-loop and you want that to pause the run for a human, not run unchecked and not block a worker. Declare the agent under the :approve mode:\n\nclass Scribe &lt; Nexo::Agent\n  model   ENV.fetch(\"NEXO_MODEL\")\n  sandbox :local\n  permissions :approve        # every gated capability needs a human decision\nend\n\nclass ApprovedWrite &lt; Nexo::Workflow\n  sandbox :local\n  agent   Scribe\n  def call(_p) = { content: run_agent(\"Write 'hi' to notes.txt\").content }\nend\n\n\nThe loop is: :approve gate with no decision → Nexo::ApprovalRequired → run_agent suspends → host renders the pending call → resume(approved:) threads the decision back through the gate.\n\nrun = ApprovedWrite.run                       # agent reaches the write gate, run suspends\nrun.status                                     # =&gt; \"suspended\"\nrun.state[\"__suspend__\"][\"reason\"]             # =&gt; \"approval: notes.txt\"\nrun.state[\"__approval__\"]                      # =&gt; { \"capability\" =&gt; \"write\",\n                                               #      \"tool\" =&gt; \"notes.txt\", \"args\" =&gt; nil }\n# \"args\" carries the tool call arguments only for an MCP-tool approval; a sandbox\n# capability gate (write/shell/fetch/search) records \"args\" =&gt; nil — the pending\n# call is identified by \"capability\" + \"tool\".\n\n# ...a human approves — possibly in another process (resume_later for the AR store):\nresumed = ApprovedWrite.resume(run.id, approved: true)\nresumed.status                                 # =&gt; \"done\" (the gate allowed the write)\n\n\n\n  Nexo::ApprovalRequired is a signal, distinct from Permissions::Denied: Denied means “no, adapt” (tools rescue it into {error:}); ApprovalRequired means “pause and ask a human”, so tools must not rescue it — it propagates out of the tool loop and out of Agent#prompt, where run_agent catches it.\n  Undecided ⇒ suspend, approved: false ⇒ deny. The default stays safe: an unresolved approval never silently allows, and a denial on resume makes the tool return {error:} (the model adapts) — the run still finishes \"done\", without the gated effect, never \"failed\".\n  \n    Scope which actions need approval with the same ask_when predicate as :ask (aliased approve_when: for readability) — unset means every gated action needs a decision; a falsey predicate auto-allows without one:\n\n    Nexo::Permissions.new(mode: :approve,\n  approve_when: -&gt;(cap, detail) { cap == :write &amp;&amp; detail.to_s.start_with?(\"/protected\") })\n    \n  \n  Synchronous :ask is untouched. :ask (in-process on_ask) is still the right choice with a human at the keyboard during a synchronous run; :approve is its durable, cross-process sibling for run_later/resume_later.\n\n\nCaveats — read before relying on it\n\n\n  Re-entry, not replay. On resume the agent re-drives #call from the top; a non-idempotent tool call before the approval gate re-runs on resume (agent tool calls generally aren’t checkpointable). Put approval gates early, or after the expensive work is already checkpointed by the workflow.\n  One approval per suspend cycle, global decision. The {approved:} answers whichever gate the re-driven agent hits first. A second gate after an approved first one simply suspends again — the next resume decides it. There is no per-tool decision granularity in v1.\n  Cross-process approval needs the ActiveRecord store + ActiveJob (like all durable resume). In-process resume works with the Memory store; a Memory run does not survive the process.\n  Branch depends on upstream ruby_llm. This works because ruby_llm’s tool loop lets a tool execute exception propagate out of chat.ask (verified, 1.16.0). If a future ruby_llm swallows tool exceptions, tool-triggered approval would be constrained — a genuine upstream dependency, stated plainly.\n\n\nLive example\n\nThe live approval-agent flow is runnable in the repo:\n\nNEXO_LIVE=1 NEXO_MODEL=… ruby -Ilib examples/approval_agent.rb\n\n\n\n  View examples/approval_agent.rb on GitHub →\n\n\nThe state column ships with fresh installs. Apps installed before this feature add it with an additive migration:\n\nrails g nexo:state\nrails db:migrate\n\n\n\n\nHonest resume semantics — read this before relying on resume\n\nResume re-enters #call from the top — Ruby has no transparent continuation capture, so this is not replay:\n\n\n  Everything outside a checkpoint re-runs on resume. Only checkpoint-guarded work is skipped (its stored result is returned). Wrap every expensive step and every side effect in a checkpoint; the idempotency of the non-checkpointed code is your responsibility.\n  A crash inside a checkpoint re-runs that checkpoint on resume (at-least-once for the in-flight step) — so a checkpoint’s side effect should tolerate being retried.\n  Checkpoint values must be json-serializable — they round-trip the store exactly like result/events.\n  Cross-process resume needs the ActiveRecord store. A run suspended under the in-memory store resumes only in-process (which is what the test suite exercises); a run that must survive the process needs the AR store with a shared database.\n  Never suspend! inside a checkpoint block (undefined — unsupported), and never name a checkpoint \"__suspend__\" (reserved for the suspend metadata) or \"__approval__\" (reserved for the pending approval call) — both are keys Nexo stores in state.\n\n\nThere is no distinct \"resumed\" status: resume re-enters execute, so a host sees the existing suspended → running → done (or suspended again) transitions over the usual nexo.workflow.status notifications. The boot reconcile_interrupted! sweep leaves \"suspended\" runs untouched — an intentional pause is never mistaken for an orphaned \"running\" run.\n\n\n\nNext steps\n\n\n  \n    \n      Workflows\n    \n    \n      The run lifecycle these durability primitives build on.\n    \n  \n\n  \n    \n      Rails\n    \n    \n      Persist runs in a shared store for cross-process resume."
        },
        {
          "id": "documentation-nexo-examples",
          "title": "Examples",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/nexo/examples/",
          "content": "Each example in the Nexo repo is a small, runnable script. Two kinds:\n\n\n  Offline — no model, no network, no API key. Run them as-is to see the primitive work.\n  Live (NEXO_LIVE=1) — needs a real tool-calling model (NEXO_MODEL takes any ruby_llm-supported model id; nothing is provider-specific) and sometimes an external service (an MCP server, docker, an API key).\n\n\nRun everything from the repo root with ruby -Ilib examples/&lt;name&gt;.rb.\n\n\n\nOffline (start here)\n\n\n  \n    \n      Example\n      Shows\n    \n  \n  \n    \n      artifact_from_template.rb\n      Staging input files into a run’s sandbox + rendering a named artifact from a trusted ERB template\n    \n    \n      approval_workflow.rb\n      Durable human-in-the-loop: checkpoint + suspend! + resume\n    \n  \n\n\n\n\nLive — agents\n\n\n  \n    \n      Example\n      Shows\n      Extra requirements\n    \n  \n  \n    \n      code_reviewer.rb\n      The minimal agent against a local Ollama model, with a skill and token accounting\n      Ollama running locally\n    \n    \n      chat_session.rb\n      A continuing, addressable Nexo::Session that remembers prior turns\n      —\n    \n    \n      container_review.rb\n      Agent tools running inside a locked-down OCI container\n      docker (or Apple container)\n    \n    \n      news_summary.rb\n      Read-only web fetch scoped by fetch_allow\n      —\n    \n    \n      news_search.rb\n      Host-injected search_backend + fetch\n      a search backend you inject\n    \n  \n\n\n\n\nLive — MCP\n\n\n  \n    \n      Example\n      Shows\n      Extra requirements\n    \n  \n  \n    \n      mcp_filesystem.rb\n      The MCP seam + permission gate with the official filesystem server — no credentials needed; start here for MCP\n      npx\n    \n    \n      inbox_digest.rb\n      Gmail through a stdio MCP server + the email_triage skill, read tools only\n      a Gmail MCP server + OAuth\n    \n    \n      inbox_digest_http.rb\n      The same digest over a hosted HTTP MCP server with a host-supplied OAuth bearer token\n      a hosted Gmail MCP server\n    \n    \n      inbox_digest_task.rb\n      The digest as a Workflow Task: agent macro + run_agent + a named artifact\n      same as inbox_digest.rb\n    \n  \n\n\n\n\nLive — workflows\n\n\n  \n    \n      Example\n      Shows\n      Extra requirements\n    \n  \n  \n    \n      approval_agent.rb\n      The :approve permission mode bridged to a durable suspend/resume\n      —\n    \n  \n\n\n\n\nSkills used by the examples\n\nThe skills/ directory holds the SKILL.md packages the examples reference — email_triage, news_summary, and ruby-code-review. The examples point Nexo.config.skills_path there; in a Rails host the default is app/skills.\n\n\n  \n    \n      Skill\n      Used by\n    \n  \n  \n    \n      email_triage\n      inbox_digest.rb, inbox_digest_http.rb, inbox_digest_task.rb\n    \n    \n      news_summary\n      news_summary.rb, news_search.rb\n    \n    \n      ruby-code-review\n      code_reviewer.rb\n    \n  \n\n\n\n\nRails walkthrough\n\nA host-side Rails walkthrough covers run_later, live progress, run helpers, and artifact access:\n\n\n  View examples/rails_usage.md on GitHub →\n\n\n\n\nNext steps\n\n\n  \n    \n      Getting started\n    \n    \n      Install Nexo and build your first agent in five lines.\n    \n  \n\n  \n    \n      GitHub Repository\n    \n    \n      Source code, issues, and the full guide set in the repo."
        },
        {
          "id": "documentation-nexo-getting-started",
          "title": "Getting started",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/nexo/getting-started/",
          "content": "Install Nexo, configure the harness in one place, and build your first agent. Defaults are safe and provider-neutral — there is intentionally no hardcoded model.\n\n\n\nInstallation\n\nAdd to your Gemfile:\n\ngem \"nexo_ai\"\n\n\nOr install directly:\n\ngem install nexo_ai\n\n\nIn a Rails app, run the install generator to create the conventional layout and an initializer:\n\nrails g nexo:install\n\n\n      create  app/agents/.keep\n      create  app/workflows/.keep\n      create  app/skills/.keep\n      create  config/initializers/nexo.rb\n\n\nrequire \"nexo\" works in plain Ruby with no Rails loaded.\n\n\n\nConfiguration\n\nConfigure the harness in one place with Nexo.configure:\n\nNexo.configure do |config|\n  config.default_model       = ENV[\"NEXO_MODEL\"] # provider-neutral: no default\n  config.default_sandbox     = :virtual          # :virtual | :local | :docker | :apple | a Hash | a Sandbox\n  config.default_permissions = :read_only        # :read_only | :auto | :ask | :approve\n  config.skills_path         = \"app/skills\"\n  config.concurrency         = :threaded         # :threaded | :async (opt-in fiber offload)\n  config.max_in_flight       = 8                 # Nexo.concurrent fan-out bound\n  config.buffer_workflow_events = false          # buffer + flush-once workflow events\nend\n\nNexo.config.default_sandbox      # =&gt; :virtual\nNexo.config.default_permissions  # =&gt; :read_only\nNexo.config.default_model        # =&gt; nil unless set\n\n\nThere is deliberately no hardcoded model — you set NEXO_MODEL (or default_model) to any ruby_llm-supported model id.\n\n\n\nBuild an agent in five lines\n\nSubclass Nexo::Agent, declare the pieces with class macros, and call #prompt. No sandbox, permission, or tool object is wired by hand, and nothing is vendor-specific — the agent runs on any ruby_llm-supported model (set NEXO_MODEL, e.g. a local gemma3:12b via Ollama, or a hosted model):\n\nrequire \"nexo\"\n\nclass CodeReviewer &lt; Nexo::Agent\n  model       ENV.fetch(\"NEXO_MODEL\")   # any ruby_llm model — never a hardcoded vendor default\n  sandbox     :local\n  permissions :read_only\n\n  instructions \"You are a careful code reviewer. Read files and report issues. Do not write files.\"\nend\n\nCodeReviewer.new(cwd: \"/path/to/repo\").prompt(\"Review the auth module\")\n\n\nDefaults are safe: an agent with no sandbox/permissions declared gets the in-memory :virtual sandbox and :read_only permissions, so an untrusted model has zero host access until you explicitly opt in.\n\n\n  Safe by default: agents start :virtual + :read_only — an untrusted model has zero host access until you explicitly opt in.\n\n\n\n\nUnregistered models — local tags, self-hosted, brand-new releases\n\nruby_llm normally validates a model id against its bundled models.json registry and infers the provider from it. A local Ollama tag (gemma3:12b), a self-hosted build, or a model newer than the installed registry isn’t listed there — so declare the provider explicitly and set assume_model_exists to skip the registry lookup:\n\nclass LocalReviewer &lt; Nexo::Agent\n  model               \"gemma3:12b\"\n  provider            :ollama         # required once the registry lookup is skipped\n  assume_model_exists true            # opt out of models.json validation\n\n  instructions \"You are a careful code reviewer.\"\nend\n\n\nBoth are class macros with the same reader/writer convention as model. provider is passed straight through to RubyLLM.chat; assume_model_exists defaults to false (registry validation on). Setting assume_model_exists without a provider raises Nexo::ConfigurationError — ruby_llm can’t infer a provider once the lookup is skipped.\n\n\n\nWhere to next\n\n\n  Sandboxes — the four execution environments and hardened defaults.\n  Permissions — the capability gate and the four modes.\n  Examples — runnable scripts including a local-Ollama code reviewer.\n\n\n\n\nNext steps\n\n\n  \n    \n      Sandboxes\n    \n    \n      Choose where an agent's tools act — Virtual, Local, Container, or Remote.\n    \n  \n\n  \n    \n      Permissions\n    \n    \n      Control what those tools may do, read-only by default."
        },
        {
          "id": "documentation-nexo",
          "title": "Nexo",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/nexo/",
          "content": "Agent = Model + Harness. Nexo is the connective tissue linking RubyLLM to tools, sandboxes, skills, and runs.\n\n\nA model alone forgets everything the moment a response ends. The harness is everything else. Nexo gives the RubyLLM ecosystem one cohesive front door with safe defaults — build a working agent in five lines without wiring anything.\n\n\n\nCompose, don’t reimplement\n\nNexo does not rebuild skill loading, the tool-call loop, MCP, or structured output — those already live in the RubyLLM ecosystem (ruby_llm core, ruby_llm-skills, ruby_llm-mcp, ruby_llm-schema). Nexo composes them behind one front door and adds only the two pieces the ecosystem is missing:\n\n\n  Sandbox + Permissions seam — a pluggable execution environment (virtual / local / remote / container) with explicit authorization gating. Default: :virtual + :read_only.\n  WorkflowRun lifecycle — a finite-job primitive (runId, status, payload, result, inspectable event log) that nothing else in the ecosystem provides cleanly.\n\n\n\n\nBuild an agent in five lines\n\nSubclass Nexo::Agent, declare the pieces with class macros, and call #prompt. No sandbox, permission, or tool object is wired by hand, and nothing is vendor-specific — the agent runs on any ruby_llm-supported model (set NEXO_MODEL, e.g. a local gemma3:12b via Ollama, or a hosted model):\n\nrequire \"nexo\"\n\nclass CodeReviewer &lt; Nexo::Agent\n  model       ENV.fetch(\"NEXO_MODEL\")   # any ruby_llm model — never a hardcoded vendor default\n  sandbox     :local\n  permissions :read_only\n\n  instructions \"You are a careful code reviewer. Read files and report issues. Do not write files.\"\nend\n\nCodeReviewer.new(cwd: \"/path/to/repo\").prompt(\"Review the auth module\")\n\n\n\n  Safe by default: agents start :virtual + :read_only — an untrusted model has zero host access until you explicitly opt in.\n\n\n\n\nInstallation\n\nAdd to your Gemfile:\n\ngem \"nexo_ai\"\n\n\nOr install directly:\n\ngem install nexo_ai\n\n\nIn a Rails app, run the install generator to create the conventional layout and an initializer:\n\nrails g nexo:install\n\n\n      create  app/agents/.keep\n      create  app/workflows/.keep\n      create  app/skills/.keep\n      create  config/initializers/nexo.rb\n\n\n\n\nThe guides\n\n\n  \n    \n      Guide\n      What’s inside\n    \n  \n  \n    \n      Getting started\n      install, configuration, first agent, unregistered/local models\n    \n    \n      Sandboxes\n      virtual / local / remote / container + hardened defaults\n    \n    \n      Permissions\n      modes, the gate, the MCP gate, :ask, :approve\n    \n    \n      Tools\n      ReadFile / WriteFile / Shell / Glob\n    \n    \n      Loops\n      RubyLLM vs AgentSDK, the turn-cap caveat\n    \n    \n      Workflows\n      lifecycle, staging, artifacts, run_agent, tasks &amp; actions\n    \n    \n      Durable workflows\n      checkpoint / suspend / resume\n    \n    \n      Skills\n      SKILL.md packages, gated tools\n    \n    \n      MCP\n      mcp macro, fail-closed gate, transports\n    \n    \n      Web\n      fetch tool + SSRF guard, search tool + injected backend\n    \n    \n      Sessions\n      continuing, addressable memory\n    \n    \n      Rails\n      engine, run_later, broadcasting, generators\n    \n    \n      Concurrency\n      opt-in async, buffered emit, fiber servers\n    \n    \n      Examples\n      runnable scripts — offline and live\n    \n  \n\n\n\n\nRequirements\n\n\n  Ruby 3.3+\n  ruby_llm &gt;= 1.16\n  ruby_llm-skills — optional, only when you use the skills macro\n  ruby_llm-mcp — optional, only when you attach an MCP server with the mcp macro\n  ruby_llm-agent_sdk — optional, only when you choose the Anthropic-oriented Loops::AgentSDK backend\n\n\n\n\nStatus\n\nEarly development. The API is not stable. Nexo ships safe defaults and honest caveats — every escalation is an explicit opt-in, and every reduced guarantee is documented rather than silently dropped.\n\n\n\nNext steps\n\n\n  \n    \n      Get started\n    \n    \n      Install Nexo, configure the harness, and build your first agent.\n    \n  \n\n  \n    \n      Examples\n    \n    \n      Runnable scripts — offline primitives and live agents, MCP, and workflows.\n    \n  \n\n  \n    \n      GitHub Repository\n    \n    \n      Source code, issues, and the full guide set in the repo.\n    \n  \n\n  \n    \n      RubyGems\n    \n    \n      Install the latest version from RubyGems."
        },
        {
          "id": "documentation-nexo-loops",
          "title": "Loops",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/nexo/loops/",
          "content": "The loop is the engine that drives one prompt to completion. Swapping it is constructor injection (loop:) — the agent class never changes. Two backends ship.\n\n\n\nThe two backends\n\n\n  \n    \n       \n      Loops::RubyLLM (default)\n      Loops::AgentSDK (opt-in)\n    \n  \n  \n    \n      Provider neutral\n      Yes — any ruby_llm model\n      No — Anthropic-oriented\n    \n    \n      Tool source\n      your sandbox-backed tools\n      the SDK’s own built-in/host tools\n    \n    \n      Turn cap\n      observability only (see caveat)\n      native max_turns hard cap\n    \n    \n      Execution location\n      your sandbox (virtual/local/remote)\n      the host process\n    \n  \n\n\nThe whole point: same agent code, swapped backends. Both examples are model-agnostic (ENV.fetch(\"NEXO_MODEL\") — never a hardcoded \"claude-…\"):\n\n# Claude fast path — AgentSDK's own loop + host tools + native max_turns\nclaude = Nexo::Agent.new(\n  model: ENV.fetch(\"NEXO_MODEL\"),\n  sandbox: Nexo::Sandboxes::Local.new(cwd: \"/srv/checkout\"),\n  permissions: Nexo::Permissions.new(mode: :auto),\n  loop: Nexo::Loops::AgentSDK.new\n)\n\n# Any-provider path — your sandbox, your tools, human-in-the-loop\ngpt = Nexo::Agent.new(\n  model: ENV.fetch(\"NEXO_MODEL\"),                # gpt-5.5, gemini, gemma3:12b via Ollama…\n  sandbox: Nexo::Sandboxes::Remote.new(client: my_container_client),\n  permissions: Nexo::Permissions.new(mode: :ask, on_ask: -&gt;(cap, detail) {\n    SlackApproval.request!(capability: cap, detail: detail)\n  }),\n  loop: Nexo::Loops::RubyLLM.new\n)\n\n\nLoops::AgentSDK wraps RubyLLM::AgentSDK.query and requires the optional ruby_llm-agent_sdk gem (lazy require; a clear Nexo::MissingDependencyError if it’s absent). It maps Nexo’s permission modes onto the SDK’s own vocabulary:\n\n\n  \n    \n      Nexo mode\n      AgentSDK permission_mode\n    \n  \n  \n    \n      :read_only\n      :default\n    \n    \n      :auto\n      :bypass_permissions\n    \n    \n      :ask\n      :default (human gating stays in Nexo’s own on_ask path, not delegated to the SDK)\n    \n    \n      :approve\n      :default (durable approval stays in Nexo’s own gate; any unmapped mode also falls back to :default)\n    \n  \n\n\n\n\nThe turn-cap caveat — read before running untrusted/expensive workloads\n\nruby_llm runs the whole tool loop inside ask, so Loops::RubyLLM has no clean public hard “stop after N turns” halt — before_tool_call gives turn-count observability, not a hard stop. (Confirmed: ruby_llm 1.16.0’s Chat exposes no public max-turns/max-iterations setting.) Your three real options:\n\n\n  (a) use Loops::AgentSDK (native max_turns) for untrusted/expensive workloads;\n  (b) have a tool return { error: \"turn limit reached, stop and summarize\" } once a turn counter trips;\n  (c) check whether the installed ruby_llm exposes a max-iterations config (in 1.16.0 it does not).\n\n\n\n  Do not ship Loops::RubyLLM for untrusted workloads claiming a hard cap that isn’t proven.\n\n\n\n\nVerified vs assumed\n\nBuilt against ruby_llm 1.16 and ruby_llm-test 0.2. The tool body method is #execute, tools attach with chat.with_tools(*instances), and instructions set with chat.with_instructions. Open3.capture3 has no timeout: keyword on the target Ruby, so Local#shell bounds the command with Timeout.timeout. These may differ on other ruby_llm versions.\n\nLoops::RubyLLM’s turn-count observability uses RubyLLM::Chat#before_tool_call / #after_tool_result, confirmed present on ruby_llm 1.16.0 and guarded with respond_to? so a version lacking them degrades to no observability rather than crashing.\n\nLoops::AgentSDK targets RubyLLM::AgentSDK.query; ruby_llm-agent_sdk is not a dependency of this release, so that signature is assumed (per the gem’s README) and verified-on-install — confirm it the moment you add the gem.\n\n\n\nLive smoke (optional)\n\nThe core suite is fully offline and deterministic (models stubbed with ruby_llm-test). A real end-to-end check is opt-in and env-gated — small local models like Gemma have weak tool-calling, so it may be flaky and is never a gating test:\n\nollama serve &amp;\nNEXO_LIVE=1 NEXO_MODEL=gemma3:12b bundle exec rake test TEST=test/live_smoke_test.rb\n\n\nIf Gemma’s tool-calling proves too weak, point NEXO_MODEL at a stronger model — the gem stays provider-neutral; only the smoke target changes.\n\n\n\nNext steps\n\n\n  \n    \n      Sessions\n    \n    \n      Give an agent memory that persists across invocations.\n    \n  \n\n  \n    \n      Skills\n    \n    \n      Teach the model how you want a task done with a SKILL.md package."
        },
        {
          "id": "documentation-nexo-mcp",
          "title": "MCP",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/nexo/mcp/",
          "content": "An MCP server exposes tools to a model over the Model Context Protocol — Gmail, a filesystem, a fetch endpoint, Drive, and so on. Nexo does not implement MCP; it composes the ruby_llm-mcp gem so you attach one or more servers with a single mcp macro and no client wiring. Because a server is reached through the protocol (never a vendor SDK), the behavior is identical on Anthropic, a local model, or anything else ruby_llm supports.\n\n\n\nAttach servers with one macro\n\nrequire \"nexo\"\n\nclass InboxDigest &lt; Nexo::Agent\n  model       ENV.fetch(\"NEXO_MODEL\")   # any ruby_llm model — never a hardcoded vendor default\n  permissions :read_only\n  mcp :gmail, transport: :stdio, command: \"npx\", args: %w[-y @modelcontextprotocol/server-gmail]\n  mcp :fs,    transport: :stdio, command: \"npx\", args: %w[-y @modelcontextprotocol/server-filesystem /data]\n  mcp :fetch, transport: :sse,   url: \"http://localhost:8080/sse\"\n  mcp_allow %w[search_threads get_thread]\nend\n\n\nEach mcp line accumulates a server declaration. name and transport map onto the client’s name:/transport_type:; every other keyword is passed through verbatim as the server’s config: — command:/args: for :stdio, url: for :sse. The server’s tools are attached to the chat after the sandbox tools and skills, and fire the same before_tool_call/after_tool_result observability callbacks, so MCP calls appear in a run’s event log automatically.\n\n\n\nEvery MCP tool call is gated — and fails closed\n\nMCP tools obey a second permission axis, separate from the sandbox capability axis, because an MCP tool executes inside the server, outside the sandbox. mcp_allow is the exact-match allow-list threaded into the agent’s permissions:\n\n\n  \n    \n      Mode\n      MCP tool behavior\n    \n  \n  \n    \n      :read_only (default)\n      allow only tool names listed in mcp_allow; everything else is denied\n    \n    \n      :ask\n      call on_ask.call(:mcp, {tool:, args:}); a truthy return allows, else deny\n    \n    \n      :approve\n      names in mcp_allow are pre-approved; any other tool needs a human decision — undecided suspends the run (Nexo::ApprovalRequired), approved: true allows, approved: false denies (the durable sibling of :ask)\n    \n    \n      :auto\n      allow every MCP tool\n    \n  \n\n\nmcp_allow defaults to [], so attaching a powerful server under :read_only with no allow-list denies every tool — a misconfigured agent fails closed, not open. A denied call returns { error: … } to the model (recoverable) and never raises into the loop — identical to the sandbox tools. Escalation (:auto, a populated mcp_allow, or :ask with a real on_ask) is always explicit in your code. Matching is exact tool-name only — no globs or regexes.\n\n\n  Safe by default: attaching an MCP server adds no permission surface. The unchanged gate denies everything under :read_only until the exact name is in mcp_allow (default [] ⇒ deny-all).\n\n\nTwo caveats — read before attaching a server\n\n\n  MCP tool effects are not sandboxed. The gate covers the authority to invoke a tool; the tool then runs in the MCP server, outside Nexo’s sandbox. Nexo cannot constrain what that server does with a call it is authorized to make — attaching a write server and allowing a write tool means real writes happen. Gate deliberately, and prefer :read_only with a tight mcp_allow.\n  \n    Connection lifecycle. Clients are built once and memoized on the agent instance, reused across prompts. A long-lived agent holding stdio/SSE servers should call Agent#close when done to tear the connections down:\n\n    agent = InboxDigest.new\nagent.prompt(\"Summarize invoices from this week\")\nagent.prompt(\"Any follow-ups needed?\")   # reuses the same live MCP connections\nagent.close                              # stops every attached server\n    \n  \n\n\n\n\nHTTP-family servers + an OAuth token: provider\n\nBeyond :stdio, Nexo attaches a server over any HTTP-family transport ruby_llm-mcp supports — transport: :http, :sse, or :streamable. For an OAuth-authenticated hosted server (Gmail, Drive, …) add a token: — a static bearer String, or a callable re-read close to connection time. Nexo resolves it and injects an Authorization: Bearer &lt;token&gt; header per connection:\n\nclass InboxTriageHTTP &lt; Nexo::Agent\n  model       ENV.fetch(\"NEXO_MODEL\")\n  permissions :read_only\n\n  # Hosted Gmail MCP server over HTTP; the host supplies the OAuth access token.\n  mcp :gmail,\n    transport: :http,\n    url:       ENV.fetch(\"GMAIL_MCP_URL\"),\n    token:     -&gt; { Current.user.gmail_access_token }   # re-read at connection time\n\n  # READ tools only — the unchanged gate denies send/trash/modify.\n  mcp_allow %w[search_threads get_thread list_messages get_message list_labels]\nend\n\n\nA static token (token: ENV.fetch(\"GMAIL_TOKEN\")) is equally valid. Under the hood Nexo strips token: and hands off:\n\nRubyLLM::MCP.client(\n  name: \"gmail\", transport_type: :http,\n  config: { url: \"https://…\", headers: { \"Authorization\" =&gt; \"Bearer &lt;resolved&gt;\" } }\n)\n\n\nAny other headers: you pass are preserved; Nexo’s Authorization wins. With no token:, config: passes through byte-for-byte (no headers key) — the :stdio path is untouched.\n\n\n  Nexo does not own the OAuth flow. It performs no authorization-code exchange, no token refresh, and keeps no token store — that is your app or an OAuth library. Nexo’s only job is to call the provider, inject the header, and hand off. The token is never logged, persisted, placed in a URL/query string, or emitted in an event.\n\n\nRefresh / reconnect caveat\n\nruby_llm-mcp’s HTTP-family transports snapshot the headers hash at construction — there is no per-request header callback for a plain headers Hash. A callable token: is therefore resolved once, when the client is built, and the client is memoized on the agent instance across prompts. So when a token rotates, tear the connection down and reconnect to pick up the new value:\n\nagent.close                  # stops the memoized MCP client\nagent.prompt(\"…\")            # a fresh prompt rebuilds the client → token: re-resolved\n\n\nThe gate is unchanged — an HTTP OAuth server’s tools are gated exactly like :stdio tools. Attaching an authenticated server adds no permission surface.\n\nTwo honest caveats — read before attaching a token\n\n\n  Refresh may require a reconnect. Because headers are construction-only, a rotated token needs agent.close + a fresh prompt, not just a new proc return. A static token stays constant for the client’s life.\n  The token is a live credential. Even gated, an authorized MCP call runs its effect server-side — a leaked bearer is a real compromise. Nexo keeps it out of logs, events, persisted WorkflowRun records, and URLs; your host code must do the same. Nexo does not police ruby_llm-mcp’s own internal logging of headers — that boundary is yours.\n\n\n\n\nAn optional dependency\n\nruby_llm-mcp is an optional dependency — required lazily only when you attach a server. Without it installed, require \"nexo\" still loads; building a server raises a clear Nexo::MissingDependencyError telling you to add gem \"ruby_llm-mcp\".\n\n\n\nLive example — start here for MCP\n\nA no-auth-required example uses the official filesystem MCP server (npx, no credentials) so you can watch the safe-by-default gate in action end to end: read tools on the mcp_allow list are allowed; write_file is not on the list and the gate returns { error: ... } (denied).\n\nNEXO_LIVE=1 NEXO_MODEL=gemma3:12b ruby -Ilib examples/mcp_filesystem.rb /tmp\n\n\n\n  View examples/mcp_filesystem.rb on GitHub →\n\n\n\n\nNext steps\n\n\n  \n    \n      Web\n    \n    \n      Host-process fetch and search, gated by a capability and allow-list.\n    \n  \n\n  \n    \n      Permissions\n    \n    \n      How the mcp_allow axis composes with tool capabilities."
        },
        {
          "id": "documentation-nexo-permissions",
          "title": "Permissions",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/nexo/permissions/",
          "content": "The permission mode is what an agent’s tools may do. Two seams compose the execution environment: the sandbox is where tools act (Sandboxes); the permission mode is what they may do.\n\n\n  Safe by default: agents start :read_only — read and glob are auto-allowed; every write, shell, fetch, and search is denied until you explicitly opt in. A denied capability returns { error: ... } and the agent loop continues — it does not raise. A path that escapes the workspace raises SecurityError; an agent built with no resolvable model raises Nexo::ConfigurationError.\n\n\n\n\nThe four modes\n\n\n  \n    \n      Mode\n      :read/:glob\n      :write/:shell/:fetch/:search\n      When to use\n    \n  \n  \n    \n      :read_only (default)\n      Yes\n      No {error}\n      Untrusted models, the safe baseline\n    \n    \n      :auto\n      Yes\n      Yes\n      Fully trusted local dev/CI\n    \n    \n      :ask\n      Yes\n      per on_ask\n      A human at the keyboard during a synchronous run\n    \n    \n      :approve\n      Yes\n      per decision\n      Durable, cross-process human-in-the-loop (see Durable workflows)\n    \n  \n\n\n:read/:glob are auto-allowed under every mode (they sit in the default allow list), so :ask/:approve never prompt for them — only :write/:shell/:fetch/:search reach the gate.\n\nYou can also grant individual capabilities without changing the mode — Permissions.new(mode: :read_only, allow: %i[read glob fetch]) lets fetch through while keeping write/shell denied. This is how the Web tools are typically enabled.\n\n\n\nThe gate\n\nA denied capability returns { error: ... } to the model (recoverable) and never raises into the loop — identical to a sandbox tool failure. The gate is a first-class capability check, not a coarse mode switch, so a :read_only agent that never calls fetch_allow gets no fetch tool at all (see Web).\n\nEscalation is always explicit in your code: :auto, an allow: list, a populated mcp_allow, or :ask with a real on_ask.\n\n\n\nThe MCP gate — a second, fail-closed axis\n\nMCP tools obey a second permission axis, separate from the sandbox capability axis, because an MCP tool executes inside the server, outside the sandbox. mcp_allow is the exact-match allow-list:\n\n\n  \n    \n      Mode\n      MCP tool behavior\n    \n  \n  \n    \n      :read_only (default)\n      allow only tool names listed in mcp_allow; everything else denied\n    \n    \n      :ask\n      call on_ask.call(:mcp, {tool:, args:}); truthy allows, else deny\n    \n    \n      :approve\n      names in mcp_allow are pre-approved; any other tool needs a human decision — undecided suspends the run, approved: true allows, approved: false denies\n    \n    \n      :auto\n      allow every MCP tool\n    \n  \n\n\nmcp_allow defaults to [], so attaching a powerful server under :read_only with no allow-list denies every tool — a misconfigured agent fails closed, not open. Matching is exact tool-name only — no globs or regexes. See MCP for the full detail.\n\n\n\nHuman-gated writes (:ask)\n\n:ask mode defers every write/shell action to your callback. Build a Permissions with an on_ask hook and pass it in:\n\ngate = Nexo::Permissions.new(mode: :ask, on_ask: -&gt;(cap, detail) {\n  $stdout.print(\"Allow #{cap} #{detail}? [y/N] \"); $stdin.gets.strip == \"y\"\n})\n\nclass Editor &lt; Nexo::Agent\n  model   ENV.fetch(\"NEXO_MODEL\")\n  sandbox :local\nend\n\nEditor.new(cwd: \".\", permissions: gate).prompt(\"Fix the typo in README.md\")\n\n\nThe bare :ask symbol resolves to Permissions.new(mode: :ask) with no callback, so writes/shell are denied — pass a pre-built Permissions with on_ask for a real gate.\n\nScope which actions prompt — ask_when\n\nUnder :ask, ask_when scopes which actions actually prompt a human. When the predicate returns falsey the action is auto-allowed without calling on_ask; truthy (or when ask_when is unset) falls through to on_ask exactly as before. Unset = ask for everything. It only ever narrows what is auto-allowed — it never widens authority.\n\n# Only prompt for writes under /protected; auto-allow everything else.\nperms = Nexo::Permissions.new(\n  mode: :ask,\n  on_ask:   -&gt;(cap, detail) { ask_the_human(cap, detail) },\n  ask_when: -&gt;(cap, detail) { cap == :write &amp;&amp; detail.to_s.start_with?(\"/protected\") }\n)\n\n\n\n\nDurable approval (:approve)\n\n:approve is the durable, cross-process sibling of :ask for the case where a run_agent-driven agent hits a permission gate mid-loop and you want that to pause the run for a human, not run unchecked and not block a worker.\n\nThe loop is: :approve gate with no decision → Nexo::ApprovalRequired → run_agent suspends → host renders the pending call → resume(approved:) threads the decision back through the gate.\n\n\n  Nexo::ApprovalRequired is a signal, distinct from Permissions::Denied: Denied means “no, adapt” (tools rescue it into {error:}); ApprovalRequired means “pause and ask a human”, so tools must not rescue it.\n  Undecided ⇒ suspend, approved: false ⇒ deny. An unresolved approval never silently allows; a denial on resume makes the tool return {error:} (the model adapts) — the run still finishes \"done\", without the gated effect, never \"failed\".\n  Scope which actions need approval with the same ask_when predicate (aliased approve_when: for readability).\n\n\nNexo::Permissions.new(mode: :approve,\n  approve_when: -&gt;(cap, detail) { cap == :write &amp;&amp; detail.to_s.start_with?(\"/protected\") })\n\n\n:ask (in-process on_ask) is the right choice with a human at the keyboard during a synchronous run; :approve is its durable, cross-process sibling for run_later/resume_later. See Durable workflows for the suspend/resume mechanics and the honest caveats (re-entry not replay, one approval per suspend cycle, needs the ActiveRecord store + ActiveJob for cross-process).\n\nLive example\n\nThe :approve mode bridged to a durable suspend/resume is exercised by a live example in the repo:\n\nNEXO_LIVE=1 NEXO_MODEL=… ruby -Ilib examples/approval_agent.rb\n\n\n\n  View examples/approval_agent.rb on GitHub →\n\n\n\n\nNext steps\n\n\n  \n    \n      Sandboxes\n    \n    \n      The other safety axis — where an agent's tools act.\n    \n  \n\n  \n    \n      Durable workflows\n    \n    \n      Take the :approve gate cross-process with suspend and resume."
        },
        {
          "id": "documentation-nexo-rails",
          "title": "Rails",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/nexo/rails/",
          "content": "Rails wiring: run a Workflow asynchronously on your existing ActiveJob adapter, broadcast its events live, and query runs and artifacts from your own controllers. Nexo ships no queue, no scheduler, no cable backend, and no UI — only the primitives plus one overridable partial.\n\nThe install generator (rails g nexo:install) is covered in Getting started; the per-feature generators (rails g nexo:workflows, nexo:artifacts, nexo:state, nexo:skill) live with their topics in Workflows, Durable workflows, and Skills.\n\n\n\nInstall the store (needed for cross-process run_later)\n\nrun_later enqueues a job that carries only the run id; the worker looks the run up in the store. For a worker in another process to find it, use the ActiveRecord store:\n\nrails g nexo:install     # config/initializers/nexo.rb\nrails g nexo:workflows   # the nexo_workflow_runs migration\nrails db:migrate\n\n\nIn config/initializers/nexo.rb, opt into the pieces you want:\n\nNexo.configure do |config|\n  config.default_model = ENV[\"NEXO_MODEL\"]\n  config.job_queue = :nexo         # route workflow jobs to a dedicated queue (optional)\n  config.broadcast_events = true   # opt-in Turbo mirror (requires turbo-rails)\nend\n\n\n\n\nBackground execution — run_later\n\nMyWorkflow.run_later(payload) enqueues the run on your existing ActiveJob adapter and hands back the run immediately (status \"queued\"), so a controller can return while the work happens in the background. The job carries only the run id — the payload lives on the run record, so no arguments (and no secrets) travel through the queue. When the worker picks it up, it reconstitutes the workflow and calls the same execute the sync path uses, so an async run reaches the identical done/failed lifecycle, event log, and status notifications:\n\nclass GenerateReport &lt; Nexo::Workflow\n  def call(payload) = { url: build_report(payload[:account_id]) }\nend\n\nrun = GenerateReport.run_later(account_id: 42)   # returns at once\nrun.status                                        # =&gt; \"queued\"\n# ...the worker runs it in the background; later:\nNexo::RunStore.default.find(run.id).status        # =&gt; \"done\"\n\n\nRoute jobs to a dedicated queue per call or globally:\n\nGenerateReport.run_later(account_id: 42, queue: :nexo)  # per-call\nNexo.configure { |c| c.job_queue = :nexo }              # or a global default\n\n\nScheduling a future run or resume\n\nrun_later and resume_later accept wait: (a duration) or wait_until: (an absolute time), forwarded straight to the installed ActiveJob’s own .set(...) scheduler — Nexo adds no scheduler of its own. Use them to defer an initial enqueue (“send this digest at 9am”) or to let a suspended run wake itself on a timer, symmetrically:\n\n# Defer the initial enqueue until tomorrow morning.\nDailyDigest.run_later({account_id: 42}, wait_until: Date.tomorrow.noon)\n\n# Let a suspended run wake itself up in an hour (no separate scheduled job).\nMyWorkflow.resume_later(run.id, {reminder: true}, wait: 1.hour)\n\n\nThe run’s status is unchanged — a scheduled run_later is still \"queued\" (no \"scheduled\" status is invented), and a scheduled resume_later leaves the run \"suspended\" until the job fires. Passing both wait: and wait_until: in one call raises ArgumentError (checked before any run is created or job enqueued). With neither given, the enqueue is byte-for-byte the immediate one above.\n\n\n  wait:/wait_until:/queue: are scheduling options, not payload. A bare-keyword call consumes them as options: run_later(wait: 60) schedules the job 60 seconds out and leaves the payload {} — it does not store \"wait\" =&gt; 60 as data. A payload that legitimately needs a key named \"wait\" must be passed as an explicit positional Hash: run_later({wait: \"value\"}).\n\n\nThis is still “no scheduler, no cron” — wait:/wait_until: schedule a single future run/resume via ActiveJob; recurring schedules stay the host’s.\n\nNo queue, no scheduler — and the honest caveats\n\nNexo ships no queue and no scheduler — ActiveJob uses whatever adapter your app configured (Sidekiq, GoodJob, Solid Queue, …), and scheduling (cron / GoodJob / whenever) stays the host’s. Without ActiveJob, run_later raises Nexo::MissingDependencyError — use run for synchronous execution.\n\n\n  Needs a shared store. For a worker in another process to find the run, use the ActiveRecord store with a real adapter — the run must live in the database, not in a per-process memory store. The in-memory store only works under the :inline/:test adapters, where the job runs in-process on enqueue.\n\n  No automatic crash recovery / no automatic retries. A crashed or retried job re-runs #call from scratch — Nexo adds no retry_on (configure retries in your host job if you want them). Pair with reconcile_interrupted! (Workflows) to sweep runs orphaned in \"running\". For an intentional pause-and-continue, see Durable workflows — checkpoint skips already-paid-for work when a run resumes.\n\n\n\n\nLive progress — notifications and opt-in Turbo\n\nEvery run broadcasts as it happens over ActiveSupport::Notifications, decoupled from persistence (events still buffer/persist separately). Two notifications fire (a no-op with no ActiveSupport, so the plain-Ruby core stays Rails-free):\n\n\n  nexo.workflow.event — one per emit, payload { run_id:, event: } (the event is the string-keyed {\"type\" =&gt;, \"data\" =&gt;, \"at\" =&gt;} hash). Fires live, even when event persistence is buffered.\n  nexo.workflow.status — on each status transition, payload { run_id:, status: }.\n\n\nThe payloads carry only what emit/the run already hold — no payload or credential dumps. Subscribe for logging, metrics, or your own UI:\n\nActiveSupport::Notifications.subscribe(\"nexo.workflow.event\") do |*, payload|\n  Rails.logger.info(\"[run #{payload[:run_id]}] #{payload[:event][\"type\"]}\")\nend\n\n\nOpt-in Turbo mirror\n\nSet config.broadcast_events = true (and have turbo-rails present) and the engine subscribes Nexo::TurboBroadcaster, which appends each event to a per-run Turbo stream, rendering the overridable partial app/views/nexo/_event.html.erb. To show live progress, add to your own page (Nexo ships no controllers, routes, or dashboard — the host owns all HTTP + UI):\n\n&lt;%= turbo_stream_from \"nexo_run_#{@run.id}\" %&gt;\n&lt;div id=\"nexo_run_&lt;%= @run.id %&gt;_events\"&gt;\n  &lt;%# appended events land here %&gt;\n&lt;/div&gt;\n\n\nOverride the appearance by defining your own app/views/nexo/_event.html.erb in the host app — it takes precedence over the engine’s default.\n\nNexo.configure { |c| c.broadcast_events = true }   # opt in; requires turbo-rails\n\n\n\n  Broadcast reachability. Broadcasts fire from wherever the run executes — under run_later, that’s the worker process. The cable backend (AnyCable, Solid Cable, Redis) must therefore be reachable from your workers, not just your web dynos. Nexo ships no cable backend — broadcasting composes whatever the host configured. Without turbo-rails, broadcast_events is a harmless no-op: the notifications still fire, so you can subscribe to them yourself.\n\n\n\n\nRun helpers for a host UI\n\nNexo::WorkflowRun exposes query helpers so a host can build its own runs UI without Nexo dictating controllers or views:\n\nNexo::WorkflowRun::STATUSES  # =&gt; %w[pending queued running done failed interrupted suspended]\n\nNexo::WorkflowRun.queued     # scope: status \"queued\"\nNexo::WorkflowRun.running    # scope: status \"running\"\nNexo::WorkflowRun.finished   # scope: status \"done\" or \"failed\"\nNexo::WorkflowRun.suspended  # scope: status \"suspended\" (paused, awaiting resume)\n\nrun.queued?  run.running?  run.done?  run.failed?  run.suspended?   # predicates\n\n# Artifact access — content only; serving files stays your\n# controller's job (Nexo ships no artifact routes/controllers):\nrun.artifact(\"digest.md\")          # =&gt; {\"name\" =&gt;, \"content\" =&gt;, \"at\" =&gt;} or nil\nrun.artifact_content(\"digest.md\")  # =&gt; \"…the body…\" or nil\n\n\nArtifact access is content only; serving files stays your controller’s job — Nexo ships no artifact routes or controllers.\n\n\n\nWalkthrough\n\nA controller + Turbo-page host-side walkthrough is in the repo — install the store, define a workflow, enqueue it from a controller, and render live progress:\n\n\n  View examples/rails_usage.md on GitHub →\n\n\nA live example also wraps an MCP-backed agent in a workflow and captures the digest as an artifact (Task + run_agent):\n\n\n  View examples/inbox_digest_task.rb on GitHub →\n\n\n\n\nNext steps\n\n\n  \n    \n      Workflows\n    \n    \n      The run primitive run_later executes in the background.\n    \n  \n\n  \n    \n      Durable workflows\n    \n    \n      Pause and continue a run across processes."
        },
        {
          "id": "documentation-nexo-sandboxes",
          "title": "Sandboxes",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/nexo/sandboxes/",
          "content": "The sandbox is where an agent’s tools act. Pick in-memory Virtual, host-backed Local, a throwaway Container, or a Remote you inject. Two seams compose the execution environment: the sandbox is where tools act; the permission mode is what they may do (Permissions).\n\nA denied capability returns { error: ... } and the agent loop continues — it does not raise. A path that escapes the workspace raises SecurityError; an agent built with no resolvable model raises Nexo::ConfigurationError.\n\n\n\nThe four sandboxes\n\n\n  \n    \n      Sandbox\n      What it is\n      :shell\n      Best for\n    \n  \n  \n    \n      Virtual (default)\n      In-memory, zero host access\n      raises NotImplementedError (intentional)\n      Reading staged data, pure-Ruby work\n    \n    \n      Local\n      Host filesystem + shell, guarded to cwd\n      Yes (narrowed ENV)\n      Trusted dev/CI\n    \n    \n      Container\n      Throwaway OCI container via docker or Apple container CLI\n      Yes (in container)\n      Model-driven work, untrusted models\n    \n    \n      Remote\n      A remote container you inject (E2B / Daytona / Modal / Docker / your own)\n      Yes (via injected client)\n      Cloud sandboxes, scale-out\n    \n  \n\n\n\n  Safe by default: agents start :virtual — escalating to :local, :container, or :remote is always an explicit choice in your code. The default never widens host access.\n\n\n\n  Virtual (default) — in-memory, zero host access. #shell raises NotImplementedError on purpose: in-memory means no command execution. That is the safety property, not a gap.\n  Local — host filesystem + shell, for trusted dev/CI. Two guards: every path is expanded against cwd and must stay inside it (else SecurityError), and the shell sees only PATH, HOME, LANG (plus explicit env: additions) — never the full process environment.\n  Container — run the tools inside a throwaway local container via the docker (default) or Apple container CLI. Shell-out only, no client gem. Hardened by default (no network, dropped caps, read-only rootfs + ephemeral scratch, read-only host binds); every hardening is an explicit opt-out. See Container sandbox below.\n  Remote — run the tools inside a remote container by injecting a client. Escalating to :remote is always an explicit choice — the default stays :virtual.\n\n\n\n\nCapability matrix\n\nTools::Fetch and Tools::WebSearch (:fetch / :search) run in the host process (stdlib net/http / a host-injected backend), so no sandbox constrains them — not even a --network none container. They are bounded only by the capability gate plus fetch_allow / the injected backend. See Web.\n\n\n  \n    \n       \n      :read\n      :glob\n      :write\n      :shell\n      :fetch\n      :search\n    \n  \n  \n    \n      Virtual sandbox\n      Yes\n      Yes\n      Yes (in-memory)\n      No NotImplementedError→{error}\n      Yes †\n      Yes †\n    \n    \n      Local sandbox\n      Yes (guarded)\n      Yes\n      Yes (guarded)\n      Yes (narrowed ENV)\n      Yes †\n      Yes †\n    \n    \n      Container sandbox\n      Yes (guarded)\n      Yes\n      Yes (guarded, scratch)\n      Yes (in container)\n      Yes †\n      Yes †\n    \n  \n\n\n† :fetch / :search run in the host process — no sandbox constrains them.\n\nA :virtual agent never advertises a Shell tool it can never run — Agent#chat attaches Shell only when @sandbox.supports?(:shell). ReadFile/WriteFile/Glob are always attached. See Tools.\n\n\n\nSafety refinements — safer, more legible real-FS sandboxes\n\nFive small refinements tighten the real-filesystem sandboxes (Local, Container) and make the execution environment more legible to the model. Each wires into an existing seam — no new sandbox tier, no new capability, no new dependency. Every one tightens a default or narrows scope; none widens authority silently.\n\n\n  Self-describing sandbox (Sandbox#instructions). A real-FS sandbox appends one plain-text system message describing where the agent runs, so a weak local tool-caller knows its environment. Local → “You run on the host machine, cwd /path/to/repo. The real host filesystem and shell are reachable; file access is guarded to /path/to/repo.”; Container → “You run inside a docker container (image node:22-slim), cwd /workspace, network none…”. Virtual says nothing. Ordering: agent instructions → sandbox instructions → skill instructions.\n  Capability-gated tool attach (Sandbox#supports?). A :virtual agent no longer advertises a Shell tool it can never run — Agent#chat attaches Shell only when @sandbox.supports?(:shell).\n  Shell output truncation (Nexo::OutputTruncator). Unbounded command output (npm install, git log) is truncated before it reaches the model — strips ANSI escapes, keeps the last max_lines lines, appends a …[truncated N lines] marker, then caps at max_chars. The integer status passes through untouched. Pure line/char truncation — no tokenizer; configurable via the kwargs only.\n  Read-before-write + stale guard (real-FS only). Within a session, the agent is blocked from overwriting a file it never read, or one that changed underneath it. Overwriting an existing, un-read file returns {error: \"read &lt;path&gt; before overwriting it\"}; a file whose mtime changed since the read returns {error: \"stale: &lt;path&gt; changed since you read it\"}; a new file writes freely. Real-FS only — skipped entirely on Virtual. Best-effort: mtime-based. Clobber-safety within a session only — no versioning, locking, or VCS semantics.\n  \n    Scoped :ask predicate (ask_when). Under :ask, Permissions.new(ask_when: -&gt;(cap, detail) { … }) scopes which actions actually prompt a human. It only ever narrows what is auto-allowed from the “ask for everything” baseline — it never widens authority.\n\n    # Only prompt for writes under /protected; auto-allow everything else.\nperms = Nexo::Permissions.new(\n  mode: :ask,\n  on_ask:   -&gt;(cap, detail) { ask_the_human(cap, detail) },\n  ask_when: -&gt;(cap, detail) { cap == :write &amp;&amp; detail.to_s.start_with?(\"/protected\") }\n)\n    \n  \n\n\n\n\nRemote sandbox — bring your own container\n\nSandboxes::Remote contains zero vendor code. It wraps any object that satisfies a four-method contract — read, write, exec, close — and delegates the Sandbox interface to it. Switching providers is swapping the injected object, nothing else:\n\nsandbox = Nexo::Sandboxes::Remote.new(client: my_container_client)\n# read(path)            -&gt; client.read(path)\n# write(path, content)  -&gt; client.write(path, content)\n# shell(cmd, timeout:)  -&gt; client.exec(cmd, timeout:)\n# glob(pattern)         -&gt; client.exec(&lt;pattern as a positional $1, never interpolated&gt;)\n# close                 -&gt; client.close\n\n\nVendor SDKs rarely expose exactly read/write/exec/close, so adapt them with a tiny shim object. Keep the vendor gem a soft dependency behind a lazy require that raises Nexo::MissingDependencyError when it’s absent:\n\n# A ~10-line adapter wrapping a hypothetical vendor client to the four-method contract.\nclass E2BAdapter\n  def initialize(api_key:)\n    require \"e2b\"            # soft dep — lazy, only when you actually use it\n    @sbx = E2B::Sandbox.create(api_key: api_key)\n  rescue LoadError\n    raise Nexo::MissingDependencyError, \"E2BAdapter needs `gem \\\"e2b\\\"` in your Gemfile.\"\n  end\n\n  def read(path)              = @sbx.files.read(path)\n  def write(path, content)    = @sbx.files.write(path, content)\n  def exec(cmd, timeout: 30)  = (r = @sbx.commands.run(cmd, timeout: timeout)\n                                 {stdout: r.stdout, stderr: r.stderr, status: r.exit_code})\n  def close                   = @sbx.kill\nend\n\nagent = Nexo::Agent.new(model: ENV.fetch(\"NEXO_MODEL\"),\n                        sandbox: Nexo::Sandboxes::Remote.new(client: E2BAdapter.new(api_key: ENV[\"E2B_API_KEY\"])))\n\n\nNexo ships only Remote plus this documented pattern — purpose-built Sandboxes::E2B / Sandboxes::Daytona classes are a possible future addition, deliberately left out of v1 because their vendor client APIs aren’t pinned yet.\n\n\n\nContainer sandbox — Docker / Apple Container\n\nSandboxes::Container runs an agent’s tools inside a throwaway OCI container via the docker (default) or Apple container CLI — shell-out only through Open3, no client gem, no Compose, no image builder. A model-driven agent never touches your host filesystem or shell directly. Declare it with the sandbox macro (image: is required — there is no default image):\n\nclass ContainerReviewer &lt; Nexo::Agent\n  model   ENV.fetch(\"NEXO_MODEL\")\n  sandbox :docker, image: \"node:22-slim\",\n          binds: { Dir.pwd =&gt; { to: \"/workspace/repo\", mode: :ro } }\nend\n\n\nThe container cwd defaults to /workspace (a container path, not your host directory); the host dir enters only through a binds: entry.\n\nruntime: — one class, two CLIs\n\nsandbox :docker (or runtime: :docker) shells out to docker; sandbox :apple (runtime: :apple) shells out to Apple’s container binary. The run/exec surface is largely shared; where the CLIs diverge (networking especially) the class branches on the runtime. Apple container parity is NOT yet verified — the flags are encoded from the reference mapping, not confirmed against a live daemon, so every Apple flag must be verified before trust. An unknown runtime raises Nexo::ConfigurationError.\n\nHardened by default — every knob an explicit opt-out\n\nAll of the following are applied to the run argv by default and individually invertible:\n\n\n  \n    \n      Concern\n      Default\n      Loosen with\n    \n  \n  \n    \n      Network\n      --network none (no egress)\n      network: :bridge / :host / a network name\n    \n    \n      Capabilities\n      --cap-drop ALL\n      cap_add: %w[NET_BIND_SERVICE ...]\n    \n    \n      Rootfs\n      --read-only\n      readonly_rootfs: false\n    \n    \n      Writable scratch\n      --tmpfs &lt;cwd&gt;:rw (ephemeral), only when readonly_rootfs\n      a :rw host bind for persistence\n    \n    \n      Privilege escalation\n      --security-opt no-new-privileges\n      (not exposed)\n    \n    \n      PIDs\n      --pids-limit 512 (fork-bomb guard)\n      pids_limit: (nil omits the flag)\n    \n    \n      Memory / CPU\n      unset (host decides)\n      memory: / cpus:\n    \n    \n      User / uid\n      left to the image\n      user: (opt-in defense-in-depth)\n    \n    \n      Host binds\n      read-only (:ro)\n      per-bind { to:, mode: :rw }\n    \n    \n      Env vars\n      none\n      env: { \"KEY\" =&gt; \"val\" } → one -e KEY=val per entry\n    \n  \n\n\nBind spec forms:\n\nbinds: { \"/host/proj\" =&gt; \"/workspace/proj\" }                       # -&gt; :ro\nbinds: { \"/host/proj\" =&gt; { to: \"/workspace/proj\", mode: :rw } }    # -&gt; :rw\n\n\nNon-root is not forced. The image’s own uid is respected; user: is an opt-in. The other hardening applies regardless of uid.\n\nEvery argument is passed to Open3 as an array, never string-interpolated, so file contents and commands can’t break out of the argv. Paths are expanded against the container cwd; a path that escapes raises SecurityError. A denied/failed tool op surfaces as { error: ... } through the gated tool layer; the sandbox itself raises only on misuse — a missing binary (Nexo::ConfigurationError naming the binary), a path escape (SecurityError), or a container start failure (Nexo::Error).\n\nLifecycle — ephemeral by default, opt-in reconnect\n\nThe container starts lazily on first tool use and its id is memoized.\n\n\n  Ephemeral (default, reconnect: false): close force-removes the container and clears the memo. Idempotent. A standalone container-backed agent tears its container down on Agent#close; a workflow driving one through run_agent shares the run’s sandbox, so teardown happens once at the end of the run in Workflow.execute’s ensure.\n  Reconnect (name: + reconnect: true): every container is tagged at run with an exact identity label — --label nexo.sandbox.id=&lt;name&gt;. On start the sandbox looks up that container by the exact label filter, not a name substring, and reuses/restarts it instead of creating a new one; close leaves it running/stopped so a later sandbox with the same name reattaches. The default container name is nexo-&lt;run-id&gt;.\n    \n      Exact match, never a substring. A container merely named &lt;name&gt;x is never reattached — the label filter is exact.\n      Ambiguity raises, never guesses. If more than one container carries the same identity label, reconnect raises Nexo::Error rather than pick one.\n      Reconnect never crosses runtimes. A :docker container is never reattached by an :apple sandbox or vice versa.\n    \n  \n\n\nHonest caveats\n\n\n  Network-none breaks installs. npm install / bundle install need egress; with the default network: :none they fail. Pass network: :bridge or bake dependencies into the image.\n  Read-only rootfs needs the scratch. With --read-only, only the tmpfs at cwd (and any :rw bind) is writable, and the tmpfs is ephemeral — lost on close. Persist via a :rw bind.\n  Non-root is recommended, not forced. The default hardening holds regardless of uid; set user: for defense-in-depth.\n  Apple container parity is NOT yet verified — especially networking. Every Apple flag in the parity table is UNVERIFIED; confirm against Apple’s CLI before trusting the :apple runtime in production.\n  Reconnect is Docker-only today. reconnect: true combined with runtime: :apple raises Nexo::ConfigurationError at the point reconnect would run. Use runtime: :docker for reconnect, or run an ephemeral :apple sandbox.\n\n\nLive example\n\nA runnable end-to-end container example is in the repo — the agent reads the mounted repo but never touches your host directly, with no network, dropped capabilities, a read-only rootfs, and the host repo bind-mounted read-only. When the run ends, agent.close tears the container down.\n\nNEXO_LIVE=1 NEXO_MODEL=gemma3:12b ruby -Ilib examples/container_review.rb /path/to/repo\n\n\n\n  View examples/container_review.rb on GitHub →\n\n\n\n\nNext steps\n\n\n  \n    \n      Permissions\n    \n    \n      The second safety axis — what tools may do, on top of where they run.\n    \n  \n\n  \n    \n      Tools\n    \n    \n      The four sandbox-backed tools gated by these seams."
        },
        {
          "id": "documentation-nexo-sessions",
          "title": "Sessions",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/nexo/sessions/",
          "content": "A Workflow is fire-and-finish. A Nexo::Session is the other half: a remembering instance of an agent, addressed by (agent_name, instance_id), that accumulates context across separate invocations.\n\nNexo::Session.resume(Assistant, \"user-42\").prompt(\"My name is Mac.\")\n# ... a later request, job, or process ...\nNexo::Session.resume(Assistant, \"user-42\").prompt(\"What is my name?\")\n# =&gt; \"...Mac...\" — the persisted thread carried the earlier turn\n\n\nresume finds-or-creates the one thread for that pair (the pair is unique — one thread per pair) and returns a session whose #prompt appends to it. #prompt takes the same max_turns: and &amp;on_event block as Agent#prompt, yielding the same (:tool_call | :tool_result | :done, payload) events. Extra keywords are forwarded to the agent constructor (e.g. Nexo::Session.resume(Assistant, \"u1\", cwd: repo)).\n\n\n  A session adds only memory + addressability — never authority. Its sandbox, permissions (default :read_only), skills, MCP servers, and fetch_allow are exactly the agent’s; opening or resuming a session never widens what the agent can do. The persisted record supplies the thread; the agent supplies the tools/skills/instructions onto it.\n\n\n\n\nComposition — acts_as_chat, owned by the host\n\nMessage persistence is RubyLLM’s acts_as_chat — Nexo defines no message table and serializes nothing. The host Rails app owns all four persistence models (Chat, Message, ToolCall, Model), generated by ruby_llm’s own installer:\n\nrails g ruby_llm:install      # generates the Chat/Message/ToolCall/Model models + migrations\n\n\nOne setup step beyond the installer: the session chat model must be addressable, so add two columns and a unique composite index to the generated chats table:\n\nclass AddNexoAddressingToChats &lt; ActiveRecord::Migration[8.0]\n  def change\n    add_column :chats, :agent_name,  :string\n    add_column :chats, :instance_id, :string\n    add_index  :chats, [:agent_name, :instance_id], unique: true\n  end\nend\n\n\nTell Nexo which model hosts sessions (only if it isn’t ruby_llm’s default Chat):\n\nNexo.configure { |c| c.session_chat_model = \"Chat\" } # default; a String class name,\n                                                     # constantized lazily at resume time\n\n\n\n\nRails-only durability — plain Ruby is in-memory\n\nDurable sessions require ActiveRecord. Backend selection guards on defined?(::ActiveRecord::Base) and the host chat model being defined (mirroring how RunStore only uses the AR store when Nexo::WorkflowRun is present):\n\n\n  Rails (durable): the thread is a chats row; acts_as_chat’s callbacks persist every message. It survives across requests, jobs, and process restarts.\n  Plain Ruby (in-memory): a process-wide store holds a live RubyLLM::Chat per pair. The thread lives only for the process — a fresh process starts empty. This is documented, non-durable behavior, not a bug.\n\n\nRe-applying the agent’s instructions on every resume is idempotent: acts_as_chat stores instructions as role: :system messages, and Nexo re-applies them with with_instructions (replace semantics) so the stored thread keeps exactly one copy across resumes rather than accumulating duplicate system messages. The runtime tools (the four sandbox tools + MCP + fetch) are re-attached each resume — they are not persisted, and that is correct.\n\n\n\nRetention, PII, and #close — the honest trade-off\n\nA continuing session is a persistence surface, and that has real costs:\n\n\n  Stored messages persist until you delete them, and may contain sensitive data. A long-lived thread accumulates whatever the user and tools put into it. Nexo does not redact, expire, or GC anything — retention is your responsibility. Treat the chats/messages tables as PII stores and apply your own retention policy.\n  \n    Close sessions that hold resources. If the agent declares MCP servers (stdio/SSE) or fetch, a session holds live subprocesses/sockets. Call #close when done — it delegates to Agent#close, tearing those down (idempotent, safe with nothing held):\n\n    session = Nexo::Session.resume(InboxAssistant, \"user-42\")\nbegin\n  session.prompt(\"Summarize my unread threads.\")\nensure\n  session.close   # releases the agent's MCP/stdio/SSE connections\nend\n    \n  \n\n\n\n\nLive example\n\nA runnable, env-gated two-prompt resume is in the repo:\n\nNEXO_LIVE=1 NEXO_MODEL=… ruby -Ilib examples/chat_session.rb\n\n\n\n  View examples/chat_session.rb on GitHub →\n\n\n\n\nNext steps\n\n\n  \n    \n      Loops\n    \n    \n      The engine that drives an agent's tool-calling turns.\n    \n  \n\n  \n    \n      Concurrency\n    \n    \n      Fan out sessions and agents without tripping rate limits."
        },
        {
          "id": "documentation-nexo-skills",
          "title": "Skills",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/nexo/skills/",
          "content": "A skill is a SKILL.md package — frontmatter plus instructions — that teaches the model how you want a task done. Skills guide reasoning; the sandbox-backed tools perform execution. Nexo does not implement skill loading; it composes the ruby_llm-skills gem so you attach a skill with one macro and no loader setup.\n\n\n\nA skill package\n\nDrop a package under app/skills/ (or scaffold one — see below):\n\napp/skills/\n└── triage/\n    ├── SKILL.md          # frontmatter (name, description) + process steps\n    └── references/       # supporting docs the skill can cite\n\n\n---\nname: triage\ndescription: Triage incoming issues by severity and route them to the right owner.\n---\n\n# Triage\n\n## Process\n1. Classify the issue severity.\n2. Route to the right owner.\n\n\nReference it with the skills macro — its instructions are layered on top of the agent’s own, in declaration order:\n\nrequire \"nexo\"\n\nclass TriageAgent &lt; Nexo::Agent\n  model ENV.fetch(\"NEXO_MODEL\")   # any ruby_llm model — never a hardcoded vendor default\n  skills :triage                  # one macro, no loader wiring\nend\n\nTriageAgent.new.chat   # chat built with the base sandbox tools + the skill's instructions\n\n\n\n\nScaffold a skill\n\nScaffold a new skill package with the generator (creates a valid SKILL.md plus a references/ directory):\n\nrails g nexo:skill triage\n#   create  app/skills/triage/references/.keep\n#   create  app/skills/triage/SKILL.md\n\n\n\n\nAn optional dependency\n\nruby_llm-skills is an optional dependency — required lazily only when you use a skill. Without it installed, require \"nexo\" still loads; touching a skill raises a clear Nexo::MissingDependencyError telling you to add gem \"ruby_llm-skills\". Referencing a skill that does not exist raises Nexo::Error naming the missing SKILL.md path.\n\n\n\nSkill tools stay gated\n\nA skill contributes instructions only. A loaded skill ships no independent tools, and Nexo deliberately does not attach ruby_llm-skills’ progressive-disclosure tool (which reads files outside the sandbox). The model reaches a skill’s references//scripts/ files through Nexo’s own permission-gated, sandbox-backed tools — so attaching a skill never widens what an agent can do beyond its configured sandbox/permission mode.\n\n\n  Safe by default: skills add reasoning, never authority. A :read_only agent with a skill is still :read_only.\n\n\n\n\nLive example\n\nA runnable example points a code-reviewer agent at a local Ollama model, attaches a ruby-code-review skill, and accounts tokens per prompt:\n\nNEXO_MODEL=gemma3:12b ruby -Ilib examples/code_reviewer.rb\n\n\nThe skill package it uses lives at examples/skills/ruby-code-review/SKILL.md in the repo.\n\n\n  View examples/code_reviewer.rb on GitHub →\n\n\n\n\nNext steps\n\n\n  \n    \n      Loops\n    \n    \n      The engine that runs the skilled agent's turns.\n    \n  \n\n  \n    \n      Tools\n    \n    \n      The sandbox-backed tools a skill teaches the model to use."
        },
        {
          "id": "documentation-nexo-tools",
          "title": "Tools",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/nexo/tools/",
          "content": "Nexo attaches four sandbox-backed tools — ReadFile, WriteFile, Shell, and Glob — each gated by the sandbox and permission seams. Which tools attach depends on what the sandbox supports; what they may do depends on the permission mode.\n\n\n\nThe four sandbox tools\n\n\n  \n    \n      Tool\n      Capability\n      What it does\n      Attached when\n    \n  \n  \n    \n      ReadFile\n      :read\n      Read a file from the sandbox\n      Always\n    \n    \n      WriteFile\n      :write\n      Write a file into the sandbox\n      Always (gated by the permission mode)\n    \n    \n      Glob\n      :glob\n      Match files by pattern\n      Always\n    \n    \n      Shell\n      :shell\n      Run a shell command in the sandbox\n      Only when sandbox.supports?(:shell)\n    \n  \n\n\nReadFile/WriteFile/Glob are always attached. Shell attaches only when the sandbox supports it — so a :virtual agent never advertises a Shell tool it can never run. Local/Container support all four capabilities; Virtual supports everything but :shell (it raises NotImplementedError on purpose — in-memory means no command execution).\n\n\n  Safe by default: under :read_only, :read/:glob are auto-allowed and :write/:shell are denied — the agent can look but not touch. Grant individual capabilities with Permissions.new(mode: :read_only, allow: %i[read glob fetch]) without changing the mode.\n\n\n\n\nShell — output truncation\n\nUnbounded command output (npm install, git log) is truncated before it reaches the model, so a single command can’t blow a small context window. Tools::Shell wraps stdout/stderr through Nexo::OutputTruncator.call(text, max_lines: 200, max_chars: 16_000) — strips ANSI escapes, keeps the last max_lines lines, appends a …[truncated N lines] marker, then caps at max_chars. The integer status passes through untouched.\n\nPure line/char truncation — no tokenizer; configurable via the kwargs only (no global config, no per-agent macro).\n\n\n\nWriteFile — read-before-write + stale guard\n\nWithin a session, the agent is blocked from overwriting a file it never read, or one that changed underneath it. Agent#chat builds one Nexo::ReadTracker per chat and threads it into ReadFile (records (path, mtime) on a successful read) and WriteFile (enforces):\n\n\n  Overwriting an existing, un-read file returns {error: \"read &lt;path&gt; before overwriting it\"}.\n  A file whose mtime changed since the read returns {error: \"stale: &lt;path&gt; changed since you read it\"}.\n  A new file writes freely.\n\n\nThe guard is real-FS only — skipped entirely on Virtual (nil mtime) and when no tracker is passed (direct tool construction). Best-effort: mtime-based, so a sub-second external edit may slip past the stale check (read-before-write is the primary guard). Clobber-safety within a session only — no versioning, locking, or VCS semantics.\n\n\n\nFailure model — errors, not exceptions\n\nA denied capability returns { error: ... } to the model (recoverable) and never raises into the loop — identical to a sandbox tool failure. A path that escapes the workspace raises SecurityError (sandbox misuse); everything else surfaces as recoverable context for the model to adapt to.\n\n\n\nWeb tools — fetch and search\n\nThe fetch tool for reading the web and the search tool for discovering URLs live in the Web guide. They are gated by their own :fetch and :search capabilities (denied under :read_only exactly like :shell) plus a host allow-list / an injected backend, and they run in the host process — no sandbox constrains them, not even a --network none container.\n\nThese are sandbox refinements as much as tool behavior — see Sandboxes for the guard details behind each capability, and MCP for attaching external tool servers through the protocol.\n\n\n\nNext steps\n\n\n  \n    \n      MCP\n    \n    \n      Attach external MCP servers behind a fail-closed allow-list.\n    \n  \n\n  \n    \n      Web\n    \n    \n      The fetch and search tools for reading and discovering the web."
        },
        {
          "id": "documentation-nexo-web",
          "title": "Web",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/nexo/web/",
          "content": "Two tools give an agent safe, default-denied web access. Nexo::Tools::Fetch reads a URL with a stdlib HTTP(S) GET, gated by a :fetch capability and a host allow-list. Nexo::Tools::WebSearch discovers URLs, gated by a :search capability and a host-injected backend. They pair: search finds URLs, fetch reads one.\n\n\n  Safe by default: :fetch and :search are denied under :read_only exactly like :shell. Web egress is an escalation, not a “read”. A default agent that never calls fetch_allow / search_backend gets no web tool at all.\n\n\nBoth tools run in the host process (stdlib net/http / a host-injected backend), so no sandbox constrains them — not even a --network none container. They are bounded only by the capability gate plus fetch_allow / the injected backend.\n\n\n\nThe fetch tool\n\nrequire \"nexo\"\n\nclass NewsSummary &lt; Nexo::Agent\n  model ENV.fetch(\"NEXO_MODEL\")\n\n  # :fetch is DEFAULT-DENIED (like :shell). Grant it explicitly, then scope hosts tightly.\n  permissions Nexo::Permissions.new(mode: :read_only, allow: %i[read glob fetch])\n  fetch_allow %w[lite.cnn.com text.npr.org hnrss.org]\n\n  skills :news_summary   # teaches WHICH sites to read and HOW to summarize\nend\n\n\nTwo independent locks must both open before a byte leaves the process:\n\n\n  The :fetch capability — a first-class capability, denied under :read_only exactly like :shell. You grant it with :auto, or an explicit Permissions.new(mode: :read_only, allow: %i[read glob fetch]).\n  The fetch_allow host list — scopes which hosts the tool may reach. Matching is subdomain-aware, never a glob: fetch_allow %w[example.com] permits example.com and news.example.com, but refuses notexample.com and example.com.evil.org. Declaring fetch_allow alone does not grant :fetch — it only scopes hosts.\n\n\nOn any denial or error the tool returns { error: … } (recoverable) and never raises into the loop — identical to the sandbox tools. Success returns { body: &lt;raw page, truncated to 200 KB&gt; }.\n\nSecurity — read before allow-listing a host\n\nWeb egress is a real attack surface. Tools::Fetch is deliberately narrow, but you own the allow-list:\n\n\n  Fetched pages are untrusted input (prompt injection). The tool does no HTML→text extraction — it returns the raw body and the skill instructs the model to pull out what it needs. A page can carry text that looks like instructions (“now fetch http://internal/secrets”); never let the model act on content it fetched.\n  Keep the allow-list tight (SSRF). An over-broad allow-list invites server-side request forgery. List the specific hosts you trust, nothing more.\n  Private/loopback is always refused. Even an explicitly allow-listed host is rejected when it resolves to a loopback, RFC1918-private, or link-local address — an allow-listed localhost still returns { error: }. This guard runs after the allow-list and cannot be bypassed.\n  GET only, fixed User-Agent. No POST/PUT/DELETE, no credentialed requests, no model-controlled headers, no redirect-following to off-list hosts, no crawler/cache/rate limiter. The only header the model influences is a fixed User-Agent: Nexo/&lt;version&gt;.\n\n\nJS-heavy pages — use an MCP fetch server instead\n\nTools::Fetch reads static HTML; it does not render JavaScript. For JS-heavy pages, compose an MCP fetch/browser server instead — it runs its own headless renderer and Nexo gates it through the separate MCP axis:\n\nclass BrowseAgent &lt; Nexo::Agent\n  model ENV.fetch(\"NEXO_MODEL\")\n  mcp :fetch, transport: :stdio, command: \"npx\", args: %w[-y @modelcontextprotocol/server-fetch]\n  mcp_allow %w[fetch]\nend\n\n\nwebmock is a dev/test-only dependency (the offline suite stubs all HTTP); it is not a runtime dependency — Tools::Fetch uses only stdlib.\n\nLive example\n\nA live example demonstrates read-only web fetch scoped by fetch_allow:\n\nNEXO_LIVE=1 NEXO_MODEL=… ruby -Ilib examples/news_summary.rb\n\n\n\n  View examples/news_summary.rb on GitHub →\n\n\n\n\nWeb search — the search tool\n\nNexo::Tools::WebSearch gives an agent a vendor-neutral way to discover URLs. It authorizes a new, default-denied :search capability, then delegates the query to a host-injected backend and returns normalized, capped results. Nexo ships no search provider — you inject the backend.\n\nrequire \"nexo\"\n\nclass ResearchAgent &lt; Nexo::Agent\n  model ENV.fetch(\"NEXO_MODEL\")\n\n  # :search is DEFAULT-DENIED (like :fetch/:shell). Grant it explicitly.\n  permissions Nexo::Permissions.new(mode: :read_only, allow: %i[read glob fetch search])\n  fetch_allow    %w[lite.cnn.com text.npr.org]\n  search_backend MyBraveAdapter.new(ENV.fetch(\"BRAVE_API_KEY\")) # host-owned; Nexo ships none\nend\n\n\nTwo things must both be true before the tool runs:\n\n\n  The :search capability — a first-class capability, denied under :read_only exactly like :fetch/:shell. Grant it with :auto, or an explicit Permissions.new(mode: :read_only, allow: %i[read glob search]).\n  A declared search_backend — the injected provider. A default agent that never calls search_backend gets no search tool at all; existing agents are byte-for-byte unchanged.\n\n\nThe backend contract\n\nThe backend is any object responding to:\n\nsearch(query, **opts) -&gt; Enumerable of {title:, url:, snippet:}\n\n\nRows may be Hashes or any object responding to #to_h. Nexo normalizes each row to {title:, url:, snippet:} (all stringified), truncates the snippet to 300 chars, and returns at most 8 rows:\n\n{ results: [{ title: \"…\", url: \"https://…\", snippet: \"… (≤300 chars)\" }, …] }   # ≤8 rows\n\n\nOn any denial or error the tool returns { error: … } (recoverable) and never raises into the loop. The v1 tool exposes only query; result count, region, safesearch and other **opts stay a host-side backend concern and are never populated by the tool.\n\nSecurity — read before injecting a backend\n\n\n  The search tool runs in the host process, not the sandbox. Like Fetch, a --network none container does not constrain it; only the :search capability and the backend’s own scope do.\n  The backend is trust-bearing. Nexo hands it the raw query and returns its results to the model as untrusted input — snippets can carry prompt-injection text. Choose a reputable backend, and never let the model act on a snippet’s instructions.\n\n\nLive example\n\nA live example demonstrates a host-injected search_backend plus fetch:\n\nNEXO_LIVE=1 NEXO_MODEL=… ruby -Ilib examples/news_search.rb\n\n\n\n  View examples/news_search.rb on GitHub →\n\n\n\n\nNext steps\n\n\n  \n    \n      Tools\n    \n    \n      The four sandbox-backed file and shell tools.\n    \n  \n\n  \n    \n      MCP\n    \n    \n      Attach external MCP servers for more capabilities."
        },
        {
          "id": "documentation-nexo-workflows",
          "title": "Workflows",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/nexo/workflows/",
          "content": "An agent accumulates context — it keeps a conversation going. A workflow fires and finishes: a finite job with a stable runId, a status, a payload, a result, and an ordered, inspectable event log. Subclass Nexo::Workflow, implement #call(payload), and run it.\n\n\n\nLifecycle\n\nrequire \"nexo\"\n\nclass SummarizeDocument &lt; Nexo::Workflow\n  def call(payload)\n    emit(:started, doc_id: payload[:doc_id])\n    summary = payload[:text].to_s.slice(0, 280)   # pure Ruby — no Agent needed\n    emit(:summarized, length: summary.length)\n    { summary: summary }\n  end\nend\n\nrun = SummarizeDocument.run(doc_id: 123, text: \"Long text…\")\nrun.id      # =&gt; \"0191d6b2-…\"  (UUID v7 string, time-ordered)\nrun.status  # =&gt; \"done\"\nrun.result  # =&gt; { \"summary\" =&gt; \"Long text…\" }\n\n\n#call receives a symbol-keyed payload; the stored payload and result read back string-keyed (they survive a JSON round-trip identically whether the run lives in memory or in the database).\n\nFailure model — workflows re-raise\n\nA workflow that raises is recorded as failed with the error message and the exception still propagates to your caller:\n\nrun = BoomWorkflow.run     # raises — but the run is persisted as failed first\n# =&gt; RuntimeError: kaboom\n\n\nThis is deliberately the opposite of a Nexo tool failure, which returns { error: … } and never raises into the agent loop. A tool error is recoverable context for the model; a workflow failure is a job that did not complete. By default a failed run is not retried — the exception is yours to handle. Runs orphaned in \"running\" by a crashed worker are swept to \"interrupted\" by reconcile_interrupted!.\n\n\n\nThe event log — emit and nexo logs\n\nemit(:type, data) appends an ordered event (type, data, at) and persists it incrementally. Inspect a run’s log in plain Ruby:\n\nNexo::Workflow.logs(run.id) { |ev| puts \"#{ev[\"at\"]} #{ev[\"type\"]}\" }\n\n\nor, in a Rails app, from the terminal:\n\n$ bundle exec rake \"nexo:logs[0191d6b2-7c4a-7e1f-9a3b-2f5c8d1e6b00]\"\n[2026-06-29T14:02:01Z] started      {\"doc_id\"=&gt;123}\n[2026-06-29T14:02:01Z] summarized   {\"length\"=&gt;280}\n\n\n\n\nWith or without Rails\n\nWith no Rails loaded, runs record to an in-memory store — workflows run, emit, and Nexo::Workflow.logs works, all offline with no database. In a Rails app, install the migration and runs persist to a nexo_workflow_runs table:\n\nrails g nexo:workflows\nrails db:migrate\n\n\nThe same Workflow code drives either backend; Nexo::RunStore.default selects ActiveRecord when it is available and the in-memory store otherwise. The schema uses portable json columns (SQLite and PostgreSQL alike) and a UUID string primary key.\n\n\n\nInput staging and artifacts\n\nA run owns a sandbox — declared with the sandbox class macro (default :virtual; :local for the host filesystem rooted at the cwd macro, default Dir.pwd). It is resolved lazily: a data-only workflow that never touches files builds nothing. A Workflow accepts the same sandbox forms as an Agent — they share one resolver (Nexo::Sandboxes.resolve), so the two can’t drift, including a hardened container:\n\nclass BuildInContainer &lt; Nexo::Workflow\n  sandbox :docker, image: \"node:22-slim\"   # or :apple, or { type: :docker, ... }\n  def call(_payload) = { ok: true }\nend\n\n\nstage(files) writes provided inputs into that sandbox before your #call work begins. It takes either a { \"path\" =&gt; \"content\" } hash or an array of { path:, content: } hashes, emits a :staged event with the count, and returns the count staged.\n\nartifact(name, content:) records a named deliverable on the run — a digest, a report, an improved file, a generated script. The body is written to the sandbox at /artifacts/&lt;name&gt; (so later steps can read it) and recorded on the run. run.artifacts reads it back as an ordered array of string-keyed hashes ({\"name\" =&gt;, \"content\" =&gt;, \"at\" =&gt;}):\n\nclass BuildDigest &lt; Nexo::Workflow\n  def call(payload)\n    stage(payload[:files])                       # baseline + extras into the sandbox\n    artifact(\"digest.md\", content: summarize(sandbox.read(\"/workspace/baseline.md\")))\n    { ok: true }\n  end\nend\n\nrun = BuildDigest.run(files: [{ path: \"baseline.md\", content: \"…\" }])\nrun.artifacts.first[\"name\"]     # =&gt; \"digest.md\"\nrun.artifacts.first[\"content\"]  # =&gt; \"…the digest body…\"\n\n\nYou can also render an artifact from a template you control with from: — no templating engine, just stdlib ERB:\n\n# from: is a real disk file when it exists, else a staged sandbox path.\nartifact(\"digest.md\", from: \"app/templates/digest.md.erb\",\n         locals: { title: \"Weekly\", baseline: sandbox.read(\"/workspace/baseline.md\") })\n\n\n\n  Templates are code, not data. ERB executes arbitrary Ruby. A template passed to artifact(from:) must be a trusted, developer-authored file — never model output or user-uploaded content. Rendering a model-generated or uploaded template is remote code execution. If a body is untrusted, pass it as content: (inert data), not as a from: template.\n\n\nThe artifacts column ships with fresh installs. Apps installed before this release add it with:\n\nrails g nexo:artifacts\nrails db:migrate\n\n\nLive example\n\nThe full offline artifact-from-template flow is runnable in the repo:\n\nruby -Ilib examples/artifact_from_template.rb\n\n\n\n  View examples/artifact_from_template.rb on GitHub →\n\n\n\n\nTasks &amp; Actions — drive an agent\n\nA workflow can declare and drive an agent so the two primitives Nexo owns — a Workflow (the run lifecycle) and an Agent (the skilled, sandbox-backed model loop) — compose into one recipe: stage inputs → run the agent → capture artifacts. The agent class macro names the Agent subclass this workflow drives; run_agent(prompt, max_turns: 25) runs it bound to the run’s own sandbox, forwards every tool call/result and the final response into the run log as agent_* events, and closes the agent afterward.\n\nclass ReviewBaseline &lt; Nexo::Workflow\n  agent CodeReviewer            # the Agent subclass this workflow drives\n\n  def call(payload)\n    stage(payload[:files])                          # inputs into the run's sandbox\n    resp = run_agent(\"Review the staged baseline and report OK or the issues.\")\n    artifact(\"review.md\", content: resp.content)    # capture the agent's output\n    { content: resp.content }\n  end\nend\n\n\nA driven run reads as one coherent story — Nexo::Workflow.logs(run.id) (and nexo:logs) interleaves the workflow’s own events with the agent’s:\n\n[…] staged            {\"count\"=&gt;1}\n[…] agent_tool_call   {\"name\"=&gt;\"read_file\", \"args\"=&gt;{\"path\"=&gt;\"/workspace/baseline.md\"}}\n[…] agent_tool_result {\"ok\"=&gt;true, \"content\"=&gt;\"…\"}\n[…] agent_done        {\"content\"=&gt;\"REVIEW OK\"}\n\n\nThe same workflow runs two ways with no code difference — Nexo stays schedulable, never a scheduler.\n\nAs a scheduled Task — invoke it from a background job (the scheduling itself lives in the host):\n\nclass ReviewBaselineJob &lt; ApplicationJob\n  def perform(files:)\n    ReviewBaseline.run(files: files)   # same run entry point\n  end\nend\n\n# scheduled elsewhere in the host — Nexo does not schedule:\nReviewBaselineJob.perform_later(files: nightly_baseline)\n\n\nAs an interactive Action — invoke the same run from a controller after staging the uploaded files:\n\nclass ReviewsController &lt; ApplicationController\n  def create\n    files = params[:files].map { |f| { path: f.original_filename, content: f.read } }\n    run = ReviewBaseline.run(files: files)   # identical call — no code difference\n    redirect_to review_path(run.id)\n  end\nend\n\n\n\n  Shared-sandbox precedence. Under run_agent the agent uses the workflow’s sandbox; the agent’s own sandbox class macro is ignored (it only applies when the agent runs standalone). The agent keeps its own permissions, skills, mcp, and mcp_allow: the workflow provides the where (sandbox), the agent owns the what (permissions) and the how (skills/instructions). Driving an agent never widens its authority — its safe default (:read_only) is untouched.\n\n\nLive example\n\nA live example wraps an MCP-backed agent in a workflow and captures the digest as an artifact:\n\n\n  View examples/inbox_digest_task.rb on GitHub →\n\n\n\n\nReconciling interrupted runs\n\nA crashed worker leaves runs stuck in \"running\". Nexo::Workflow.reconcile_interrupted! is a one-shot boot/deploy sweep that rewrites only \"running\" → \"interrupted\" (never touching \"done\" or \"failed\") and returns the count. It is never auto-invoked — call it from a boot hook or the shipped rake task:\n\nbundle exec rake nexo:reconcile\n\n\n\n  This is not a liveness check. It cannot tell a genuinely-running run in another process from an orphaned one — so run it once at boot, before any worker starts new runs, not while workers are live.\n\n\n\n\nWhere to next\n\n\n  Durable workflows — checkpoint / suspend! / resume for long-running and human-in-the-loop jobs.\n  Rails — run_later, live progress broadcasting, and run-query helpers for a host UI.\n\n\n\n\nNext steps\n\n\n  \n    \n      Durable workflows\n    \n    \n      Pause a run durably and resume it later without redoing work.\n    \n  \n\n  \n    \n      Rails\n    \n    \n      Run the same workflow in the background with run_later."
        },
        {
          "id": "documentation-tools-equipr",
          "title": "equipr: skills and MCP servers for coding agents",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/tools/equipr/",
          "content": "Install skills, commands, and MCP servers from marketplaces and Agent Plugins (AP) sources into your coding agents: Claude Code, Codex CLI, OpenCode, and Pi.\n\nCurrent version: 0.4.0, which is what this page documents. equipr is pre-1.0, so the surface is still settling.\n\n\n\nWhat Is This?\n\nA single Go binary that:\n\n\n  Fetches a source once, whether that is a git repository, an npm package, an archive URL, or a local path\n  Resolves what is inside it: the plugins, and the skills, commands, and MCP servers each plugin holds\n  Places every component where its target agent expects to find it, by copy or by symlink\n  Records what it did, so update, status, doctor, and uninstall work from facts instead of guesses\n\n\nThe more interesting part is what equipr refuses to do.\n\nIt never registers itself as a native plugin in any agent. No /plugin, no global npm install, nothing that turns up in an agent’s own plugin list; it writes to personal and global config surfaces and stops there. It also never touches a config key it does not own. Every write into a shared agent config is a targeted single-key merge, which is why your hand-edits, your comments, and your key ordering all survive it.\n\nWhy It Exists\n\nSkills and MCP servers are distributed as git repositories, npm packages, and archives, but every coding agent expects them in a different place, in a different shape. Claude Code reads ~/.claude/skills/. OpenCode reads ~/.config/opencode/skills/ and uses a different MCP config schema. The manual answer is copying directories around and hand-editing shared JSON and TOML config files that also hold your credentials.\n\nDoing that by hand is fine once. It stops being fine at four agents, a dozen skills, and a config file you have already customized.\n\n\n\nQuick Start\n\n1. Install\n\nbrew install maquina-app/tap/equipr\n\n\n2. Add a source\n\n$ equipr add https://github.com/coreyhaines31/marketingskills\nFetching https://github.com/coreyhaines31/marketingskills\nAdded marketingskills (marketplace, fetched via git) with 1 plugin(s)\n  - marketing-skills 2.10.0\n\n\n3. See what it holds\n\n$ equipr list\nmarketingskills     marketplace\n  marketing-skills  2.10.0  49 skills\n\n\n4. Install a component into your agents\n\n$ equipr install marketingskills/marketing-skills:seo-audit --yes\nInstalled 1 component(s) into 3 agent(s):\n  [claude-code] seo-audit (copy -&gt; ~/.claude/skills/seo-audit)\n  [opencode] seo-audit (symlink -&gt; ~/.config/opencode/skills/seo-audit)\n  [pi] seo-audit (symlink -&gt; ~/.pi/agent/skills/seo-audit)\n\n\nThree agents rather than four, because Codex was not installed on the machine this run was captured on.\n\nWithout --yes, install is interactive: pick agents, then pick components. Components all start checked, and so do the agents whose binary is on $PATH, so pressing enter through both installs everything into every agent you actually have.\n\n5. Check your installs\n\n$ equipr status\nSTATE  COMPONENT     SOURCE/PLUGIN                     AGENT        MECH     TARGET\nok     seo-audit(s)  marketingskills/marketing-skills  claude-code  copy     ~/.claude/skills/seo-audit\nok     seo-audit(s)  marketingskills/marketing-skills  opencode     symlink  ~/.config/opencode/skills/seo-audit\nok     seo-audit(s)  marketingskills/marketing-skills  pi           symlink  ~/.pi/agent/skills/seo-audit\n\n$ equipr doctor\nNo issues found.\n\n\n\n\nInstallation\n\nEvery release publishes binaries for darwin, linux, and windows on amd64 and arm64. macOS binaries are ad-hoc codesigned, because an unsigned Mach-O binary is killed on Apple Silicon.\n\n# Homebrew (macOS and Linux)\nbrew install maquina-app/tap/equipr\n\n# Install script: /usr/local/bin when that is writable, otherwise ~/.local/bin\ncurl -fsSL https://github.com/maquina-app/equipr/releases/latest/download/install.sh | sh\n\n# From source (Go 1.24+), into any directory on your PATH\ngo build -o ~/.local/bin/equipr ./cmd/equipr\n\n\ngo install ./cmd/equipr works too, but it puts the binary in $(go env GOPATH)/bin, which is not on everyone’s PATH.\n\nDebian and RPM packages and raw archives are on the releases page.\n\nVerify the install:\n\n$ equipr --version\nequipr version 0.3.3\n\n\nequipr doctor reports on the health of your installs. It says nothing about the binary itself, and with nothing installed yet it prints No issues found. and exits 0.\n\n\n\nConcepts\n\n\n  \n    \n      Term\n      What it means\n    \n  \n  \n    \n      origin\n      Where content comes from, as you type it into add. Auto-detected as git, npm, archive, or local path.\n    \n    \n      source\n      One added origin, fetched into the cache and recorded in the registry. Either a marketplace or a single AP package.\n    \n    \n      source-id\n      The short handle a source is addressed by, derived from the origin, usually the repository or directory name.\n    \n    \n      plugin\n      One installable unit inside a source. An AP source has exactly one; a marketplace can have many.\n    \n    \n      component\n      One installable thing inside a plugin: a skill, a command, or an MCP server.\n    \n    \n      agent\n      An install target: claude-code, codex, opencode, or pi.\n    \n    \n      mechanism\n      How a component is placed: copy or symlink.\n    \n  \n\n\nAddressing Grammar\n\nEverything nests, and every command addresses one of these three levels:\n\n&lt;source-id&gt;                       marketingskills\n&lt;source-id&gt;/&lt;plugin&gt;              marketingskills/marketing-skills\n&lt;source-id&gt;/&lt;plugin&gt;:&lt;component&gt;  marketingskills/marketing-skills:seo-audit\n\n\nSource Types\n\n\n  \n    \n      Type\n      Detected by\n      Contains\n    \n  \n  \n    \n      Marketplace\n      .claude-plugin/marketplace.json at the root\n      Many plugins, in subdirectories\n    \n    \n      Agent Plugins package\n      plugin.json at the root\n      Exactly one plugin\n    \n  \n\n\nOrigin Kinds\n\nThere are three kinds: git, npm, and archive. They are resolved in a fixed order, and the order does more work than the individual rules:\n\n\n  An explicit npm: prefix wins immediately: npm\n  Anything ending in .zip, .tar.gz, or .tgz: archive. The test is on the string suffix alone, so a local ./pkg.zip classifies here too. It is checked before git, which is why https://host/pkg.zip resolves as an archive\n  Contains ://, starts with git@, or ends in .git: git\n  Exists on disk: npm if it is a directory holding package.json and no .git, otherwise git. Any existing path qualifies, including a plain file\n  Nothing above matched and nothing exists at that path: npm. This is the fallback, and it is how a bare package name like express resolves\n\n\nA local path is not a fourth kind. Steps 2 and 4 sort it into one of the three, and step 4 holds a trap: if a directory named express happens to exist in your working directory, equipr add express takes it as that directory rather than the npm package. Write npm:express to force the package.\n\nAn origin can also name a subdirectory, either as a fragment or as a forge tree URL:\n\nequipr add https://github.com/owner/repo#plugins/foo\nequipr add https://github.com/owner/repo/tree/main/plugins/foo\n\n\nThe /tree/&lt;ref&gt;/&lt;path&gt; form is http(s) only, and equipr clones the default branch shallowly. A tree URL naming some other branch is reported back to you with the # form as the way forward, rather than being quietly resolved against the default branch.\n\nComponent Types\n\n\n  \n    \n      Type\n      Where it lives in a plugin\n    \n  \n  \n    \n      skill\n      A directory under skills/ holding a SKILL.md\n    \n    \n      command\n      A .md file under commands/\n    \n    \n      MCP server\n      An entry in the plugin’s mcp.json\n    \n  \n\n\nCommands are normalized to &lt;name&gt;/SKILL.md for every agent, so a plugin’s commands and skills land in the same place and are discovered the same way.\n\n\n\nCommands\n\n\n  \n    \n      Command\n      Purpose\n    \n  \n  \n    \n      equipr add &lt;origin&gt;\n      Fetch an origin, resolve its contents, register it\n    \n    \n      equipr list [source-id \\| source-id/plugin]\n      List the registry: sources, their plugins, and what each holds\n    \n    \n      equipr show &lt;source-id&gt;[/plugin]\n      Details for a source or one plugin\n    \n    \n      equipr install &lt;source&gt;/&lt;plugin&gt;[:&lt;component&gt;]\n      Place a plugin’s components into agents\n    \n    \n      equipr update\n      Re-fetch every source that has installs and re-apply each recorded component\n    \n    \n      equipr remove &lt;source-id&gt;\n      Drop a source from the registry\n    \n    \n      equipr uninstall &lt;source&gt;/&lt;plugin&gt;[:&lt;component&gt;]\n      Remove installed files and records\n    \n    \n      equipr doctor\n      Read-only health check\n    \n    \n      equipr status\n      Read-only report of every recorded install\n    \n  \n\n\nadd\n\nRe-running add on a source that already exists refreshes it. Git sources pull; npm and archive sources are re-fetched.\n\nequipr add https://github.com/coreyhaines31/marketingskills\n\n\nlist\n\n-c / --components expands each plugin to its component names. Passing &lt;source&gt;/&lt;plugin&gt; prints what show prints.\n\nequipr list marketingskills --components\n\n\ninstall\n\nInteractive by default: pick agents, then pick components. Components start checked; agents start checked when their binary is on $PATH (see Detection). Use -a to pin targets and --yes to skip the prompts.\n\nequipr install marketingskills/marketing-skills:seo-audit --yes -a claude-code\n\n\nupdate\n\nTakes no arguments. It operates on every source that has installs.\n\nequipr update --dry-run\n\n\n--dry-run still re-fetches, so it can report “would update v1 to v2”, but it writes nothing and never prompts.\n\nremove and uninstall\n\nThe two are deliberately separate:\n\n\n  remove deregisters the source but leaves the installed files and their records in place, so uninstall can still clean them up. doctor reports those records as orphaned-record.\n  remove --purge does it all at once: drops the records, deletes the installed files, and removes the cache clone.\n  uninstall removes files and records for a plugin or a single component, and works on orphaned records after a remove.\n\n\nequipr uninstall marketingskills/marketing-skills:seo-audit\nequipr remove marketingskills --purge\n\n\ndoctor\n\nRead-only. Reports four conditions: orphaned-record, vanished-config-dir, broken-symlink, and modified-cache. It exits 5 when it finds issues and 0 when clean, so scripts checking $? should expect that.\n\nstatus\n\nA report of every recorded install: state (ok, modified, or missing), component, source and plugin, agent, mechanism, and target path. Writes nothing.\n\n\n\nFlags\n\nPersistent\n\nOne flag is registered on the root command and applies everywhere.\n\n\n  \n    \n      Flag\n      Effect\n    \n  \n  \n    \n      -y, --yes\n      Assume yes to all prompts: install into every present agent, install all components, resolve conflicts as overwrite\n    \n  \n\n\n--json is not persistent, despite looking like it should be. It is registered on each of the nine commands that produce results, and completion and help never register it. So equipr --json on its own is an error, and so is equipr completion zsh --json.\n\nPer Command\n\n\n  \n    \n      Command\n      Flag\n      Values\n      Default\n    \n  \n  \n    \n      install\n      -a, --agent (repeatable)\n      claude-code, codex, opencode, pi\n      Prompt\n    \n    \n      install\n      -c, --component\n      A component name, the same thing as the :name suffix\n      Prompt (all with --yes)\n    \n    \n      install\n      --mechanism\n      copy, symlink\n      The per-agent default\n    \n    \n      install\n      --on-conflict\n      prompt, overwrite, skip, fail\n      prompt\n    \n    \n      update\n      --on-conflict\n      prompt, overwrite, skip, fail\n      prompt\n    \n    \n      update\n      --dry-run\n      Report only, write nothing\n      Off\n    \n    \n      remove\n      --purge\n      Also delete records, files, and cache\n      Off\n    \n    \n      list\n      -c, --components\n      Expand plugins to component names\n      Off\n    \n  \n\n\nChoosing a Mechanism\n\n\n  \n    \n      Mechanism\n      Pick it when\n    \n  \n  \n    \n      symlink\n      You want the agent to track the cache, so update is instant and the source stays the single copy on disk\n    \n    \n      copy\n      You want the installed version pinned and independent of the cache, or the agent does not reliably follow links\n    \n  \n\n\nThe per-agent defaults exist for a reason: whole-directory symlinks proved unreliable for Claude Code and Codex, while OpenCode and Pi dereference links at any depth. Symlinks are always created per component, never for a whole directory.\n\nChoosing a Conflict Policy\n\n--on-conflict applies when a target has been locally modified since it was installed.\n\n\n  \n    \n      Value\n      Behavior\n      Use it for\n    \n  \n  \n    \n      prompt\n      Ask per file\n      Interactive use; the default\n    \n    \n      overwrite\n      Discard local edits\n      When the source is the truth\n    \n    \n      skip\n      Keep local edits and move on\n      When you have deliberately customized a skill\n    \n    \n      fail\n      Abort the whole run on the first modification\n      Scripts and CI, where a surprise should stop the pipeline\n    \n  \n\n\nExit Codes\n\nStable, and safe to script against.\n\n\n  \n    \n      Code\n      Meaning\n    \n  \n  \n    \n      0\n      Success\n    \n    \n      1\n      Generic or usage error\n    \n    \n      2\n      Not found\n    \n    \n      3\n      Conflict\n    \n    \n      4\n      Environment error\n    \n    \n      5\n      doctor found issues\n    \n  \n\n\n\n\nAgents\n\n\n  \n    \n      Agent\n      Detected by\n      Components land in\n      MCP config\n    \n  \n  \n    \n      claude-code\n      ~/.claude/ or claude on $PATH\n      ~/.claude/skills/&lt;name&gt;/\n      ~/.claude.json\n    \n    \n      codex\n      ~/.codex/ or codex on $PATH\n      ~/.codex/skills/&lt;name&gt;/\n      ~/.codex/config.toml\n    \n    \n      opencode\n      ~/.config/opencode/ or opencode on $PATH\n      ~/.config/opencode/skills/&lt;name&gt;/\n      ~/.config/opencode/opencode.json\n    \n    \n      pi\n      ~/.pi/agent/ or pi on $PATH\n      ~/.pi/agent/skills/&lt;name&gt;/\n      ~/.config/mcp/mcp.json\n    \n  \n\n\nDefault mechanism: copy for claude-code and codex, symlink for opencode and pi.\n\nDetection\n\nAn agent is offered when either signal holds: a config directory, or a binary on $PATH. Both are needed, because a freshly installed agent has no config directory until its first run, and a config directory outlives an uninstalled agent. The picker labels each agent with the evidence it found (configured, on PATH, or both), so a surprising entry explains itself.\n\nBeing offered and being selected are different things. Since 0.4.0 the picker pre-checks only the agents whose binary is on $PATH. A config-only agent is listed but starts unchecked, because a leftover config directory outlives an uninstalled agent and enter should not push skills into a directory nothing reads. The exception: when nothing at all is on $PATH, every row starts checked, so that enter is never a silent no-op. --yes is unaffected and still installs into every agent present, config-only ones included.\n\nMCP Server Writes\n\nMCP servers are never written as a whole file. equipr merges a single key into the agent’s existing config, atomically. It writes to a temporary file, then renames it, preserving the file mode. JSON goes through sjson and gjson, TOML through a dedicated writer, specifically so key order, formatting, and comments in a file you hand-edited survive the write.\n\nAgent schemas differ, and equipr writes each in its native shape. OpenCode uses an mcp key with an explicit type and command as an array, where Claude Code uses mcpServers with string inference.\n\n\n\nFiles and State\n\n\n  \n    \n      Path\n      Holds\n    \n  \n  \n    \n      $XDG_CONFIG_HOME/equipr/config.toml\n      Reserved; nothing reads it yet\n    \n    \n      $XDG_STATE_HOME/equipr/sources.json\n      The registry: every added source and what is in it\n    \n    \n      $XDG_STATE_HOME/equipr/installs.json\n      Install records: component, agent, target path, content hash\n    \n    \n      $XDG_STATE_HOME/equipr/equipr.lock\n      Lock file, held across mutating operations\n    \n    \n      $XDG_CACHE_HOME/equipr/sources/&lt;id&gt;/\n      The fetched tree, per source\n    \n  \n\n\nOn macOS those XDG defaults resolve under ~/Library/Application Support/equipr and ~/Library/Caches/equipr. On Linux, ~/.local/state/equipr and ~/.cache/equipr.\n\nHand-Editing\n\nUse the commands, not an editor. sources.json and installs.json are rewritten wholesale under the lock, and installs.json records a content hash per target that the conflict check compares against, so editing it by hand makes status and doctor report the wrong thing.\n\nDeleting the cache directory is the one safe destructive act: add or update re-fetches it.\n\n\n\nJSON Output\n\nNine commands accept --json: add, list, show, install, update, remove, uninstall, doctor, and status. Results go to stdout and progress to stderr, so stdout stays parseable.\n\nScript against --json, not the plain-text output. The text layout is still moving: list was restructured in 0.3.2 and status in 0.3.3, both on the same day.\n\nequipr list --json, trimmed. The real skills array holds 49 entries:\n\n{\n  \"sources\": [\n    {\n      \"id\": \"marketingskills\",\n      \"type\": \"marketplace\",\n      \"kind\": \"git\",\n      \"origin\": \"https://github.com/coreyhaines31/marketingskills\",\n      \"plugin_count\": 1,\n      \"plugins\": [\n        {\n          \"name\": \"marketing-skills\",\n          \"version\": \"2.10.0\",\n          \"description\": \"Marketing skills for AI agents — conversion optimization, copywriting, SEO, paid ads, ad creative, and growth\",\n          \"author\": \"Corey Haines\",\n          \"skills\": [\"ab-testing\", \"ad-creative\", \"ads\", \"ai-seo\", \"...\"]\n        }\n      ]\n    }\n  ]\n}\n\n\nequipr status --json, trimmed to one of three entries:\n\n{\n  \"entries\": [\n    {\n      \"source_id\": \"marketingskills\",\n      \"plugin\": \"marketing-skills\",\n      \"component\": \"seo-audit\",\n      \"component_type\": \"skill\",\n      \"agent\": \"claude-code\",\n      \"mechanism\": \"copy\",\n      \"target\": \"~/.claude/skills/seo-audit\",\n      \"version\": \"2.10.0\",\n      \"state\": \"ok\"\n    }\n  ]\n}\n\n\n\n\nShell Completion\n\nCobra-generated completion is available for four shells: bash, zsh, fish, and powershell.\n\nequipr completion zsh &gt; \"${fpath[1]}/_equipr\"     # then restart your shell\nequipr completion bash &gt; /etc/bash_completion.d/equipr\nequipr completion fish &gt; ~/.config/fish/completions/equipr.fish\n\n\nequipr completion &lt;shell&gt; --help prints the install instructions for that shell.\n\nCompletion covers commands and flags. Source, plugin, and component names are resolved at runtime, so they are not completed.\n\n\n\nRecent Changes\n\n\n  \n    \n      Version\n      Change\n    \n  \n  \n    \n      0.4.0\n      Local paths that are neither a git repository nor an npm package are rejected up front with a message naming the problem, instead of failing later as a git error. The install picker pre-checks only agents found on $PATH\n    \n    \n      0.3.4\n      MIT license added. No change to commands, flags, output, or JSON\n    \n    \n      0.3.3\n      status columns sized from the rows\n    \n    \n      0.3.2\n      list nests plugins under sources with component counts, gained --components, and accepts &lt;source&gt;/&lt;plugin&gt;\n    \n    \n      0.3.1\n      Install prompts pre-check every option; agents detected by config directory or binary on $PATH; skills/ and commands/ walked recursively; marketplace entries rooted at \"./\" resolve\n    \n  \n\n\nBefore 0.3.1, install prompts started with nothing selected, so pressing enter installed nothing.\n\n\n\nTroubleshooting\n\nNothing was installed\n\nThe picker toggles with space and submits with enter. Components start checked, so enter installs all of them. Deselect everything and press enter, though, and nothing installs; equipr says as much. Two other ways to end up with nothing: on 0.3.0 and earlier every row started unselected and the error wrongly claimed the plugin had no components, so upgrade or pass --yes; and since 0.4.0 a config-only agent starts unchecked, so an install can succeed while skipping an agent you expected it to reach.\n\nAn agent you do not use is offered\n\nA leftover config directory counts as configured, even with no binary installed. Check the label in the picker: configured on its own means no binary was found on $PATH, and since 0.4.0 that row starts unchecked. Delete the stale directory, or pin your targets with -a. Note that --yes ignores the distinction and installs into every agent present.\n\nA plugin shows no components\n\nMost often it ships only an agents/ directory, holding Claude Code subagents, which equipr does not model yet. equipr handles skills, commands, and MCP servers. That is a known gap in equipr; the source itself is fine.\n\nAdding a local path is rejected\n\nOrigin classification resolves any existing path it does not recognize to git, so a directory that is not a git repository, a plain file, and a local archive are each rejected before anything is fetched, with a message naming the actual problem:\n\n$ equipr add ./plaindir\nequipr: ./plaindir is neither a git repository nor an npm package directory; a local\nsource must be one or the other (run `git init` there, or point equipr at a remote origin)\n\n$ equipr add ./notes.txt\nequipr: ./notes.txt is a file, not a source; add the directory that holds plugin.json\nor .claude-plugin/marketplace.json\n\n$ equipr add ./pkg.zip\nequipr: ./pkg.zip is a local archive; equipr fetches archives over http(s) only, so\nextract it and add the directory instead\n\n\nExit code 1 in all three cases. Before 0.4.0 the first of these surfaced as a raw git clone: repository does not exist, which read like a bug in equipr and was not one.\n\nA marketplace reports fewer plugins than it has\n\nAn entry whose source is a remote reference equipr cannot resolve in-tree (a github, npm, or archive object form) is skipped and named in the output, rather than failing the whole marketplace. The fix belongs upstream in the marketplace.\n\ndoctor reports modified-cache\n\nThe cached tree no longer matches the digest recorded when it was added, which usually means something outside equipr touched the cache. Run equipr add &lt;origin&gt; to re-fetch and clear it.\n\ndoctor exits 5\n\nExit code 5 is documented behavior: doctor found something. Nothing crashed.\n\nRecords survive remove\n\nBy design. remove deregisters the source but leaves files and records so uninstall can still clean them, and doctor reports them as orphaned-record. Use remove --purge for the one-shot version.\n\nTwo components with the same name fail the whole source\n\nComponents are named by their own directory or file, which is not unique across a nested tree, and skills and commands share one flat install namespace. Rather than silently overwriting one with the other, add fails and names both paths. The fix belongs upstream in the source.\n\n\n\nLicense\n\nMIT. Copyright (c) 2026 Mario Alberto Chávez.\n\n\n\nNext Steps\n\n\n  \n    \n      GitHub Repository\n    \n    \n      Source code, releases, issues, and contribution guidelines.\n    \n  \n\n  \n    \n      Announcement\n    \n    \n      Why equipr exists, and what it refuses to do.\n    \n  \n\n  \n    \n      AI Tools\n    \n    \n      MCP servers and Claude Code plugins, the kind of thing equipr installs.\n    \n  \n\n  \n    \n      Agent Plugins Specification\n    \n    \n      The plugin format equipr resolves from a source.\n    \n  \n\n  \n    \n      Agent Skills Specification\n    \n    \n      The skill format behind every SKILL.md equipr installs."
        },
        {
          "id": "documentation-tools",
          "title": "Tools",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/tools/",
          "content": "Developer utilities for your local environment. Menu bar apps for managing databases, CLI tools for workflow automation, and helpers that make development easier.\n\n\n\nAvailable Tools\n\n\n  \n    \n      equipr\n    \n    \n      Install skills, commands, and MCP servers into your coding agents.\n    \n  \n\n  \n    \n      Redis Menu\n    \n    \n      macOS menu bar app for managing local Redis instances.\n    \n  \n\n  \n    \n      Mongo Menu\n    \n    \n      macOS menu bar app for managing local MongoDB instances.\n    \n  \n\n  \n    \n      Git Continuity\n    \n    \n      Transfer work-in-progress between machines without commits.\n    \n  \n\n\n\n\nMenu Bar Apps\n\nRedis Menu and Mongo Menu are native macOS applications that sit in your menu bar. They provide:\n\n\n  One-click start/stop controls\n  Visual status indicators\n  Custom configuration options\n  Auto-start and launch at login\n  Bundled database binaries (no separate installation needed)\n\n\nRequirements\n\nBoth menu bar apps require:\n\n  macOS 15.0 (Sequoia) or later\n  Xcode 16.0 or later (for building from source)\n\n\n\n\nCLI Tools\n\nequipr\n\nInstall skills, commands, and MCP servers from marketplaces and Agent Plugins sources into your coding agents: Claude Code, Codex CLI, OpenCode, and Pi.\n\nequipr add https://github.com/coreyhaines31/marketingskills\nequipr install marketingskills/marketing-skills:seo-audit\n\n\nequipr places each component where each agent expects it, and merges MCP entries into your existing agent config a single key at a time, leaving your hand-edits intact.\n\nView equipr Documentation\n\nGit Continuity\n\nTransfer work-in-progress between machines without committing to git history. Perfect for moving unfinished work between office and home.\n\n# On your work machine\ngit continuity push\n\n# On your home machine\ngit continuity pull\n\n\nYour uncommitted changes, staged files, and untracked files are transferred without polluting your git history."
        },
        {
          "id": "documentation-tools-mongo-menu",
          "title": "Mongo Menu",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/tools/mongo-menu/",
          "content": "A macOS menu bar application that makes managing local MongoDB instances simple and convenient. Start, stop, and configure MongoDB with a single click.\n\n\n  \n  \n\n\n\n\nOverview\n\nMongo Menu sits in your macOS menu bar, providing easy access to start, stop, and configure MongoDB instances. Designed for developers who need to work with MongoDB locally and want a straightforward way to manage the database without terminal commands.\n\n\n\nFeatures\n\n\n  Menu Bar Controls: Start and stop MongoDB with a single click\n  Visual Status Indicator: Instantly see if MongoDB is running\n  Custom Configuration: Configure data directory, log path, and port\n  Auto-start Options: Start MongoDB automatically when the app launches\n  Launch at Login: Start Mongo Menu when your Mac boots\n  Lightweight Footprint: Minimal resource usage in the background\n\n\n\n\nRequirements\n\n\n  macOS 15.0 (Sequoia) or later\n  Admin privileges (for first-time setup)\n\n\n\n\nInstallation\n\nBuild from Source\n\n\n  \n    Clone the repository:\n\n    git clone https://github.com/maquina-app/mongo-menu.git\ncd mongo-menu\n    \n  \n  \n    Run the build script:\n\n    ./build.sh\n    \n\n    The build script will:\n\n    \n      Check if MongoDB binaries exist and download them if needed\n      Build the application\n      Place the built app in build/Release/MongoMenu.app\n    \n  \n  \n    Move the built app to your Applications folder\n  \n\n\nBuild Requirements\n\n\n  Xcode 16.0 or later\n  Command Line Tools for Xcode\n  macOS 15.0 (Sequoia) or later\n\n\n\n\nUsage\n\n\n  Click the MongoDB icon in the menu bar to see status and control options\n  Use “Start MongoDB” or “Stop MongoDB” to control the service\n  Click “Preferences” to configure settings:\n    \n      Data directory location\n      Log file path\n      MongoDB port (default: 27017)\n      Auto-start options\n      Launch at login option\n    \n  \n\n\n\n\nConfiguration\n\nDefault Locations\n\nMongo Menu stores data in these default locations:\n\n\n  \n    \n      Setting\n      Default Path\n    \n  \n  \n    \n      Data directory\n      ~/.local/share/mongodb/data\n    \n    \n      Log file\n      ~/.local/state/mongodb/logs/mongodb.log\n    \n    \n      Port\n      27017\n    \n  \n\n\nAll locations can be customized in the app preferences.\n\nBundled MongoDB\n\nMongo Menu bundles MongoDB binaries (version 8.0.6) specifically for Apple Silicon Macs. You don’t need to install MongoDB separately. The app handles downloading the appropriate MongoDB binaries for your Mac.\n\n\n\nTroubleshooting\n\nMongoDB Won’t Start\n\n\n  Check if the port is already in use by another application\n  Ensure you have write permissions to the data directory and log path\n  Check the log file for specific error messages\n\n\nApp Won’t Launch\n\n\n  Make sure you have macOS Sequoia (15.0) or later installed\n  Try rebuilding from source using the build script\n\n\n\n\nNext Steps\n\n\n  \n    \n      GitHub Repository\n    \n    \n      Source code, issues, and contribution guidelines.\n    \n  \n\n  \n    \n      Redis Menu\n    \n    \n      Similar menu bar app for managing Redis instances."
        },
        {
          "id": "documentation-tools-redis-menu",
          "title": "Redis Menu",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/documentation/tools/redis-menu/",
          "content": "A macOS menu bar application that makes managing local Redis instances simple and convenient. Start, stop, and configure Redis with a single click.\n\n\n  \n  \n\n\n\n\nOverview\n\nRedis Menu sits in your macOS menu bar, providing easy access to start, stop, and configure Redis instances. Designed for developers who need to work with Redis locally and want a straightforward way to manage the database without terminal commands.\n\n\n\nFeatures\n\n\n  Menu Bar Controls: Start and stop Redis with a single click\n  Visual Status Indicator: Instantly see if Redis is running\n  Custom Configuration: Configure data directory, log path, and port\n  Auto-start Options: Start Redis automatically when the app launches\n  Launch at Login: Start Redis Menu when your Mac boots\n  Lightweight Footprint: Minimal resource usage in the background\n\n\n\n\nRequirements\n\n\n  macOS 15.0 (Sequoia) or later\n  Admin privileges (for first-time setup)\n\n\n\n\nInstallation\n\nBuild from Source\n\n\n  \n    Clone the repository:\n\n    git clone https://github.com/maquina-app/redis-menu.git\ncd redis-menu\n    \n  \n  \n    Run the build script:\n\n    ./build.sh\n    \n\n    The build script will:\n\n    \n      Download and compile Redis if needed\n      Build the application\n      Place the built app in build/Release/RedisMenu.app\n    \n  \n  \n    Move the built app to your Applications folder\n  \n\n\nBuild Requirements\n\n\n  Xcode 16.0 or later\n  Command Line Tools for Xcode\n  macOS 15.0 (Sequoia) or later\n\n\n\n\nUsage\n\n\n  Click the Redis icon in the menu bar to see status and control options\n  Use “Start Redis” or “Stop Redis” to control the service\n  Click “Preferences” to configure settings:\n    \n      Data directory location\n      Log file path\n      Redis port (default: 6379)\n      Auto-start options\n      Launch at login option\n    \n  \n\n\n\n\nConfiguration\n\nDefault Locations\n\nRedis Menu stores data in these default locations:\n\n\n  \n    \n      Setting\n      Default Path\n    \n  \n  \n    \n      Data directory\n      ~/.local/share/redis/data\n    \n    \n      Log file\n      ~/.local/state/redis/logs/redis.log\n    \n    \n      Port\n      6379\n    \n  \n\n\nAll locations can be customized in the app preferences.\n\nBundled Redis\n\nRedis Menu downloads, compiles, and bundles Redis binaries for your Mac. You don’t need to install Redis separately. The app handles downloading and compiling the appropriate Redis source code for your system.\n\n\n\nTroubleshooting\n\nRedis Won’t Start\n\n\n  Check if the port is already in use by another application\n  Ensure you have write permissions to the data directory and log path\n  Check the log file for specific error messages\n\n\nApp Won’t Launch\n\n\n  Make sure you have macOS Sequoia (15.0) or later installed\n  Try rebuilding from source using the build script\n\n\n\n\nNext Steps\n\n\n  \n    \n      GitHub Repository\n    \n    \n      Source code, issues, and contribution guidelines.\n    \n  \n\n  \n    \n      Mongo Menu\n    \n    \n      Similar menu bar app for managing MongoDB instances."
        },
        {
          "id": "",
          "title": "Open Source Tools for Rails Developers",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/",
          "content": "Recuerd0\n              \n              \n                Documentation\n              \n              \n                Open Source\n              \n              \n                Blog\n              \n          \n\n          \n            mobile-nav#toggle\"\n            >\n              \n                \n              \n            \n          \n        \n      \n    \n  \n\n\n    \n  \n\n  \n    \n      \n      \n    \n\n    \n      \n        \n          \n            Open Source Rails Tools\n          \n\n          \n            Tools for developers\n            who ship alone\n          \n\n          \n            Generators, UI components, and AI tools extracted from production Rails\n            applications. No build pipelines. No framework fatigue. Built for the\n            one-person framework philosophy.\n          \n\n          \n            \n              Get Started\n            \n\n            \n              View all projects\n            \n          \n        \n      \n    \n  \n\n  \n    \n      \n        Production-tested Rails tools\n      \n\n      \n          \n            \n              UI Library\n              Maquina Components\n              \n                Modern UI components for Ruby on Rails. ERB partials styled with Tailwind CSS 4.0 and Stimulus controllers. Inspired by shadcn/ui, built for the Rails way.\n              \n              \n                \n                  Documentation\n                \n                \n                  \n                    \n                  \n                  GitHub\n                \n              \n            \n          \n          \n            \n              AI Tools\n              Rails Claude Code\n              \n                A marketplace of Claude Code plugins for Rails — code simplification, Rails upgrades, UI standards, MVP planning, Stimulus best practices, spec-driven development, security audits, and a Hotwire dev-server driver.\n              \n              \n                \n                  Documentation\n                \n                \n                  \n                    \n                  \n                  GitHub\n                \n              \n            \n          \n          \n            \n              App Scaffolding\n              Maquina Generators\n              \n                Rails generators that produce standalone application code. Authentication, job queues, error tracking, and security — no runtime dependency. Generate once, own forever.\n              \n              \n                \n                  Documentation\n                \n                \n                  \n                    \n                  \n                  GitHub\n                \n              \n            \n          \n          \n            \n              Engine\n              Maquina Newsletters\n              \n                A mountable Rails 8 engine for drafting, approving, scheduling, and batch-sending HTML newsletters. Action Text editing, an approval workflow, and background batch delivery.\n              \n              \n                \n                  Documentation\n                \n                \n                  \n                    \n                  \n                  GitHub\n                \n              \n            \n          \n      \n    \n  \n\n  \n    \n      \n        \n          \n            \n              Practical tools, not perfect abstractions\n            \n            \n              Every tool here earned its place in a shipping Rails app before it\n              earned a name. No theory, no lock-in — patterns simple enough to\n              own outright.\n            \n            \n          \n\n          \n              \n                \n                  Production first\n                \n                Every tool starts in a real application. No theoretical exercises, no “what if” features. If it&#39;s here, it&#39;s been shipped.\n              \n              \n                \n                  Standard Rails\n                \n                ERB partials, Tailwind CSS, Stimulus only where needed. No new paradigms to learn — the Rails way, refined.\n              \n              \n                \n                  One-person scale\n                \n                Built for developers who ship alone. Simple enough to understand, powerful enough to build real applications.\n              \n          \n        \n      \n    \n  \n\n\n    \n      \n        \n          \n            \n              \n                What we're building\n              \n            \n            \n              View all posts\n              \n                \n              \n            \n          \n\n          \n              \n                  \n\n                \n                  \n                    August 10, 2026\n                  \n                  \n                    \n                      \n                      equipr: Cross-Agent Skill and MCP Server Manager\n                    \n                  \n                  \n                    equipr is out: one Go binary that installs skills, commands, and MCP servers into Claude Code, Codex, OpenCode, and Pi, with no plugin registration.\n                  \n                    \n                        \n                      Mario Alberto Chávez Cárdenas\n                    \n                \n              \n              \n                  \n\n                \n                  \n                    August 4, 2026\n                  \n                  \n                    \n                      \n                      Why I Removed execute_ruby from Rails MCP Server\n                    \n                  \n                  \n                    Rails MCP Server 2.0.0 removes the execute_ruby tool entirely, because the reasoning that justified it in 2025 stopped holding once agentic coding tools could run Ruby themselves.\n                  \n                    \n                        \n                      Mario Alberto Chávez Cárdenas\n                    \n                \n              \n          \n\n          \n            View all posts\n          \n        \n      \n    \n\n\n  \n    \n      \n        \n\n          \n            \n              \n            \n\n            \n              Turn a feature into a shipped pull request — on a new Rails app or\n              one you already have. Durable context, disciplined workflow, full\n              observability from brief to branch.\n            \n            \n              Your host. Your keys. Your repo. Tokens bill straight to your\n              Anthropic account — never proxied, never marked up.\n            \n\n            \n              \n                Explore Fragua\n                \n                  \n                \n              \n            \n          \n\n          \n            \n          \n\n        \n      \n    \n  \n\n  \n    \n      \n        \n\n          \n            \n              \n            \n\n            \n              A dedicated knowledge base for managing the context your AI tools\n              consume. Curate project knowledge once, serve it to every tool via\n              REST API.\n            \n            \n              Works with Claude Code, Cursor, ChatGPT, and any tool that makes\n              HTTP requests.\n            \n\n            \n              \n                Explore Recuerd0\n                \n                  \n                \n              \n            \n          \n\n          \n            \n          \n\n        \n      \n    \n  \n\n  \n    \n      \n        \n\n          \n            \n          \n\n          \n            \n              \n            \n\n            \n              Know your daily vibe, spend without the spiral. Resto tells you\n              exactly what you can spend today — no more \"can I afford this?\"\n              anxiety. Kakeibo-inspired personal finance, simplified.\n            \n            \n              Track checking accounts, credit cards, and buffers. Reflect on\n              your spending. Plan ahead. All in one place.\n            \n\n            \n              \n                Explore Resto\n                \n                  \n                \n              \n            \n          \n\n        \n      \n    \n  \n\n  \n    \n      \n        \n          Need help with your Rails project?\n        \n        \n          I'm Mario Alberto Ch&aacute;vez&mdash;Rails architect available for consulting,\n          architecture review, AI integration, and code review.\n        \n        \n          \n            Get in Touch\n          \n          \n            \n              \n            \n            Visit My Blog\n          \n        \n      \n    \n  \n\n  \n  \n    \n\n    \n      \n        \n          \n            Get started\n          \n          \n            Ready to build faster?\n            Start using Maquina today.\n          \n          \n            Build robust multi-tenant Rails applications with a solid foundation and best practices built-in.\n          \n          \n            \n              Get started\n            \n          \n        \n\n        \n          \n            \n              \n              \n            \n\n            \n              \n                \n                  \n                    \n  \n\n                  \n                \n              \n\n              \n                  \n                    Products\n                    \n                        \n                          \n                            Recuerd0\n                          \n                        \n                    \n                  \n                  \n                    Open Source\n                    \n                        \n                          \n                            Documentation\n                          \n                        \n                        \n                          \n                            Generators\n                          \n                        \n                        \n                          \n                            Components\n                          \n                        \n                        \n                          \n                            All Projects\n                          \n                        \n                    \n                  \n                  \n                    Company\n                    \n                        \n                          \n                            Blog\n                          \n                        \n                    \n                  \n                  \n                    Resources\n                    \n                        \n                          \n                            GitHub\n                          \n                        \n                        \n                          \n                            RubyGems\n                          \n                        \n                    \n                  \n              \n            \n          \n\n          \n            \n              \n              \n              \n              \n            \n\n            \n              \n                \n                  &copy; 2026 Maquina.\n                  Mario Alberto Chávez Cárdenas"
        },
        {
          "id": "open-source",
          "title": "Open Source Rails Tools",
          "collection": {
            "label": "pages",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "/open-source/",
          "content": "Recuerd0\n              \n              \n                Documentation\n              \n              \n                Open Source\n              \n              \n                Blog\n              \n          \n\n          \n            mobile-nav#toggle\"\n            >\n              \n                \n              \n            \n          \n        \n      \n    \n  \n\n\n\n      \n        Open Source Rails Tools\n      \n\n      \n        Every Maquina project is MIT licensed and built in the open. Read the\n        source, open an issue, or fork it for your own apps.\n      \n    \n  \n\n\n  \n    \n      \n\n          \n            \n              \n                Rails Libraries\n              \n\n              \n                4 tools\n              \n            \n\n            \n              Drop-in gems for production Rails apps.\n            \n\n            \n                \n                  \n                    \n                      \n                        Maquina Generators\n                      \n\n                      \n                        App Scaffolding\n                      \n                    \n\n                    \n                      Rails generators that produce standalone application code. Authentication, job queues, error tracking, and security — no runtime dependency. Generate once, own forever.\n                    \n                  \n\n                  \n                    \n                      Docs\n                    \n\n                    \n                      \n                        \n                      \n                      GitHub\n                    \n                  \n                \n                \n                  \n                    \n                      \n                        Maquina Components\n                      \n\n                      \n                        UI Library\n                      \n                    \n\n                    \n                      Modern UI components for Ruby on Rails. ERB partials styled with Tailwind CSS 4.0 and Stimulus controllers. Inspired by shadcn/ui, built for the Rails way.\n                    \n                  \n\n                  \n                    \n                      Docs\n                    \n\n                    \n                      \n                        \n                      \n                      GitHub\n                    \n                  \n                \n                \n                  \n                    \n                      \n                        Maquina Newsletters\n                      \n\n                      \n                        Rails Engine\n                      \n                    \n\n                    \n                      A mountable Rails 8 engine for drafting, approving, scheduling, and batch-sending HTML newsletters. Action Text editing, an approval workflow, and background batch delivery.\n                    \n                  \n\n                  \n                    \n                      Docs\n                    \n\n                    \n                      \n                        \n                      \n                      GitHub\n                    \n                  \n                \n                \n                  \n                    \n                      \n                        Nexo\n                      \n\n                      \n                        Agent Harness\n                      \n                    \n\n                    \n                      The connective tissue linking RubyLLM to tools, sandboxes, skills, and runs. Build a working agent in five lines with safe defaults — virtual sandbox and read-only until you explicitly opt in.\n                    \n                  \n\n                  \n                    \n                      Docs\n                    \n\n                    \n                      \n                        \n                      \n                      GitHub\n                    \n                  \n                \n            \n          \n\n          \n            \n              \n                AI &amp; Editor Tooling\n              \n\n              \n                3 tools\n              \n            \n\n            \n              Connect Rails to LLMs, your editor, and Claude Code.\n            \n\n            \n                \n                  \n                    \n                      \n                        Rails Claude Code\n                      \n\n                      \n                        Claude Code Plugins\n                      \n                    \n\n                    \n                      A marketplace of Claude Code plugins for Rails — code simplification, Rails upgrades, UI standards, MVP planning, Stimulus best practices, spec-driven development, security audits, and a Hotwire dev-server driver.\n                    \n                  \n\n                  \n                    \n                      Docs\n                    \n\n                    \n                      \n                        \n                      \n                      GitHub\n                    \n                  \n                \n                \n                  \n                    \n                      \n                        Rails MCP Server\n                      \n\n                      \n                        MCP Server\n                      \n                    \n\n                    \n                      A Model Context Protocol server that lets LLMs interact with Rails projects. Analyze models, routes, schemas, and execute read-only Ruby code in your Rails context.\n                    \n                  \n\n                  \n                    \n                      Docs\n                    \n\n                    \n                      \n                        \n                      \n                      GitHub\n                    \n                  \n                \n                \n                  \n                    \n                      \n                        Neovim MCP Server\n                      \n\n                      \n                        MCP Server\n                      \n                    \n\n                    \n                      MCP server for Neovim integration. Read and update buffers, coordinate file changes across your editor and AI assistants.\n                    \n                  \n\n                  \n                    \n                      Docs\n                    \n\n                    \n                      \n                        \n                      \n                      GitHub\n                    \n                  \n                \n            \n          \n\n          \n            \n              \n                Developer Tools\n              \n\n              \n                4 tools\n              \n            \n\n            \n              Standalone utilities for your local workflow.\n            \n\n            \n                \n                  \n                    \n                      \n                        equipr\n                      \n\n                      \n                        CLI\n                      \n                    \n\n                    \n                      Installs skills, commands, and MCP servers from marketplaces and Agent Plugins sources into Claude Code, Codex, OpenCode, and Pi, without registering as a plugin or rewriting your agent&#39;s config.\n                    \n                  \n\n                  \n                    \n                      Docs\n                    \n\n                    \n                      \n                        \n                      \n                      GitHub\n                    \n                  \n                \n                \n                  \n                    \n                      \n                        Git Continuity\n                      \n\n                      \n                        CLI\n                      \n                    \n\n                    \n                      Seamlessly transfer work-in-progress between machines without committing to git history. Perfect for moving unfinished work between office and home.\n                    \n                  \n\n                  \n                    \n                      Docs\n                    \n\n                    \n                      \n                        \n                      \n                      GitHub\n                    \n                  \n                \n                \n                  \n                    \n                      \n                        Redis Menu\n                      \n\n                      \n                        macOS App\n                      \n                    \n\n                    \n                      A macOS menu bar application for managing local Redis instances. Start, stop, and monitor Redis without touching the terminal.\n                    \n                  \n\n                  \n                    \n                      Docs\n                    \n\n                    \n                      \n                        \n                      \n                      GitHub\n                    \n                  \n                \n                \n                  \n                    \n                      \n                        Mongo Menu\n                      \n\n                      \n                        macOS App\n                      \n                    \n\n                    \n                      A macOS menu bar application for managing local MongoDB instances. Simple controls for your development database.\n                    \n                  \n\n                  \n                    \n                      Docs\n                    \n\n                    \n                      \n                        \n                      \n                      GitHub\n                    \n                  \n                \n            \n          \n      \n    \n  \n\n  \n    \n      \n        \n          \n            Contribute\n          \n\n          \n            Code, docs, bug reports, feature requests — all welcome. Open a\n            pull request or start a discussion on GitHub.\n          \n\n          \n            \n              View on GitHub\n            \n          \n        \n\n        \n          \n            Built for simplicity\n          \n\n          \n            Every tool starts in a real production application. No build\n            pipelines, no framework fatigue. Just Rails the way it was meant to\n            be.\n          \n\n          \n            \n              Read Documentation\n            \n          \n        \n      \n    \n  \n\n  \n    \n      \n\n  \n    \n        Need help with your Rails project?\n    \n    \n        I'm Mario Alberto Ch&aacute;vez&mdash;Rails architect available for consulting, AI integration, and code review.\n    \n    \n      \n        Get in Touch\n      \n      \n        \n          \n        \n        My Personal Website\n      \n    \n  \n\n\n    \n  \n\n  \n  \n    \n\n    \n      \n        \n          \n              \n                \n                  \n                    \n  \n\n                  \n                \n              \n\n              \n                  \n                    Products\n                    \n                        \n                          \n                            Recuerd0 \n                          \n                        \n                    \n                  \n                  \n                    Open Source\n                    \n                        \n                          \n                            Documentation \n                          \n                        \n                        \n                          \n                            Generators \n                          \n                        \n                        \n                          \n                            Components \n                          \n                        \n                        \n                          \n                            All Projects \n                          \n                        \n                    \n                  \n                  \n                    Company\n                    \n                        \n                          \n                            Blog \n                          \n                        \n                    \n                  \n                  \n                    Resources\n                    \n                        \n                          \n                            GitHub \n                          \n                        \n                        \n                          \n                            RubyGems \n                          \n                        \n                    \n                  \n              \n            \n          \n\n          \n            \n              \n              \n              \n              \n            \n\n            \n              \n                \n                  &copy; 2026 Maquina. Mario Alberto Chávez Cárdenas"
        },
        {
          "id": "",
          "title": "Maquina",
          "collection": {
            "label": "data",
            "name": "Posts"
          },
          "categories": "",
          "tags": "",
          "url": "",
          "content": ""
        }
]
