npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

fullstackgtm

v0.30.0

Published

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 connect

Readme

fullstackgtm

CI npm license

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.

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.

Licensed under Apache-2.0. The boundary is deliberate and stable: the framework, CLI, and MCP server are open source; the hosted Full Stack GTM application (dashboard, sync backend, broker service, team workflows) is a separate, proprietary product built on top of this package. Features never move from open to closed. See CONTRIBUTING.md for how development and mirroring work.

Status: beta (0.x). The surfaces in docs/api.md — the canonical model, rule interface, plan/apply contract, connector contract, merge/diff, config, CLI, and MCP tools — are settling but may still break in minor releases until 1.0; the path there is docs/roadmap-to-1.0.md. The safety invariants (read-only audits, approval-gated writes, placeholder refusal) are not beta and do not change. Connectors: HubSpot (read/write), Salesforce (read/write), Stripe (read-only billing).

Install

npm install fullstackgtm                    # library + CLI in a project
npx fullstackgtm audit --demo               # or zero-install via npx
npm install github:fullstackgtm/core        # or straight from this repo (project-local)
npx github:fullstackgtm/core audit --demo   # zero-install from the repo

(Global npm install -g from a git URL is unreliable on npm 11 — it symlinks into npm's temp cache. Use the registry for global installs, or the project-local/npx forms above.)

Requires Node 20+. The core has zero runtime dependencies; only the MCP server entrypoint uses the optional peers @modelcontextprotocol/sdk and zod.

npx fullstackgtm doctor   # verify the install: node version, credentials, MCP peers, next step

Installing for an AI agent? The fastest path is the agent skill:

npx skills add fullstackgtm/core   # Claude Code, Cursor, Codex, and other skills-compatible agents

It installs a compact operating guide (skills/fullstackgtm/SKILL.md) — the governed loop, the safety invariants, and the verb map — so the agent reaches for plans instead of raw writes. For a deterministic install-and-verify script with expected outputs, hand it INSTALL_FOR_AGENTS.md. A documentation map lives in llms.txt.

Five-minute loop

# 0. No credentials? Try it on a realistic, deliberately messy demo CRM
npx fullstackgtm audit --demo

# 1. Audit your real HubSpot portal (private app token or OAuth access token)
HUBSPOT_ACCESS_TOKEN=pat-... npx fullstackgtm audit --provider hubspot --out plan.json

# 2. Review plan.json, then apply ONLY the operations you approve
HUBSPOT_ACCESS_TOKEN=pat-... npx fullstackgtm apply \
  --plan plan.json --provider hubspot \
  --approve op_abc123,op_def456 \
  --value op_def456=2026-09-30

Nothing is ever written without an explicit --approve. Operations whose value is a human decision (requires_human_* placeholders, e.g. which owner to assign) are refused unless you supply a concrete --value override.

Call workflows: calls become governed evidence

Calls are where pipeline truth lives. call parse normalizes any transcript dialect — Speaker: text lines (Fathom, Gong exports), [Speaker]: labels, or raw Granola utterance JSON — into canonical segments, insights, and GtmEvidence records.

Extraction is LLM-powered by default, with your own key. The first time you run call parse or call score, the CLI asks for an Anthropic or OpenAI API key (auto-detected from the prefix), validates it against the provider, and stores it in the 0600 credential store — same treatment as CRM logins. Or skip the prompt: set ANTHROPIC_API_KEY/OPENAI_API_KEY, or echo "$KEY" | fullstackgtm login anthropic (or openai). The key talks directly to your provider — raw fetch, no SDK, no middleman. --model overrides the defaults (claude-haiku-4-5 / gpt-4o-mini). LLM insights carry verbatim-quote evidence and, for next steps, owner/deadline/commitment. Pass --deterministic for the free, instant, byte-stable keyword baseline (no key needed — right for CI and warehouse bulk loads). Every insight is provenance-marked (extractor: "llm:anthropic:…" vs "deterministic").

call score rates the call against a coaching rubric — five built-in dimensions (discovery, next steps, stakeholders, value, objections) or your own via --rubric rubric.json ({ scale, dimensions: [{ name, weight, rubric }] }); the weighted overall is computed deterministically client-side, every dimension score is evidence-quoted, and the rubric file is where your client-specific coaching framework lives.

call link answers "which deal was this call about" from attendee domains (account domain or contact emails → open deals, most recent activity first, with confidence + reason). call plan turns next-step insights into the same governed plan lifecycle as everything else.

# Coaching pipeline (Slack + CRM): parse → score → link → govern the writeback
fullstackgtm call parse --transcript call.txt --title "Acme disco" --out parsed.json   # LLM extraction
fullstackgtm call score --call parsed.json --rubric team-rubric.json                   # evidence-quoted scorecard
fullstackgtm call link --attendees [email protected] --provider hubspot                    # → deal id + reason
fullstackgtm call plan --call parsed.json --deal 123 --provider hubspot --save
# review → plans approve → apply: deal.next_step + follow-up tasks, compare-and-set protected
# (pipe the parse/score JSON into Slack/Notion however you like)

# Analytics pipeline (warehouse): one flat NDJSON row per insight
for t in transcripts/*; do fullstackgtm call parse --transcript "$t" --ndjson --deterministic; done > insights.ndjson
# free keyword baseline for bulk loads; drop --deterministic for LLM-quality rows
# COPY into your warehouse (stable call/evidence ids make reloads idempotent)

The boundary that remains: Slack/Notion/warehouse sinks are your pipeline, composed around the JSON — and your rubrics and keys stay yours.

The create gate: no new dupes

Detection cleans up yesterday's duplicates; the resolve gate prevents tomorrow's. Before any writer — a sync job, a webhook handler, an agent, your own script — creates a record, ask the gate:

fullstackgtm resolve contact --email [email protected] --input snap.json   # exit 0 = safe to create
fullstackgtm resolve account --domain acme.com --provider hubspot      # exit 2 = exists/ambiguous: do NOT create
fullstackgtm resolve deal --name "Acme Expansion" --account-id 123 --input snap.json

Identity keys match the audit/merge engines exactly: account domain (normalized), contact email, and the open-deal key (account + normalized name). Names alone are never identity — they return ambiguous with the candidates, not a guess. Exit codes are gate-shaped for scripts: 0 safe to create, 2 match found, 1 error. For high-volume writers, pair it with a cron-refreshed snapshot file rather than a live --provider fetch per call. Also exposed as resolveRecord() and the MCP tool fullstackgtm_resolve.

Provenance attribution closes the loop on recurring dupes: snapshots now capture each record's source (HubSpot's read-only hs_object_source* fields), and duplicate findings name the writer — "3 accounts share acme.com … Created by: Gojiberry (app-123) ×2, CRM_UI" — so you fix the integration, not just the records. Records created by this CLI stamp their own provenance (hs_object_source_detail_2, best-effort).

From findings to fixes: the suggest chain

Most placeholder answers are already derivable from your own CRM data. suggest computes them deterministically — account-name matching cross-checked against contact associations — with a confidence level and a written reason per operation, so you (or an agent) approve evidence, not guesses:

fullstackgtm audit --provider hubspot --save        # → Saved plan patch_plan_abc123
fullstackgtm suggest --plan-id patch_plan_abc123 --provider hubspot --out suggestions.json
# review suggestions.json: every value carries confidence (high/low/create/none) + a reason
fullstackgtm plans approve patch_plan_abc123 --values-from suggestions.json   # high-confidence only by default
fullstackgtm apply --plan-id patch_plan_abc123 --provider hubspot

Widen the bar deliberately: --min-confidence low accepts single-signal matches and merge survivor suggestions (irreversible merges are capped at low confidence by design, so the default bar never bulk-approves one); --include-creates accepts create:<Name> values — approving one creates the missing company/account record and links to it in a single audited operation, so even record creation stays inside the typed, human-approved model. Conflicting or ambiguous evidence always yields no suggestion with an explanation, never a guess.

# 3. Hand the findings to whoever owns the CRM: a client-ready report
npx fullstackgtm report --provider hubspot --client "Acme" --out acme-health.html

report renders the same audit as a deliverable — severity counts up front, a prose summary, per-rule detail with example records, and next steps — as markdown or self-contained HTML (printable, emailable, no external assets).

Routine maintenance as governed verbs: bulk-update, dedupe, reassign, fix

The maintenance work RevOps actually does in bulk — backfills, book transfers, duplicate sweeps, "just fix everything that rule found" — gets first-class verbs. Each one builds a plan; nothing executes without the same approve → apply gauntlet as everything else.

fullstackgtm bulk-update deal --where "stage=closedwon" --where "amount:empty" \
  --set amount=from:account.annualrevenue --save     # per-record derived values; empty sources skipped, never guessed
fullstackgtm dedupe account --key domain --keep richest --save   # one merge_records op per duplicate group
fullstackgtm reassign --from 411 --to 902 --except-deal-stage closing --save   # ownership handoff playbook
fullstackgtm fix --rule missing-deal-owner --provider hubspot --yes  # audit one rule → suggest → approve → apply, one command

bulk-update filters the snapshot (=, !=, ~ substring, !~ not-substring, :empty/:notempty, type-aware comparisons < > <= >= where today resolves to the policy date — e.g. closeDate<today — and date/numeric fields coerce by value form, | any-of, relational pseudo-fields like account.domain or openDealStages) into a dry-run patch plan — and the full filter is re-verified per record at apply time, with mid-apply rechecks, so a record that stopped matching between audit and apply is skipped, not clobbered. For date/count hygiene (past close dates, stale deals, missing accounts, duplicates), prefer the rule-backed fix --rule <id> — the rule encodes the open-deal + date logic deterministically; use bulk-update only when no rule covers the task. Equality filters double as preconditions; --require adds explicit ones; --guard asserts cross-record conditions; --max-operations caps blast radius. --set field=from:<sourceField> derives values per record; --create-task <text> is the third change mode, emitting approval-gated create_task operations instead of field writes; --archive refuses records whose identity key (account domain, contact email) is shared with another record — that's a duplicate, and duplicates are merged with dedupe, not archived around (--force-archive-duplicates overrides that refusal explicitly).

dedupe finds duplicate groups by normalized identity key and emits one merge_records operation per group with a deterministic survivor (richest = most populated fields, ties to lowest id; oldest). Merges stay irreversible-and-therefore-low-confidence-capped on approval, exactly like merge suggestions from the audit. reassign is the ownership-handoff playbook: one plan per object type, extra scoping account-lifted to deals and contacts, and --except-deal-stage excludes both deals in that stage and every record whose account has an open deal in it. fix is the one-shot composite for a single rule: audit → save → suggest → approve suggestion-backed operations at the confidence bar → with --yes, apply and print the stage-by-stage summary; without it, stop after approval and print the apply command.

The market map: the category, observed

Your CRM records what happened in your own deals; nothing records the shape of the category you sell into. The market map does: vendors and a claim taxonomy live in a reviewable market.config.json, vendor pages are captured into a content-addressed cache, every vendor × claim cell gets a messaging-intensity reading (LOUD / QUIET / ABSENT — with UNOBSERVABLE for failed captures, never a fake absence), and deterministic front states fall out per claim: open, contested, owned, saturated.

fullstackgtm market init --category creative-intelligence   # seed vendors + claims, edit by hand
fullstackgtm market capture                                 # fetch pages → content-addressed captures
fullstackgtm market classify                                # LLM readings (BYO key), every quote verified
fullstackgtm market fronts --diff run-1                     # what changed since last run
fullstackgtm market report --format html --out map.html     # the client-ready field report
fullstackgtm market refresh                                 # all of the above, weekly, one command

The discipline matches the rest of the tool. Intensity readings are proposals — from the LLM (classify, same bring-your-own-key seam as call parse, provenance-marked) or from any agent/human (market worksheetmarket observe) — and every quoted evidence span is verified character-for-character against the stored capture it cites before an observation is accepted. Quotes that aren't on the page bounce. Everything downstream of the store is deterministic: same observations, same map.

market axes is for earning a strategic 2×2 instead of asserting one: PCA over the intensity matrix (PC1 = the category's own primary axis, PC2 = the most differentiating direction orthogonal to it), triangulation of your configured axes against the data, and an orthogonality screen that flags two axes that are secretly one. Axes are claim-scoring rubrics in the config; the report renders the primary pair as the strategic map. Captures and observations are profile-scoped (~/.fullstackgtm/market/<category>), so one client's category intel never bleeds into another's.

Two more derivations close the loop from map to action. market overlay --snapshot <crm.json> [--calls <files>] joins the observation store against your own ground truth — which claims and vendors actually come up in your deals and call transcripts (deterministic word-boundary matching, no LLM) — and emits OCCUPY / PROMOTE / URGENT / RETREAT directives, each carrying at least one observation and one CRM stat with its sample size; below the evidence thresholds the honest answer is no directive. --save turns directives into approval-gated create_task operations through the normal plan chain. market scale estimates each vendor's size from citable signals you record in the config (G2 review counts, LinkedIn headcount, revenue claims — each with source URL, verbatim quote, and caveat): every signal is converted into revenue space first, calibrated within the vendor set and stratified by ACV band, then combined as a weighted geometric mean with the uncertainty spread and calibration table disclosed. The report's strategic-map bubbles become area-proportional to estimated revenue share — captioned "citable but NOT audited" — and without signals, dots are uniform and the caption says size carries no meaning.

Governed enrichment: a diff you approve before third-party data touches your CRM

Every enrichment vendor ships fire-and-forget writeback. The enrich layer inverts that: declare once which fields come from which source under which conflict policy (enrich.config.json — sources, ordered match keys, field mappings, policy), then enrich append fills the gaps and enrich refresh keeps them current — with every write passing through the normal dry-run → approval → apply contract, and every value traceable to the source payload that produced it.

echo "$APOLLO_API_KEY" | fullstackgtm login apollo          # BYO key, stored 0600
fullstackgtm enrich append --provider hubspot               # pull → match → dry-run diff, writes NOTHING
fullstackgtm enrich append --provider hubspot --save        # persist the plan (needs_approval) + run record
fullstackgtm enrich ingest clay-export.csv --source clay    # stage a push-style source (Clay CSV / webhook JSON)
fullstackgtm enrich refresh --source apollo --save          # re-check stale stamped fields; ops only where the source changed
fullstackgtm enrich status --runs                           # last run per source, counts, staleness, interrupted-run cursor

Matching is deterministic: ordered keys, unique hit wins, zero hits falls through to the next key, and multiple hits are never guessed away — they skip (recorded with candidate ids) or flow into the existing suggestplans approve chain as requires_human_record_selection placeholders. The MVP conflict policy is never: enrich only fills blank fields, and refresh only re-touches fields its own run-store ledger proves it stamped (per-record/per-field enrichedAt, profile-scoped, never written into your portal as custom properties). The system-only and always rungs of the ladder are phase 2 and are refused explicitly, not silently accepted. Recurring execution belongs to the scheduler — enrich owns no cron logic.

Schedules: declare a cadence once, keep the governance contract under automation

Everything the CLI produces is accurate the moment it runs and silently stale afterward. The schedule layer is the horizontal fix — any read/plan-side command on a cron cadence, one component, every verb (no feature owns its own cron logic):

fullstackgtm schedule add "enrich refresh --source apollo --save" --cron "0 6 * * 1" --label weekly-apollo
fullstackgtm schedule add "audit --provider hubspot --save" --cron "0 2 * * *"   # nightly drift baseline
fullstackgtm schedule list                       # declarative entries; nothing runs yet
fullstackgtm schedule install                    # materialize enabled entries into a managed crontab block
fullstackgtm schedule run <id>                   # execute now; same run record a cron firing produces
fullstackgtm schedule status --runs 5            # last runs, exit codes, artifacts, next + missed firings
fullstackgtm schedule uninstall                  # remove the managed block, touch nothing else

Scheduling never auto-approves. Schedulable commands are read/plan-side only — audit, snapshot, enrich append|refresh, market capture|refresh, suggest, report, doctor — so unattended runs accumulate proposals (plans in the queue, run records, reports), never CRM writes. apply is schedulable only as apply --plan-id <id>, and every firing re-checks the plan's status is approved: an unapproved plan records a plan_not_approved no-op run instead of executing, and no flag relaxes this. Arbitrary shell is not schedulable — an entry's argv must resolve to a known fullstackgtm command (validated at add time and re-checked at run time), and the crontab line you audit is always fullstackgtm schedule run <id> and nothing else.

install renders enabled entries into a sentinel-delimited block (# >>> fullstackgtm <profile> >>># <<< fullstackgtm <profile> <<<) in your user crontab; re-install replaces the block wholesale and never touches lines outside it. Honest limitation: local cron has no catch-up — a laptop asleep at firing time means a missed run. schedule status surfaces missed firings by comparing expected-vs-actual run history, so the gap is at least visible. Entries are provider-agnostic; cloud providers (Modal, AWS) arrive as scaffold generators that call the same schedule run <id> contract, and are refused as "not yet implemented" until then.

Working across organizations

Consultants and fractional operators hold credentials for several CRMs at once. A profile scopes stored logins and stored plans to one organization:

fullstackgtm --profile acme login hubspot
fullstackgtm --profile acme audit --provider hubspot --save
fullstackgtm profiles            # list profiles, * marks the active one

Set FULLSTACKGTM_PROFILE=acme to pin a shell (or agent sandbox) to one client. Plans saved under a profile are invisible to every other profile, so a patch plan proposed against one client's CRM can never be applied through another client's credentials.

Built for agents (and the RevOps humans they work for)

Every command is designed to compose in an agent loop — deterministic output, machine-readable everywhere, meaningful exit codes:

# Discover what the auditor checks
fullstackgtm rules --json

# Fetch once (expensive), audit offline as many times as you like (cheap)
fullstackgtm snapshot --provider hubspot --out snap.json
fullstackgtm audit --input snap.json --json
fullstackgtm audit --input snap.json --rules stale-deal --stale-days 45 --json

# Gate a nightly CI job or agent run on hygiene: exit 2 if findings ≥ threshold
fullstackgtm audit --provider hubspot --fail-on warning

# Gate CI on hygiene drift instead: exit 2 only when a NEW (rule, record) finding appears
fullstackgtm diff --before old.json --after new.json --fail-on-new-findings
  • Finding and operation ids are stable hashes of rule + record, so two runs over the same data produce identical ids — agents can diff plans, track findings across runs, and approve operations by id without re-parsing.
  • --demo (with --seed) generates a realistic mid-market CRM with injected real-world failure modes — departed owners, unlinked deals, orphan accounts, stale pipeline — so agents and CI can exercise the full snapshot → audit → apply pipeline with zero credentials.
  • Exit codes: 0 success, 1 error, 2 findings at/above --fail-on.

"Built for agents" is measured, not asserted: a 1,088-run benchmark (17 scenarios = 14 synthetic + 3 seeded from an anonymized real portal, × 3 tool-surface arms × 4 trials, across six models from three vendors, deterministic graders over final CRM state, τ-bench-style pass^k) shows the gated CLI surface beating raw CRM-API access on completion-under-policy for every model tested — and the tool-surface effect is monotonic and vendor-independent. Full matrix and methodology: the leaderboard.

The design is deterministic apply, governed suggest: the parts that touch your CRM — the audit rules, the plan/apply contract, compare-and-set, the survivor/merge logic — are deterministic and replayable; the parts that read free text (call parse/score, market classify) are LLM-powered but bounded, with every quoted span mechanically verified against the source before it can drive a writeback. Nondeterministic suggestion, deterministic governance.

Authentication: CLI-first, browser only at the consent moment

Credential resolution is a ladder — the first rung that yields a token wins:

  1. --token-env <NAME> — explicit env var for one invocation (agent sandboxes, scripts)
  2. HUBSPOT_ACCESS_TOKEN — ambient env (CI)
  3. Stored loginfullstackgtm login hubspot, kept in ~/.fullstackgtm/credentials.json (0600; override location with FSGTM_HOME)
  4. Broker pairingfullstackgtm login --via <hosted url>: the team's deployment holds the CRM credentials; the CLI holds only a revocable pairing token

Teams: auth once, point every CLI at the stored sync credentials

fullstackgtm login --via https://gtm.yourco.com
#   Pairing code: ABCD-2345
#   Approve this CLI in your dashboard: https://gtm.yourco.com/dashboard/cli-auth?code=ABCD-2345

An admin connects HubSpot once in the hosted dashboard (the org's OAuth tokens live encrypted in the deployment). Pairing a CLI is a device-flow handshake: the CLI prints a code, an admin or manager approves it in the dashboard, and the CLI receives a long-lived broker token (stored hashed server-side, revocable per CLI). From then on, every provider command silently exchanges the broker token for a short-lived CRM access token minted from the org's stored sync credentials — and inherits the org's field mappings. No one pastes CRM tokens; revoking a laptop is one row.

Individuals: no deployment needed

# HubSpot, zero web flow: paste a private app token once (validated, then stored)
fullstackgtm login hubspot

# HubSpot, bring-your-own-app OAuth. The browser is used exactly once — the
# consent grant — captured on a 127.0.0.1 loopback (RFC 8252); the CLI
# exchanges the code itself and refreshes silently from then on. The client
# secret is read from stdin or an interactive prompt — never as a flag.
echo "$CLIENT_SECRET" | fullstackgtm login hubspot --oauth --client-id <id>
#   (register http://localhost:8763/callback as a redirect URL on your app)

# Salesforce: native device flow — confirm a code on any device, no localhost
# server, no client secret, silent refresh. Needs a Connected App consumer key
# with device flow enabled (see "Connect your CRM" below).
fullstackgtm login salesforce --device --client-id <consumer key>
# ...or a session token directly (token on stdin, never as a flag):
echo "$SF_SESSION_TOKEN" | fullstackgtm login salesforce --instance-url https://yourorg.my.salesforce.com

fullstackgtm logout hubspot   # or: salesforce | broker

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.

Connect your CRM

What each provider actually requires before audit --provider <name> works on your data.

Connector capabilities

Connectors differ in what the provider's API allows — stated up front so nothing surprises you mid-evaluation:

| Operation | HubSpot | Salesforce | Stripe | | --- | --- | --- | --- | | Read / snapshot / audit | ✅ | ✅ | ✅ (read-only) | | Field writes (set_field, clear_field, link_record) | ✅ | ✅ | — | | create_task | ✅ | ✅ | — | | archive_record | ✅ | ✅ | — | | merge_records (dedupe) | ✅ | ✅ Account/Contact (SOAP); ❌ Opportunity | — |

Salesforce merge uses the SOAP merge() call (REST has no merge resource), so dedupe works on Salesforce Accounts and Contacts — the OAuth token doubles as the SOAP session, and groups larger than three records are merged in batches (master + 2 per call). Opportunities cannot be merged (Salesforce exposes no opportunity merge in the API or the UI) — for duplicate opportunities, pick a survivor and close/archive the rest. As always, merges are irreversible and go through the same approval gate + drift guard as every other write. (Validate against a sandbox first if you're wiring it into automation — the SOAP path is exercised by unit tests but a live Salesforce org is the real proof.)

HubSpot: create a private app (~2 minutes, needs super-admin)

  1. In HubSpot: Settings → Integrations → Private Apps → Create a private app.
  2. On the Scopes tab, grant the read scopes the audit needs:
    • crm.objects.owners.read
    • crm.objects.companies.read
    • crm.objects.contacts.read
    • crm.objects.deals.read
  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).
  4. Create the app, copy the token (pat-...), then: echo "$TOKEN" | fullstackgtm login hubspot.

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.

Salesforce: a Connected App (one-time, usually needs an admin)

Device-flow login requires a Connected App in your org — if you're not an admin, this is the step to ask one for:

  1. Setup → App Manager → New Connected App, enable OAuth settings.
  2. Check Enable Device Flow.
  3. OAuth scopes: Manage user data via APIs (api) and Perform requests at any time (refresh_token) — the CLI requests exactly these.
  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>.

Writeback needs no extra OAuth scope — applies are gated by the logged-in user's normal object/field permissions.

Stripe: a restricted key is enough (read-only connector)

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.

Concepts

| Concept | What it is | |---|---| | 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. | | Audit rule | A deterministic function (context) => { findings, operations }. Twelve 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. | | 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. | | 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. | | Patch plan run | The audit record of one apply attempt: per-operation applied/failed/skipped results. |

Write a custom rule

import {
  auditSnapshot, auditFindingId, builtinAuditRules, defaultPolicy,
  type GtmAuditRule,
} from "fullstackgtm";

const missingAmount: GtmAuditRule = {
  id: "missing-deal-amount",
  title: "Deal has no amount",
  description: "Amountless deals make forecast coverage meaningless.",
  evaluate: ({ snapshot }) => ({
    findings: snapshot.deals
      .filter((deal) => !deal.amount)
      .map((deal) => ({
        id: auditFindingId("missing-deal-amount", deal.id),
        objectType: "deal", objectId: deal.id,
        ruleId: "missing-deal-amount",
        title: "Deal has no amount", severity: "warning",
        summary: `${deal.name} has no amount.`,
        recommendation: "Set an amount or close the deal out.",
      })),
    operations: [],
  }),
};

const plan = auditSnapshot(snapshot, defaultPolicy(), [...builtinAuditRules, missingAmount]);

Use a connector programmatically

import { applyPatchPlan, auditSnapshot, createHubspotConnector } from "fullstackgtm";

const hubspot = createHubspotConnector({
  getAccessToken: () => process.env.HUBSPOT_ACCESS_TOKEN!,
});

const snapshot = await hubspot.fetchSnapshot();
const plan = auditSnapshot(snapshot);

// Later, after human review:
const run = await applyPatchPlan(hubspot, plan, {
  approvedOperationIds: ["op_abc123"],
  valueOverrides: { op_abc123: "9001" },
});

Implementing a new provider means implementing one type:

import type { GtmConnector } from "fullstackgtm";

const myConnector: GtmConnector = {
  provider: "my-crm",
  fetchSnapshot: async () => ({ /* canonical snapshot */ }),
  applyOperation: async (operation) => ({ operationId: operation.id, status: "applied" }),
};

