fullstackgtm 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,52 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and the project adheres to [Semantic Versioning](https://semver.org/).
6
6
  The path to 1.0 is planned in [docs/roadmap-to-1.0.md](./docs/roadmap-to-1.0.md).
7
7
 
8
+ ## [0.10.1] — 2026-06-11
9
+
10
+ Fixes from a full fresh-user journey audit (install → demo → MCP → real CRM),
11
+ focused on the first-contact experience.
12
+
13
+ ### Added
14
+
15
+ - **`fullstackgtm --version`** (also `-v` / `version`) — previously exited 1
16
+ with a usage dump.
17
+ - **README "Connect your CRM" section**: HubSpot private-app walkthrough with
18
+ the exact read scopes the audit needs (`crm.objects.{owners,companies,contacts,deals}.read`)
19
+ and the write scopes `apply` needs; Salesforce Connected App prerequisites
20
+ (admin-created, device flow enabled, `api` + `refresh_token` scopes,
21
+ propagation delay); Stripe restricted-key guidance. Previously the word
22
+ "scope" appeared nowhere in the docs.
23
+ - Roadmap: documented the known real-portal gaps to close before 1.0
24
+ (pipeline-aware closed-deal detection, 429/retry, fetchChanges 10k
25
+ truncation, MCP plan hand-off, scope-complete login validation).
26
+
27
+ ### Fixed
28
+
29
+ - **`FSGTM_NO_BROWSER=1` suppresses OS browser opens** in all login flows
30
+ (broker pairing, Salesforce device flow, HubSpot OAuth) — for agent
31
+ sandboxes, CI, and headless use; verification URLs are always printed.
32
+ The broker test suite sets it, so running `npm test` no longer pops a
33
+ real browser tab at a mock pairing URL on every run.
34
+ - **MCP server now works from inside existing projects.** When launched via
35
+ `npx -p ... fullstackgtm-mcp` from a directory whose node_modules already
36
+ contains the peers (but not fullstackgtm), npx skips installing them into
37
+ its cache and module resolution failed; the server now falls back to
38
+ resolving the peers from the working directory — peer-dependency semantics.
39
+ Previously this made the README's `claude mcp add` setup produce a dead
40
+ server in most agent-tooling repos.
41
+ - **README auth examples matched the CLI again**: `login salesforce --token ...`
42
+ and `login hubspot --oauth ... --client-secret ...` showed argv-secret forms
43
+ the CLI deliberately rejects; both now use stdin.
44
+ - The no-credentials Stripe error suggested `login stripe --token sk_...`,
45
+ which the CLI itself refuses — it now shows the stdin form and mentions
46
+ restricted keys.
47
+ - Unreachable hosts no longer surface as a bare `fetch failed`: HubSpot and
48
+ Salesforce connection errors name the target URL and (for Salesforce) point
49
+ at SALESFORCE_INSTANCE_URL.
50
+ - Credential errors now point at `fullstackgtm doctor` and the README scope
51
+ guide; the MCP audit tool description now mentions the `demo` source;
52
+ README rule count corrected (11 built-ins, not five).
53
+
8
54
  ## [0.10.0] — 2026-06-10
9
55
 
10
56
  **Versioning reset to reflect beta status.** The 1.x numbering below
@@ -45,7 +45,15 @@ Credential resolution ladder, first match wins:
45
45
  4. Broker pairing: `fullstackgtm login --via <hosted url>` (a human approves the pairing code)
46
46
 
47
47
  In an agent sandbox, prefer rung 1 or 2. Never echo tokens into argv —
48
- `login` reads secrets from stdin only.
48
+ `login` reads secrets from stdin only. Set `FSGTM_NO_BROWSER=1` in headless
49
+ environments — login flows then print verification URLs instead of opening
50
+ the OS browser.
51
+
52
+ Provider prerequisites (what the human must create, and which scopes) are in
53
+ the README's **"Connect your CRM"** section: HubSpot needs a private app with
54
+ four `crm.objects.*.read` scopes (plus write scopes only for `apply`);
55
+ Salesforce needs an admin-created Connected App with device flow enabled;
56
+ Stripe works with a restricted key (Customers + Subscriptions read).
49
57
 
50
58
  ```bash
51
59
  HUBSPOT_ACCESS_TOKEN=$TOKEN fullstackgtm audit --provider hubspot --json --out plan.json
@@ -71,9 +79,13 @@ The MCP entrypoint needs optional peers that plain `npx fullstackgtm-mcp`
71
79
  does not install:
72
80
 
73
81
  ```bash
74
- npx -p fullstackgtm -p @modelcontextprotocol/sdk -p zod fullstackgtm-mcp
82
+ npx -y -p fullstackgtm -p @modelcontextprotocol/sdk -p zod fullstackgtm-mcp
75
83
  ```
76
84
 
85
+ If the working directory's project already has the peers in its node_modules,
86
+ the server resolves them from there (peer-dependency semantics) — so this
87
+ works from inside existing projects too.
88
+
77
89
  Tools exposed over stdio: `fullstackgtm_audit` (read-only),
78
90
  `fullstackgtm_rules`, `fullstackgtm_apply` (requires `approvedOperationIds`).
79
91
 
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # fullstackgtm
2
2
 
3
+ [![CI](https://github.com/fullstackgtm/core/actions/workflows/ci.yml/badge.svg)](https://github.com/fullstackgtm/core/actions/workflows/ci.yml) [![npm](https://img.shields.io/npm/v/fullstackgtm)](https://www.npmjs.com/package/fullstackgtm) [![license](https://img.shields.io/badge/license-Apache--2.0-blue)](./LICENSE)
4
+
3
5
  **Plan/apply for your GTM stack.** An open-source framework for managing disparate go-to-market data spread across third-party systems: a canonical CRM/GTM data model, deterministic hygiene audits, reviewable dry-run patch plans, and approval-gated write-back to providers.
4
6
 
5
7
  Think `terraform plan` for your CRM: agents and scripts may *read* everything, but every proposed change becomes a typed patch operation — object, field, before, after, reason, risk — that a human approves before any provider write happens.
@@ -93,28 +95,61 @@ fullstackgtm login hubspot
93
95
 
94
96
  # HubSpot, bring-your-own-app OAuth. The browser is used exactly once — the
95
97
  # consent grant — captured on a 127.0.0.1 loopback (RFC 8252); the CLI
96
- # exchanges the code itself and refreshes silently from then on.
97
- fullstackgtm login hubspot --oauth --client-id <id> --client-secret <secret>
98
+ # exchanges the code itself and refreshes silently from then on. The client
99
+ # secret is read from stdin or an interactive prompt — never as a flag.
100
+ echo "$CLIENT_SECRET" | fullstackgtm login hubspot --oauth --client-id <id>
98
101
  # (register http://localhost:8763/callback as a redirect URL on your app)
99
102
 
100
103
  # Salesforce: native device flow — confirm a code on any device, no localhost
101
104
  # server, no client secret, silent refresh. Needs a Connected App consumer key
102
- # with device flow enabled.
105
+ # with device flow enabled (see "Connect your CRM" below).
103
106
  fullstackgtm login salesforce --device --client-id <consumer key>
104
- # ...or a session token directly:
105
- fullstackgtm login salesforce --token <t> --instance-url https://yourorg.my.salesforce.com
107
+ # ...or a session token directly (token on stdin, never as a flag):
108
+ echo "$SF_SESSION_TOKEN" | fullstackgtm login salesforce --instance-url https://yourorg.my.salesforce.com
106
109
 
107
110
  fullstackgtm logout hubspot # or: salesforce | broker
108
111
  ```
109
112
 
110
113
  A direct `login hubspot` always wins over a broker pairing, so an operator can override the team default. HubSpot does not support the device-authorization grant or secretless public clients, which is why the bring-your-own-app OAuth path requires client credentials; they are stored locally for silent refresh, the same model as `gcloud` and `aws` CLI profiles.
111
114
 
115
+ ## Connect your CRM
116
+
117
+ What each provider actually requires before `audit --provider <name>` works on your data.
118
+
119
+ ### HubSpot: create a private app (~2 minutes, needs super-admin)
120
+
121
+ 1. In HubSpot: **Settings → Integrations → Private Apps → Create a private app.**
122
+ 2. On the **Scopes** tab, grant the read scopes the audit needs:
123
+ - `crm.objects.owners.read`
124
+ - `crm.objects.companies.read`
125
+ - `crm.objects.contacts.read`
126
+ - `crm.objects.deals.read`
127
+ 3. If you plan to **apply** approved operations (not just audit), also grant write scopes for the objects you'll let it touch: `crm.objects.deals.write` (covers `deal.next_step` and other deal fields), plus `crm.objects.contacts.write` / `crm.objects.companies.write` for contact/company patches, and the **Tasks** write scope for `create_task` operations (search "tasks" in the scope picker; naming varies by portal).
128
+ 4. Create the app, copy the token (`pat-...`), then: `echo "$TOKEN" | fullstackgtm login hubspot`.
129
+
130
+ If a scope is missing you'll see a `403` mid-run whose body names the exact missing scope (`requiredGranularScopes`) — add it to the private app and re-run. Note that `login` only validates the token itself; it can't tell whether every scope you'll need is granted.
131
+
132
+ ### Salesforce: a Connected App (one-time, usually needs an admin)
133
+
134
+ Device-flow login requires a Connected App in your org — if you're not an admin, this is the step to ask one for:
135
+
136
+ 1. **Setup → App Manager → New Connected App**, enable OAuth settings.
137
+ 2. Check **Enable Device Flow**.
138
+ 3. OAuth scopes: **Manage user data via APIs (`api`)** and **Perform requests at any time (`refresh_token`)** — the CLI requests exactly these.
139
+ 4. Save (Salesforce can take ~2–10 minutes to propagate a new Connected App), copy the **Consumer Key**, then: `fullstackgtm login salesforce --device --client-id <consumer key>`.
140
+
141
+ Writeback needs no extra OAuth scope — applies are gated by the logged-in user's normal object/field permissions.
142
+
143
+ ### Stripe: a restricted key is enough (read-only connector)
144
+
145
+ The Stripe connector only reads customers and subscriptions, and `apply` is read-only by construction. Create a **restricted key** with just **Customers: Read** and **Subscriptions: Read** (Developers → API keys → Create restricted key) instead of pasting a full-access secret key: `echo "$KEY" | fullstackgtm login stripe`.
146
+
112
147
  ## Concepts
113
148
 
114
149
  | Concept | What it is |
115
150
  |---|---|
116
151
  | **Canonical snapshot** | Provider-independent view of users, accounts, contacts, deals, activities. Records carry `identities` — `(provider, externalId)` claims — so the same real-world entity can be tracked across several systems. |
117
- | **Audit rule** | A deterministic function `(context) => { findings, operations }`. Five built-ins cover orphan accounts, ownerless deals, unlinked deals, past close dates, and stale pipeline. Write your own in ~10 lines. |
152
+ | **Audit rule** | A deterministic function `(context) => { findings, operations }`. Eleven built-ins cover orphan accounts, ownerless/unlinked/amount-less deals, past close dates, stale pipeline, duplicates, and more — `fullstackgtm rules` lists them all. Write your own in ~10 lines. |
118
153
  | **Patch plan** | The dry-run output of an audit: findings plus typed patch operations with before/after values, reasons, risk levels, and approval flags. Always a proposal, never a mutation. |
119
154
  | **Connector** | A provider adapter: `fetchSnapshot()` for reads, optional `applyOperation()` for writes. HubSpot and Salesforce reference connectors ship in the package; connectors never drop records they can't fully resolve — the audit flags them instead. |
120
155
  | **Patch plan run** | The audit record of one apply attempt: per-operation applied/failed/skipped results. |
package/dist/cli.js CHANGED
@@ -132,7 +132,7 @@ async function hubspotConnection(args) {
132
132
  const stored = await resolveHubspotConnection();
133
133
  if (stored)
134
134
  return stored;
135
- throw new Error("No HubSpot credentials. Run `fullstackgtm login hubspot`, pair with a hosted deployment via `fullstackgtm login --via <url>`, or set HUBSPOT_ACCESS_TOKEN.");
135
+ throw new Error("No HubSpot credentials. Run `fullstackgtm login hubspot`, pair with a hosted deployment via `fullstackgtm login --via <url>`, or set HUBSPOT_ACCESS_TOKEN. (`fullstackgtm doctor` shows credential status; see the README's \"Connect your CRM\" section for the private-app scopes to grant.)");
136
136
  }
137
137
  async function salesforceConnection() {
138
138
  if (process.env.SALESFORCE_ACCESS_TOKEN && process.env.SALESFORCE_INSTANCE_URL) {
@@ -144,7 +144,7 @@ async function salesforceConnection() {
144
144
  const stored = await resolveSalesforceConnection();
145
145
  if (stored)
146
146
  return stored;
147
- throw new Error("No Salesforce credentials. Run `fullstackgtm login salesforce`, pair with a hosted deployment via `fullstackgtm login --via <url>`, or set SALESFORCE_ACCESS_TOKEN and SALESFORCE_INSTANCE_URL.");
147
+ throw new Error("No Salesforce credentials. Run `fullstackgtm login salesforce`, pair with a hosted deployment via `fullstackgtm login --via <url>`, or set SALESFORCE_ACCESS_TOKEN and SALESFORCE_INSTANCE_URL. (`fullstackgtm doctor` shows credential status; device-flow login needs an admin-created Connected App — see the README's \"Connect your CRM\" section.)");
148
148
  }
149
149
  async function connectorFor(provider, args) {
150
150
  if (provider === "hubspot") {
@@ -164,7 +164,7 @@ async function connectorFor(provider, args) {
164
164
  if (provider === "stripe") {
165
165
  const key = process.env.STRIPE_SECRET_KEY ?? getCredential("stripe")?.accessToken;
166
166
  if (!key) {
167
- throw new Error("No Stripe credentials. Run `fullstackgtm login stripe --token sk_...` or set STRIPE_SECRET_KEY.");
167
+ throw new Error("No Stripe credentials. Run `echo \"$STRIPE_KEY\" | fullstackgtm login stripe` or set STRIPE_SECRET_KEY. A restricted key with read access to Customers and Subscriptions is enough. (`fullstackgtm doctor` shows credential status.)");
168
168
  }
169
169
  return createStripeConnector({ getApiKey: () => key });
170
170
  }
@@ -868,6 +868,10 @@ export async function runCli(argv) {
868
868
  console.log(usage());
869
869
  return;
870
870
  }
871
+ if (command === "--version" || command === "-v" || command === "version") {
872
+ console.log(readPackageInfo().version);
873
+ return;
874
+ }
871
875
  if (command === "login") {
872
876
  await login(args);
873
877
  return;
@@ -24,14 +24,21 @@ export function createHubspotConnector(options) {
24
24
  const mappings = options.fieldMappings;
25
25
  async function request(path, init = {}) {
26
26
  const token = await options.getAccessToken();
27
- const response = await fetchImpl(`${baseUrl}${path}`, {
28
- ...init,
29
- headers: {
30
- Authorization: `Bearer ${token}`,
31
- "Content-Type": "application/json",
32
- ...(init.headers ?? {}),
33
- },
34
- });
27
+ let response;
28
+ try {
29
+ response = await fetchImpl(`${baseUrl}${path}`, {
30
+ ...init,
31
+ headers: {
32
+ Authorization: `Bearer ${token}`,
33
+ "Content-Type": "application/json",
34
+ ...(init.headers ?? {}),
35
+ },
36
+ });
37
+ }
38
+ catch (error) {
39
+ const cause = error instanceof Error && error.cause instanceof Error ? `: ${error.cause.message}` : "";
40
+ throw new Error(`Cannot reach HubSpot at ${baseUrl}${cause}. Check network access.`);
41
+ }
35
42
  if (!response.ok) {
36
43
  const body = await response.text();
37
44
  throw new Error(`HubSpot API error ${response.status}: ${body}`);
@@ -154,6 +154,10 @@ export async function runHubspotLoopbackLogin(options) {
154
154
  });
155
155
  }
156
156
  export async function openInBrowser(url) {
157
+ // Headless contexts (agent sandboxes, CI, tests) suppress the OS browser;
158
+ // every flow that opens a URL also prints it for manual use.
159
+ if (process.env.FSGTM_NO_BROWSER)
160
+ return;
157
161
  // The URL may come from an external source (e.g. a broker deployment's
158
162
  // verification URL). Only ever hand a well-formed http(s) URL to the OS
159
163
  // opener — this prevents a leading `-` from being read as a flag by
@@ -28,14 +28,21 @@ export function createSalesforceConnector(options) {
28
28
  const url = path.startsWith("http")
29
29
  ? path
30
30
  : `${connection.instanceUrl.replace(/\/$/, "")}${path}`;
31
- const response = await fetchImpl(url, {
32
- ...init,
33
- headers: {
34
- Authorization: `Bearer ${connection.accessToken}`,
35
- "Content-Type": "application/json",
36
- ...(init.headers ?? {}),
37
- },
38
- });
31
+ let response;
32
+ try {
33
+ response = await fetchImpl(url, {
34
+ ...init,
35
+ headers: {
36
+ Authorization: `Bearer ${connection.accessToken}`,
37
+ "Content-Type": "application/json",
38
+ ...(init.headers ?? {}),
39
+ },
40
+ });
41
+ }
42
+ catch (error) {
43
+ const cause = error instanceof Error && error.cause instanceof Error ? `: ${error.cause.message}` : "";
44
+ throw new Error(`Cannot reach Salesforce at ${connection.instanceUrl}${cause}. Check SALESFORCE_INSTANCE_URL (your My Domain URL, e.g. https://yourco.my.salesforce.com) and network access.`);
45
+ }
39
46
  if (!response.ok) {
40
47
  const body = await response.text();
41
48
  throw new Error(`Salesforce API error ${response.status}: ${body}`);
package/dist/mcp.js CHANGED
@@ -1,8 +1,39 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
1
9
  import { readFileSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
- import { z } from "zod/v4";
10
+ import { createRequire } from "node:module";
11
+ import { join, resolve } from "node:path";
12
+ import { pathToFileURL } from "node:url";
13
+ /**
14
+ * The MCP peers resolve normally when installed alongside this package, but
15
+ * `npx -p fullstackgtm -p @modelcontextprotocol/sdk -p zod` skips installing
16
+ * peers into the npx cache when the invoking project's node_modules already
17
+ * satisfies them — and the cache can't reach the project's tree. Fall back to
18
+ * resolving from the working directory: peer dependencies' natural home.
19
+ */
20
+ async function importPeer(specifier) {
21
+ try {
22
+ return (await import(__rewriteRelativeImportExtension(specifier)));
23
+ }
24
+ catch (error) {
25
+ try {
26
+ const projectRequire = createRequire(join(process.cwd(), "package.json"));
27
+ return (await import(__rewriteRelativeImportExtension(pathToFileURL(projectRequire.resolve(specifier)).href)));
28
+ }
29
+ catch {
30
+ throw error; // the original error carries the missing-peer signal mcp-bin reports on
31
+ }
32
+ }
33
+ }
34
+ const { McpServer } = await importPeer("@modelcontextprotocol/sdk/server/mcp.js");
35
+ const { StdioServerTransport } = await importPeer("@modelcontextprotocol/sdk/server/stdio.js");
36
+ const { z } = await importPeer("zod/v4");
6
37
  import { auditSnapshot, defaultPolicy } from "./audit.js";
7
38
  import { loadConfig, mergePolicy, resolveConfiguredRules } from "./config.js";
8
39
  import { applyPatchPlan } from "./connector.js";
@@ -84,7 +115,8 @@ export async function startMcpServer() {
84
115
  server.registerTool("fullstackgtm_audit", {
85
116
  title: "GTM Ops Audit",
86
117
  description: "Run a dry-run GTM hygiene audit and return a reviewable patch plan. " +
87
- "Reads from the sample dataset, a snapshot file, or a live provider.",
118
+ "Sources: the realistic zero-credential demo CRM (provider: \"demo\" richest test data), " +
119
+ "the minimal sample dataset, a snapshot file, or a live provider.",
88
120
  inputSchema: {
89
121
  provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
90
122
  inputPath: z.string().optional(),
@@ -106,11 +106,39 @@ The original thesis: GTM data disagrees across systems.
106
106
  - Docs site with the operating-model registry as browsable reference.
107
107
  - Performance pass: streaming snapshots for very large orgs.
108
108
 
109
- ## 1.0.0 The contract (reached)
109
+ ## Known real-portal gaps to close before 1.0
110
+
111
+ Found by exercising the published package as a fresh RevOps user with a real
112
+ CRM (2026-06):
113
+
114
+ - **Pipeline-aware closed-deal detection (HubSpot).** `isClosed`/`isWon` are
115
+ derived by substring-matching the raw `dealstage` value against
116
+ `closedwon`/`closedlost`. Custom pipelines use opaque stage ids, so closed
117
+ deals read as open and flood stale-deal/past-close findings. Fix: resolve
118
+ stage metadata from `/crm/v3/pipelines` once per snapshot.
119
+ - **Rate-limit resilience.** No 429/retry/backoff handling anywhere; a
120
+ mid-size portal snapshot is ~1,000+ sequential page requests and one
121
+ transient failure aborts the run. Fix: honor `Retry-After`, retry
122
+ idempotent reads with backoff.
123
+ - **`fetchChanges` 10k truncation.** The HubSpot search API caps at 10,000
124
+ results; the connector stops silently at MAX_PAGES. Fix: detect the cap
125
+ and fall back to (or instruct) a full snapshot, loudly.
126
+ - **MCP plan hand-off.** `fullstackgtm_audit` can only return the full plan
127
+ inline (200KB+ on real data) while `fullstackgtm_apply` requires a file
128
+ path. Fix: an `outPath` option on the audit tool plus a summary-only
129
+ output mode.
130
+ - **Scope-complete login validation.** `login hubspot` validates against the
131
+ owners endpoint only, so an under-scoped token passes login and fails
132
+ mid-audit. Fix: probe each required object endpoint at login and report
133
+ missing scopes up front.
134
+
135
+ ## 1.0.0 — The contract (not yet declared)
110
136
 
111
137
  Semver stability commitment on the canonical model, rule interface, connector
112
- contract, plan format, CLI, and MCP tools. From here, new providers and new
113
- rules are minor releases; the model only breaks at 2.0.
138
+ contract, plan format, CLI, and MCP tools. Declared only after the API
139
+ surface has survived external usage and the real-portal gaps above are
140
+ closed. From there, new providers and new rules are minor releases; the
141
+ model only breaks at 2.0.
114
142
 
115
143
  ## Deliberately out of scope until after 1.0
116
144
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullstackgtm",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Open-source agentic GTM ops framework: canonical GTM data model, pluggable deterministic audits, reviewable dry-run patch plans, approval-gated write-back with conflict detection, and cross-system entity resolution. HubSpot, Salesforce, and Stripe connectors included.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Full Stack GTM",
package/src/cli.ts CHANGED
@@ -157,7 +157,7 @@ async function hubspotConnection(args: string[]) {
157
157
  const stored = await resolveHubspotConnection();
158
158
  if (stored) return stored;
159
159
  throw new Error(
160
- "No HubSpot credentials. Run `fullstackgtm login hubspot`, pair with a hosted deployment via `fullstackgtm login --via <url>`, or set HUBSPOT_ACCESS_TOKEN.",
160
+ "No HubSpot credentials. Run `fullstackgtm login hubspot`, pair with a hosted deployment via `fullstackgtm login --via <url>`, or set HUBSPOT_ACCESS_TOKEN. (`fullstackgtm doctor` shows credential status; see the README's \"Connect your CRM\" section for the private-app scopes to grant.)",
161
161
  );
162
162
  }
163
163
 
@@ -171,7 +171,7 @@ async function salesforceConnection() {
171
171
  const stored = await resolveSalesforceConnection();
172
172
  if (stored) return stored;
173
173
  throw new Error(
174
- "No Salesforce credentials. Run `fullstackgtm login salesforce`, pair with a hosted deployment via `fullstackgtm login --via <url>`, or set SALESFORCE_ACCESS_TOKEN and SALESFORCE_INSTANCE_URL.",
174
+ "No Salesforce credentials. Run `fullstackgtm login salesforce`, pair with a hosted deployment via `fullstackgtm login --via <url>`, or set SALESFORCE_ACCESS_TOKEN and SALESFORCE_INSTANCE_URL. (`fullstackgtm doctor` shows credential status; device-flow login needs an admin-created Connected App — see the README's \"Connect your CRM\" section.)",
175
175
  );
176
176
  }
177
177
 
@@ -198,7 +198,7 @@ async function connectorFor(provider: string, args: string[]): Promise<GtmConnec
198
198
  process.env.STRIPE_SECRET_KEY ?? getCredential("stripe")?.accessToken;
199
199
  if (!key) {
200
200
  throw new Error(
201
- "No Stripe credentials. Run `fullstackgtm login stripe --token sk_...` or set STRIPE_SECRET_KEY.",
201
+ "No Stripe credentials. Run `echo \"$STRIPE_KEY\" | fullstackgtm login stripe` or set STRIPE_SECRET_KEY. A restricted key with read access to Customers and Subscriptions is enough. (`fullstackgtm doctor` shows credential status.)",
202
202
  );
203
203
  }
204
204
  return createStripeConnector({ getApiKey: () => key });
@@ -993,6 +993,10 @@ export async function runCli(argv: string[]) {
993
993
  console.log(usage());
994
994
  return;
995
995
  }
996
+ if (command === "--version" || command === "-v" || command === "version") {
997
+ console.log(readPackageInfo().version);
998
+ return;
999
+ }
996
1000
 
997
1001
  if (command === "login") {
998
1002
  await login(args);
@@ -57,14 +57,20 @@ export function createHubspotConnector(options: HubspotConnectorOptions): Requir
57
57
 
58
58
  async function request(path: string, init: RequestInit = {}): Promise<any> {
59
59
  const token = await options.getAccessToken();
60
- const response = await fetchImpl(`${baseUrl}${path}`, {
61
- ...init,
62
- headers: {
63
- Authorization: `Bearer ${token}`,
64
- "Content-Type": "application/json",
65
- ...(init.headers ?? {}),
66
- },
67
- });
60
+ let response: Response;
61
+ try {
62
+ response = await fetchImpl(`${baseUrl}${path}`, {
63
+ ...init,
64
+ headers: {
65
+ Authorization: `Bearer ${token}`,
66
+ "Content-Type": "application/json",
67
+ ...(init.headers ?? {}),
68
+ },
69
+ });
70
+ } catch (error) {
71
+ const cause = error instanceof Error && error.cause instanceof Error ? `: ${error.cause.message}` : "";
72
+ throw new Error(`Cannot reach HubSpot at ${baseUrl}${cause}. Check network access.`);
73
+ }
68
74
  if (!response.ok) {
69
75
  const body = await response.text();
70
76
  throw new Error(`HubSpot API error ${response.status}: ${body}`);
@@ -212,6 +212,9 @@ export async function runHubspotLoopbackLogin(
212
212
  }
213
213
 
214
214
  export async function openInBrowser(url: string) {
215
+ // Headless contexts (agent sandboxes, CI, tests) suppress the OS browser;
216
+ // every flow that opens a URL also prints it for manual use.
217
+ if (process.env.FSGTM_NO_BROWSER) return;
215
218
  // The URL may come from an external source (e.g. a broker deployment's
216
219
  // verification URL). Only ever hand a well-formed http(s) URL to the OS
217
220
  // opener — this prevents a leading `-` from being read as a flag by
@@ -69,14 +69,22 @@ export function createSalesforceConnector(
69
69
  const url = path.startsWith("http")
70
70
  ? path
71
71
  : `${connection.instanceUrl.replace(/\/$/, "")}${path}`;
72
- const response = await fetchImpl(url, {
73
- ...init,
74
- headers: {
75
- Authorization: `Bearer ${connection.accessToken}`,
76
- "Content-Type": "application/json",
77
- ...(init.headers ?? {}),
78
- },
79
- });
72
+ let response: Response;
73
+ try {
74
+ response = await fetchImpl(url, {
75
+ ...init,
76
+ headers: {
77
+ Authorization: `Bearer ${connection.accessToken}`,
78
+ "Content-Type": "application/json",
79
+ ...(init.headers ?? {}),
80
+ },
81
+ });
82
+ } catch (error) {
83
+ const cause = error instanceof Error && error.cause instanceof Error ? `: ${error.cause.message}` : "";
84
+ throw new Error(
85
+ `Cannot reach Salesforce at ${connection.instanceUrl}${cause}. Check SALESFORCE_INSTANCE_URL (your My Domain URL, e.g. https://yourco.my.salesforce.com) and network access.`,
86
+ );
87
+ }
80
88
  if (!response.ok) {
81
89
  const body = await response.text();
82
90
  throw new Error(`Salesforce API error ${response.status}: ${body}`);
package/src/mcp.ts CHANGED
@@ -1,8 +1,35 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
- import { z } from "zod/v4";
2
+ import { createRequire } from "node:module";
3
+ import { join, resolve } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+
6
+ /**
7
+ * The MCP peers resolve normally when installed alongside this package, but
8
+ * `npx -p fullstackgtm -p @modelcontextprotocol/sdk -p zod` skips installing
9
+ * peers into the npx cache when the invoking project's node_modules already
10
+ * satisfies them — and the cache can't reach the project's tree. Fall back to
11
+ * resolving from the working directory: peer dependencies' natural home.
12
+ */
13
+ async function importPeer<T>(specifier: string): Promise<T> {
14
+ try {
15
+ return (await import(specifier)) as T;
16
+ } catch (error) {
17
+ try {
18
+ const projectRequire = createRequire(join(process.cwd(), "package.json"));
19
+ return (await import(pathToFileURL(projectRequire.resolve(specifier)).href)) as T;
20
+ } catch {
21
+ throw error; // the original error carries the missing-peer signal mcp-bin reports on
22
+ }
23
+ }
24
+ }
25
+
26
+ const { McpServer } = await importPeer<typeof import("@modelcontextprotocol/sdk/server/mcp.js")>(
27
+ "@modelcontextprotocol/sdk/server/mcp.js",
28
+ );
29
+ const { StdioServerTransport } = await importPeer<typeof import("@modelcontextprotocol/sdk/server/stdio.js")>(
30
+ "@modelcontextprotocol/sdk/server/stdio.js",
31
+ );
32
+ const { z } = await importPeer<typeof import("zod/v4")>("zod/v4");
6
33
  import { auditSnapshot, defaultPolicy } from "./audit.ts";
7
34
  import { loadConfig, mergePolicy, resolveConfiguredRules } from "./config.ts";
8
35
  import { applyPatchPlan } from "./connector.ts";
@@ -113,7 +140,8 @@ export async function startMcpServer() {
113
140
  title: "GTM Ops Audit",
114
141
  description:
115
142
  "Run a dry-run GTM hygiene audit and return a reviewable patch plan. " +
116
- "Reads from the sample dataset, a snapshot file, or a live provider.",
143
+ "Sources: the realistic zero-credential demo CRM (provider: \"demo\" richest test data), " +
144
+ "the minimal sample dataset, a snapshot file, or a live provider.",
117
145
  inputSchema: {
118
146
  provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
119
147
  inputPath: z.string().optional(),