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:

+
/integrate hubspot   (or salesforce / attio / pipedrive)
+

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.

+
+ +

Join the RevAOps engine

+

One minute from mint to working PM copilot — CRM, memory, + and 48 skills wired into Claude Desktop.

+

Manufactured where it makes sense

+
1 Mint API key
@@ -303,55 +399,65 @@ def _extract_detail(resp: httpx.Response, fallback: str) -> str:
-

Step 2 — Install the plugin

+

Step 2 — Install the plugin

+
+ 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. +
  1. Download the latest plugin zip from GitHub Releases — look for reva-turbo-<version>.zip - (v2.1.1 or later). Don't unzip it.
  2. + (v2.1.2 or later). Don't unzip it.
  3. Claude Desktop → Plugins → Personal → Local uploads → + and drop in the zip. Click Enable.
  4. -
  5. No settings to fill in. The 2.1.1 plugin +
  6. 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:

/integrate hubspot   (or salesforce / attio / pipedrive)

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.

-

+

Need to do this from a terminal instead? CLI install flow →

+ +
+ Rev A Manufacturing · RevAOps + v2.1.2 +