From 56bcd98878c2cff5c781a57077adfb84e6eade6e Mon Sep 17 00:00:00 2001
From: MRDula
Date: Tue, 21 Apr 2026 10:18:50 -0400
Subject: [PATCH 1/5] feat(signup): restrict key minting to @revamfg.com and
@mrdula.solutions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
A valid signup token alone isn't enough — the token is shared in a
group chat, and a leak would hand out Rev A workspace keys to random
accounts. Add a fail-closed email domain allowlist on the server and
mirror it in the browser form so typos are caught before the POST.
- `REVA_ALLOWED_EMAIL_DOMAINS` env var, default
`revamfg.com,mrdula.solutions`. Override per tenant.
- `_email_domain_allowed()` is strict: empty allowlist => deny all,
case-insensitive match on the exact domain (subdomain spoofs like
`eve@fake.revamfg.com` are rejected).
- Server check runs after the token check (so a random attacker who
guesses the domain but not the token still sees `invalid signup
token`, not a leaked hint about the allowlist).
- HTML form shows the allowed domains inline under the email field
and blocks submission client-side with a regex the server injects —
same contract on both sides.
Tested in prod against the live deploy: `@gmail.com` and
`@fake.revamfg.com` both return 403 "email domain not allowed" with
a valid signup token.
Co-Authored-By: Claude Opus 4.7
---
services/mcp-router/router/signup.py | 71 ++++++++++++++++++++++++++--
1 file changed, 67 insertions(+), 4 deletions(-)
diff --git a/services/mcp-router/router/signup.py b/services/mcp-router/router/signup.py
index 37093b0..b1c209a 100644
--- a/services/mcp-router/router/signup.py
+++ b/services/mcp-router/router/signup.py
@@ -47,9 +47,33 @@
NAKATOMI_ADMIN_TOKEN = os.environ.get("NAKATOMI_ADMIN_TOKEN", "")
PUBLIC_MCP_URL = os.environ.get("PUBLIC_MCP_URL", "") # optional hint for the UI
+# Email domains allowed to self-serve a key. A valid signup_token alone isn't
+# enough — the signup token is shared in a group chat, and we don't want a
+# leaked token to hand out keys to random gmail accounts. Comma-separated,
+# case-insensitive, no leading `@`. Override via env to re-use this router
+# for a different tenant.
+_DEFAULT_ALLOWED_DOMAINS = "revamfg.com,mrdula.solutions"
+ALLOWED_EMAIL_DOMAINS: frozenset[str] = frozenset(
+ d.strip().lower().lstrip("@")
+ for d in os.environ.get("REVA_ALLOWED_EMAIL_DOMAINS", _DEFAULT_ALLOWED_DOMAINS).split(",")
+ if d.strip()
+)
+
SIGNUP_ENABLED = bool(REVA_SIGNUP_TOKEN and NAKATOMI_ADMIN_TOKEN)
+def _email_domain_allowed(email: str) -> bool:
+ """True if the email's domain (case-insensitive) is in the allowlist.
+
+ Empty allowlist => deny everything (fail-closed). Returning True on an
+ empty set would turn a misconfigured env var into an open signup.
+ """
+ if not ALLOWED_EMAIL_DOMAINS:
+ return False
+ _, _, domain = email.rpartition("@")
+ return domain.lower() in ALLOWED_EMAIL_DOMAINS
+
+
class SignupRequest(BaseModel):
email: EmailStr
password: str = Field(min_length=12)
@@ -81,6 +105,17 @@ async def signup(req: SignupRequest) -> SignupResponse:
if not secrets.compare_digest(req.signup_token, REVA_SIGNUP_TOKEN):
raise HTTPException(status_code=403, detail="invalid signup token")
+ # Domain allowlist. Checked AFTER the signup-token check so rejection
+ # here also implies the caller already knew the (shared) token — we
+ # just won't hand them a key unless their mailbox matches.
+ if not _email_domain_allowed(req.email):
+ allowed = ", ".join(f"@{d}" for d in sorted(ALLOWED_EMAIL_DOMAINS))
+ log.info("signup blocked: email domain not allowed (email=%s)", req.email)
+ raise HTTPException(
+ status_code=403,
+ detail=f"email domain not allowed — use one of: {allowed}",
+ )
+
# Throwaway personal workspace for the new user — Nakatomi requires one
# at signup time. Uses a random slug so re-runs don't collide.
personal_slug = f"personal-{secrets.token_hex(4)}"
@@ -253,8 +288,8 @@ def _extract_detail(resp: httpx.Response, fallback: str) -> str:
-
-
+
+
@@ -328,8 +363,21 @@ def _extract_detail(resp: httpx.Response, fallback: str) -> str:
const s2 = document.getElementById('s2');
const mcpUrl = new URL('/mcp', location.href).toString();
+// Case-insensitive regex matching the server's ALLOWED_EMAIL_DOMAINS. If
+// the server set no allowlist, this regex is `IMPOSSIBLE\.DOMAIN` — any
+// submission will fail client-side before the POST, which matches the
+// fail-closed behavior of `_email_domain_allowed()` on the server.
+const ALLOWED_DOMAIN_RE = new RegExp('@(?:__ALLOWED_DOMAINS_REGEX__)$', 'i');
+
form.addEventListener('submit', async (e) => {
e.preventDefault();
+ const emailVal = document.getElementById('email').value.trim();
+ if (!ALLOWED_DOMAIN_RE.test(emailVal)) {
+ out.classList.add('err'); out.style.display = 'block';
+ out.innerHTML = 'Email not allowed: use __ALLOWED_DOMAINS_TEXT__. ' +
+ 'Contact your admin if you need access under a different domain.';
+ return;
+ }
btn.disabled = true; btn.textContent = 'Minting…';
out.className = 'result'; out.style.display = 'none'; out.innerHTML = '';
post.className = 'post';
@@ -339,7 +387,7 @@ def _extract_detail(resp: httpx.Response, fallback: str) -> str:
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
display_name: document.getElementById('name').value,
- email: document.getElementById('email').value,
+ email: emailVal,
password: document.getElementById('password').value,
signup_token: document.getElementById('token').value,
})
@@ -383,4 +431,19 @@ async def signup_page() -> HTMLResponse:
"on the mcp-router service.
",
status_code=503,
)
- return HTMLResponse(SIGNUP_HTML)
+ # Inject the domain allowlist so the form can show it inline and
+ # provide an HTML5 pattern for the email field — catches typos
+ # client-side before the POST round-trip.
+ domains = sorted(ALLOWED_EMAIL_DOMAINS)
+ domains_text = " or ".join(f"@{d}" for d in domains) if domains else ""
+ # Build a regex that matches any of the allowed domains (case-insensitive
+ # via the JS flag below). Each domain is escape-safe — only letters, dots,
+ # hyphens expected, but we belt-and-suspenders with re.escape.
+ import re as _re
+ domain_alt = "|".join(_re.escape(d) for d in domains) if domains else "IMPOSSIBLE\\.DOMAIN"
+ html = (
+ SIGNUP_HTML
+ .replace("__ALLOWED_DOMAINS_TEXT__", domains_text)
+ .replace("__ALLOWED_DOMAINS_REGEX__", domain_alt)
+ )
+ return HTMLResponse(html)
From 6f8f8e8d1def4afe6bf3f4ecc89b93b70c943474 Mon Sep 17 00:00:00 2001
From: MRDula
Date: Tue, 21 Apr 2026 10:29:22 -0400
Subject: [PATCH 2/5] =?UTF-8?q?feat(reva):=20RevAOps=20Plugin=20v2.1.2=20?=
=?UTF-8?q?=E2=80=94=20BYO=20CRM=20+=20PM-friendly=20README?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Three user-facing fixes from PM feedback:
1. **README stopped lying to PMs.** The "For end users" section still
said "Claude prompts for mcp_url and api_key" — 2.1.1 removed the
userConfig block, so Desktop no longer prompts for anything. Rewrote
the section with the literal Rev A signup URL, the /connect flow,
the email allowlist (@revamfg.com / @mrdula.solutions), and a note
about BYO CRM.
2. **Friendlier branding.** Added `displayName: "RevAOps Plugin"` to
plugin.json and rewrote the description as product copy instead of
changelog. Release titles follow suit.
3. **Bring-your-own CRM.** Nakatomi + AutoMem stay as zero-config
defaults, but teams that already use HubSpot / Salesforce / Attio /
Pipedrive in Claude Desktop can now make that their system of
record via `/integrate `. Nakatomi + AutoMem always
shadow-write so the shared Rev A timeline stays complete, and reads
fall back to Nakatomi if the primary is unreachable.
New surface:
- `reva_set_primary_crm` router tool — admin-escalated PATCH of
`workspace.data.primary_crm` with a 10-entry audit trail
(`primary_crm_history`).
- `reva_whoami` + `reva_get_workspace_config` now return `primary_crm`
and `connector_registry` so skills can route writes correctly.
- Seed populates `workspace.data.connector_registry` with 5 options
(nakatomi + 4 external) and defaults primary to `nakatomi`.
- `/integrate` inline command in revmyengine with detection of
installed connectors via MCP tool-prefix matching.
- `docs/CONNECTORS.md` documents the shadow-write contract for skill
authors — primary write → Nakatomi shadow with source tags → AutoMem
linked to the Nakatomi ID → return the primary's record to the PM.
Signup HTML gains a "Already using HubSpot…" panel so new PMs see the
BYO-CRM story during onboarding.
Tested: re-seeded against prod, verified workspace.data has
`primary_crm: nakatomi` and 5 connector slugs; company_profile and
user_roles untouched (non-destructive seed).
Co-Authored-By: Claude Opus 4.7
---
README.md | 114 ++++++++----
docs/CONNECTORS.md | 200 ++++++++++++++++++++++
plugin/.claude-plugin/plugin.json | 5 +-
plugin/VERSION | 2 +-
plugin/skills/revmyengine/SKILL.md | 84 ++++++++-
services/mcp-router/router/signup.py | 12 ++
services/mcp-router/router/tools/cross.py | 84 +++++++++
services/nakatomi-backend/seed/reva.py | 65 ++++++-
8 files changed, 529 insertions(+), 37 deletions(-)
create mode 100644 docs/CONNECTORS.md
diff --git a/README.md b/README.md
index aabd52e..2ff48eb 100644
--- a/README.md
+++ b/README.md
@@ -67,41 +67,91 @@ RevOps-RevAMfg/
## For end users (Rev A PMs)
-Your admin deploys the backend once and shares two things with you: the
-**router URL** (e.g. `https://mcp-router-production-460a.up.railway.app/mcp`)
-and a one-time **signup token**. From there it's three steps — no terminal
-needed.
-
-**1. Get your personal API key.** Visit `/signup`
-in your browser. Enter your name, work email, a password (12+ chars — only
-used for future key resets), and the signup token. The page shows your
-`nk_...` key once. Copy it.
-
-**2. Download the plugin bundle.** Grab the latest `reva-turbo-.zip`
-from the project's [GitHub Releases](https://github.com/mrdulasolutions/RevOps-RevAMfg/releases)
-page. (The zip contains a single top-level `reva-turbo/` directory — don't
-unzip it before upload.)
-
-**3. Upload it into Claude Desktop.** Open **Plugins → Personal → Local
-uploads → `+`** and drop in the zip. On enable, Claude prompts for two
-values:
-
-- **`mcp_url`** — the router URL your admin shared (the full `/mcp` form)
-- **`api_key`** — the `nk_...` key from step 1 (stored in your OS keychain,
- not a plaintext file)
-
-That's it. Run `/reva-turbo:revmyengine` and the engine is connected to the
-shared CRM and memory. Everything you log is available to the whole team,
-and every action is attributed to your user on the Nakatomi timeline.
-
-> Prefer the terminal? The legacy CLI path still works for Claude Code
-> users:
+**You need:** a work email (`@revamfg.com` or `@mrdula.solutions`), the
+one-time **signup token** from your admin, and Claude Desktop installed.
+No terminal, no settings hunting.
+
+**1. Mint your API key.** Click this link:
+
+> **https://mcp-router-production-460a.up.railway.app/signup**
+
+Fill in your name, your `@revamfg.com` or `@mrdula.solutions` email, a
+password (12+ chars — only used if you ever need the admin to reset your
+key), and paste the signup token. The page shows a key that starts with
+`nk_`. **Copy it now** — it's only shown once.
+
+**2. Download the plugin.** Grab the latest `reva-turbo-.zip`
+from [GitHub Releases →
+latest](https://github.com/mrdulasolutions/RevOps-RevAMfg/releases/latest).
+Don't unzip it.
+
+**3. Install it in Claude Desktop.** Open **Plugins → Personal → Local
+uploads → `+`** and drop in the zip. Click **Enable**. No settings to
+fill in — the plugin self-configures in the next step.
+
+**4. Connect your key in chat.** In any Claude Desktop conversation,
+type:
+
+```
+/reva-turbo:revmyengine
+```
+
+The engine greets you and notices it doesn't have a key yet. Reply with:
+
+```
+/connect nk_yourkeyhere
+```
+
+The engine validates the key against the router, saves it locally
+(mode-600 file at `~/.reva-turbo/state/mcp-credentials.env`), and tells
+you to quit and reopen Claude Desktop (Cmd-Q, then relaunch).
+
+**5. Pick your role.** On the next launch, run `/reva-turbo:revmyengine`
+again. It asks one question — *what's your role?* (PM / Sales /
+Compliance / C-level / Eng) — and you're in.
+
+Everything you log is shared with the Rev A team, and every action is
+attributed to your user on the Nakatomi timeline.
+
+> **Only `@revamfg.com` and `@mrdula.solutions` emails can sign up.** The
+> signup token alone isn't enough — the router enforces the email
+> allowlist. If you need access under a different domain, contact the
+> admin (`matt@mrdula.solutions`) to add yours to
+> `REVA_ALLOWED_EMAIL_DOMAINS` on the router.
+
+> **Prefer the terminal?** The legacy CLI path still works for Claude
+> Code users:
> ```bash
> curl -fsSL https://raw.githubusercontent.com/mrdulasolutions/RevOps-RevAMfg/main/plugin/install.sh \
-> | REVA_MCP_URL=https://.up.railway.app/mcp bash
+> | REVA_MCP_URL=https://mcp-router-production-460a.up.railway.app/mcp bash
> ```
-> It drops into the same signup wizard and writes `~/.claude/mcp.json`
-> directly.
+> It drops into the same signup wizard and writes
+> `~/.reva-turbo/state/mcp-credentials.env` directly.
+
+### Have your own CRM connector in Claude?
+
+REVA-TURBO ships with Nakatomi (CRM) and AutoMem (semantic memory) as
+the defaults — those come over the router automatically. But if you've
+already connected **HubSpot**, **Salesforce**, **Attio**, or any other
+CRM to Claude Desktop, you can keep using it as your source of truth:
+
+```
+/integrate hubspot
+```
+
+After you do this, skills that write customer/deal/contact data will:
+
+1. **Write to HubSpot first** (your primary CRM — this is the record of
+ truth).
+2. **Shadow-write the same data to Nakatomi** so the whole Rev A team
+ sees the activity on the shared timeline.
+3. **Store the semantic memory in AutoMem** linked to both IDs.
+
+Reads prefer HubSpot when it's available and fall back to Nakatomi on
+an outage. Run `/integrate nakatomi` (the default) to revert.
+
+See [`docs/CONNECTORS.md`](./docs/CONNECTORS.md) for the full list of
+supported connectors and the shadow-write contract.
See [`docs/AUTH.md`](./docs/AUTH.md) for the full auth flow and rotation
story.
diff --git a/docs/CONNECTORS.md b/docs/CONNECTORS.md
new file mode 100644
index 0000000..eea4743
--- /dev/null
+++ b/docs/CONNECTORS.md
@@ -0,0 +1,200 @@
+# Connectors — primary CRM + shadow-writes
+
+REVA-TURBO ships with Nakatomi (CRM) and AutoMem (semantic memory)
+bundled under the router's `crm_*` and `mem_*` tool prefixes. That's
+the zero-config default — every PM on a fresh signup already has them
+working.
+
+Teams that already use a different CRM in Claude Desktop — HubSpot,
+Salesforce, Attio, Pipedrive — can make that their **primary system of
+record** without losing the shared Rev A timeline. The pattern is:
+
+- **Primary writes go to the external connector** (e.g. HubSpot). That
+ record is the source of truth.
+- **Every write is also mirrored** to Nakatomi + AutoMem so the Rev A
+ team sees the activity on the shared timeline and can search
+ semantic memory without depending on the external CRM's uptime.
+- **Reads prefer the primary** and fall back to the Nakatomi mirror if
+ the primary is unreachable (with a clear "data may be stale" notice).
+
+One setting controls this at the workspace level. Individual PMs don't
+choose independently — the team needs to agree on one system of truth.
+
+## The `/integrate` command
+
+```
+/integrate # show current config + which connectors Desktop has installed
+/integrate nakatomi # revert to bundled default (no shadow-writes)
+/integrate hubspot # HubSpot becomes primary; Nakatomi + AutoMem mirror
+/integrate salesforce # Salesforce becomes primary; …
+/integrate attio # Attio becomes primary; …
+/integrate pipedrive # Pipedrive becomes primary; …
+```
+
+Under the hood this calls `mcp__reva__reva_set_primary_crm` which
+writes `workspace.data.primary_crm` and an audit trail in
+`workspace.data.primary_crm_history` (last 10 flips, with actor email
+and timestamps). Only the admin token can PATCH `/workspace`, so the
+router escalates on the caller's behalf after verifying their
+identity.
+
+## Supported connectors
+
+The registry lives in `workspace.data.connector_registry`, seeded by
+`services/nakatomi-backend/seed/reva.py`. Each entry has:
+
+| Field | Purpose |
+|-------------------|----------------------------------------------------------------|
+| `slug` | Identifier used with `/integrate ` and in shadow-write source tags |
+| `display` | Human-readable name shown in `/integrate` output |
+| `mcp_tool_prefix` | How skills detect whether Desktop has the connector installed |
+| `bundled` | `true` only for Nakatomi — the router exposes it natively |
+| `primary_compatible` | `true` if the connector can hold the system of record (today all are) |
+
+Defaults shipped at seed time:
+
+| Slug | Display | Prefix | Install path |
+|--------------|----------------------|--------------|---------------------------------------------|
+| `nakatomi` | Nakatomi (bundled) | `crm_` | automatic via the router |
+| `hubspot` | HubSpot | `hubspot_` | Desktop → Settings → Connectors → HubSpot |
+| `salesforce` | Salesforce | `sf_` | Desktop → Settings → Connectors → Salesforce |
+| `attio` | Attio | `attio_` | Desktop → Settings → Connectors → Attio |
+| `pipedrive` | Pipedrive | `pipedrive_` | Desktop → Settings → Connectors → Pipedrive |
+
+Adding a new connector to the registry is a one-line seed change —
+append an entry to `REVA_CONNECTOR_REGISTRY` in
+`services/nakatomi-backend/seed/reva.py`, re-run `./railway/deploy.sh
+seed`, and the `/integrate` picker picks it up automatically.
+
+## The shadow-write contract
+
+Any skill that writes a customer, contact, deal, note, activity,
+task, or pipeline change MUST implement this four-step sequence:
+
+### 1. Primary write
+
+Resolve the primary CRM for the workspace:
+
+```
+cfg = read ~/.reva-turbo/state/workspace-config.json
+primary = cfg.primary_crm # e.g. "hubspot"
+```
+
+If `primary == "nakatomi"`, go straight to the bundled tools — no
+mirroring needed, Nakatomi IS the primary.
+
+Otherwise, call the external connector's MCP tools. Example for
+HubSpot (`mcp_tool_prefix: hubspot_`):
+
+```
+mcp__hubspot__hubspot_create_contact {
+ email: "jane@acme.com",
+ firstname: "Jane",
+ lastname: "Doe"
+}
+→ returns hubspot_id: "1234567"
+```
+
+### 2. Shadow-write to Nakatomi
+
+Re-issue the same logical write through the router's `crm_*` tools,
+tagging the source and external ID so future reads can reconcile:
+
+```
+mcp__reva__crm_create_contact {
+ email: "jane@acme.com",
+ first_name: "Jane",
+ last_name: "Doe",
+ properties: {
+ source: "hubspot",
+ source_id: "1234567"
+ }
+}
+→ returns nakatomi_id: "c_abc123"
+```
+
+The `source`/`source_id` pair is the reconciliation key. If HubSpot's
+contact is later mutated (primary write), the next shadow-write should
+look up the Nakatomi record by `(source, source_id)` and update it
+rather than creating a duplicate.
+
+### 3. Memory write
+
+Always store semantic memory against the Nakatomi ID — AutoMem's graph
+stays consistent regardless of which external CRM is primary:
+
+```
+mcp__reva__reva_remember_about_entity {
+ entity_type: "contact",
+ entity_id: "c_abc123", # Nakatomi ID from step 2
+ content: "Jane is the buyer for the Q3 RFQ — prefers email, hates calls.",
+ tags: ["reva/rfq", "source:hubspot"]
+}
+```
+
+This way, memories are searchable by the Rev A team even if HubSpot
+goes down, and they're linked to both systems via the `source` tag.
+
+### 4. Return the primary's record to the PM
+
+When presenting back to the PM, use data from the **primary** write
+(step 1), not the Nakatomi shadow. "Look up Acme Corp" should show
+the HubSpot Acme Corp, not a stale mirror. The Nakatomi record is
+infrastructure — the PM shouldn't see it unless HubSpot is unreachable.
+
+## Read path
+
+Reads prefer the primary connector:
+
+```
+if primary == "nakatomi":
+ mcp__reva__crm_search_contacts {...}
+elif primary == "hubspot" and hubspot tools available:
+ mcp__hubspot__hubspot_search_contacts {...}
+elif primary == "hubspot" and hubspot tools MISSING:
+ # warn PM, fall back
+ mcp__reva__crm_search_contacts {...}
+ tell PM: "HubSpot is unreachable — showing the Rev A mirror from
+ Nakatomi. Data may be up to 5 min behind the primary."
+```
+
+Skills detect connector availability by checking whether any tool in
+the current surface starts with the registry's `mcp_tool_prefix`.
+
+## Failure modes
+
+- **Primary write succeeds, shadow-write fails.** The record is in
+ HubSpot but not in Nakatomi. Log the failure to
+ `~/.reva-turbo/state/shadow-write-failures.jsonl` and surface it in
+ `/connected`. A later skill invocation or `/refresh` can replay
+ pending shadow-writes. Never fail the PM's ask just because the
+ mirror is slow.
+- **Primary write fails.** Hard-fail — tell the PM, don't shadow-write
+ anything (that would create a Nakatomi record that has no HubSpot
+ counterpart, breaking the reconciliation contract).
+- **Memory write fails.** Log and continue — memory is best-effort,
+ not authoritative.
+
+## Why workspace-level and not per-user
+
+Two PMs disagreeing on primary CRM would create a split-brain: Jane
+writes to HubSpot, Tom writes to Nakatomi, shadow-writes collide on
+the reconciliation key. The workspace-level setting forces the team to
+agree on one system of truth — `/integrate` changes it for everyone.
+The `primary_crm_history` audit log in `workspace.data` tells you who
+flipped it and when.
+
+## Not (yet) supported
+
+- **Read-only observer connectors** — the `primary_compatible: false`
+ flag is reserved for future "see-only" integrations (e.g. Gmail for
+ email ingestion — shouldn't be the primary, but skills can read from
+ it). Today every registered connector is primary-compatible.
+- **Cross-connector reconciliation.** If you flip from HubSpot →
+ Salesforce mid-workflow, existing Nakatomi shadows still reference
+ `source: hubspot`. They don't auto-migrate. Reconciliation is a
+ future feature — for now, run a one-off backfill.
+- **Per-entity-type primary overrides** (e.g. "contacts from HubSpot,
+ deals from Nakatomi"). Today primary is workspace-wide for all
+ entity types. Multi-primary routing could be added to the registry
+ entry as an `entity_types: ["contact", "company"]` allowlist.
diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json
index 00e868b..2f3bae5 100644
--- a/plugin/.claude-plugin/plugin.json
+++ b/plugin/.claude-plugin/plugin.json
@@ -1,7 +1,8 @@
{
"name": "reva-turbo",
- "version": "2.1.1",
- "description": "REVA-TURBO Skills Engine — Rev A Manufacturing PM workflow from RFQ intake through customer delivery. 48 skills + zero-friction onboarding: PM pastes their nk_… key in chat, plugin wires up the router automatically (no Settings UI hunting). Covers quoting, China sourcing, compliance, quality, and fulfillment.",
+ "displayName": "RevAOps Plugin",
+ "version": "2.1.2",
+ "description": "The Rev A Manufacturing PM copilot — full RFQ-to-delivery workflow with CRM, memory, and 48 skills. Paste your key in chat and you're in; bring your own CRM (HubSpot, Salesforce, Attio, Pipedrive) or use the bundled Nakatomi + AutoMem defaults.",
"author": {
"name": "Rev A Manufacturing / MrDula Solutions",
"url": "https://github.com/mrdulasolutions"
diff --git a/plugin/VERSION b/plugin/VERSION
index 3e3c2f1..eca07e4 100644
--- a/plugin/VERSION
+++ b/plugin/VERSION
@@ -1 +1 @@
-2.1.1
+2.1.2
diff --git a/plugin/skills/revmyengine/SKILL.md b/plugin/skills/revmyengine/SKILL.md
index eff5ef4..c76ce70 100644
--- a/plugin/skills/revmyengine/SKILL.md
+++ b/plugin/skills/revmyengine/SKILL.md
@@ -1,7 +1,7 @@
---
name: revmyengine
preamble-tier: 1
-version: 2.1.1
+version: 2.1.2
description: |
REVA-TURBO master orchestrator for Rev A Manufacturing PM workflow.
Routes requests to the correct sub-skill based on context. Chains the
@@ -28,6 +28,7 @@ allowed-tools:
- mcp__reva__reva_get_company_profile
- mcp__reva__reva_get_workspace_config
- mcp__reva__reva_set_user_role
+ - mcp__reva__reva_set_primary_crm
- mcp__reva__reva_remember_about_entity
- mcp__reva__reva_recall_for_entity
- mcp__reva__crm_search_contacts
@@ -370,6 +371,7 @@ Voice applies to greeting style, signoff, tone, email length, technical depth, f
| `/refresh` | inline | Re-pull `reva_get_company_profile` + `reva_get_workspace_config` into local cache |
| `/connect ` | inline | Paste-key-in-chat onboarding: validate key against `/auth/me`, write `~/.reva-turbo/state/mcp-credentials.env`, prompt restart |
| `/connected` | inline | Diagnostic: confirm router + show tool counts (`crm_*`, `mem_*`, `reva_*`) and current `mcp_url` |
+| `/integrate [connector]` | inline | Show or change the team's primary CRM (nakatomi/hubspot/salesforce/attio/pipedrive). Calls `reva_set_primary_crm`; Nakatomi + AutoMem always shadow-write |
| `/send-logs` | inline | Package dev log + email to matt@mrdula.solutions |
| `/logs` | inline | Display recent telemetry entries in readable format |
@@ -576,6 +578,86 @@ curl -fsS -o /dev/null -w "%{http_code}" \
- Connection error → *"Can't reach the router. Check Wi-Fi; if it
persists, ping your admin — the router may be down."*
+## `/integrate` — choose the team's primary CRM
+
+REVA-TURBO ships with Nakatomi and AutoMem as the built-in CRM + memory
+pair. If the PM's team already uses HubSpot, Salesforce, Attio, or
+Pipedrive in Claude Desktop, they can make that the **system of record**
+so reads/writes prefer it — while Nakatomi + AutoMem continue to mirror
+the data so the shared Rev A timeline stays complete.
+
+**No arg** — show current config:
+
+1. Read `~/.reva-turbo/state/workspace-config.json` → `primary_crm` +
+ `connector_registry`.
+2. For each registered connector, detect whether the PM's Desktop has
+ its MCP tools available (check the tool surface for tools whose
+ names start with `mcp_tool_prefix` — e.g. `hubspot_` implies the
+ HubSpot connector is installed in Desktop → Settings → Connectors).
+3. Display:
+
+ ```
+ Primary CRM: ()
+ Shadow-writes to: Nakatomi + AutoMem (always)
+
+ Available connectors:
+ ✓ nakatomi — Nakatomi (bundled) [currently primary]
+ ✓ hubspot — HubSpot [installed, can be primary]
+ ✗ salesforce — Salesforce [not installed in Desktop]
+ …
+
+ Change: /integrate
+ ```
+
+**With arg** (e.g. `/integrate hubspot`):
+
+1. Validate against `connector_registry` — if the slug isn't in the
+ registry, tell the PM which slugs are valid and stop.
+2. If the chosen connector isn't `nakatomi` and we don't detect its
+ tools (`mcp__*` missing from the surface), warn the PM:
+ *"HubSpot MCP tools aren't available — install the HubSpot
+ connector in Desktop → Settings → Connectors first, then re-run
+ /integrate hubspot. Switching anyway will put every skill in
+ shadow-write-only mode until the connector is installed. Continue?
+ (yes/no)."* Don't proceed without explicit yes.
+3. Call `mcp__reva__reva_set_primary_crm {"connector": ""}`.
+4. Re-pull config into `~/.reva-turbo/state/workspace-config.json`
+ (same as `/refresh`).
+5. Confirm: *"✓ Primary CRM is now . All skills will read
+ from first and shadow-write to Nakatomi + AutoMem. Revert
+ any time with /integrate nakatomi."*
+
+### Shadow-write contract for skill authors
+
+When a skill writes a customer, contact, deal, note, or activity, it
+MUST follow this sequence:
+
+1. **Primary write.** Call the external connector (if primary) or
+ `mcp__reva__crm_*` (if primary is nakatomi). Capture the returned
+ external/internal ID.
+2. **Shadow-write to Nakatomi.** If primary is NOT nakatomi, ALSO call
+ `mcp__reva__crm_*` with the same payload, tagging
+ `{"source": "", "source_id": ""}` in
+ the note/description so later reads can resolve back to the primary.
+3. **Memory write.** Call `mcp__reva__reva_remember_about_entity` (or
+ `mcp__reva__mem_store` + `mcp__reva__mem_associate`) with the
+ Nakatomi entity_id from step 2 — this keeps AutoMem's graph
+ consistent regardless of where the primary record lives.
+4. **Return the primary's record to the PM** (not Nakatomi's shadow
+ copy) — when the PM says "look up Acme Corp" they should see the
+ HubSpot Acme Corp, not a stale mirror.
+
+**Read path.** Reads prefer the primary connector. If the primary
+connector errors or is unavailable (tools missing), fall back to
+Nakatomi and tell the PM: *"HubSpot is unreachable — showing the Rev A
+mirror from Nakatomi. Data may be up to 5 min behind the primary."*
+
+**When primary is `nakatomi`** (the default): skip the shadow-write
+dance entirely. Nakatomi IS the primary.
+
+See [`docs/CONNECTORS.md`](../../docs/CONNECTORS.md) for the full
+contract and per-connector field mappings.
+
## Workflow State
Log every workflow transition to `~/.reva-turbo/state/workflow-state.jsonl`:
diff --git a/services/mcp-router/router/signup.py b/services/mcp-router/router/signup.py
index b1c209a..9d696d1 100644
--- a/services/mcp-router/router/signup.py
+++ b/services/mcp-router/router/signup.py
@@ -348,6 +348,18 @@ def _extract_detail(resp: httpx.Response, fallback: str) -> str:
+
Already using HubSpot, Salesforce, Attio, or Pipedrive?
+
+
You can keep using your existing CRM as
+ the system of record. After Step 3, run:
Skills will then write to your CRM
+ first and shadow-write to Nakatomi + AutoMem so the shared Rev A
+ timeline stays complete. Reads prefer your CRM and fall back to
+ Nakatomi if it's unreachable. Revert any time with
+ /integrate nakatomi.
+
+
Need to do this from a terminal instead?
CLI install flow →
diff --git a/services/mcp-router/router/tools/cross.py b/services/mcp-router/router/tools/cross.py
index 331e76e..b78d8a2 100644
--- a/services/mcp-router/router/tools/cross.py
+++ b/services/mcp-router/router/tools/cross.py
@@ -165,6 +165,10 @@ async def whoami(ctx: Context) -> dict:
"memory": settings.mem_tool_prefix,
"cross": "reva",
},
+ # Primary system of record. Skills read this to decide whether
+ # to hit Nakatomi directly (crm_*) or route through an external
+ # connector the PM has installed in Desktop (hubspot_*, sf_*…).
+ "primary_crm": data.get("primary_crm") or "nakatomi",
}
@mcp.tool(name="reva_get_company_profile")
@@ -212,6 +216,12 @@ async def get_workspace_config(ctx: Context) -> dict:
"memory_taxonomy": data.get("memory_taxonomy") or [],
"escalation_matrix": data.get("escalation_matrix") or [],
"role_skill_map": data.get("role_skill_map") or {},
+ # Connector story — which external CRM (if any) is the team's
+ # system of record. Skills use this to decide whether to talk
+ # to an external connector first (e.g. HubSpot MCP tools) and
+ # shadow-write to Nakatomi, or to stay internal.
+ "primary_crm": data.get("primary_crm") or "nakatomi",
+ "connector_registry": data.get("connector_registry") or [],
}
@mcp.tool(name="reva_set_user_role")
@@ -245,3 +255,77 @@ async def set_user_role(ctx: Context, role: str) -> dict:
"PATCH", "/workspace", token=admin, json={"data": data}
)
return {"user_id": user_id, "role": role, "ok": True}
+
+ @mcp.tool(name="reva_set_primary_crm")
+ async def set_primary_crm(ctx: Context, connector: str) -> dict:
+ """Set the workspace's primary CRM (system of record).
+
+ Writes ``workspace.data.primary_crm``. ``connector`` must be the
+ ``slug`` of an entry in ``workspace.data.connector_registry`` (e.g.
+ ``nakatomi``, ``hubspot``, ``salesforce``, ``attio``, ``pipedrive``).
+
+ This is a **team-level** setting: whoever runs it changes the
+ system of record for the whole Rev A workspace. Skills read this
+ on every write to decide where the authoritative record goes;
+ the bundled Nakatomi + AutoMem always mirror the write so the
+ shared Rev A timeline stays complete.
+
+ Requires admin-token escalation (PATCH /workspace is owner-only
+ in Nakatomi). The caller's own token is still checked via
+ ``/auth/me`` so we can log *who* flipped it.
+ """
+ slug = (connector or "").strip().lower()
+ if not slug:
+ raise RuntimeError("connector is required")
+
+ # Identify caller (for logging) and sanity-check workspace membership.
+ token = token_from_ctx(ctx)
+ me = await nakatomi.request("GET", "/auth/me", token=token)
+ actor_id = (me or {}).get("id")
+ if not actor_id:
+ raise RuntimeError("could not resolve caller user_id")
+
+ admin = _admin_token()
+ ws = await nakatomi.request("GET", "/workspace", token=admin)
+ data = dict(ws.get("data") or {})
+
+ registry = data.get("connector_registry") or []
+ valid_slugs = {entry.get("slug") for entry in registry if entry.get("slug")}
+ if slug not in valid_slugs:
+ raise RuntimeError(
+ f"connector '{slug}' is not in the workspace registry. "
+ f"Valid options: {sorted(valid_slugs) or ['']}. "
+ "Ask your admin to extend `REVA_CONNECTOR_REGISTRY` if you "
+ "need a connector that isn't listed."
+ )
+ entry = next(e for e in registry if e.get("slug") == slug)
+ if not entry.get("primary_compatible", True):
+ raise RuntimeError(
+ f"connector '{slug}' is registered but not primary-compatible "
+ "(it's observe-only). Pick a different connector."
+ )
+
+ previous = data.get("primary_crm") or "nakatomi"
+ data["primary_crm"] = slug
+ # Audit trail. Lightweight — we don't want a full log table here,
+ # but the last N flips are useful for "who changed the CRM?" debug.
+ history = list(data.get("primary_crm_history") or [])
+ history.append({
+ "from": previous,
+ "to": slug,
+ "actor_user_id": actor_id,
+ "actor_email": (me or {}).get("email"),
+ })
+ data["primary_crm_history"] = history[-10:] # keep last 10 flips
+ await nakatomi.request(
+ "PATCH", "/workspace", token=admin, json={"data": data}
+ )
+ return {
+ "primary_crm": slug,
+ "previous": previous,
+ "display": entry.get("display", slug),
+ "mcp_tool_prefix": entry.get("mcp_tool_prefix"),
+ "bundled": bool(entry.get("bundled")),
+ "notes": entry.get("notes", ""),
+ "ok": True,
+ }
diff --git a/services/nakatomi-backend/seed/reva.py b/services/nakatomi-backend/seed/reva.py
index f798e40..b32faf4 100644
--- a/services/nakatomi-backend/seed/reva.py
+++ b/services/nakatomi-backend/seed/reva.py
@@ -178,6 +178,64 @@
{"tag": "reva/itar", "purpose": "ITAR-specific findings (maximum scrutiny)"},
]
+# Connector registry — which external CRMs can be set as the team's
+# primary system of record via `reva_set_primary_crm` / `/integrate`.
+# Each entry describes how skills can *detect* whether the PM already
+# has that connector wired into Claude Desktop (by looking for tools
+# whose names start with `mcp_tool_prefix`). Adding an entry here
+# unlocks /integrate ; it does NOT install anything — the PM
+# still has to add the connector in Desktop → Settings → Connectors.
+#
+# `bundled: true` means the router ships this connector natively (no
+# external Desktop setup needed). `primary_compatible` is a belt-and-
+# suspenders flag for future read-only/observer integrations — today
+# all entries can be primary.
+REVA_CONNECTOR_REGISTRY: list[dict[str, Any]] = [
+ {
+ "slug": "nakatomi",
+ "display": "Nakatomi (bundled)",
+ "mcp_tool_prefix": "crm_",
+ "bundled": True,
+ "primary_compatible": True,
+ "notes": "Default. Shipped with the router — every PM has this.",
+ },
+ {
+ "slug": "hubspot",
+ "display": "HubSpot",
+ "mcp_tool_prefix": "hubspot_",
+ "primary_compatible": True,
+ "notes": "Install HubSpot connector in Desktop → Settings → Connectors first.",
+ },
+ {
+ "slug": "salesforce",
+ "display": "Salesforce",
+ "mcp_tool_prefix": "sf_",
+ "primary_compatible": True,
+ "notes": "Install Salesforce connector in Desktop → Settings → Connectors first.",
+ },
+ {
+ "slug": "attio",
+ "display": "Attio",
+ "mcp_tool_prefix": "attio_",
+ "primary_compatible": True,
+ "notes": "Install Attio connector in Desktop → Settings → Connectors first.",
+ },
+ {
+ "slug": "pipedrive",
+ "display": "Pipedrive",
+ "mcp_tool_prefix": "pipedrive_",
+ "primary_compatible": True,
+ "notes": "Install Pipedrive connector in Desktop → Settings → Connectors first.",
+ },
+]
+
+# Which connector is the team's system of record. Reads prefer the
+# primary CRM when available; writes go to the primary first, then
+# shadow-write to Nakatomi + AutoMem so the shared Rev A timeline
+# still sees everything. Per-workspace, not per-user — the team needs
+# to agree on one system of truth.
+REVA_DEFAULT_PRIMARY_CRM: str = "nakatomi"
+
def _slug(name: str) -> str:
return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
@@ -259,14 +317,19 @@ def upsert_workspace_profile(self) -> None:
data["escalation_matrix"] = REVA_ESCALATION_MATRIX
data["role_skill_map"] = REVA_ROLE_SKILL_MAP
data["memory_taxonomy"] = REVA_MEMORY_TAXONOMY
+ data["connector_registry"] = REVA_CONNECTOR_REGISTRY
# Leave partners alone if already populated — they're PM-editable.
data.setdefault("partners", [])
+ # Only set primary_crm if not already chosen — don't clobber an
+ # admin's /integrate decision on re-seed.
+ data.setdefault("primary_crm", REVA_DEFAULT_PRIMARY_CRM)
self._req("PATCH", "/workspace", json={"data": data})
print(
f"+ workspace.data published: company_profile, escalation_matrix "
f"({len(REVA_ESCALATION_MATRIX)}), role_skill_map "
f"({len(REVA_ROLE_SKILL_MAP)} roles), memory_taxonomy "
- f"({len(REVA_MEMORY_TAXONOMY)} tags)"
+ f"({len(REVA_MEMORY_TAXONOMY)} tags), connector_registry "
+ f"({len(REVA_CONNECTOR_REGISTRY)} options, primary={data['primary_crm']})"
)
def run(self) -> None:
From 097c2782e1336b72cf68e201515e6cc70c01234b Mon Sep 17 00:00:00 2001
From: MRDula
Date: Tue, 21 Apr 2026 12:51:30 -0400
Subject: [PATCH 3/5] ui(signup): re-theme /signup to Rev A Mfg brand; detect
stale installs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Rebuild SIGNUP_HTML with the palette from revamfg.com (warm near-black
bg, cream copy, #bf6a3f copper CTA, warm-stone step pills). Mirrors the
homepage's data-theme="dark" look so the signup flow reads as an
extension of the brand rather than a separate tool. Adds warm
radial-gradient vignette, uppercase-tracked section heads,
brand-matching footer.
- Strengthen Step 2 with a hero "uninstall first" callout. Claude
Desktop's plugin uploader does NOT overwrite existing installs, so
PMs on v2.0.x stay stuck on the pre-launcher mcpServers config. The
only fix is Plugins → Installed → Remove, quit Cmd-Q, reinstall.
- Teach reva-turbo-update-check to detect stale plugin.json (greps for
reva-mcp-launch.sh reference + absence of ${user_config.*} template)
and print a loud banner with the recovery steps. Healthy installs
stay silent. Runs on every engine start, so existing stale installs
surface the warning the first time the PM runs /reva-turbo:revmyengine
after pulling the update.
- README: same uninstall-first callout under Step 3 for the link-first
(no-terminal) install path.
Co-Authored-By: Claude Opus 4.7
---
README.md | 7 +
plugin/bin/reva-turbo-update-check | 92 ++++++++-
services/mcp-router/router/signup.py | 277 +++++++++++++++++++--------
3 files changed, 285 insertions(+), 91 deletions(-)
diff --git a/README.md b/README.md
index 2ff48eb..4a1d00a 100644
--- a/README.md
+++ b/README.md
@@ -89,6 +89,13 @@ Don't unzip it.
uploads → `+`** and drop in the zip. Click **Enable**. No settings to
fill in — the plugin self-configures in the next step.
+> ⚠️ **Already have RevAOps installed from before?** Claude Desktop's
+> plugin uploader does **not** auto-upgrade an existing install. If
+> you're on `v2.0.x` or earlier, go to **Plugins → Installed → RevAOps →
+> ⋯ → Remove**, then **quit Desktop (Cmd-Q)**, relaunch, and _then_
+> upload the new zip. Skipping this leaves you on the old launcher and
+> the engine can't pick up your key.
+
**4. Connect your key in chat.** In any Claude Desktop conversation,
type:
diff --git a/plugin/bin/reva-turbo-update-check b/plugin/bin/reva-turbo-update-check
index 83172c2..8150ab4 100755
--- a/plugin/bin/reva-turbo-update-check
+++ b/plugin/bin/reva-turbo-update-check
@@ -1,24 +1,100 @@
#!/usr/bin/env bash
-# reva-turbo-update-check — check for engine version updates
+# reva-turbo-update-check — check for engine version updates + stale installs.
#
-# Compares local VERSION against remote. Outputs upgrade notice if available.
-# Silent on success (no update) or error (network issues).
+# Two jobs:
+# 1. Compare local VERSION against remote (TODO — still local-only).
+# 2. Detect a stale plugin.json from a pre-2.1.x install. Claude
+# Desktop's plugin uploader does NOT overwrite an installed plugin
+# when the user drops in a newer zip — the old plugin.json (with
+# `${user_config.*}` substitution in mcpServers) stays on disk,
+# and the MCP server never loads the PM's key file. This is the
+# #1 support issue for existing PMs. We detect it loudly so the
+# engine's startup output tells them exactly what to do.
+#
+# Silent on success (no update + clean install) or network errors.
set -uo pipefail
REVA_TURBO_DIR="${REVA_TURBO_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
STATE_DIR="${REVA_TURBO_STATE_DIR:-$HOME/.reva-turbo}"
VERSION_FILE="$REVA_TURBO_DIR/VERSION"
+PLUGIN_JSON="$REVA_TURBO_DIR/.claude-plugin/plugin.json"
SNOOZE_FILE="$STATE_DIR/.update-snoozed"
-# Skip if snoozed (within last 24h)
+# Skip version gossip if snoozed (within last 24h). Stale-install check
+# is NOT snoozable — it's a hard breakage, we want it surfaced every run.
+snoozed=0
if [ -f "$SNOOZE_FILE" ]; then
- SNOOZE_AGE="$(find "$SNOOZE_FILE" -mmin -1440 2>/dev/null | wc -l | tr -d ' ')"
- [ "$SNOOZE_AGE" -gt 0 ] 2>/dev/null && exit 0
+ age_min="$(find "$SNOOZE_FILE" -mmin -1440 2>/dev/null | wc -l | tr -d ' ')"
+ [ "$age_min" -gt 0 ] 2>/dev/null && snoozed=1
fi
LOCAL_VERSION="$(cat "$VERSION_FILE" 2>/dev/null | tr -d '[:space:]' || echo "0.0.0")"
-# For now, no remote check — just validate local version exists
+# ------------------------------------------------------------------
+# Stale-install detection.
+#
+# Signature of a healthy 2.1.x install (what the release zip ships):
+# mcpServers.reva.command = "bash"
+# mcpServers.reva.args[0] contains "reva-mcp-launch.sh"
+#
+# Signature of a stale pre-2.1 install:
+# mcpServers.reva.command = "npx" (runs mcp-remote directly)
+# args contain "${user_config.mcp_url}" / "${user_config.api_key}"
+#
+# We don't want to hard-parse JSON in bash. grep signals are enough:
+# the strings are stable and distinctive.
+# ------------------------------------------------------------------
+stale=0
+if [ -f "$PLUGIN_JSON" ]; then
+ # Healthy install reference the launcher script by name.
+ if ! grep -q 'reva-mcp-launch.sh' "$PLUGIN_JSON" 2>/dev/null; then
+ stale=1
+ fi
+ # Or: the old `${user_config.*}` template is still in place.
+ if grep -q '\${user_config\.' "$PLUGIN_JSON" 2>/dev/null; then
+ stale=1
+ fi
+fi
+
+if [ "$stale" = "1" ]; then
+ # Intentionally verbose — this is the moment we save the PM from a
+ # half-hour of "why won't my engine see the key?" debugging.
+ cat >&2 <<'EOF'
+
+┌──────────────────────────────────────────────────────────────────────┐
+│ ⚠ STALE INSTALL DETECTED — upgrade required │
+├──────────────────────────────────────────────────────────────────────┤
+│ Your RevAOps plugin was installed from an older zip (pre-v2.1.1). │
+│ Claude Desktop's plugin uploader does NOT auto-upgrade, so the │
+│ old launcher is still on disk and your key file is being ignored. │
+│ │
+│ Fix (takes 60 seconds): │
+│ 1. Claude Desktop → Plugins → Installed → RevAOps → ⋯ → Remove │
+│ 2. Quit Desktop entirely (Cmd-Q), then relaunch. │
+│ 3. Download the latest zip: │
+│ https://github.com/mrdulasolutions/RevOps-RevAMfg/releases/ │
+│ latest │
+│ 4. Plugins → Personal → Local uploads → + → drop the new zip. │
+│ 5. Run /reva-turbo:revmyengine again — it'll pick up your │
+│ existing key automatically. │
+│ │
+│ No need to re-mint — your API key is stored separately under │
+│ ~/.reva-turbo/state/mcp-credentials.env and survives re-install. │
+└──────────────────────────────────────────────────────────────────────┘
+
+EOF
+ # Don't exit non-zero — update-check is called on every engine launch,
+ # and the engine skill needs to continue even when stale. The loud
+ # stderr banner is the signal; revmyengine can also detect the
+ # missing mcp__reva__* tools and echo the same message in-chat.
+fi
+
+# ------------------------------------------------------------------
+# Version sanity.
+# ------------------------------------------------------------------
if [ "$LOCAL_VERSION" = "0.0.0" ]; then
- echo "WARNING: REVA-TURBO VERSION file missing or empty"
+ echo "WARNING: REVA-TURBO VERSION file missing or empty" >&2
fi
+
+# Remote version check — not wired yet. When it lands, skip if snoozed=1.
+: "$snoozed"
diff --git a/services/mcp-router/router/signup.py b/services/mcp-router/router/signup.py
index 9d696d1..99a7713 100644
--- a/services/mcp-router/router/signup.py
+++ b/services/mcp-router/router/signup.py
@@ -197,86 +197,182 @@ def _extract_detail(resp: httpx.Response, fallback: str) -> str:
SIGNUP_HTML = """
-
+
-REVA-OPS · Join the team
+REV A MFG · Join the RevAOps engine
+
-
-
REVA-OPS
- Rev A Manufacturing
-
-
You're one minute from having the full PM engine — CRM,
- memory, and 48 skills — connected to Claude Desktop.
+
+
Rev A Mfg
+
Join the RevAOps engine
+
One minute from mint to working PM copilot — CRM, memory,
+ and 48 skills wired into Claude Desktop.
+ Already have RevAOps installed? Remove it first.
+ Claude Desktop's plugin uploader does not auto-upgrade an
+ existing install — if you're on v2.0.x or earlier you
+ must uninstall before uploading the new zip. Go to
+ Plugins → Installed → RevAOps → ⋯ → Remove, then
+ quit Desktop (Cmd-Q), relaunch, and continue below.
+ Skipping this leaves you on a stale launcher and the engine won't
+ load your key.
+
Download the latest plugin zip from
GitHub Releases
— look for reva-turbo-<version>.zip
- (v2.1.1 or later). Don't unzip it.
+ (v2.1.2 or later). Don't unzip it.
Claude Desktop → Plugins → Personal → Local uploads → +
and drop in the zip. Click Enable.
-
No settings to fill in. The 2.1.1 plugin
+
No settings to fill in. The 2.1.2 plugin
self-configures — you'll paste your key in chat in Step 3.
- Important — remove any legacy connectors.
+ Also remove any legacy Nakatomi / AutoMem connectors.
If you previously added a standalone Nakatomi or
AutoMem MCP connector in Claude Desktop → Settings →
- Connectors, remove it now. This plugin wraps
- both behind the router with prefixed tool names
- (crm_* / mem_* / reva_*).
- Duplicates show up as raw search_contacts /
- memory_recall tool names and break intent routing.
+ Connectors, remove it now. This plugin wraps both behind the
+ router with prefixed tool names (crm_* /
+ mem_* / reva_*). Duplicates show up as
+ raw search_contacts / memory_recall
+ names and break intent routing.
-
Step 3 — Run the engine & paste your key
+
Step 3 — Run the engine & paste your key
In any Claude Desktop chat, type:
/reva-turbo:revmyengine
-
The engine will greet you and notice it doesn't have a key yet.
- It'll ask you to paste one. Reply with:
+
The engine greets you and notices it doesn't have a key yet.
+ Reply with:
/connect <paste your nk_... key here>
The engine validates the key against the router, saves it to
your local config, and tells you to quit & reopen Claude
- Desktop (Cmd-Q, relaunch). That one restart is the only manual
- step — after it, say "let's go" and you're in.
+ Desktop (Cmd-Q, relaunch). That one restart is the
+ only manual step — after it, say "let's go" and you're in.
- Behind the scenes: the plugin is connected to the shared
+ Behind the scenes: the plugin is already connected to the shared
Rev A workspace, so it asks exactly one question
(what's your role?) and pulls company profile,
partners, and pipelines from the router — no local setup.
-
Already using HubSpot, Salesforce, Attio, or Pipedrive?
+
Already using HubSpot, Salesforce, Attio, or Pipedrive?
-
You can keep using your existing CRM as
- the system of record. After Step 3, run:
+
You can keep your existing CRM as the
+ system of record. After Step 3, run:
Skills will then write to your CRM
first and shadow-write to Nakatomi + AutoMem so the shared Rev A
@@ -360,11 +466,16 @@ def _extract_detail(resp: httpx.Response, fallback: str) -> str:
/integrate nakatomi.