MCP server

The MCP entrypoint needs the optional peer dependencies @modelcontextprotocol/sdk and zod — plain npx fullstackgtm-mcp won't install optional peers, so pull them in explicitly:

# In a project
npm install fullstackgtm @modelcontextprotocol/sdk zod
HUBSPOT_ACCESS_TOKEN=pat-... npx fullstackgtm-mcp

# Zero-install
npx -p fullstackgtm -p @modelcontextprotocol/sdk -p zod fullstackgtm-mcp

Add it to Claude Code in one command:

claude mcp add fullstackgtm -e HUBSPOT_ACCESS_TOKEN=pat-... -- npx -y -p fullstackgtm -p @modelcontextprotocol/sdk -p zod fullstackgtm-mcp

Or configure any MCP client (Cursor, Claude Desktop, …) with:

{
  "mcpServers": {
    "fullstackgtm": {
      "command": "npx",
      "args": ["-y", "-p", "fullstackgtm", "-p", "@modelcontextprotocol/sdk", "-p", "zod", "fullstackgtm-mcp"],
      "env": { "HUBSPOT_ACCESS_TOKEN": "pat-..." }
    }
  }
}

Eight tools are exposed over stdio.

Read-only: fullstackgtm_audit (sample, demo, file, or live provider sources with optional rule scoping), fullstackgtm_rules (rule discovery), fullstackgtm_suggest (deterministic placeholder values with confidence + reasons), fullstackgtm_call_parse (transcripts → provenance-marked segments, insights, and evidence), fullstackgtm_resolve (the create gate: exists / ambiguous / safe_to_create), and fullstackgtm_market_worksheet (the classification packet for one vendor: claims, judging rules, captured page texts).

Gated: fullstackgtm_apply (requires explicit approvedOperationIds; placeholders still need value overrides) and fullstackgtm_market_observe (verifies every quoted span against the stored captures before appending — nothing is stored unless the whole set passes).

Tokens stored via fullstackgtm login are picked up automatically — the env var is only needed when no stored login exists.

Safety model

  1. Reads are safe by default; audits never mutate anything.
  2. Every proposed write is a typed patch operation with before/after values, a reason, and a risk level.
  3. applyPatchPlan enforces the contract for all connectors: only explicitly approved operation ids are written, placeholders require concrete override values, and every attempt produces a per-operation result record.

Development

npm run build   # compiles src/ to dist/ (tsc, type declarations included)

Tests live in the repository root tests/ directory and run with npm test.