diff --git a/README.md b/README.md index aabd52e..4a1d00a 100644 --- a/README.md +++ b/README.md @@ -67,41 +67,98 @@ 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. + +> ⚠️ **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: + +``` +/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/bin/reva-turbo-update-check b/plugin/bin/reva-turbo-update-check index 83172c2..c0d64ba 100755 --- a/plugin/bin/reva-turbo-update-check +++ b/plugin/bin/reva-turbo-update-check @@ -1,24 +1,120 @@ #!/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. Lead + # with the hands-free path (say "/heal" in chat); fall back to the + # manual 5-step recovery for PMs whose Claude doesn't have the + # required connectors. + cat >&2 <<'EOF' + +┌──────────────────────────────────────────────────────────────────────┐ +│ ⚠ STALE INSTALL DETECTED — RevAOps needs a refresh │ +├──────────────────────────────────────────────────────────────────────┤ +│ Your plugin was installed from a pre-v2.1.1 zip. Claude Desktop's │ +│ uploader does NOT auto-upgrade, so the old launcher is still on │ +│ disk and the engine can't pick up your API key. │ +│ │ +│ HANDS-FREE FIX (recommended — takes ~30 seconds): │ +│ Say /heal in this chat. │ +│ │ +│ Claude will download the latest zip, replace the stale install, │ +│ write your credentials, and relaunch Desktop. Requires one of: │ +│ • "Control your Mac" connector (Desktop → Settings → │ +│ Connectors → Add) │ +│ • Claude Code's built-in Bash tool │ +│ If neither is present Claude will tell you exactly which │ +│ connector to enable, then run /heal again. │ +│ │ +│ TERMINAL FIX (zero connectors needed): │ +│ Open Terminal and paste: │ +│ curl -fsSL https://raw.githubusercontent.com/mrdulasolutions/\ │ +│ RevOps-RevAMfg/main/plugin/scripts/desktop-install.sh \ │ +│ | REVA_API_KEY=nk_YOURKEY bash │ +│ │ +│ MANUAL FIX (last resort — 60 seconds): │ +│ 1. Claude Desktop → Plugins → Installed → RevAOps → ⋯ → Remove │ +│ 2. Quit Desktop (Cmd-Q), then relaunch. │ +│ 3. Download the latest zip from │ +│ https://github.com/mrdulasolutions/RevOps-RevAMfg/releases/ │ +│ latest │ +│ 4. Plugins → Personal → Local uploads → + → drop the new zip. │ +│ 5. Run /reva-turbo:revmyengine. │ +│ │ +│ Your key is stored separately under │ +│ ~/.reva-turbo/state/mcp-credentials.env │ +│ and survives re-install — no need to re-mint. │ +└──────────────────────────────────────────────────────────────────────┘ + +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/plugin/scripts/desktop-install.sh b/plugin/scripts/desktop-install.sh new file mode 100755 index 0000000..4dc575a --- /dev/null +++ b/plugin/scripts/desktop-install.sh @@ -0,0 +1,204 @@ +#!/usr/bin/env bash +# REVA-TURBO Claude Desktop self-heal / fresh-install. +# +# Target scenario: PM is on a stale pre-2.1.1 install (plugin.json still +# uses `${user_config.*}` substitution instead of our bash launcher) and +# Claude Desktop's uploader won't overwrite it. This script removes the +# old install, drops in the latest release zip, and writes the PM's +# API key to `~/.reva-turbo/state/mcp-credentials.env` so the launcher +# picks it up on the next Claude Desktop launch. +# +# Hands-free usage (Claude Cowork / Control-your-Mac can run this via +# osascript's `do shell script`, Claude Code can run it via the Bash +# tool, and PMs on a terminal can paste it directly): +# +# curl -fsSL https://raw.githubusercontent.com/mrdulasolutions/RevOps-RevAMfg/main/plugin/scripts/desktop-install.sh \ +# | REVA_API_KEY=nk_yourkey bash +# +# Env overrides: +# REVA_API_KEY (required) nk_... bearer minted at /signup +# REVA_MCP_URL override router URL (default: rev a production) +# REVA_RELEASE_TAG install a specific tag (default: latest) +# REVA_NO_DOWNLOAD skip zip download (testing only) +# REVA_INSTALL_ROOT override plugins root (default: +# ~/Library/Application Support/Claude/plugins/ +# marketplaces/local-desktop-app-uploads) +# If the dir doesn't exist we fall back to +# ~/.claude/plugins/... which is where newer +# Desktop builds actually keep uploads. +# +# Exit codes: 0 ok, 2 bad args, 3 download failed, 4 fs failed. +set -uo pipefail + +REPO="mrdulasolutions/RevOps-RevAMfg" +TAG="${REVA_RELEASE_TAG:-latest}" +DEFAULT_MCP_URL="https://mcp-router-production-460a.up.railway.app/mcp" +REVA_MCP_URL="${REVA_MCP_URL:-$DEFAULT_MCP_URL}" + +# Pretty-ish output that still reads fine inside AppleScript's `do shell +# script` capture. No tput — we may not be on a tty. +say() { printf "[reva] %s\n" "$*"; } +warn() { printf "[reva] WARN: %s\n" "$*" >&2; } +die() { printf "[reva] ERROR: %s\n" "$*" >&2; exit "${2:-1}"; } + +# ── 1. Arg sanity ──────────────────────────────────────────────────── +REVA_API_KEY="${REVA_API_KEY:-}" +if [ -z "$REVA_API_KEY" ]; then + die "REVA_API_KEY not set. Get a key at ${REVA_MCP_URL%/mcp*}/signup and re-run with REVA_API_KEY=nk_... bash." 2 +fi +case "$REVA_API_KEY" in + nk_*) ;; + *) warn "REVA_API_KEY does not start with 'nk_' — proceeding anyway. If the router rejects the key, mint a fresh one at ${REVA_MCP_URL%/mcp*}/signup." ;; +esac + +# ── 2. Find the plugins root ───────────────────────────────────────── +# Claude Desktop writes uploads to one of two paths depending on build: +# macOS (most builds): ~/Library/Application Support/Claude/plugins/marketplaces/local-desktop-app-uploads +# Newer builds: ~/.claude/plugins/marketplaces/local-desktop-app-uploads +# We try the explicit override first, then both defaults. If neither +# exists (Claude Desktop never launched) we create the Library one and +# trust Desktop to adopt it on next launch. +guess_plugins_root() { + if [ -n "${REVA_INSTALL_ROOT:-}" ]; then + echo "$REVA_INSTALL_ROOT"; return + fi + for cand in \ + "$HOME/Library/Application Support/Claude/plugins/marketplaces/local-desktop-app-uploads" \ + "$HOME/.claude/plugins/marketplaces/local-desktop-app-uploads" + do + [ -d "$cand" ] && { echo "$cand"; return; } + done + # Neither exists. Default to the Library path — that's where macOS + # Claude Desktop keeps uploads on current builds. + echo "$HOME/Library/Application Support/Claude/plugins/marketplaces/local-desktop-app-uploads" +} +PLUGINS_ROOT="$(guess_plugins_root)" +INSTALL_DIR="$PLUGINS_ROOT/reva-turbo" + +say "plugins root: $PLUGINS_ROOT" +mkdir -p "$PLUGINS_ROOT" || die "cannot create $PLUGINS_ROOT" 4 + +# ── 3. Download the release zip ────────────────────────────────────── +TMPDIR_REVA="$(mktemp -d)" +trap 'rm -rf "$TMPDIR_REVA"' EXIT +ZIP="$TMPDIR_REVA/reva-turbo.zip" + +if [ -z "${REVA_NO_DOWNLOAD:-}" ]; then + if [ "$TAG" = "latest" ]; then + # GitHub's /releases/latest redirects to the versioned URL — follow with -L. + URL="https://github.com/$REPO/releases/latest/download/reva-turbo-$( + curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" \ + | grep -oE '"tag_name"[[:space:]]*:[[:space:]]*"[^"]+"' \ + | head -1 | sed 's/.*"\(v[^"]*\)"/\1/' | sed 's/^v//' + ).zip" + else + VER="${TAG#v}" + URL="https://github.com/$REPO/releases/download/${TAG}/reva-turbo-${VER}.zip" + fi + + say "downloading: $URL" + if ! curl -fsSL --retry 3 --max-time 60 "$URL" -o "$ZIP"; then + die "download failed — check network / tag name ($TAG)" 3 + fi +else + warn "REVA_NO_DOWNLOAD set — expecting a preseeded $ZIP" + [ -f "$ZIP" ] || die "REVA_NO_DOWNLOAD set but no zip at $ZIP" 3 +fi + +# Sanity: zip must contain reva-turbo/.claude-plugin/plugin.json +if ! unzip -l "$ZIP" 2>/dev/null | grep -q 'reva-turbo/\.claude-plugin/plugin\.json'; then + die "downloaded zip does not look like a reva-turbo release" 3 +fi + +# ── 4. Quit Claude Desktop if it's running ─────────────────────────── +# Desktop locks files while running. osascript exits quietly if the +# app isn't open, so this is safe to run unconditionally. +if command -v osascript >/dev/null 2>&1; then + if osascript -e 'tell application "System Events" to (name of processes) contains "Claude"' 2>/dev/null | grep -qi true; then + say "quitting Claude Desktop (will relaunch at end)" + osascript -e 'tell application "Claude" to quit' 2>/dev/null || true + # Give it up to 6s to actually exit. + for _ in 1 2 3 4 5 6; do + osascript -e 'tell application "System Events" to (name of processes) contains "Claude"' 2>/dev/null | grep -qi true || break + sleep 1 + done + fi +fi + +# ── 5. Remove stale install, extract new one ───────────────────────── +if [ -d "$INSTALL_DIR" ]; then + say "removing stale install: $INSTALL_DIR" + # Preserve the PM's credential file if for some reason it lives inside + # the plugin dir. Canonically it lives at ~/.reva-turbo/state/ but + # defense-in-depth. + BACKUP="$TMPDIR_REVA/preserved-creds" + if [ -f "$INSTALL_DIR/.reva-turbo/state/mcp-credentials.env" ]; then + mkdir -p "$BACKUP" + cp "$INSTALL_DIR/.reva-turbo/state/mcp-credentials.env" "$BACKUP/" + fi + rm -rf "$INSTALL_DIR" || die "could not remove $INSTALL_DIR — check permissions" 4 +fi + +say "extracting release into $PLUGINS_ROOT" +if ! unzip -q "$ZIP" -d "$PLUGINS_ROOT"; then + die "unzip failed" 4 +fi + +# Make sure bin scripts are executable (zip preserves bits but some +# extraction tools drop them). +chmod +x "$INSTALL_DIR/bin/"* 2>/dev/null || true +chmod +x "$INSTALL_DIR/scripts/"*.sh 2>/dev/null || true + +# ── 6. Write the credentials file ──────────────────────────────────── +STATE_DIR="$HOME/.reva-turbo/state" +mkdir -p "$STATE_DIR" +CRED_FILE="$STATE_DIR/mcp-credentials.env" + +# mode 600 — contains a bearer token. +cat > "$CRED_FILE" </dev/null 2>&1; then + PING_STATUS="$(curl -fsSL -o /dev/null -w '%{http_code}' --max-time 10 \ + -H "Authorization: Bearer $REVA_API_KEY" "$PING_URL" 2>/dev/null || echo "000")" + case "$PING_STATUS" in + 200|204) say "router reachable (HTTP $PING_STATUS)" ;; + 401|403) warn "router rejected the key ($PING_STATUS). Mint a fresh one at ${REVA_MCP_URL%/mcp*}/signup and re-run." ;; + 000) warn "router unreachable — no network? will retry on Desktop launch." ;; + *) warn "router returned HTTP $PING_STATUS — install continues, but double-check the URL." ;; + esac +fi + +# ── 7. Relaunch Claude Desktop ─────────────────────────────────────── +if command -v open >/dev/null 2>&1; then + say "relaunching Claude Desktop" + open -a "Claude" 2>/dev/null || open -a "Claude Desktop" 2>/dev/null || \ + warn "could not relaunch Claude automatically — open it yourself." +fi + +cat <` | 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 | +| `/heal` | inline | Hands-free recovery when install is stale. Downloads the latest zip, replaces the installed plugin dir, writes credentials. See **`/heal` — hands-free recovery** section below | | `/send-logs` | inline | Package dev log + email to matt@mrdula.solutions | | `/logs` | inline | Display recent telemetry entries in readable format | @@ -576,6 +579,194 @@ 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. + +## `/heal` — hands-free recovery from a stale Claude Desktop install + +Claude Desktop's plugin uploader does **not** overwrite an existing +install when a PM drops in a newer zip. If a PM upgraded from `v2.0.x` +(the `${user_config.*}`-substitution era) to `v2.1.x+` (the bash +launcher era), the old `plugin.json` is still on disk, the MCP server +fails to pick up the key file, and `mcp__reva__*` tools never load. +`reva-turbo-update-check` prints a banner flagging this at every engine +start — `/heal` is how we fix it hands-free. + +**Trigger.** Run `/heal` when any of these are true: +- `reva-turbo-update-check` printed the `⚠ STALE INSTALL DETECTED` banner in the preamble. +- The PM reports the engine "hangs" / "tools not showing" after an upload. +- `mcp__reva__reva_whoami` is still missing **after** the PM confirmed they ran `/connect` and restarted Desktop. + +**How `/heal` works.** It runs +[`plugin/scripts/desktop-install.sh`](../../scripts/desktop-install.sh) +one-shot: quits Desktop, removes the stale install dir, downloads the +latest release zip, extracts it to the plugins root, writes +`~/.reva-turbo/state/mcp-credentials.env` with the PM's key, pings the +router, and relaunches Desktop. End-to-end under 30 seconds. + +### Capability detection (check BEFORE running) + +The PM's Claude Desktop needs at least one of these to run the heal +script hands-free. Check the tool surface for exact tool names: + +| Capability | Detection signal | How /heal uses it | +|---|---|---| +| **Bash tool** (Claude Code native) | `Bash` in tool surface | Runs `curl … \| bash` directly | +| **Control your Mac** (`mcp__computer-use__*` / `mcp__Control_your_Mac__osascript`) | `mcp__Control_your_Mac__osascript` present | Runs `osascript -e 'do shell script "curl … | bash"'` | +| **Filesystem + Bash-like fallback** | `mcp__*Filesystem*` write tools present but no shell | Degraded: see "Filesystem-only fallback" below | + +**If NONE of the above is available**, do not attempt the heal. Tell +the PM exactly what to do: + +> I can install this hands-free, but I need one of these connectors to +> run the recovery script: +> +> - **Control your Mac** (preferred — does the whole thing in one go) +> - **Filesystem** with write access to `~/.claude/plugins` and `~/.reva-turbo` +> +> **To add it:** Claude Desktop → Settings → Connectors → Add, search +> for "Control your Mac", enable, then come back and say `/heal` again. +> +> **Or**, one-shot from Terminal (zero connectors needed): +> +> ```bash +> curl -fsSL https://raw.githubusercontent.com/mrdulasolutions/RevOps-RevAMfg/main/plugin/scripts/desktop-install.sh \ +> | REVA_API_KEY= bash +> ``` + +### Run the heal (capability present) + +1. **Find the PM's key.** Read + `~/.reva-turbo/state/mcp-credentials.env` if it exists. If not, + ask the PM to paste their `nk_...` key (or direct them to + `/signup`). Never proceed without a key — the script exits non-zero. + +2. **Run the one-liner.** Via Bash tool: + ```bash + curl -fsSL https://raw.githubusercontent.com/mrdulasolutions/RevOps-RevAMfg/main/plugin/scripts/desktop-install.sh \ + | REVA_API_KEY="$NK_KEY" bash + ``` + Via `mcp__Control_your_Mac__osascript`: + ```applescript + do shell script "curl -fsSL https://raw.githubusercontent.com/mrdulasolutions/RevOps-RevAMfg/main/plugin/scripts/desktop-install.sh | REVA_API_KEY='" & theKey & "' bash 2>&1" + ``` + +3. **Read the script's stdout**. It prints: + - `[reva] router reachable (HTTP 200)` on success → relaunch line follows + - `[reva] WARN: router rejected the key` → bad key; have them re-mint at `/signup` + - `[reva] ERROR: …` with exit code 2/3/4 → report the exact message + +4. **Confirm in chat.** On success say exactly: + > **✓ Healed.** Claude Desktop is relaunching. When it comes back, + > say *"let's go"* and we'll pick up where we left off — no need to + > run `/connect` again. + +### Filesystem-only fallback + +If only a Filesystem MCP with write access is available (no shell +execution), we can't download the zip but we CAN patch the installed +`plugin.json` in place and drop in the launcher. This is the path the +prior session's agent discovered: + +1. Overwrite + `$PLUGINS_ROOT/reva-turbo/.claude-plugin/plugin.json` with the + content of the currently-running plugin's `.claude-plugin/plugin.json` + (read via Filesystem, write via Filesystem). +2. Copy + `$PLUGINS_ROOT/reva-turbo/bin/reva-mcp-launch.sh` from the running + plugin's `bin/`. +3. Write `~/.reva-turbo/state/mcp-credentials.env` with the key. +4. Ask the PM to **Cmd-Q + relaunch** (we can't do this without shell). + +This path only works if the running plugin is ≥ v2.1.2 (it is, because +`/heal` itself is defined here). If the running plugin is stale the +only option is the Terminal one-liner. + +### When NOT to run `/heal` + +- First-time install (no prior plugin dir) — regular `/signup` flow is + shorter and doesn't need shell access. +- MCP tools are loading fine (`mcp__reva__reva_whoami` responds). The + heal is destructive (removes the install dir); don't run it on a + healthy install. + ## 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 37093b0..54aeb94 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)}" @@ -162,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
@@ -253,13 +384,14 @@ def _extract_detail(resp: httpx.Response, fallback: str) -> str: - - + + - + @@ -268,56 +400,99 @@ def _extract_detail(resp: httpx.Response, fallback: str) -> str:
-

