Skip to content
31Carlton7Public

About

Local-first agentic development and work environment

Resources

Stars

105 stars

Watchers

0 watching

Forks

Repository files navigation

Realm

One workspace for every coding agent, on your Mac.

realm.computer  ·  Download  ·  Changelog

macOS · Apple silicon · in active development

Realm 2.0: every space in the sidebar under Needs you, a session that fixed a crash with the card for the two files it edited, and the file its answer named open in the side panel at the line it named.

Every space in the sidebar, a session and the two files its turn edited, and the file its answer named, open beside it at the line it named.

Local-first agent control plane for macOS — profiles → spaces, every space in one sidebar, split panes for sessions beside one side panel for terminals / browsers / devices / documents, a context pool, and an MCP gateway. See docs/superpowers/specs/2026-08-17-realm-v1-design.md.

Dev

  • Node ≥ 22.13, pnpm 10, macOS.
  • Hugeicons Pro token in .npmrc (see .npmrc.example).
  • pnpm install && pnpm dev
  • Tests: pnpm test · Types: pnpm typecheck
  • Data lives in ~/Realm/ (override with REALM_HOME).

Website

site/ is the marketing site and docs (Next.js, deployed on Vercel with Root Directory = site). It is deliberately outside the pnpm workspace and carries its own lockfile — see site/README.md for why that matters and how to verify the WebGPU hero shader headlessly.

Agent sessions

  • Claude sessions run on @anthropic-ai/claude-agent-sdk, which drives the claude CLI: install it and log in first (claude auth login). An expired login shows up as an error in the transcript.
  • ACP agents — Cursor, Gemini, OpenCode, GitHub Copilot, goose, Qwen Code, Grok and fx all speak the Agent Client Protocol, so they share one generic adapter (packages/adapters/src/acp/) and adding another is an AcpAgentSpec entry in apps/server/src/app.ts. Install and sign in to each out of band; Settings → Engines probes what is on the machine and shows the exact install or login command for what is not. Realm never calls ACP's authenticate itself.
    • Note that ACP has deprecated modes/models in favour of configOptions, and agents have split: Cursor still answers with the old shape, OpenCode answers with only the new one, Copilot sends both. acpSessionConfig (in @realm/contracts) normalizes both and carries the id to write back through — reading one channel and writing on the other is a silent no-op.
  • Offline / UI work: REALM_ENABLE_FAKE_AGENT=1 pnpm dev registers a scripted Fake agent (echoes what you send) in the model picker.
  • MCP gateway — third-party MCP servers are configured in a space's settings, not per-agent: every session gets one Realm gateway endpoint, and credentials or OAuth tokens never reach the agent CLI. Every proxied tool call shows up in the Activity view (Connections ▸ Activity, or "MCP Activity" in the command palette).

Code graphs (Graphify)

Graphify extracts a queryable knowledge graph from a checkout. It is a Python CLI, installed out of band like the agent CLIs — Realm probes for it and never installs it.

  • Install it with the mcp extra: uv tool install "graphifyy[mcp]". A plain uv tool install graphifyy still puts a graphify-mcp on PATH, but it dies on ModuleNotFoundError: No module named 'mcp' the first time the hub dials it.
  • graphify.probe / graphify.update are Realm's own RPC methods, not agent kinds — graphify has no models, no login and no transcript, so it stays off AgentKind. graphify update runs graphify update . in the space's primary checkout and needs no LLM and no API key.
  • As an MCP server, add it in a space's Connections: transport stdio, command graphify-mcp, args the absolute path of graphify-out/graph.json. Its ten graph tools then reach every session in that space through the one Realm gateway endpoint.
  • The rendered graphify-out/graph.html opens in the documents pane. graphify points its only script tag at unpkg with an SRI hash; the guide CSP is default-src 'none', so the preview server rewrites that tag to a vendored vis-network and drops the hash, which no longer describes the bytes being served (localizeVisNetwork in apps/server/src/documents/preview.ts).
  • Proof: pnpm --filter @realm/server exec tsx scripts/live-graphify-check.ts drives the real CLI, the real hub and the real preview server, and skips rather than passes when graphify is absent.

