---
name: nexuscrm-connect
version: 1.9.9
updated: '2026-08-14'
description: >-
  Connect Claude or Codex to NexusCRM and pull read-only CRM data — accounts,
  opportunities, contacts, leads, pipeline — as the signed-in user, then turn it
  into tables, reports, dashboards, or spreadsheets. Use this whenever the user
  wants to connect/sign in to NexusCRM, query NexusCRM data, or build any
  report/dashboard/list/export from NexusCRM (e.g. "list my accounts", "open
  opportunities by stage", "my pipeline", "top 10 deals this quarter", "export my
  account list to Excel"). Also use it when a NexusCRM connection "isn't working"
  or returns "interactive login is required" / "access_denied". Prefer this over
  hand-rolling an OAuth flow or pasting a connect message — those are flaky.
---

# NexusCRM Connect

Get NexusCRM data into Claude/Codex with one bundled tool, then shape it into
whatever the user asked for. The connection is **public OAuth Authorization-Code
+ PKCE**: no password is shared, no client secret, the scope is read-only
(`external.records.read`), and the token **runs as the signed-in user** — they
see exactly the records they'd see logged into NexusCRM, nothing more.

Why a bundled tool instead of improvising the OAuth flow: the flow has several
fiddly steps (PKCE challenge, a loopback listener on an exact registered port,
the code exchange with no secret). Getting any of them subtly wrong is why the
old "paste a connect message" approach failed for people. `scripts/nexus_connect.mjs`
does all of it deterministically, so connecting becomes two predictable commands.

## Requirements

- **Node.js 18+** (the script uses built-in `fetch`). Check with `node --version`.
  If Node is missing, point the user to https://nodejs.org (LTS).
- **Any OS** — Windows, macOS, or Linux. The script opens the browser the right way
  per platform, so commands below are identical everywhere.
- A browser on the same machine where they're (or can get) **signed in to NexusCRM**.

> Using **Codex** (or any non-Claude CLI agent)? The same engine works there — see
> `AGENTS.md` in this folder (Codex reads `AGENTS.md`, not `SKILL.md`). Humans can read
> `README.md` for install steps on both tools and both operating systems.

## Step 1 — Connect (once per session; the token lasts ~1 hour)

```
node scripts/nexus_connect.mjs auth
```

A browser tab opens.

- **If it shows a NexusCRM sign-in page, the user signs in there** (their normal
  SSO login). After login it returns automatically.
- **If they're already signed in, it goes straight through** — no approval screen
  (the app is Trusted) — to a plain page reading *"Authorized — you can close this
  tab."* Close it.

The token is cached at `~/.nexuscrm-connect/token.json`, so later commands in the
same session don't re-open the browser.

> **The single most common failure** is that the browser tab is **not signed in to
> NexusCRM**, so authorize returns `access_denied` / "interactive login is
> required." Two flavors of this, both handled:
>
> - **Your default browser isn't where you're signed in.** The script auto-opens
>   your OS default browser, which may not be the one holding your NexusCRM
>   session. The listener now **stays open and waits** instead of giving up — so
>   just copy the printed authorize URL into the browser where you *are* signed in,
>   and it completes there.
> - **Cold start.** The very first attempt sometimes fails and an immediate retry
>   succeeds (the sign-in step warms up). If `auth` errors once, run it again, or
>   complete it in an already-open signed-in tab.
>
> If a coworker says "I followed the steps and it still won't connect," it's almost
> always one of these — have them sign in to NexusCRM first, then finish the
> sign-in **in that same signed-in browser**.

## When something keeps failing — `diagnose`

```
node scripts/nexus_connect.mjs diagnose 2>&1
```

Offline and token-free, so it works even when the thing that is broken is the connection. It says
whether this copy of the tooling is intact (a component replaced on its own reads as merely "old"
to every other check, and as `drifted` here, with the file named), and turns recent failures into
counts plus a concrete action per class rather than a list of error codes.

**Redirect stderr** — the readable report goes to stderr so `query` can keep stdout clean JSON, so
`diagnose > out.txt` writes an empty file. Use `2>&1`, or `--json` for stdout.

**Scope.** The install check applies however this tooling is used. The activity summary covers
every call this machine makes through the tooling, tagged with the `surface` it came from so a
report can tell an MCP client apart from a CLI agent:

| how you are calling NexusCRM | recorded? |
|---|---|
| bundled MCP server registered from `mcp/mcp.json` | yes — `surface: mcp` |
| `nexus_connect.mjs query` / `auth` / `whoami` in a terminal | yes — `surface: cli` (since v1.9.0) |
| the shipped Claude plugin | no — it ships skills only, declares no MCP server, and contains no installer. Register the local server by downloading the separate CLI artifact (`nexuscrm-connect-cli.tgz`) and running the `install.sh` inside it; that is the path that records |

Entries written before v1.9.0 carry no surface and are reported as `unknown`; they all came from
the MCP server, but the report does not rewrite its own history. On the plugin-only path there is
nothing to record, so `diagnose` is the install check plus an empty activity section — expected,
not a fault. `diagnose` and `selftest` never record: reading the log must not append to it.

**What a record holds.** A failed call: tool name, error code, duration, and the query with quoted
text, numbers and dates replaced by `?` — bare identifier-shaped tokens (field and object names)
survive by design. A successful call: tool name, row count and duration; the query text is not
kept, but the call is.

Recording is local only: `~/.nexuscrm-connect/diagnostics.jsonl`, mode 0600, and there is no upload
path. The report is written to be read in full before you decide to share it.
`NEXUS_NO_DIAGNOSTICS=1` turns recording off; deleting the file wipes it.

## Step 2 — Pull data

```
node scripts/nexus_connect.mjs query "SELECT Id, Name, Industry FROM Account ORDER BY Name LIMIT 50"
```

Prints clean JSON on stdout: `{ "status": 200, "totalSize": N, "records": [...] }`.
It paginates automatically, runs `auth` for you if there's no cached token, and
re-authenticates once if the token has expired (HTTP 401). Read the `records` and
present them however the user wants.

Other commands: `whoami` (identity + token status), `selftest` (offline sanity
check — Node version, PKCE math, reachability; no browser).

## Translating a request into SOQL

NexusCRM speaks SOQL (Salesforce-style). Map plain English to a query, keep
`LIMIT` sane, and `ORDER BY` something sensible.

| The user asks for | Query sketch |
| --- | --- |
| My accounts | `SELECT Id, Name, Industry, Type FROM Account ORDER BY Name LIMIT 200` |
| Open opportunities by stage | `SELECT StageName, ARR_Amount_USD__c FROM Opportunity WHERE StageName NOT IN ('Launched','Closed Lost','Closed Won')` then group in code — NOT `IsClosed = false`, which counts realized `Launched` deals as open (`Amount` is DEPRECATED — not a deal-size field) |
| Top deals | `SELECT Name, ARR_Amount_USD__c, StageName, Launch_Date__c, Account.Name FROM Opportunity ORDER BY ARR_Amount_USD__c DESC LIMIT 20` (deal size = Net New ARR; Amount deprecated, ACV hidden) |
| This quarter's pipeline | add `WHERE CloseDate = THIS_QUARTER` (also `THIS_MONTH`, `LAST_N_DAYS:30`) |
| My contacts / leads | `SELECT Id, Name, Email, Title FROM Contact LIMIT 200` · `... FROM Lead WHERE IsConverted = false` |
| "mine" / "my team's" | the token already scopes to what the user can see — usually no `OwnerId` filter is needed |

If a query 400s on an unknown field, fall back to a minimal field set
(`Id, Name`) and widen from there, or use `selftest`/`whoami` to confirm you're
connected before assuming a data problem.

## Data caveats (real, observed in PingCAP's CRM data)

These will skew reports if ignored — bake the workarounds into your analysis:

- **`IsWon`/`IsClosed` disagree with the stage.** Derive win/loss and open/closed
  from `StageName` (`Closed Won` / `Closed Lost` / `Launched`), never from
  `IsWon`/`IsClosed`. Measured on prod 2026-08-04: only 13 rows in the whole
  object have `IsWon` true and none is one of the 872 `Closed Won`, so win rates
  come out near 0%; `IsClosed` reports not-closed for 197 of 214 `Launched`, so a
  closed-deal filter silently drops nearly all realized ARR. These are populated
  now, not blank — the wrong answer arrives looking perfectly healthy.
- **Duplicate Opportunity rows exist.** When summing amounts or counting deals,
  dedupe by `Id` (or note the caveat) so totals aren't inflated.
- **Stage picklist is inconsistent** (e.g. `Negotiation` vs `Negotiating`). Group
  case-insensitively / merge obvious variants when bucketing by stage.

## Shaping the output

- Default to a clean **Markdown table** of the key columns.
- "as a chart" / "dashboard" → build a small self-contained HTML or PNG chart from
  the records (group/aggregate in code first).
- "export" / "Excel" / "spreadsheet" / "CSV" → write a `.csv` (or `.xlsx`) and tell
  the user the file path.
- Lead with the headline number the user asked for, then the supporting table.

## Environments

Defaults target **production** (`https://nexuscrm-new-prod.pingcap.com`, the
official "Public BI" app). To point at staging, pass **flags** — these work the same
in Windows PowerShell, cmd, macOS zsh, and Linux bash (inline `VAR=x node …` env
syntax does **not** work in PowerShell, which is why flags exist):

```
node scripts/nexus_connect.mjs auth --base https://nexuscrm-new-staging.pingcap.com --client <staging_app_key>
```

Flags: `--base <url>`, `--client <client_id>`, `--port <n>`, `--scope <scope>`
(env vars `NEXUS_BASE_URL` etc. also work). `--port` must stay matched to a redirect
URI registered on the app — default `1717`; don't change it unless the admin
registered another port.

## Security & honesty

- **Read-only.** The scope is `external.records.read`; never request or imply write
  access. The client_id is a public Consumer Key, not a secret — fine to keep in
  the script.
- **Acts as the user.** Never log in as someone else, never type a password into the
  script, and never read auth tokens out of the browser's localStorage to fake a
  session. The script obtains the user's own token through a real sign-in.
- **If sign-in fails, surface the blocker** (the script's error messages already
  do this) and ask the user to sign in — don't paper over it.
