Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
584 changes: 97 additions & 487 deletions README.md

Large diffs are not rendered by default.

File renamed without changes.
94 changes: 94 additions & 0 deletions docs/ARCHITECTURE_V2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# REVA-OPS v2 Architecture

> v2 replaces the single-plugin layout with a monorepo: plugin + CRM backend + memory backend + MCP router, all deploying to one Railway project. **One public `/mcp` endpoint; everything else is private.**

## Components

### 1. REVA-TURBO plugin (`plugin/`)
46 Claude Code skills + 20 slash commands + one installer. Installed to `~/.claude/skills/reva-turbo` via symlink; shipped with `.claude/settings.json` pinning Sonnet 4.6. Unchanged from v1 except:
- Two new skills (`reva-turbo-reva-crm`, `reva-turbo-reva-memory`) that talk to the router
- `install.sh` now accepts `REVA_MCP_URL` + `REVA_API_KEY` and registers the MCP server in `~/.claude/mcp.json`
- Plugin lives under `plugin/` instead of at repo root; `install.sh` auto-detects either layout

### 2. MCP router (`services/mcp-router/`)
FastAPI app mounting a FastMCP streamable-HTTP server at `/mcp`. Four responsibilities:
1. **Auth** — pull `Authorization: Bearer nk_...` off each MCP request and forward it to Nakatomi.
2. **CRM tools** (`crm_*` namespace) — proxy to Nakatomi's REST API over the private network.
3. **Memory tools** (`mem_*` namespace) — proxy to AutoMem's REST API, attaching the service token on each call (AutoMem is per-workspace, not per-user).
4. **Cross-system tools** (`reva_*` namespace) — orchestrate multi-hop flows like `remember_about_entity` (write memory → link to CRM entity in one call).

Zero business logic. If you're tempted to add validation or workflow here, push it upstream into Nakatomi (structured) or a plugin skill (contextual).

### 3. Nakatomi backend (`services/nakatomi-backend/`)
The structured system of record. Railway builds from the upstream repo (`mrdulasolutions/NakatomiCRM`) — we don't vendor the code, just bump refs. Rev A overlays are applied at deploy time by `seed/reva.py`:
- `Manufacturing RFQ` pipeline (12 stages)
- Custom-field manifest (`company.compliance`, `deal.quality_gates`, `deal.ncrs`, …)
- Memory-connector env pointed at internal AutoMem

### 4. AutoMem backend (`services/automem-backend/`)
The hybrid memory store (FalkorDB graph + Qdrant vectors). Same pattern: Railway builds from `mrdulasolutions/automem`. Rev A overlays are purely conventional — tag-based scoping (`reva-crm`, `reva/<topic>`, `{entity_type}:{entity_id}`) enforced by the router.

## Data flow

### Write path (`reva_remember_about_entity`)
```
Claude Code / plugin
│ MCP streamable-HTTP
mcp-router.reva_remember_about_entity
├─► AutoMem POST /memory (returns memory_id)
└─► Nakatomi POST /memory-links (attaches memory_id to entity)
```

### Read path (`crm_timeline` with memory hydration)
```
Claude Code asks: "what's on with Acme?"
mcp-router.crm_timeline(company, acme_id)
├─► Nakatomi GET /timeline?entity_type=company&entity_id=acme_id
└─► (optional follow-up) reva_recall_for_entity(company, acme_id)
└─► AutoMem GET /recall?tags=company:acme_id
```

## Auth model

| Principal | Token | Validated by |
|------------------------|----------------------|-----------------------------|
| Human PM via Claude | `nk_...` bearer | Nakatomi on each call |
| Router → Nakatomi | forwarded bearer | Nakatomi on each call |
| Router → AutoMem | `AUTOMEM_API_TOKEN` | AutoMem (single service key)|
| Admin CLI | Railway service env | Railway |

The client never sees or needs `AUTOMEM_API_TOKEN`. Memory calls from the router attach it automatically.

## Deployment topology