Saved sign-ins (browser panes)

An agent driving a browser pane can sign you in to a site without ever seeing the password.

  • You enroll your own passwords in Settings → Sign-ins. That is the only path for a value you supply. No tool, no RPC method, no file import and no chat message can hand a password to the store — which is what makes the origin check below worth anything, since an agent that could enroll one could enroll a password you use elsewhere against whatever page it is standing on.
  • An agent can ask Realm to GENERATE one, with browser_fill_credential's generate argument instead of a credentialId: Realm mints a random password in the main process, saves it to the same store, and fills it under the same three gates. The agent never receives the value and cannot read it back, so this is the way to set a password on a sign-up form — strictly better for secret hygiene than an agent printing one into the transcript for you to copy. The origin is not the agent's to choose: it is read off the pane, shown on the card, and checked again against the live page before anything is typed. What it costs you is stated on that card — nothing can read the value back afterwards, so the site's own reset is the way in if you ever need the password outside Realm. Those rows are marked "Generated by Realm" in Settings, and the audit log records them as generated.
  • Values live in the OS Keychain, encrypted by Electron's safeStorage (apps/desktop/src/main/secret-store.ts), decrypted only inside Realm's main process, and never readable back — not by you, not over IPC, not through the gateway. MCP OAuth tokens share the same store, so realm.db no longer holds them in the clear. (Per-server MCP env/headers still do; MCP_SECRET_STORAGE_NOTE says so.)
  • Every fill is gated three ways: the pane's current origin, read from CDP, must exactly equal the origin the credential belongs to (no subdomains, no lookalikes); you approve that specific fill on a card naming the origin, username and label; and Touch ID confirms you are there. A generated fill runs the identical three, in the identical order, through the same executor. The card appears in every permission mode including bypassPermissions, is never batched, and answering "always" licenses nothing. Fills are logged to ~/Realm/logs/credential-audit.log — timestamp, origin, credential id, outcome, never the value.
  • Typing into a password field with browser_act is still refused, in every mode. browser_fill_credential is a separate op, not a way around it.
  • Two-factor is not automated, and will not be. A Duo/Okta push cannot be driven from here, and a TOTP prompt is out of scope. Realm fills the username and password and stops; an SSO + 2FA sign-in stays partly manual, and you finish it in the pane. If TOTP support is ever added it would be a separately enrolled secret under the same rules — never a code the model sees.
  • A Mac without a Touch ID sensor can save sign-ins but cannot fill them: promptTouchID is biometrics-only, with no password fallback. Settings says so on the tab.