Step 2 — Install the plugin

+

Step 2 — Install the plugin

-
    -
  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. -
  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 - self-configures — you'll paste your key in chat in Step 3.
  6. -
+

Pick a path. Both end with the same working engine.

+ +
+

+ Option A — Hands-free (recommended) +

+

Paste this one-liner into Terminal. It downloads the latest zip, + drops it into Claude Desktop's plugins dir, writes your key, and + relaunches Desktop — no clicking through menus, safely re-runs + if you already had an old version.

+
curl -fsSL https://raw.githubusercontent.com/mrdulasolutions/RevOps-RevAMfg/main/plugin/scripts/desktop-install.sh \
+  | REVA_API_KEY=<your nk_... key from Step 1> bash
+

Already inside Claude? + If you have the Control your Mac connector + enabled (Desktop → Settings → Connectors), just say + /heal in any chat and Claude runs the one-liner + for you — truly hands-free.

+
+ +
+

+ Option B — Manual upload +

+
    +
  1. Download reva-turbo-<version>.zip (v2.1.2+) from + GitHub Releases. Don't unzip.
  2. +
  3. If you already have RevAOps installed (v2.0.x or earlier), go to + Plugins → Installed → RevAOps → ⋯ → Remove, + then quit Desktop (Cmd-Q) and relaunch. The + uploader doesn't auto-upgrade — skipping this leaves you on + a stale launcher and the engine won't load your key.
  4. +
  5. Claude Desktop → Plugins → Personal → Local uploads → + + and drop in the zip. Click Enable. No + settings to fill in — you'll paste your key in Step 3.
  6. +
+
- Important — remove any legacy connectors. + 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. + AutoMem MCP connector under 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 + 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?

+
+

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 + 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 →

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