```
Railway project: reva-ops
├── mcp-router [public domain] ← only externally reachable service
├── nakatomi-backend [private only]
├── automem-backend [private only]
├── postgres [Railway plugin]
├── falkordb [Railway plugin] (redis image with FalkorDB module)
└── qdrant [Railway plugin] (custom docker image)
```

All private services communicate over `*.railway.internal` DNS. No traffic leaves the Railway project for internal calls.

## Why not submodules / why not a single Docker image

- **Submodules.** Rejected: makes `git clone` slower, breaks shallow clones on Railway, and the upstream Nakatomi/AutoMem repos are already pinnable via Railway's source-repo field. We get the same version-pinning behavior with none of the friction.
- **Single fat image.** Rejected: Railway separates scaling and environment per service. Running AutoMem (Python + FalkorDB client + Qdrant client + embeddings) inside the same process as Nakatomi (SQLAlchemy + FastAPI) makes restarts expensive and OOM events catastrophic. The router's one job is to *look* like a single endpoint; it doesn't need to *be* a single process.

## Rollback / disaster recovery

- Postgres, FalkorDB, Qdrant are Railway-managed with daily volume snapshots.
- Service source is pinned to git refs in `railway/template.yaml` — rolling back is a `railway redeploy --ref <old-sha>`.
- Plugin uninstall: `rm -rf ~/.claude/skills/reva-turbo ~/.reva-turbo` and remove the `reva` entry from `~/.claude/mcp.json`.

## Open questions tracked for v2.1

- Per-user API keys in the plugin (today: one shared Rev A key). Nakatomi already supports per-user; plugin UX just needs to call `POST /workspace/api-keys`.
- Auth mode `service` (the router validates + re-mints tokens) — skeleton is in `router/auth.py`; needs a `/auth/whoami` check before we flip the env var.
- Streaming progress through MCP for long-running tools (`reva_remember_about_entity` is 2 HTTP round-trips — nice to surface per-step status).
- Attachment surface — AutoMem has `upload_document`; router hasn't wrapped it yet (deliberate: agents should be reading files themselves and summarizing).
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
108 changes: 91 additions & 17 deletions install.sh → plugin/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,31 @@
# REVA-TURBO one-line installer.
#
# Usage (fresh install, update, or reinstall — all idempotent):
# curl -fsSL https://raw.githubusercontent.com/mrdulasolutions/RevOps-RevAMfg/main/install.sh | bash
# curl -fsSL https://raw.githubusercontent.com/mrdulasolutions/RevOps-RevAMfg/main/plugin/install.sh | bash
#
# Or from a local clone:
# bash install.sh
# With an existing REVA-OPS Railway backend:
# curl -fsSL https://.../plugin/install.sh | \
# REVA_MCP_URL=https://<router>.up.railway.app/mcp \
# REVA_API_KEY=nk_... \
# bash
#
# What it does:
# 1. Clones (or fast-forwards) the repo to $REVA_TURBO_DIR (default ~/reva-turbo)
# 2. Creates ~/.reva-turbo/ state dirs
# 3. Symlinks the repo into ~/.claude/skills/reva-turbo so Claude Code loads it
# 3. Symlinks the plugin into ~/.claude/skills/reva-turbo so Claude Code loads it
# 4. Makes bin scripts executable
# 5. npm-installs docx converter deps (if npm is present)
# 6. Writes default config.yaml
# 7. (If REVA_MCP_URL set) registers the REVA MCP server in ~/.claude/mcp.json
#
# Env overrides:
# REVA_TURBO_DIR Override clone location (default: ~/reva-turbo)
# REVA_TURBO_REPO Override repo URL (default: mrdulasolutions/RevOps-RevAMfg)
# REVA_TURBO_REF Override branch/tag (default: main)
# REVA_TURBO_NO_NPM If set, skip npm install (docx reports disabled)
# REVA_TURBO_SKIP_GIT If set, skip clone/pull (use current $REVA_TURBO_DIR as-is; for CI)
# REVA_MCP_URL Public MCP URL (from railway/deploy.sh output)
# REVA_API_KEY Nakatomi bearer token (nk_...)

set -euo pipefail

Expand All @@ -29,6 +35,7 @@ REF="${REVA_TURBO_REF:-main}"
REVA_TURBO_DIR="${REVA_TURBO_DIR:-$HOME/reva-turbo}"
STATE_DIR="$HOME/.reva-turbo"
SKILLS_DIR="$HOME/.claude/skills/reva-turbo"
MCP_CONFIG="$HOME/.claude/mcp.json"

say() { printf "\033[1;36m[reva-turbo]\033[0m %s\n" "$*"; }
die() { printf "\033[1;31m[reva-turbo]\033[0m %s\n" "$*" >&2; exit 1; }
Expand All @@ -37,6 +44,16 @@ command -v git >/dev/null 2>&1 || die "git is required but not installed"

say "Installing REVA-TURBO ($REF) -> $REVA_TURBO_DIR"

# Detect whether we're pointing at the v2 monorepo (plugin/ lives as a subdir)
# or the flat v1 layout. Works for both during the transition.
find_plugin_root() {
if [ -d "$1/plugin" ] && [ -d "$1/plugin/skills" ]; then
echo "$1/plugin"
else
echo "$1"
fi
}

# ── Step 1: clone or update ─────────────────────────────────────────────
if [ -n "${REVA_TURBO_SKIP_GIT:-}" ]; then
say "REVA_TURBO_SKIP_GIT set — using existing tree at $REVA_TURBO_DIR"
Expand All @@ -52,29 +69,31 @@ else
git clone --quiet --branch "$REF" "$REPO" "$REVA_TURBO_DIR"
fi

PLUGIN_ROOT="$(find_plugin_root "$REVA_TURBO_DIR")"
say "Plugin root: $PLUGIN_ROOT"

# ── Step 2: state dirs ──────────────────────────────────────────────────
mkdir -p "$STATE_DIR"/{sessions,analytics,state,reports/REVA-TURBO-Reports,contributor-logs,users}

# ── Step 3: symlink into Claude Code skills ─────────────────────────────
mkdir -p "$HOME/.claude/skills"
if [ -L "$SKILLS_DIR" ]; then
# Already a symlink — point it at the current checkout
rm "$SKILLS_DIR"
elif [ -d "$SKILLS_DIR" ]; then
backup="${SKILLS_DIR}.bak.$(date +%Y%m%d%H%M%S)"
say "Backing up existing $SKILLS_DIR -> $backup"
mv "$SKILLS_DIR" "$backup"
fi
ln -s "$REVA_TURBO_DIR" "$SKILLS_DIR"
ln -s "$PLUGIN_ROOT" "$SKILLS_DIR"

# ── Step 4: chmod ───────────────────────────────────────────────────────
chmod +x "$REVA_TURBO_DIR/bin/"* 2>/dev/null || true
find "$REVA_TURBO_DIR/skills" -type f -name '*.sh' -exec chmod +x {} \; 2>/dev/null || true
find "$REVA_TURBO_DIR/skills" -type d -name 'bin' -exec chmod -R +x {} \; 2>/dev/null || true
chmod +x "$PLUGIN_ROOT/bin/"* 2>/dev/null || true
find "$PLUGIN_ROOT/skills" -type f -name '*.sh' -exec chmod +x {} \; 2>/dev/null || true
find "$PLUGIN_ROOT/skills" -type d -name 'bin' -exec chmod -R +x {} \; 2>/dev/null || true

# ── Step 5: docx converter deps ─────────────────────────────────────────
if [ -z "${REVA_TURBO_NO_NPM:-}" ] && command -v npm >/dev/null 2>&1; then
pkg_dir="$REVA_TURBO_DIR/skills/reva-turbo-docx/scripts"
pkg_dir="$PLUGIN_ROOT/skills/reva-turbo-docx/scripts"
if [ -f "$pkg_dir/package.json" ]; then
( cd "$pkg_dir" && npm install --silent --no-fund --no-audit --loglevel=error >/dev/null 2>&1 ) \
&& say "docx converter deps installed" \
Expand All @@ -85,12 +104,12 @@ else
fi

# ── Step 6: default config ──────────────────────────────────────────────
CONFIG_CMD="$REVA_TURBO_DIR/bin/reva-turbo-config"
if [ ! -f "$STATE_DIR/config.yaml" ]; then
CONFIG_CMD="$PLUGIN_ROOT/bin/reva-turbo-config"
if [ -x "$CONFIG_CMD" ] && [ ! -f "$STATE_DIR/config.yaml" ]; then
"$CONFIG_CMD" set telemetry off
"$CONFIG_CMD" set proactive true
"$CONFIG_CMD" set report_format docx
"$CONFIG_CMD" set crm_type none
"$CONFIG_CMD" set crm_type reva-mcp
if [ "$(uname)" = "Darwin" ]; then
"$CONFIG_CMD" set platform mac
else
Expand All @@ -99,17 +118,72 @@ if [ ! -f "$STATE_DIR/config.yaml" ]; then
say "Default config written to $STATE_DIR/config.yaml"
fi

# If REVA_MCP_URL / REVA_API_KEY provided, persist them to config too
if [ -n "${REVA_MCP_URL:-}" ] && [ -x "$CONFIG_CMD" ]; then
"$CONFIG_CMD" set reva_mcp_url "$REVA_MCP_URL"
say "Saved reva_mcp_url to config.yaml"
fi
if [ -n "${REVA_API_KEY:-}" ] && [ -x "$CONFIG_CMD" ]; then
"$CONFIG_CMD" set reva_api_key "$REVA_API_KEY"
say "Saved reva_api_key to config.yaml"
fi

# ── Step 7: register REVA MCP in Claude Code's mcp.json ─────────────────
# Only when both URL and key are set. We write JSON by hand (no jq
# dependency) using a tiny Python one-liner if Python is available, else
# falling back to a simple append-or-replace pattern.
if [ -n "${REVA_MCP_URL:-}" ] && [ -n "${REVA_API_KEY:-}" ]; then
if command -v python3 >/dev/null 2>&1; then
mkdir -p "$(dirname "$MCP_CONFIG")"
python3 - "$MCP_CONFIG" "$REVA_MCP_URL" "$REVA_API_KEY" <<'PY'
import json, os, sys
path, url, key = sys.argv[1], sys.argv[2], sys.argv[3]
cfg = {}
if os.path.exists(path):
try:
with open(path) as f:
cfg = json.load(f)
except Exception:
cfg = {}
cfg.setdefault("mcpServers", {})
cfg["mcpServers"]["reva"] = {
"type": "http",
"url": url,
"headers": {"Authorization": f"Bearer {key}"},
}
with open(path, "w") as f:
json.dump(cfg, f, indent=2)
PY
say "Registered REVA MCP server in $MCP_CONFIG"
else
say "python3 not found — skipping MCP registration. Add manually:"
cat <<EOF

"reva": {
"type": "http",
"url": "$REVA_MCP_URL",
"headers": { "Authorization": "Bearer $REVA_API_KEY" }
}

EOF
fi
else
say "REVA_MCP_URL / REVA_API_KEY not set — skipping MCP registration."
say " (Deploy the backend with railway/deploy.sh, then re-run with those env vars.)"
fi

# ── Done ────────────────────────────────────────────────────────────────
VERSION="$(cat "$REVA_TURBO_DIR/VERSION" 2>/dev/null | tr -d '[:space:]' || echo unknown)"
VERSION="$(cat "$PLUGIN_ROOT/VERSION" 2>/dev/null | tr -d '[:space:]' || echo unknown)"
cat <<EOF

════════════════════════════════════════════════════════
REVA-TURBO v$VERSION installed
════════════════════════════════════════════════════════

Skills: $SKILLS_DIR -> $REVA_TURBO_DIR
State: $STATE_DIR
Reports: $STATE_DIR/reports/REVA-TURBO-Reports/
Plugin: $SKILLS_DIR -> $PLUGIN_ROOT
State: $STATE_DIR
Reports: $STATE_DIR/reports/REVA-TURBO-Reports/
$( [ -n "${REVA_MCP_URL:-}" ] && echo " MCP: $REVA_MCP_URL" )

Next: restart Claude Code, then run
/reva-turbo:revmyengine
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading
Loading