Packaging

  • pnpm dist — full build + DMG and zip in apps/desktop/release/ (pnpm dist:dir stops at an unpacked Realm.app for fast iteration; pnpm app:update runs it and swaps the result into /Applications/Realm.app, quitting a running copy first). Under the hood: root pnpm build, then apps/desktop/scripts/stage-pack.mjs stages .pack-stage/ (a pnpm deploy of realm-server with its production node_modules, the bundled skills/, the ScrollPhase helper, the icon), then electron-builder (apps/desktop/electron-builder.yml) packs it — server and skills as real files under Contents/Resources/, never inside the asar, because node-pty's native prebuilds and a spawnable server entry can't load from an archive.
  • pnpm app:update — the fast local update loop: builds the unpacked app, gracefully quits the installed /Applications/Realm.app, replaces it with rollback protection, and relaunches it. Set REALM_APP_PATH=/another/location/Realm.app to target a nonstandard install. This is for locally built unsigned copies; published builds continue to use the signed updater below.
  • No system Node needed: the packaged app runs realm-server under its own binary with ELECTRON_RUN_AS_NODE. Launched from Finder (launchd's minimal PATH), main adopts the login shell's PATH at startup (login-shell-path.ts) before anything spawns, so agent CLIs and mac resolve; if the login shell can't be asked (exotic shell, timeout), it falls back to the inherited PATH plus /opt/homebrew/bin:/usr/local/bin. Terminals spawn login shells (-l).
  • Proof: node apps/desktop/scripts/packaged-smoke.cjs launches the packaged binary with a scratch REALM_HOME and a stripped PATH=/usr/bin:/bin and asserts boot, agents.probe finding claude, bundled skills, and a terminal resolving claude.
  • Unsigned by default: with no signing credentials in the env, pnpm dist builds an unsigned, un-notarized app (scripts/pack.mjs passes -c.mac.identity=null and says so). A copy downloaded to another Mac will be quarantined: first launch needs right-click → Open, and on Apple Silicon Gatekeeper may report the app "damaged" — clear it with xattr -cr /Applications/Realm.app. Locally built copies launch normally. Signing and notarization are fully wired and env-activated (CSC_LINK/CSC_KEY_PASSWORD + APPLE_ID/APPLE_APP_SPECIFIC_PASSWORD/APPLE_TEAM_ID): with those set, the same pnpm dist signs, notarizes and staples with zero code changes — exact steps in docs/dev/signing.md.

Releasing

  • pnpm release — bumps the app version (patch; --minor / --major for more), prepends a changelog stub to CHANGELOG.md from merged PR titles since the last tag (via gh when it answers, else git log's squash-merge subjects; offline it degrades to plain commit subjects), builds the dmg + zip + latest-mac.yml through the normal pnpm dist, commits, and creates the vX.Y.Z tag locally. It never pushes and never publishes — it ends by printing the exact manual next steps (review the stub, push branch + tag, gh release create with the artifacts). --dry-run shows the whole plan without touching anything.

Updates

  • Signed packaged builds check the public GitHub release feed on launch, download a newer version in the background, and ask before restarting to install it. Settings → General → Updates also supports manual checks and installing a downloaded update.
  • Public releases must carry the dmg, zip, and latest-mac.yml artifacts produced by pnpm release. The updater never embeds a GitHub token.
  • The hard gate in apps/desktop/src/main/updater.ts still disables updates in development and in unsigned builds: macOS cannot apply an unsigned Squirrel update. Configure signing and notarization as described in docs/dev/signing.md; pnpm app:update handles local unsigned builds.

Importing from the agent CLIs

Settings → Import brings what Claude Code, Codex and Cursor already have on disk into Realm: transcripts, the Claude memory tool's per-project fact folders, and user-level skills.

  • The agents' directories are read-only. ~/.claude, ~/.codex, ~/.cursor, ~/.agents and ~/.gemini are copied from — never written, moved or cleaned up. Everything the import produces lands in Realm's database or under ~/Realm/.
  • import.scan writes nothing. It opens files, matches candidates to spaces and answers; no space, session or environment is created by looking. Only import.apply writes, and only for the keys it is handed — so the preview you approve is the work that happens.
  • Space matching is most-specific-location-wins (apps/server/src/import/match.ts): walking the cwd and its parent, asking in turn for an environment, a project root, a space folder, and a directory named after a space. The walk is bounded (MATCH_MAX_HOPS) because one broadly-registered ancestor would otherwise capture every session on the machine. Anything unmatched falls to a profile's Imported space, and every row shows the rule that placed it so a wrong guess is visible.
  • Imported sessions keep their provider id when the recorded cwd still exists, so sending a message resumes the real CLI conversation. Where the directory is gone the link is left off and the session imports as searchable history. Re-target rows in the preview: sessions.moveToSpace refuses once a session has events, and an imported session has a transcript from the moment it exists.
  • Memory is not flattened into the space doc. Fact files are copied to ~/Realm/memory/imported/<spaceId>/<project>/ and the index goes into the space's memory document between <!-- realm:imported-memory --> markers (replaced on re-import, never duplicated). The largest folder here was 712k characters against a 100k doc cap; inlining would have dropped most of it and called that an import.
  • Skills are copied, never symlinked or overwritten, and land unscoped — visible in every space, the honest translation of "installed for my user".

One transcript is not one file. Codex rewrites a whole thread into a new rollout file every time it is resumed — on one machine here, 241 files were 71 conversations, with 158 of them replays of a single Stora thread. The scan keeps the fullest copy of each and counts the rest, so the panel offers conversations rather than files.

Two scripts, from apps/server:

  • tsx scripts/live-import-check.ts — prints what an import would do against this machine's real CLI directories, reading a VACUUM INTO copy of the database so it can reason over your actual spaces without being able to write to them.
  • tsx scripts/undo-import.ts --all-imported --sweep-environments — takes an import back out. Only rows whose dispatch origin is import are touched, and the agents' own directories are never read or written. Worth knowing about before a big import: an imported session cannot be re-targeted afterwards, and the environment rows an import leaves behind will out-match everything on the next run if they are not swept with it.

Screens

Real captures of a real space, taken against the built app by site/scripts/capture-product.mjs. These are the site's frames with Realm's page colour painted in behind them — the window is made of translucent material, so a capture laid straight onto GitHub's white theme washes its sidebar out. node scripts/readme-images.mjs repaints them after a re-capture.

Every space, in one sidebar. Each space is a section of one list, and Needs you heads it with every session waiting on a permission or a question, from every space and every profile.

The sidebar: Needs you lists two questions and a sub-agent's permission, one from another profile, above the Realm, Dashboard, Site and School spaces and their sessions.

Three sessions waiting on you, above every space's sessions.

Bring the agent you already use. Claude Code, Codex, Cursor, Gemini, OpenCode, GitHub Copilot, goose, Qwen Code and Grok all run here, each keeping its own login, models and permission modes.

The model picker open on a new session: Claude's and Codex's models grouped by harness, Opus 5.5 picked with its description, context and price, and the effort track at Max, lit, with fast mode on.

Every installed agent's models in one list: Opus 5.5 picked, at Max, with fast mode on.

Your tools, without your credentials. Connections belong to the space rather than to an agent, so a session is handed the tools and never the token — it calls Realm, and Realm calls the server.

The Connections page, showing Linear, Notion, Slack, GitHub, Jira & Confluence, Figma and Sentry as cards, each with what it grants and a Connect button.

Connecting an app to a space. One click each, and every session in the space can use them.

Confine what an agent can touch. A space can put its agents and terminals behind a macOS Seatbelt policy. It ships off, per space — Seatbelt is not a container, and the network stays open.

The Sandbox settings page for a space, showing its three postures — Workspace write, Read only and No sandbox — with No sandbox selected.

The three postures a space can take, on the one it ships with.

Skills

skills/ holds skills Realm ships, one folder per skill, laid out exactly like the library at ~/Realm/skills/ (spec §7) so installing one is a copy. SkillSync — per-profile enablement and the symlink into each session's .claude/skills/ — is not built yet, so until it is, enable a bundled skill by hand:

ln -s "$PWD/skills/mac" ~/.claude/skills/mac
  • study-guide / lecture-notes — the school workflow (Plan 22): how to write a self-contained interactive HTML guide the Documents pane renders (quizzes, step-throughs, flashcards, KaTeX; progress in a sidecar), and how to answer questions during a lecture and wrap one up afterwards. Both lean on the realm-docs gateway tools (docs_search, docs_list, docs_open, docs_progress), and the palette's New lecture… / Wrap up a lecture… / Import recording from Plynn… entries drive the loop.
  • mac — the mac-cli binary: Calendar, Reminders, Contacts, Mail, Messages, Notes, Music, TV, Shortcuts, Finder, and iWork from the shell. Realm spawns agents and terminals with its own environment inherited, so mac is already on a session's PATH whenever it is on the PATH Realm was launched from — the skill exists to make it discoverable, not reachable. Note that a Realm launched from Finder rather than a terminal inherits launchd's minimal PATH, which has neither mac nor claude/codex/node on it; that is a packaging problem for all of them, not a mac-cli one.

About

Local-first agentic development and work environment

Resources

Stars

105 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages