fullstackgtm 0.56.1 → 0.58.0
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 +43 -0
- package/README.md +163 -492
- package/dist/cli/enrich.d.ts +2 -2
- package/dist/cli/enrich.js +39 -12
- package/dist/cli/help.js +4 -4
- package/dist/cli/icp.js +99 -6
- package/dist/cli/signals.js +19 -4
- package/dist/enrich.d.ts +1 -1
- package/dist/enrich.js +1 -1
- package/dist/enrichZoomInfo.d.ts +33 -0
- package/dist/enrichZoomInfo.js +144 -0
- package/dist/hostedArtifacts.d.ts +35 -1
- package/dist/hostedArtifacts.js +35 -10
- package/dist/icp.d.ts +3 -0
- package/dist/icp.js +7 -4
- package/dist/icpSync.d.ts +55 -0
- package/dist/icpSync.js +93 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/runReport.js +11 -0
- package/docs/api.md +1 -1
- package/docs/architecture.md +5 -0
- package/llms.txt +3 -2
- package/package.json +2 -2
- package/src/cli/enrich.ts +40 -13
- package/src/cli/help.ts +4 -4
- package/src/cli/icp.ts +80 -5
- package/src/cli/signals.ts +20 -3
- package/src/enrich.ts +2 -2
- package/src/enrichZoomInfo.ts +197 -0
- package/src/hostedArtifacts.ts +51 -22
- package/src/icp.ts +10 -4
- package/src/icpSync.ts +74 -0
- package/src/index.ts +24 -0
- package/src/runReport.ts +12 -0
package/README.md
CHANGED
|
@@ -2,15 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/fullstackgtm/core/actions/workflows/ci.yml) [](https://www.npmjs.com/package/fullstackgtm) [](./LICENSE)
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**The open-source control plane and safety toolbox for AI agents working in your CRM.**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
An agent can read HubSpot or Salesforce in seconds. The dangerous part is what happens next: duplicate creation, stale-data overwrites, unbounded batch edits, guessed owners, and writes nobody can reconstruct later.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
fullstackgtm puts one governed contract between the agent and the CRM:
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
```text
|
|
12
|
+
source data → canonical snapshot → deterministic checks → patch plan → human approval → guarded apply → receipt
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Every proposed change carries its before/after value, reason, risk, and stable operation ID. Nothing is written until specific operations are approved. At apply time, the connector re-checks identity, preconditions, and current provider state rather than trusting an old plan.
|
|
16
|
+
|
|
17
|
+
Think **`terraform plan` for your CRM**.
|
|
18
|
+
|
|
19
|
+
fullstackgtm is not another B2B database, autonomous SDR, or enrichment vendor. It is the provider-neutral control layer around those systems. HubSpot and Salesforce are the first write targets; Stripe, Apollo, Clay, ZoomInfo, Pipe0, Explorium, HeyReach, and other systems can supply evidence or source data without bypassing the same safety gate.
|
|
12
20
|
|
|
13
|
-
Open source
|
|
21
|
+
Open source under Apache-2.0. Beta (0.x): APIs may shift before 1.0; the safety invariants do not.
|
|
14
22
|
|
|
15
23
|
## Install
|
|
16
24
|
|
|
@@ -21,513 +29,229 @@ npm install github:fullstackgtm/core # or straight from this repo (projec
|
|
|
21
29
|
npx github:fullstackgtm/core audit --demo # zero-install from the repo
|
|
22
30
|
```
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Requires Node 20+. The core has zero runtime dependencies; only the MCP server entrypoint uses the optional peers `@modelcontextprotocol/sdk` and `zod`.
|
|
32
|
+
Requires Node 20+. The core has zero runtime dependencies. The MCP entrypoint uses the optional peers `@modelcontextprotocol/sdk` and `zod`.
|
|
27
33
|
|
|
28
34
|
```bash
|
|
29
|
-
npx fullstackgtm doctor
|
|
35
|
+
npx fullstackgtm doctor
|
|
30
36
|
```
|
|
31
37
|
|
|
32
|
-
Installing for an
|
|
38
|
+
Installing for an agent:
|
|
33
39
|
|
|
34
40
|
```bash
|
|
35
|
-
npx skills add fullstackgtm/core
|
|
41
|
+
npx skills add fullstackgtm/core
|
|
36
42
|
```
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
See [INSTALL_FOR_AGENTS.md](./INSTALL_FOR_AGENTS.md) for deterministic install verification and [llms.txt](./llms.txt) for the machine-readable documentation map.
|
|
39
45
|
|
|
40
|
-
##
|
|
46
|
+
## The governed CRM loop
|
|
41
47
|
|
|
42
48
|
```bash
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
# 1. Read the CRM and produce a plan. This never writes.
|
|
50
|
+
HUBSPOT_ACCESS_TOKEN=pat-... npx fullstackgtm audit \
|
|
51
|
+
--provider hubspot --out plan.json
|
|
46
52
|
|
|
47
|
-
#
|
|
48
|
-
npx fullstackgtm
|
|
53
|
+
# 2. Review findings and proposed operations.
|
|
54
|
+
npx fullstackgtm plans show <plan-id>
|
|
49
55
|
|
|
50
|
-
#
|
|
51
|
-
|
|
56
|
+
# 3. Approve only the operations you intend to authorize.
|
|
57
|
+
npx fullstackgtm plans approve <plan-id> --operations op_abc123,op_def456
|
|
52
58
|
|
|
53
|
-
#
|
|
59
|
+
# 4. Apply the approved subset. Current provider state is checked again.
|
|
54
60
|
HUBSPOT_ACCESS_TOKEN=pat-... npx fullstackgtm apply \
|
|
55
|
-
--plan plan
|
|
56
|
-
--approve op_abc123,op_def456 \
|
|
57
|
-
--value op_def456=2026-09-30
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
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.
|
|
61
|
-
|
|
62
|
-
`init` keeps any files you already have (`--force` overwrites) and works with `--source pipe0|explorium|linkedin` and `--provider hubspot|salesforce`. The PLAYBOOK.md it writes wires the cold-start and outbound-loop recipes to your workspace; the full play set — cold start, trigger-based outbound, scheduling, ABM, hygiene-gating, TAM — is [docs/recipes.md](./docs/recipes.md).
|
|
61
|
+
--plan-id <plan-id> --provider hubspot
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
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.
|
|
67
|
-
|
|
68
|
-
**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"`).
|
|
69
|
-
|
|
70
|
-
**`call classify`** picks the call type before scoring, because a renewal scored on "Depth of Discovery" is just wrong. It runs a **deterministic, key-free** signal classifier over the transcript — 12 call types (prospecting, discovery, demo, technical_validation, negotiation, closing, onboarding, check_in, renewal_expansion, qbr, internal, other) — returning the type with a confidence and a written reason; pass `--llm` for a model tiebreak on the ambiguous middle. It needs no API key, so it runs free in CI and on warehouse bulk loads (`--list` prints the taxonomy).
|
|
71
|
-
|
|
72
|
-
**`call score`** rates the call against a coaching rubric. By default it **auto-selects the type-specific rubric** from the classification — each call type ships its own dimensions with anchored high/low examples and evidence cues (cuts scoring variance). Override with `--call-type <type>` to force a preset, or `--rubric rubric.json` for your own (`{ scale, name?, callType?, bands?, dimensions: [{ name, weight, rubric, anchorsHigh?, anchorsLow?, evidenceCues?, coachingPrompts? }] }`). The weighted overall is computed deterministically client-side, mapped to a qualitative band, every dimension score is evidence-quoted, and the rubric file is where your client-specific coaching framework lives (`--list-rubrics` prints the built-in presets).
|
|
73
|
-
|
|
74
|
-
`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.
|
|
75
|
-
|
|
76
|
-
```bash
|
|
77
|
-
# Coaching pipeline (Slack + CRM): parse → classify → score → link → govern the writeback
|
|
78
|
-
fullstackgtm call parse --transcript call.txt --title "Acme disco" --out parsed.json # LLM extraction
|
|
79
|
-
fullstackgtm call classify --call parsed.json # deterministic call type
|
|
80
|
-
fullstackgtm call score --call parsed.json # auto-selects the type's rubric
|
|
81
|
-
fullstackgtm call score --call parsed.json --rubric team-rubric.json # …or bring your own framework
|
|
82
|
-
fullstackgtm call link --attendees jane@acme.com --provider hubspot # → deal id + reason
|
|
83
|
-
fullstackgtm call plan --call parsed.json --deal 123 --provider hubspot --save
|
|
84
|
-
# review → plans approve → apply: deal.next_step + follow-up tasks, compare-and-set protected
|
|
85
|
-
# (pipe the parse/score JSON into Slack/Notion however you like)
|
|
86
|
-
|
|
87
|
-
# Analytics pipeline (warehouse): one flat NDJSON row per insight
|
|
88
|
-
for t in transcripts/*; do fullstackgtm call parse --transcript "$t" --ndjson --deterministic; done > insights.ndjson
|
|
89
|
-
# free keyword baseline for bulk loads; drop --deterministic for LLM-quality rows
|
|
90
|
-
# COPY into your warehouse (stable call/evidence ids make reloads idempotent)
|
|
63
|
+
# 5. Verify the signed execution record.
|
|
64
|
+
npx fullstackgtm audit-log verify
|
|
91
65
|
```
|
|
92
66
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
## The create gate: no new dupes
|
|
67
|
+
For an unsaved file plan, `apply --plan plan.json --approve <ids>` provides the same explicit operation selection. A `requires_human_*` placeholder is never executable until a concrete value is supplied.
|
|
96
68
|
|
|
97
|
-
|
|
69
|
+
## The safety contract
|
|
98
70
|
|
|
99
|
-
|
|
100
|
-
fullstackgtm resolve contact --email jane@acme.com --input snap.json # exit 0 = safe to create
|
|
101
|
-
fullstackgtm resolve account --domain acme.com --provider hubspot # exit 2 = exists/ambiguous: do NOT create
|
|
102
|
-
fullstackgtm resolve deal --name "Acme Expansion" --account-id 123 --input snap.json
|
|
103
|
-
```
|
|
71
|
+
These rules apply across the CLI, library, scheduler, and MCP server:
|
|
104
72
|
|
|
105
|
-
|
|
73
|
+
1. **Read and plan by default.** Audits and previews do not mutate providers.
|
|
74
|
+
2. **Explicit authority.** Apply writes only approved operation IDs; approval is never inferred from a prompt.
|
|
75
|
+
3. **No guessed decisions.** Human placeholders require concrete value overrides.
|
|
76
|
+
4. **Resolve before create.** Domain, email, and open-deal identity gates stop duplicate creation.
|
|
77
|
+
5. **Compare before set.** Before-values and preconditions prevent silent stale-plan overwrites.
|
|
78
|
+
6. **Bound the blast radius.** Bulk operations support guards, filters, and operation caps.
|
|
79
|
+
7. **Record the outcome.** Every attempt returns applied, skipped, or failed receipts; audit logs are tamper-evident.
|
|
80
|
+
8. **Separate clients.** Profiles scope credentials, plans, enrichment state, and schedules to one organization.
|
|
81
|
+
9. **Never auto-approve.** Scheduled work may generate plans; it cannot grant itself authority to execute them.
|
|
106
82
|
|
|
107
|
-
|
|
83
|
+
## CRM safety toolbox
|
|
108
84
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
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:
|
|
85
|
+
### Prevent
|
|
112
86
|
|
|
113
87
|
```bash
|
|
114
|
-
|
|
115
|
-
fullstackgtm
|
|
116
|
-
|
|
117
|
-
fullstackgtm plans approve patch_plan_abc123 --values-from suggestions.json # high-confidence only by default
|
|
118
|
-
fullstackgtm apply --plan-id patch_plan_abc123 --provider hubspot
|
|
88
|
+
# Exit 0: safe to create. Exit 2: exists or ambiguous; do not create.
|
|
89
|
+
fullstackgtm resolve contact --email jane@acme.com --provider hubspot
|
|
90
|
+
fullstackgtm resolve account --domain acme.com --provider salesforce
|
|
119
91
|
```
|
|
120
92
|
|
|
121
|
-
|
|
93
|
+
Identity is based on normalized account domains, contact emails, and account + deal-name keys. Names alone are not identity. When the provider exposes record provenance, duplicate findings identify the writer that created them.
|
|
94
|
+
|
|
95
|
+
### Detect
|
|
122
96
|
|
|
123
97
|
```bash
|
|
124
|
-
|
|
125
|
-
|
|
98
|
+
fullstackgtm snapshot --provider hubspot --out snapshot.json
|
|
99
|
+
fullstackgtm audit --input snapshot.json --json
|
|
100
|
+
fullstackgtm audit --provider hubspot --fail-on warning
|
|
101
|
+
fullstackgtm diff --before old.json --after new.json --fail-on-new-findings
|
|
102
|
+
fullstackgtm health
|
|
126
103
|
```
|
|
127
104
|
|
|
128
|
-
|
|
105
|
+
Built-in deterministic rules cover duplicates, orphaned records, missing owners, unlinked or amount-less deals, stale pipeline, past close dates, and related hygiene failures. Stable finding and operation IDs make repeated runs diffable.
|
|
129
106
|
|
|
130
|
-
|
|
107
|
+
### Remediate
|
|
131
108
|
|
|
132
|
-
|
|
109
|
+
Every write-shaped command below produces a patch plan before it can execute:
|
|
133
110
|
|
|
134
111
|
```bash
|
|
112
|
+
fullstackgtm fix --rule missing-deal-owner --provider hubspot
|
|
135
113
|
fullstackgtm bulk-update deal --where "stage=closedwon" --where "amount:empty" \
|
|
136
|
-
--set amount=from:account.annualrevenue --save
|
|
137
|
-
fullstackgtm dedupe account --key domain --keep richest --save
|
|
138
|
-
fullstackgtm reassign --from 411 --to 902 --except-deal-stage closing --save
|
|
139
|
-
fullstackgtm
|
|
140
|
-
fullstackgtm
|
|
141
|
-
fullstackgtm backfill stripe --save # paid Stripe invoices → proposed closed-won deals, deduped by invoice id
|
|
114
|
+
--set amount=from:account.annualrevenue --save
|
|
115
|
+
fullstackgtm dedupe account --key domain --keep richest --save
|
|
116
|
+
fullstackgtm reassign --from 411 --to 902 --except-deal-stage closing --save
|
|
117
|
+
fullstackgtm route --provider hubspot --save
|
|
118
|
+
fullstackgtm backfill stripe --save
|
|
142
119
|
```
|
|
143
120
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
`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).
|
|
147
|
-
|
|
148
|
-
`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.
|
|
121
|
+
Irreversible merges remain low-confidence-capped. Filters and guards are re-evaluated against live state during apply, so a record that stopped matching is skipped rather than clobbered.
|
|
149
122
|
|
|
150
|
-
|
|
123
|
+
### Govern third-party enrichment
|
|
151
124
|
|
|
152
|
-
|
|
125
|
+
Enrichment data never writes directly to the CRM. A source record is matched deterministically, mapped under a declared policy, and rendered as a reviewable diff. The current conflict policy is deliberately conservative: fill blanks, and refresh only fields the enrichment ledger proves it previously stamped.
|
|
153
126
|
|
|
154
127
|
```bash
|
|
155
|
-
|
|
156
|
-
fullstackgtm
|
|
157
|
-
fullstackgtm market classify # LLM readings (BYO key), every quote verified
|
|
158
|
-
fullstackgtm market fronts --diff run-1 # what changed since last run
|
|
159
|
-
fullstackgtm market report --format html --out map.html # the client-ready field report
|
|
160
|
-
fullstackgtm market refresh # all of the above, weekly, one command
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
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 worksheet` → `market 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.
|
|
164
|
-
|
|
165
|
-
`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.
|
|
166
|
-
|
|
167
|
-
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.
|
|
128
|
+
# Apollo pull
|
|
129
|
+
fullstackgtm enrich append --source apollo --provider hubspot --save
|
|
168
130
|
|
|
169
|
-
|
|
131
|
+
# Clay export / push-style source
|
|
132
|
+
fullstackgtm enrich ingest clay.csv --source clay --provider hubspot --save
|
|
170
133
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
```bash
|
|
174
|
-
echo "$APOLLO_API_KEY" | fullstackgtm login apollo # BYO key, stored 0600
|
|
175
|
-
fullstackgtm enrich append --provider hubspot # pull → match → dry-run diff, writes NOTHING
|
|
176
|
-
fullstackgtm enrich append --provider hubspot --save # persist the plan (needs_approval) + run record
|
|
177
|
-
fullstackgtm enrich ingest clay-export.csv --source clay # stage a push-style source (Clay CSV / webhook JSON)
|
|
178
|
-
fullstackgtm enrich refresh --source apollo --save # re-check stale stamped fields; ops only where the source changed
|
|
179
|
-
fullstackgtm enrich status --runs # last run per source, counts, staleness, interrupted-run cursor
|
|
134
|
+
# Inspect source runs, stamps, and stale fields
|
|
135
|
+
fullstackgtm enrich status --runs
|
|
180
136
|
```
|
|
181
137
|
|
|
182
|
-
|
|
138
|
+
#### ZoomInfo as a governed source
|
|
183
139
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
`enrich append/refresh` fill blanks on records you already have. **`enrich acquire`** creates *net-new* ones — and routes them through the same dry-run → approve → apply gate, so prospecting can never silently write to your CRM. It discovers people, resolves their work email, scores them against your ICP, dedupes against your CRM, and proposes governed `create_record` operations.
|
|
140
|
+
fullstackgtm uses ZoomInfo's official [`gtm` CLI](https://github.com/Zoominfo/gtm-ai-cli) for read-only retrieval. ZoomInfo keeps ownership of OAuth, entitlements, and credit accounting; fullstackgtm keeps ownership of matching, policy, approval, CRM apply, and receipts. No ZoomInfo token is copied into the fullstackgtm credential store.
|
|
187
141
|
|
|
188
142
|
```bash
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
echo "$PIPE0_API_KEY" | fullstackgtm login pipe0 # discovery + work-email provider, stored 0600
|
|
192
|
-
|
|
193
|
-
fullstackgtm enrich acquire --provider hubspot # zero-config: ICP → discover → score → dedup → dry-run diff
|
|
194
|
-
fullstackgtm enrich acquire --provider hubspot --max 25 --save # cap the batch, persist the needs_approval plan
|
|
195
|
-
fullstackgtm enrich acquire --provider hubspot --max 25 --scan-limit 500 --save # scan through duplicate-heavy pages for 25 fresh leads
|
|
196
|
-
fullstackgtm plans approve <id> --operations all
|
|
197
|
-
fullstackgtm apply --plan-id <id> --provider hubspot # the only step that creates contacts
|
|
143
|
+
brew install zoominfo/gtm-ai/gtm-ai-cli # or: npm install -g @zoominfo/gtm-ai-cli
|
|
144
|
+
gtm auth login
|
|
198
145
|
```
|
|
199
146
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
**You don't pay to re-discover dupes.** Before the (credit-spending) email step, acquire drops prospects already in your CRM and any seen in a prior run:
|
|
203
|
-
|
|
204
|
-
- **Pre-email CRM dedup** matches the live snapshot. The LinkedIn URL (`hs_linkedin_url`, read into the snapshot by default — safe everywhere, HubSpot ignores unknown properties) is the strong key; name+domain is the fallback. Created contacts get their LinkedIn URL written back, so coverage — and dedup precision — grows over time. If your CRM has no LinkedIn URLs, acquire says so and recommends populating it.
|
|
205
|
-
- **A cross-run seen cache** remembers everyone already resolved, so re-running the same ICP costs nothing for known people.
|
|
206
|
-
- Two more checkpoints stay precise: the email-level match at plan build, and **resolve-first at apply** (re-checks the live CRM, never double-creates, never charges the meter).
|
|
207
|
-
|
|
208
|
-
Every run is **metered**: a per-profile budget caps both record count and provider spend (per-day + per-month, whichever binds first), charged only for creates that actually land. `enrich acquire` runs with just an `icp.json` and a `login` — sensible defaults for budget, provider, and create-mapping ship in the box.
|
|
209
|
-
|
|
210
|
-
Discovery continuation is profile-scoped and independently keyed by the exact
|
|
211
|
-
provider, source, list, and ICP query. Scheduled runs resume the saved Pipe0
|
|
212
|
-
cursor or HeyReach offset instead of rereading page one, even when several
|
|
213
|
-
lists alternate. When the CLI is paired, this non-PII checkpoint is mirrored
|
|
214
|
-
to the hosted organization with compare-and-swap so another authenticated
|
|
215
|
-
worker can resume without moving a newer cursor backwards. `--max` is the desired net-new plan size;
|
|
216
|
-
`--scan-limit` independently bounds raw candidates scanned after ICP filtering
|
|
217
|
-
and dedupe. `enrich status --runs` exposes the full funnel and whether the
|
|
218
|
-
audience is continuing or provider-confirmed exhausted.
|
|
219
|
-
|
|
220
|
-
When paired, every saved plan is also mirrored into the hosted Patch Plans
|
|
221
|
-
review queue and the CLI prints its exact deep-link. The mirror is immutable
|
|
222
|
-
and organization-scoped: replaying the same plan is idempotent, while the same
|
|
223
|
-
id with different content is rejected. Hosted reviewers can approve or reject;
|
|
224
|
-
the plan is executable from any synchronized surface with a compatible CRM
|
|
225
|
-
connection. A CLI-created plan can be applied in hosted, and a hosted approval
|
|
226
|
-
can be applied from the CLI—the plan's origin does not permanently own it.
|
|
227
|
-
|
|
228
|
-
This is local-first replication, not remote control. The local plan store works
|
|
229
|
-
offline, and plan commands perform a best-effort hosted check-in. Run
|
|
230
|
-
`fullstackgtm plans sync` to explicitly exchange every plan's latest approval,
|
|
231
|
-
execution claim, status, and receipts. If hosted applies while the CLI is
|
|
232
|
-
offline, the next check-in imports the exact per-operation results and marks
|
|
233
|
-
the local plan applied. If the CLI applies while hosted is unavailable, it
|
|
234
|
-
keeps the result locally and uploads the receipt on a later check-in.
|
|
235
|
-
|
|
236
|
-
Before either surface writes, it verifies the immutable plan hash, selected
|
|
237
|
-
operation ids, current approval revision, connector capability, and provider
|
|
238
|
-
state. Online replicas use a short-lived execution claim to coordinate; stable
|
|
239
|
-
operation ids, resolve-before-create, compare-and-set guards, and provider
|
|
240
|
-
readback protect offline retries. Receipts record each operation as `applied`,
|
|
241
|
-
`skipped`, or `failed`, plus provider record ids when available, so another
|
|
242
|
-
replica learns what happened rather than inferring it from plan status. A
|
|
243
|
-
conflicting plan body or approval revision is surfaced for review and is never
|
|
244
|
-
merged by expanding authority.
|
|
245
|
-
|
|
246
|
-
Interactive command output is decision-first by default: compact cards show
|
|
247
|
-
status, selected scope, expected effect, per-record outcomes, hosted links, and
|
|
248
|
-
the next safe command. Use `--verbose` on supported human-facing commands for
|
|
249
|
-
the complete findings/evidence/payload document; use `--json` for the stable
|
|
250
|
-
machine-readable contract. Progress and warnings remain on stderr, while JSON
|
|
251
|
-
and requested report data remain clean on stdout.
|
|
252
|
-
|
|
253
|
-
Local signing keys and HMAC digests never upload. Operation before/after values
|
|
254
|
-
do upload to the paired organization because they are required for informed
|
|
255
|
-
review and hosted execution.
|
|
256
|
-
|
|
257
|
-
**Discovery sources** are zero-config presets behind `--source`: `explorium` and `pipe0` run ICP-driven people search, while `linkedin` (Phase 1 — discovery + dry-run) reads a pre-built **HeyReach** lead list:
|
|
147
|
+
Declare ZoomInfo paths in `enrich.config.json` (the raw JSON record is retained as operation evidence):
|
|
258
148
|
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"sources": { "zoominfo": { "kind": "api" } },
|
|
152
|
+
"match": {
|
|
153
|
+
"company": { "keys": ["domain", "name"], "onAmbiguous": "skip" },
|
|
154
|
+
"contact": { "keys": ["email", "name"], "onAmbiguous": "skip" }
|
|
155
|
+
},
|
|
156
|
+
"fields": {
|
|
157
|
+
"company": [
|
|
158
|
+
{ "crm": "employeeCount", "from": { "zoominfo": "attributes.employeeCount" }, "refresh": true },
|
|
159
|
+
{ "crm": "annualRevenue", "from": { "zoominfo": "attributes.revenue" }, "refresh": true }
|
|
160
|
+
],
|
|
161
|
+
"contact": [
|
|
162
|
+
{ "crm": "title", "from": { "zoominfo": "attributes.jobTitle" }, "refresh": true },
|
|
163
|
+
{ "crm": "phone", "from": { "zoominfo": "attributes.phone" }, "refresh": true }
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
"policy": { "overwrite": "never", "defaultStaleDays": 90 }
|
|
167
|
+
}
|
|
262
168
|
```
|
|
263
169
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
**Leads are never born ownerless.** An `acquire.assign` policy stamps an owner onto every created lead at create time (mapped to HubSpot `hubspot_owner_id`), routed by one of four strategies — `fixed`, `round-robin` (distributed deterministically, no rotation state to drift), `territory` (by geo / industry / size / department / title), or `account-owner` (inherit the matched company's owner). With no policy and a single-owner portal, acquire defaults every lead to that owner (or pass `--assign-owner <id>`); with multiple owners and no policy it warns and leaves them unassigned rather than guess. To clear *existing* ownerless records, `reassign --assign-unowned --to <ownerId>` applies the same intent as a backfill.
|
|
267
|
-
|
|
268
|
-
## Signal-based outbound: reach accounts the week something changes
|
|
269
|
-
|
|
270
|
-
Cleaning and filling the CRM tells you *who* to reach; it never tells you *when*. The **signal → judge → draft** loop adds timing — and, like everything else, it stays on the dry-run → approve → apply spine and sends nothing.
|
|
170
|
+
Then preview or save the plan:
|
|
271
171
|
|
|
272
172
|
```bash
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
fullstackgtm
|
|
276
|
-
fullstackgtm signals fetch --connector serpapi-news,hubspot-forms --save # news + first-party form demand
|
|
277
|
-
fullstackgtm signals fetch --connector file --save # webhook landing zone (see docs/signal-spool-format.md)
|
|
278
|
-
echo "$EXA_API_KEY" | fullstackgtm login exa # once; or keep the key in env
|
|
279
|
-
fullstackgtm signals discover --icp ./icp.json --source exa --since 30d --max-searches 12 --max-usd 0.25 --save
|
|
280
|
-
fullstackgtm signals list --since 7d # ranked triggers, each with a verbatim source quote
|
|
281
|
-
|
|
282
|
-
# 2. Decide who's worth a touch — and who isn't. Scores timing × fit × memory into send/nurture/skip.
|
|
283
|
-
fullstackgtm icp judge --signals-from latest --with-history --save
|
|
284
|
-
fullstackgtm icp eval --golden default # gate: prove the judge is calibrated before any send (exits 2 if not)
|
|
285
|
-
|
|
286
|
-
# 3. Draft the opener from the trigger. A create_task plan — proposed, never transmitted.
|
|
287
|
-
fullstackgtm draft --from-judge latest --min-score 80 --channel email --save
|
|
288
|
-
fullstackgtm plans approve <id> --operations all
|
|
289
|
-
fullstackgtm apply --plan-id <id> --provider hubspot # log the touch as a CRM task
|
|
290
|
-
fullstackgtm apply --plan-id <id> --channel outbox # OR render to the outbox for a sender — transmits nothing (docs/outbox-format.md)
|
|
291
|
-
|
|
292
|
-
# 4. Close the loop. Outcomes re-weight which signals earn a touch.
|
|
293
|
-
fullstackgtm signals outcome --account acme.com --result replied
|
|
173
|
+
fullstackgtm enrich append --source zoominfo --provider hubspot
|
|
174
|
+
fullstackgtm enrich append --source zoominfo --provider hubspot --save
|
|
175
|
+
fullstackgtm enrich refresh --source zoominfo --provider hubspot --save
|
|
294
176
|
```
|
|
295
177
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
**`icp judge`** turns raw signals into a short ranked list: it scores each account on **timing × fit × memory** — the signal's weight and recency, your ICP fit, and whether you've touched the account in the last 7 days — and returns `send` / `nurture` / `skip`. Every *why-now* it produces must quote a real trigger **verbatim** (the same evidence gate as `call` and `market`); an ungrounded why-now is never stored. With no LLM key it runs a deterministic baseline. **`icp eval`** is the gate the demos skip: it grades the judge against a labeled golden set (and, once outcomes exist, checks that accounts scored hot actually book more than cold), exiting `2` below the bar so a miscalibrated judge can't reach a live send.
|
|
299
|
-
|
|
300
|
-
**`draft`** writes one opener per hot account whose first line quotes the trigger in the buyer's own words — no "Hi {{firstName}}", one ask, no manufactured urgency. It emits a `create_task` plan through the normal approval gate; it has **no send capability** and adds none — execution stays in your sender of choice. Without an LLM key it emits a clearly-labeled stub, never wooden copy passed off as a draft.
|
|
178
|
+
`ZOOMINFO_GTM_BIN` can point to a non-default `gtm` executable. Source retrieval can consume ZoomInfo bulk data credits even when the resulting FullstackGTM plan is only a dry run; use ZoomInfo admin credit limits and start with a small CRM snapshot. The CRM still receives no write until the plan is approved and applied.
|
|
301
179
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
## Schedules: declare a cadence once, keep the governance contract under automation
|
|
305
|
-
|
|
306
|
-
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):
|
|
180
|
+
### Verify and recover
|
|
307
181
|
|
|
308
182
|
```bash
|
|
309
|
-
fullstackgtm
|
|
310
|
-
fullstackgtm
|
|
311
|
-
fullstackgtm
|
|
312
|
-
fullstackgtm
|
|
313
|
-
fullstackgtm
|
|
314
|
-
fullstackgtm
|
|
315
|
-
fullstackgtm
|
|
183
|
+
fullstackgtm plans list
|
|
184
|
+
fullstackgtm plans show <id>
|
|
185
|
+
fullstackgtm plans approve <id> --operations <ids|all>
|
|
186
|
+
fullstackgtm plans reject <id>
|
|
187
|
+
fullstackgtm plans sync
|
|
188
|
+
fullstackgtm audit-log export
|
|
189
|
+
fullstackgtm audit-log verify
|
|
316
190
|
```
|
|
317
191
|
|
|
318
|
-
|
|
192
|
+
Stored plans are signed. Apply receipts preserve per-operation outcomes and provider record IDs. An interrupted provider attempt is not blindly replayed; reconcile provider state before issuing fresh authority.
|
|
319
193
|
|
|
320
|
-
|
|
194
|
+
## Connectors and authentication
|
|
321
195
|
|
|
322
|
-
|
|
196
|
+
| Provider | Read | Governed writes | Notes |
|
|
197
|
+
|---|---:|---:|---|
|
|
198
|
+
| HubSpot | Yes | Yes | OAuth/broker or private-app token |
|
|
199
|
+
| Salesforce | Yes | Yes | Field writes and Account/Contact merges; Opportunities cannot be merged |
|
|
200
|
+
| Stripe | Yes | No | Restricted read key; source for revenue backfill plans |
|
|
201
|
+
| Apollo | Source | No direct CRM write | API key; enrichment input |
|
|
202
|
+
| Clay | Source | No direct CRM write | CSV/webhook ingest or Public API |
|
|
203
|
+
| ZoomInfo | Source | No direct CRM write | Official `gtm` CLI OAuth; enrichment input |
|
|
323
204
|
|
|
324
|
-
|
|
205
|
+
Secrets are accepted through environment variables, stdin, the owner-only local credential store, or a paired hosted broker—never argv flags.
|
|
325
206
|
|
|
326
207
|
```bash
|
|
327
|
-
fullstackgtm
|
|
208
|
+
fullstackgtm login hubspot
|
|
209
|
+
fullstackgtm login salesforce --device --client-id <consumer-key>
|
|
210
|
+
echo "$HUBSPOT_TOKEN" | fullstackgtm login hubspot --private-token
|
|
211
|
+
echo "$APOLLO_API_KEY" | fullstackgtm login apollo
|
|
212
|
+
|
|
213
|
+
# Keep client credentials and plans isolated.
|
|
328
214
|
fullstackgtm --profile acme audit --provider hubspot --save
|
|
329
|
-
fullstackgtm
|
|
215
|
+
FULLSTACKGTM_PROFILE=acme fullstackgtm plans list
|
|
330
216
|
```
|
|
331
217
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
## Built for agents (and the RevOps humans they work for)
|
|
218
|
+
Run `fullstackgtm doctor --json` to inspect the active profile and credential state. Detailed provider setup and data flows: [DATA-FLOWS.md](./DATA-FLOWS.md).
|
|
335
219
|
|
|
336
|
-
|
|
220
|
+
## Built for agents
|
|
337
221
|
|
|
338
222
|
```bash
|
|
339
|
-
# Discover the machine-readable contract: every command, read-only vs
|
|
340
|
-
# write-shaped access, exit codes, safety defaults (derived from the same
|
|
341
|
-
# help table humans read, so it can't drift)
|
|
342
223
|
fullstackgtm capabilities --json
|
|
343
|
-
|
|
344
|
-
# Per-command help as JSON; plain --help stays human-shaped
|
|
345
224
|
fullstackgtm audit --help --json
|
|
346
|
-
|
|
347
|
-
# Print the shipped agent operating guide (the same SKILL.md `npx skills add` installs)
|
|
348
|
-
fullstackgtm robot-docs
|
|
349
|
-
|
|
350
|
-
# Discover what the auditor checks
|
|
351
225
|
fullstackgtm rules --json
|
|
352
|
-
|
|
353
|
-
# Fetch once (expensive), audit offline as many times as you like (cheap)
|
|
354
|
-
fullstackgtm snapshot --provider hubspot --out snap.json
|
|
355
|
-
fullstackgtm audit --input snap.json --json
|
|
356
|
-
fullstackgtm audit --input snap.json --rules stale-deal --stale-days 45 --json
|
|
357
|
-
|
|
358
|
-
# Gate a nightly CI job or agent run on hygiene: exit 2 if findings ≥ threshold
|
|
359
|
-
fullstackgtm audit --provider hubspot --fail-on warning
|
|
360
|
-
|
|
361
|
-
# Gate CI on hygiene drift instead: exit 2 only when a NEW (rule, record) finding appears
|
|
362
|
-
fullstackgtm diff --before old.json --after new.json --fail-on-new-findings
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
- Terminal polish never leaks into machine output: color, spinners, progress bars, and sparklines render **only on an interactive TTY** (stderr for anything animated). Piped, redirected, `--json`, CI, and `NO_COLOR` output carries **zero ANSI bytes** — enforced by tests and a CI contract check — so transcripts and pipes see the same plain text they always did (`FORCE_COLOR=1` opts back in).
|
|
366
|
-
- 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.
|
|
367
|
-
- `--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.
|
|
368
|
-
- Exit codes: `0` success, `1` error, `2` findings at/above `--fail-on`.
|
|
369
|
-
|
|
370
|
-
"Built for agents" is measured, not asserted: a 1,904-run benchmark (17 CRM-operations scenarios × 3 tool-surface arms × up to 4 trials, across ten models from six vendors, deterministic graders over final CRM state, τ-bench-style pass^k) shows the gated CLI surface matching or beating raw CRM-API access on completion-under-policy for every model tested — strictly better in nine of ten — and the effect is vendor-independent. Full matrix and methodology: [the leaderboard](https://github.com/fullstackgtm/core/blob/main/evals/crm/leaderboard/RESULTS.md).
|
|
371
|
-
|
|
372
|
-
Caveat: the leaderboard documents the Opus reduced protocol and mixed-version Informed arms; treat the headline as a pointer to those exact RESULTS.md conditions.
|
|
373
|
-
|
|
374
|
-
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.
|
|
375
|
-
|
|
376
|
-
## Authentication: CLI-first, browser only at the consent moment
|
|
377
|
-
|
|
378
|
-
Credential resolution is a ladder — the first rung that yields a token wins:
|
|
379
|
-
|
|
380
|
-
1. **`--token-env <NAME>`** — explicit env var for one invocation (agent sandboxes, scripts)
|
|
381
|
-
2. **`HUBSPOT_ACCESS_TOKEN` / Salesforce env** — ambient env (CI)
|
|
382
|
-
3. **BYO direct login** — advanced token/OAuth paths stored in `~/.fullstackgtm/credentials.json` (0600; override location with `FSGTM_HOME`); these override hosted
|
|
383
|
-
4. **Hosted OAuth / broker** — `fullstackgtm login hubspot` or `fullstackgtm login salesforce` opens hosted browser OAuth by default, stores only a broker credential locally, and mints provider tokens server-side
|
|
384
|
-
|
|
385
|
-
### Teams: auth once, point every CLI at the stored sync credentials
|
|
386
|
-
|
|
387
|
-
```bash
|
|
388
|
-
fullstackgtm login --via https://gtm.yourco.com
|
|
389
|
-
# Pairing code: ABCD-2345
|
|
390
|
-
# Approve this CLI in your dashboard: https://gtm.yourco.com/dashboard/cli-auth?code=ABCD-2345
|
|
226
|
+
fullstackgtm robot-docs
|
|
391
227
|
```
|
|
392
228
|
|
|
393
|
-
|
|
229
|
+
Machine output is kept clean: progress stays on stderr, `--json` stays on stdout, IDs are stable, and exit codes are meaningful (`0` success, `1` error, `2` policy finding or create-gate match).
|
|
394
230
|
|
|
395
|
-
|
|
231
|
+
The safety surface has been benchmarked across models and providers. See the [evaluation methodology and results](https://github.com/fullstackgtm/core/blob/main/evals/crm/leaderboard/RESULTS.md) for the exact protocol and caveats.
|
|
396
232
|
|
|
397
|
-
|
|
233
|
+
## MCP server
|
|
398
234
|
|
|
399
235
|
```bash
|
|
400
|
-
#
|
|
401
|
-
|
|
402
|
-
fullstackgtm login salesforce
|
|
403
|
-
# optional: point at a self-hosted/team deployment
|
|
404
|
-
fullstackgtm login hubspot --hosted --via https://gtm.yourco.com
|
|
405
|
-
|
|
406
|
-
# Advanced HubSpot private app token (validated, then stored; token on stdin).
|
|
407
|
-
# Note: hosted HubSpot OAuth covers reads and object writes but cannot create
|
|
408
|
-
# tasks (HubSpot exposes no tasks scope to public apps) — use a private-app
|
|
409
|
-
# token if your workflow relies on `create_task` proposals.
|
|
410
|
-
echo "$HUBSPOT_TOKEN" | fullstackgtm login hubspot --private-token
|
|
411
|
-
|
|
412
|
-
# Advanced HubSpot BYO-app OAuth. Client secret is read from stdin/prompt — never a flag.
|
|
413
|
-
echo "$CLIENT_SECRET" | fullstackgtm login hubspot --oauth --client-id <id>
|
|
414
|
-
# (register http://localhost:8763/callback as a redirect URL on your app)
|
|
415
|
-
|
|
416
|
-
# Advanced Salesforce BYO Connected App device flow.
|
|
417
|
-
fullstackgtm login salesforce --device --client-id <consumer key>
|
|
418
|
-
# ...or a session token directly (token on stdin, never as a flag):
|
|
419
|
-
echo "$SF_SESSION_TOKEN" | fullstackgtm login salesforce --instance-url https://yourorg.my.salesforce.com
|
|
420
|
-
|
|
421
|
-
fullstackgtm logout hubspot # or: salesforce | broker
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
BYO direct credentials always win over a hosted broker pairing, so an operator can override the team default. First-party app secrets never ship in the npm package; hosted login stores a local broker token and mints provider tokens server-side. 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.
|
|
425
|
-
|
|
426
|
-
Salesforce credential-bearing URLs are restricted to canonical HTTPS
|
|
427
|
-
Salesforce-owned production, sandbox, and My Domain origins. Lookalike hosts,
|
|
428
|
-
userinfo, paths/query/fragment, nonstandard ports, unsafe token-response
|
|
429
|
-
instance URLs, and credential-forwarding redirects are refused both when the
|
|
430
|
-
credential is stored and every time it is used.
|
|
431
|
-
|
|
432
|
-
## Connect your CRM
|
|
433
|
-
|
|
434
|
-
What each provider actually requires before `audit --provider <name>` works on your data.
|
|
435
|
-
|
|
436
|
-
### Connector capabilities
|
|
437
|
-
|
|
438
|
-
Connectors differ in what the provider's API allows — stated up front so nothing surprises you mid-evaluation:
|
|
439
|
-
|
|
440
|
-
| Operation | HubSpot | Salesforce | Stripe |
|
|
441
|
-
| --- | --- | --- | --- |
|
|
442
|
-
| Read / snapshot / audit | ✅ | ✅ | ✅ (read-only) |
|
|
443
|
-
| Field writes (`set_field`, `clear_field`, `link_record`) | ✅ | ✅ | — |
|
|
444
|
-
| `create_task` | ✅ | ✅ | — |
|
|
445
|
-
| `archive_record` | ✅ | ✅ | — |
|
|
446
|
-
| `merge_records` (`dedupe`) | ✅ | ✅ Account/Contact (SOAP); ❌ Opportunity | — |
|
|
447
|
-
|
|
448
|
-
**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.)
|
|
449
|
-
|
|
450
|
-
### HubSpot: create a private app (~2 minutes, needs super-admin)
|
|
451
|
-
|
|
452
|
-
1. In HubSpot: **Settings → Integrations → Private Apps → Create a private app.**
|
|
453
|
-
2. On the **Scopes** tab, grant the read scopes the audit needs:
|
|
454
|
-
- `crm.objects.owners.read`
|
|
455
|
-
- `crm.objects.companies.read`
|
|
456
|
-
- `crm.objects.contacts.read`
|
|
457
|
-
- `crm.objects.deals.read`
|
|
458
|
-
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).
|
|
459
|
-
4. Create the app, copy the token (`pat-...`), then: `echo "$TOKEN" | fullstackgtm login hubspot --private-token`.
|
|
460
|
-
|
|
461
|
-
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.
|
|
462
|
-
|
|
463
|
-
### Salesforce: a Connected App (one-time, usually needs an admin)
|
|
464
|
-
|
|
465
|
-
Device-flow login requires a Connected App in your org — if you're not an admin, this is the step to ask one for:
|
|
466
|
-
|
|
467
|
-
1. **Setup → App Manager → New Connected App**, enable OAuth settings.
|
|
468
|
-
2. Check **Enable Device Flow**.
|
|
469
|
-
3. OAuth scopes: **Manage user data via APIs (`api`)** and **Perform requests at any time (`refresh_token`)** — the CLI requests exactly these.
|
|
470
|
-
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>`.
|
|
471
|
-
|
|
472
|
-
Writeback needs no extra OAuth scope — applies are gated by the logged-in user's normal object/field permissions.
|
|
473
|
-
|
|
474
|
-
**Quickest token for a one-off or sandbox test (the `sf` CLI).** If you already have the [Salesforce CLI](https://developer.salesforce.com/tools/salesforcecli) authenticated to an org, you can skip the Connected App entirely and feed a session token straight in:
|
|
236
|
+
# Zero-install wrapper with MCP peers bundled
|
|
237
|
+
npx -y fullstackgtm-mcp
|
|
475
238
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
sf org display --target-org <alias> --verbose --json
|
|
480
|
-
# then hand the accessToken + instanceUrl to the CLI (token on stdin, never a flag):
|
|
481
|
-
echo "$ACCESS_TOKEN" | fullstackgtm login salesforce --instance-url "$INSTANCE_URL"
|
|
239
|
+
# Or in a project
|
|
240
|
+
npm install fullstackgtm @modelcontextprotocol/sdk zod
|
|
241
|
+
npx fullstackgtm-mcp
|
|
482
242
|
```
|
|
483
243
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
### Stripe: a restricted key is enough (read-only connector)
|
|
487
|
-
|
|
488
|
-
The Stripe connector reads customers, subscriptions, and paid invoices (`backfill stripe` calls `/v1/invoices`), and `apply` is read-only by construction. Create a **restricted key** with just **Customers: Read**, **Subscriptions: Read**, and **Invoices: Read** (Developers → API keys → Create restricted key) instead of pasting a full-access secret key: `echo "$KEY" | fullstackgtm login stripe`.
|
|
244
|
+
Nine tools are exposed over stdio.
|
|
489
245
|
|
|
490
|
-
|
|
246
|
+
**Read-only:** `fullstackgtm_audit`, `fullstackgtm_capabilities`, `fullstackgtm_rules`, `fullstackgtm_suggest`, `fullstackgtm_call_parse`, `fullstackgtm_resolve`, and `fullstackgtm_market_worksheet`.
|
|
491
247
|
|
|
492
|
-
|
|
493
|
-
|---|---|
|
|
494
|
-
| **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. |
|
|
495
|
-
| **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. |
|
|
496
|
-
| **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. |
|
|
497
|
-
| **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. |
|
|
498
|
-
| **Patch plan run** | The audit record of one apply attempt: per-operation applied/failed/skipped results. |
|
|
248
|
+
**Gated:** `fullstackgtm_apply` accepts only a stored plan ID whose operation approvals and values are already HMAC-signed in the plan store; `fullstackgtm_market_observe` verifies quoted evidence before storage.
|
|
499
249
|
|
|
500
|
-
|
|
250
|
+
The MCP caller cannot approve its own operations, inject an external plan, or execute rule-package code. See [docs/api.md](./docs/api.md) for the public contracts.
|
|
501
251
|
|
|
502
|
-
|
|
503
|
-
import {
|
|
504
|
-
auditSnapshot, auditFindingId, builtinAuditRules, defaultPolicy,
|
|
505
|
-
type GtmAuditRule,
|
|
506
|
-
} from "fullstackgtm";
|
|
507
|
-
|
|
508
|
-
const missingAmount: GtmAuditRule = {
|
|
509
|
-
id: "missing-deal-amount",
|
|
510
|
-
title: "Deal has no amount",
|
|
511
|
-
description: "Amountless deals make forecast coverage meaningless.",
|
|
512
|
-
evaluate: ({ snapshot }) => ({
|
|
513
|
-
findings: snapshot.deals
|
|
514
|
-
.filter((deal) => !deal.amount)
|
|
515
|
-
.map((deal) => ({
|
|
516
|
-
id: auditFindingId("missing-deal-amount", deal.id),
|
|
517
|
-
objectType: "deal", objectId: deal.id,
|
|
518
|
-
ruleId: "missing-deal-amount",
|
|
519
|
-
title: "Deal has no amount", severity: "warning",
|
|
520
|
-
summary: `${deal.name} has no amount.`,
|
|
521
|
-
recommendation: "Set an amount or close the deal out.",
|
|
522
|
-
})),
|
|
523
|
-
operations: [],
|
|
524
|
-
}),
|
|
525
|
-
};
|
|
526
|
-
|
|
527
|
-
const plan = auditSnapshot(snapshot, defaultPolicy(), [...builtinAuditRules, missingAmount]);
|
|
528
|
-
```
|
|
252
|
+
## Library
|
|
529
253
|
|
|
530
|
-
|
|
254
|
+
The framework-independent package exports the same canonical model, rule engine, plan contract, connectors, and apply orchestrator used by the CLI.
|
|
531
255
|
|
|
532
256
|
```ts
|
|
533
257
|
import { applyPatchPlan, auditSnapshot, createHubspotConnector } from "fullstackgtm";
|
|
@@ -537,89 +261,36 @@ const hubspot = createHubspotConnector({
|
|
|
537
261
|
});
|
|
538
262
|
|
|
539
263
|
const snapshot = await hubspot.fetchSnapshot();
|
|
540
|
-
const plan = auditSnapshot(snapshot);
|
|
264
|
+
const plan = auditSnapshot(snapshot); // read-only
|
|
541
265
|
|
|
542
|
-
// Later, after
|
|
266
|
+
// Later, after review:
|
|
543
267
|
const run = await applyPatchPlan(hubspot, plan, {
|
|
544
268
|
approvedOperationIds: ["op_abc123"],
|
|
545
|
-
valueOverrides: { op_abc123: "9001" },
|
|
546
269
|
});
|
|
547
270
|
```
|
|
548
271
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
```ts
|
|
552
|
-
import type { GtmConnector } from "fullstackgtm";
|
|
553
|
-
|
|
554
|
-
const myConnector: GtmConnector = {
|
|
555
|
-
provider: "my-crm",
|
|
556
|
-
fetchSnapshot: async () => ({ /* canonical snapshot */ }),
|
|
557
|
-
applyOperation: async (operation) => ({ operationId: operation.id, status: "applied" }),
|
|
558
|
-
};
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
## MCP server
|
|
562
|
-
|
|
563
|
-
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:
|
|
564
|
-
|
|
565
|
-
```bash
|
|
566
|
-
# In a project
|
|
567
|
-
npm install fullstackgtm @modelcontextprotocol/sdk zod
|
|
568
|
-
HUBSPOT_ACCESS_TOKEN=pat-... npx fullstackgtm-mcp
|
|
569
|
-
|
|
570
|
-
# Zero-install (the fullstackgtm-mcp wrapper package bundles the MCP peers)
|
|
571
|
-
npx -y fullstackgtm-mcp
|
|
572
|
-
```
|
|
573
|
-
|
|
574
|
-
Add it to Claude Code in one command:
|
|
272
|
+
Implement another CRM by satisfying the `GtmConnector` contract. Add deterministic organization policy with a `GtmAuditRule`. Architecture and extension points are documented in [docs/architecture.md](./docs/architecture.md) and [docs/api.md](./docs/api.md).
|
|
575
273
|
|
|
576
|
-
|
|
577
|
-
claude mcp add fullstackgtm -e HUBSPOT_ACCESS_TOKEN=pat-... -- npx -y fullstackgtm-mcp
|
|
578
|
-
```
|
|
579
|
-
|
|
580
|
-
Or configure any MCP client (Cursor, Claude Desktop, …) with:
|
|
581
|
-
|
|
582
|
-
```json
|
|
583
|
-
{
|
|
584
|
-
"mcpServers": {
|
|
585
|
-
"fullstackgtm": {
|
|
586
|
-
"command": "npx",
|
|
587
|
-
"args": ["-y", "fullstackgtm-mcp"],
|
|
588
|
-
"env": { "HUBSPOT_ACCESS_TOKEN": "pat-..." }
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
```
|
|
274
|
+
## Appendix: additional modules and recipes
|
|
593
275
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
**Read-only:** `fullstackgtm_audit` (sample, demo, file, or live provider sources with optional rule scoping), `fullstackgtm_capabilities` (server/tool capability manifest), `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).
|
|
597
|
-
|
|
598
|
-
**Gated:** `fullstackgtm_apply` (accepts only a stored plan id; approvals and values must already be human-approved and HMAC-signed in the plan store) and `fullstackgtm_market_observe` (verifies every quoted span against the stored captures before appending — nothing is stored unless the whole set passes).
|
|
599
|
-
|
|
600
|
-
Tokens stored via `fullstackgtm login` are picked up automatically — the env var is only needed when no stored login exists.
|
|
276
|
+
The package includes composable GTM workflows, but they are intentionally downstream of the control-plane story above. None bypasses the patch-plan safety contract.
|
|
601
277
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
278
|
+
- **Call evidence:** parse, classify, score, and link transcripts; convert next steps into governed CRM plans. Run `fullstackgtm call --help`.
|
|
279
|
+
- **ICP and net-new acquire:** discover, score, dedupe, meter, assign, and propose new contacts. See [docs/recipes.md](./docs/recipes.md).
|
|
280
|
+
- **Signals and drafting:** collect buying triggers, judge timing/fit, and produce governed task/outbox plans; the package never sends. See [docs/signal-spool-format.md](./docs/signal-spool-format.md) and [docs/outbox-format.md](./docs/outbox-format.md).
|
|
281
|
+
- **TAM:** estimate a transparent market model, populate it through governed acquire, and track coverage. See [docs/tam.md](./docs/tam.md).
|
|
282
|
+
- **Competitive market map:** capture and verify category evidence, classify messaging fronts, and generate reports. Run `fullstackgtm market --help`.
|
|
283
|
+
- **LinkedIn list intake:** read prebuilt HeyReach lists as a discovery source; no connect/message operation ships. See [docs/linkedin-connector-spec.md](./docs/linkedin-connector-spec.md).
|
|
284
|
+
- **Scheduling:** run read/plan-side commands continuously without auto-approval. Run `fullstackgtm schedule --help`.
|
|
285
|
+
- **End-to-end playbooks:** [docs/recipes.md](./docs/recipes.md).
|
|
606
286
|
|
|
607
|
-
##
|
|
287
|
+
## License, boundary, and development
|
|
608
288
|
|
|
609
|
-
|
|
610
|
-
2. Every proposed write is a typed patch operation with before/after values, a reason, and a risk level.
|
|
611
|
-
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.
|
|
289
|
+
Licensed under [Apache-2.0](./LICENSE). The canonical model, audits, patch plans, connectors, CLI, and MCP server are open source. The hosted Full Stack GTM dashboard, sync backend, broker, and team workflows are a separate proprietary product built on the package. Features never move from open to closed.
|
|
612
290
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
Licensed under [Apache-2.0](./LICENSE). 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](./CONTRIBUTING.md) for how development and mirroring work.
|
|
616
|
-
|
|
617
|
-
The surfaces in [docs/api.md](./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 safety invariants (read-only audits, approval-gated writes, placeholder refusal) are not beta and do not change.
|
|
618
|
-
|
|
619
|
-
## Development
|
|
291
|
+
Public API stability and the open-core boundary are documented in [docs/api.md](./docs/api.md) and [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
620
292
|
|
|
621
293
|
```bash
|
|
622
|
-
npm run build
|
|
294
|
+
npm run build
|
|
295
|
+
npm test
|
|
623
296
|
```
|
|
624
|
-
|
|
625
|
-
Tests live in the repository root `tests/` directory and run with `npm test`.
|