fullstackgtm 0.57.0 → 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 +21 -0
- package/README.md +162 -508
- package/dist/cli/enrich.d.ts +2 -2
- package/dist/cli/enrich.js +27 -14
- package/dist/cli/help.js +2 -2
- package/dist/cli/signals.js +14 -3
- 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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/runReport.js +11 -0
- package/docs/api.md +1 -1
- package/llms.txt +3 -2
- package/package.json +2 -2
- package/src/cli/enrich.ts +30 -15
- package/src/cli/help.ts +2 -2
- package/src/cli/signals.ts +14 -2
- package/src/enrich.ts +2 -2
- package/src/enrichZoomInfo.ts +197 -0
- package/src/index.ts +11 -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**.
|
|
12
18
|
|
|
13
|
-
|
|
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.
|
|
20
|
+
|
|
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,530 +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).
|
|
63
|
-
|
|
64
|
-
## Call workflows: calls become governed evidence
|
|
61
|
+
--plan-id <plan-id> --provider hubspot
|
|
65
62
|
|
|
66
|
-
|
|
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
|
-
|
|
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.
|
|
94
68
|
|
|
95
|
-
## The
|
|
69
|
+
## The safety contract
|
|
96
70
|
|
|
97
|
-
|
|
71
|
+
These rules apply across the CLI, library, scheduler, and MCP server:
|
|
98
72
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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.
|
|
104
82
|
|
|
105
|
-
|
|
83
|
+
## CRM safety toolbox
|
|
106
84
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
## From findings to fixes: the suggest chain
|
|
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).
|
|
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.
|
|
147
122
|
|
|
148
|
-
|
|
123
|
+
### Govern third-party enrichment
|
|
149
124
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
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.
|
|
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
|
-
```
|
|
128
|
+
# Apollo pull
|
|
129
|
+
fullstackgtm enrich append --source apollo --provider hubspot --save
|
|
162
130
|
|
|
163
|
-
|
|
131
|
+
# Clay export / push-style source
|
|
132
|
+
fullstackgtm enrich ingest clay.csv --source clay --provider hubspot --save
|
|
164
133
|
|
|
165
|
-
|
|
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.
|
|
168
|
-
|
|
169
|
-
## Governed enrichment: a diff you approve before third-party data touches your CRM
|
|
170
|
-
|
|
171
|
-
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.
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
## Acquire: net-new leads, ICP-targeted and dupe-safe by default
|
|
138
|
+
#### ZoomInfo as a governed source
|
|
185
139
|
|
|
186
|
-
|
|
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
|
-
|
|
147
|
+
Declare ZoomInfo paths in `enrich.config.json` (the raw JSON record is retained as operation evidence):
|
|
201
148
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
+
}
|
|
210
168
|
```
|
|
211
169
|
|
|
212
|
-
|
|
213
|
-
sync metadata in `./.fullstackgtm/icp.json.sync.json`; the portable `icp.json`
|
|
214
|
-
schema is unchanged. `sync` never silently replaces the local file: if hosted
|
|
215
|
-
changed, it writes `icp.json.hosted-rN.json` for review. If both sides changed,
|
|
216
|
-
it reports a conflict rather than unioning ordered targeting arrays. Signal and
|
|
217
|
-
lead-preview records retain the exact ICP revision used for later analysis.
|
|
218
|
-
|
|
219
|
-
**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:
|
|
220
|
-
|
|
221
|
-
- **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.
|
|
222
|
-
- **A cross-run seen cache** remembers everyone already resolved, so re-running the same ICP costs nothing for known people.
|
|
223
|
-
- 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).
|
|
224
|
-
|
|
225
|
-
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.
|
|
226
|
-
|
|
227
|
-
Discovery continuation is profile-scoped and independently keyed by the exact
|
|
228
|
-
provider, source, list, and ICP query. Scheduled runs resume the saved Pipe0
|
|
229
|
-
cursor or HeyReach offset instead of rereading page one, even when several
|
|
230
|
-
lists alternate. When the CLI is paired, this non-PII checkpoint is mirrored
|
|
231
|
-
to the hosted organization with compare-and-swap so another authenticated
|
|
232
|
-
worker can resume without moving a newer cursor backwards. `--max` is the desired net-new plan size;
|
|
233
|
-
`--scan-limit` independently bounds raw candidates scanned after ICP filtering
|
|
234
|
-
and dedupe. `enrich status --runs` exposes the full funnel and whether the
|
|
235
|
-
audience is continuing or provider-confirmed exhausted.
|
|
236
|
-
|
|
237
|
-
When paired, every saved plan is also mirrored into the hosted Patch Plans
|
|
238
|
-
review queue and the CLI prints its exact deep-link. The mirror is immutable
|
|
239
|
-
and organization-scoped: replaying the same plan is idempotent, while the same
|
|
240
|
-
id with different content is rejected. Hosted reviewers can approve or reject;
|
|
241
|
-
the plan is executable from any synchronized surface with a compatible CRM
|
|
242
|
-
connection. A CLI-created plan can be applied in hosted, and a hosted approval
|
|
243
|
-
can be applied from the CLI—the plan's origin does not permanently own it.
|
|
244
|
-
|
|
245
|
-
This is local-first replication, not remote control. The local plan store works
|
|
246
|
-
offline, and plan commands perform a best-effort hosted check-in. Run
|
|
247
|
-
`fullstackgtm plans sync` to explicitly exchange every plan's latest approval,
|
|
248
|
-
execution claim, status, and receipts. If hosted applies while the CLI is
|
|
249
|
-
offline, the next check-in imports the exact per-operation results and marks
|
|
250
|
-
the local plan applied. If the CLI applies while hosted is unavailable, it
|
|
251
|
-
keeps the result locally and uploads the receipt on a later check-in.
|
|
252
|
-
|
|
253
|
-
Before either surface writes, it verifies the immutable plan hash, selected
|
|
254
|
-
operation ids, current approval revision, connector capability, and provider
|
|
255
|
-
state. Online replicas use a short-lived execution claim to coordinate; stable
|
|
256
|
-
operation ids, resolve-before-create, compare-and-set guards, and provider
|
|
257
|
-
readback protect offline retries. Receipts record each operation as `applied`,
|
|
258
|
-
`skipped`, or `failed`, plus provider record ids when available, so another
|
|
259
|
-
replica learns what happened rather than inferring it from plan status. A
|
|
260
|
-
conflicting plan body or approval revision is surfaced for review and is never
|
|
261
|
-
merged by expanding authority.
|
|
262
|
-
|
|
263
|
-
Interactive command output is decision-first by default: compact cards show
|
|
264
|
-
status, selected scope, expected effect, per-record outcomes, hosted links, and
|
|
265
|
-
the next safe command. Use `--verbose` on supported human-facing commands for
|
|
266
|
-
the complete findings/evidence/payload document; use `--json` for the stable
|
|
267
|
-
machine-readable contract. Progress and warnings remain on stderr, while JSON
|
|
268
|
-
and requested report data remain clean on stdout.
|
|
269
|
-
|
|
270
|
-
Local signing keys and HMAC digests never upload. Operation before/after values
|
|
271
|
-
do upload to the paired organization because they are required for informed
|
|
272
|
-
review and hosted execution.
|
|
273
|
-
|
|
274
|
-
**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:
|
|
170
|
+
Then preview or save the plan:
|
|
275
171
|
|
|
276
172
|
```bash
|
|
277
|
-
|
|
278
|
-
fullstackgtm enrich
|
|
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
|
|
279
176
|
```
|
|
280
177
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
**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.
|
|
284
|
-
|
|
285
|
-
## Signal-based outbound: reach accounts the week something changes
|
|
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.
|
|
286
179
|
|
|
287
|
-
|
|
180
|
+
### Verify and recover
|
|
288
181
|
|
|
289
182
|
```bash
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
fullstackgtm
|
|
293
|
-
fullstackgtm
|
|
294
|
-
fullstackgtm
|
|
295
|
-
|
|
296
|
-
fullstackgtm
|
|
297
|
-
fullstackgtm signals list --since 7d # ranked triggers, each with a verbatim source quote
|
|
298
|
-
|
|
299
|
-
# 2. Decide who's worth a touch — and who isn't. Scores timing × fit × memory into send/nurture/skip.
|
|
300
|
-
fullstackgtm icp judge --signals-from latest --with-history --save
|
|
301
|
-
fullstackgtm icp eval --golden default # gate: prove the judge is calibrated before any send (exits 2 if not)
|
|
302
|
-
|
|
303
|
-
# 3. Draft the opener from the trigger. A create_task plan — proposed, never transmitted.
|
|
304
|
-
fullstackgtm draft --from-judge latest --min-score 80 --channel email --save
|
|
305
|
-
fullstackgtm plans approve <id> --operations all
|
|
306
|
-
fullstackgtm apply --plan-id <id> --provider hubspot # log the touch as a CRM task
|
|
307
|
-
fullstackgtm apply --plan-id <id> --channel outbox # OR render to the outbox for a sender — transmits nothing (docs/outbox-format.md)
|
|
308
|
-
|
|
309
|
-
# 4. Close the loop. Outcomes re-weight which signals earn a touch.
|
|
310
|
-
fullstackgtm signals outcome --account acme.com --result replied
|
|
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
|
|
311
190
|
```
|
|
312
191
|
|
|
313
|
-
|
|
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.
|
|
314
193
|
|
|
315
|
-
|
|
194
|
+
## Connectors and authentication
|
|
316
195
|
|
|
317
|
-
|
|
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 |
|
|
318
204
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
## Schedules: declare a cadence once, keep the governance contract under automation
|
|
322
|
-
|
|
323
|
-
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):
|
|
205
|
+
Secrets are accepted through environment variables, stdin, the owner-only local credential store, or a paired hosted broker—never argv flags.
|
|
324
206
|
|
|
325
207
|
```bash
|
|
326
|
-
fullstackgtm
|
|
327
|
-
fullstackgtm
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
fullstackgtm schedule run <id> # execute now; same run record a cron firing produces
|
|
331
|
-
fullstackgtm schedule status --runs 5 # last runs, exit codes, artifacts, next + missed firings
|
|
332
|
-
fullstackgtm schedule uninstall # remove the managed block, touch nothing else
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
**Scheduling never auto-approves.** Schedulable commands are read/plan-side only — `audit`, `snapshot`, `enrich append|refresh`, `enrich acquire --save`, `market capture|refresh`, `signals fetch|discover`, `icp judge`, `icp eval`, `draft`, `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 timer line you audit — crontab entry or LaunchAgent `ProgramArguments` — is always `fullstackgtm schedule run <id>` and nothing else.
|
|
336
|
-
|
|
337
|
-
`install` materializes enabled entries into the system timer; `--timer` defaults by platform. On macOS it writes one LaunchAgent plist per entry (`com.fullstackgtm.<profile>.<id>` in `~/Library/LaunchAgents`, loaded via `launchctl bootstrap`) — macOS gates `crontab` writes behind Full Disk Access, a permission no program can request, while LaunchAgents need none; launchd also coalesces firings missed during sleep into one run on wake. Re-install replaces the profile's plist fleet wholesale and never touches foreign plists. Elsewhere (or with `--timer crontab`) it renders 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: 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.
|
|
338
|
-
|
|
339
|
-
### Working across organizations
|
|
340
|
-
|
|
341
|
-
Consultants and fractional operators hold credentials for several CRMs at once. A profile scopes stored logins *and* stored plans to one organization:
|
|
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
|
|
342
212
|
|
|
343
|
-
|
|
344
|
-
fullstackgtm --profile acme login hubspot
|
|
213
|
+
# Keep client credentials and plans isolated.
|
|
345
214
|
fullstackgtm --profile acme audit --provider hubspot --save
|
|
346
|
-
fullstackgtm
|
|
215
|
+
FULLSTACKGTM_PROFILE=acme fullstackgtm plans list
|
|
347
216
|
```
|
|
348
217
|
|
|
349
|
-
|
|
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).
|
|
350
219
|
|
|
351
|
-
## Built for agents
|
|
352
|
-
|
|
353
|
-
Every command is designed to compose in an agent loop — deterministic output, machine-readable everywhere, meaningful exit codes:
|
|
220
|
+
## Built for agents
|
|
354
221
|
|
|
355
222
|
```bash
|
|
356
|
-
# Discover the machine-readable contract: every command, read-only vs
|
|
357
|
-
# write-shaped access, exit codes, safety defaults (derived from the same
|
|
358
|
-
# help table humans read, so it can't drift)
|
|
359
223
|
fullstackgtm capabilities --json
|
|
360
|
-
|
|
361
|
-
# Per-command help as JSON; plain --help stays human-shaped
|
|
362
224
|
fullstackgtm audit --help --json
|
|
363
|
-
|
|
364
|
-
# Print the shipped agent operating guide (the same SKILL.md `npx skills add` installs)
|
|
365
|
-
fullstackgtm robot-docs
|
|
366
|
-
|
|
367
|
-
# Discover what the auditor checks
|
|
368
225
|
fullstackgtm rules --json
|
|
369
|
-
|
|
370
|
-
# Fetch once (expensive), audit offline as many times as you like (cheap)
|
|
371
|
-
fullstackgtm snapshot --provider hubspot --out snap.json
|
|
372
|
-
fullstackgtm audit --input snap.json --json
|
|
373
|
-
fullstackgtm audit --input snap.json --rules stale-deal --stale-days 45 --json
|
|
374
|
-
|
|
375
|
-
# Gate a nightly CI job or agent run on hygiene: exit 2 if findings ≥ threshold
|
|
376
|
-
fullstackgtm audit --provider hubspot --fail-on warning
|
|
377
|
-
|
|
378
|
-
# Gate CI on hygiene drift instead: exit 2 only when a NEW (rule, record) finding appears
|
|
379
|
-
fullstackgtm diff --before old.json --after new.json --fail-on-new-findings
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
- 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).
|
|
383
|
-
- 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.
|
|
384
|
-
- `--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.
|
|
385
|
-
- Exit codes: `0` success, `1` error, `2` findings at/above `--fail-on`.
|
|
386
|
-
|
|
387
|
-
"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).
|
|
388
|
-
|
|
389
|
-
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.
|
|
390
|
-
|
|
391
|
-
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.
|
|
392
|
-
|
|
393
|
-
## Authentication: CLI-first, browser only at the consent moment
|
|
394
|
-
|
|
395
|
-
Credential resolution is a ladder — the first rung that yields a token wins:
|
|
396
|
-
|
|
397
|
-
1. **`--token-env <NAME>`** — explicit env var for one invocation (agent sandboxes, scripts)
|
|
398
|
-
2. **`HUBSPOT_ACCESS_TOKEN` / Salesforce env** — ambient env (CI)
|
|
399
|
-
3. **BYO direct login** — advanced token/OAuth paths stored in `~/.fullstackgtm/credentials.json` (0600; override location with `FSGTM_HOME`); these override hosted
|
|
400
|
-
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
|
|
401
|
-
|
|
402
|
-
### Teams: auth once, point every CLI at the stored sync credentials
|
|
403
|
-
|
|
404
|
-
```bash
|
|
405
|
-
fullstackgtm login --via https://gtm.yourco.com
|
|
406
|
-
# Pairing code: ABCD-2345
|
|
407
|
-
# Approve this CLI in your dashboard: https://gtm.yourco.com/dashboard/cli-auth?code=ABCD-2345
|
|
226
|
+
fullstackgtm robot-docs
|
|
408
227
|
```
|
|
409
228
|
|
|
410
|
-
|
|
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).
|
|
411
230
|
|
|
412
|
-
|
|
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.
|
|
413
232
|
|
|
414
|
-
|
|
233
|
+
## MCP server
|
|
415
234
|
|
|
416
235
|
```bash
|
|
417
|
-
#
|
|
418
|
-
|
|
419
|
-
fullstackgtm login salesforce
|
|
420
|
-
# optional: point at a self-hosted/team deployment
|
|
421
|
-
fullstackgtm login hubspot --hosted --via https://gtm.yourco.com
|
|
422
|
-
|
|
423
|
-
# Advanced HubSpot private app token (validated, then stored; token on stdin).
|
|
424
|
-
# Note: hosted HubSpot OAuth covers reads and object writes but cannot create
|
|
425
|
-
# tasks (HubSpot exposes no tasks scope to public apps) — use a private-app
|
|
426
|
-
# token if your workflow relies on `create_task` proposals.
|
|
427
|
-
echo "$HUBSPOT_TOKEN" | fullstackgtm login hubspot --private-token
|
|
428
|
-
|
|
429
|
-
# Advanced HubSpot BYO-app OAuth. Client secret is read from stdin/prompt — never a flag.
|
|
430
|
-
echo "$CLIENT_SECRET" | fullstackgtm login hubspot --oauth --client-id <id>
|
|
431
|
-
# (register http://localhost:8763/callback as a redirect URL on your app)
|
|
432
|
-
|
|
433
|
-
# Advanced Salesforce BYO Connected App device flow.
|
|
434
|
-
fullstackgtm login salesforce --device --client-id <consumer key>
|
|
435
|
-
# ...or a session token directly (token on stdin, never as a flag):
|
|
436
|
-
echo "$SF_SESSION_TOKEN" | fullstackgtm login salesforce --instance-url https://yourorg.my.salesforce.com
|
|
437
|
-
|
|
438
|
-
fullstackgtm logout hubspot # or: salesforce | broker
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
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.
|
|
442
|
-
|
|
443
|
-
Salesforce credential-bearing URLs are restricted to canonical HTTPS
|
|
444
|
-
Salesforce-owned production, sandbox, and My Domain origins. Lookalike hosts,
|
|
445
|
-
userinfo, paths/query/fragment, nonstandard ports, unsafe token-response
|
|
446
|
-
instance URLs, and credential-forwarding redirects are refused both when the
|
|
447
|
-
credential is stored and every time it is used.
|
|
448
|
-
|
|
449
|
-
## Connect your CRM
|
|
450
|
-
|
|
451
|
-
What each provider actually requires before `audit --provider <name>` works on your data.
|
|
452
|
-
|
|
453
|
-
### Connector capabilities
|
|
454
|
-
|
|
455
|
-
Connectors differ in what the provider's API allows — stated up front so nothing surprises you mid-evaluation:
|
|
456
|
-
|
|
457
|
-
| Operation | HubSpot | Salesforce | Stripe |
|
|
458
|
-
| --- | --- | --- | --- |
|
|
459
|
-
| Read / snapshot / audit | ✅ | ✅ | ✅ (read-only) |
|
|
460
|
-
| Field writes (`set_field`, `clear_field`, `link_record`) | ✅ | ✅ | — |
|
|
461
|
-
| `create_task` | ✅ | ✅ | — |
|
|
462
|
-
| `archive_record` | ✅ | ✅ | — |
|
|
463
|
-
| `merge_records` (`dedupe`) | ✅ | ✅ Account/Contact (SOAP); ❌ Opportunity | — |
|
|
464
|
-
|
|
465
|
-
**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.)
|
|
466
|
-
|
|
467
|
-
### HubSpot: create a private app (~2 minutes, needs super-admin)
|
|
468
|
-
|
|
469
|
-
1. In HubSpot: **Settings → Integrations → Private Apps → Create a private app.**
|
|
470
|
-
2. On the **Scopes** tab, grant the read scopes the audit needs:
|
|
471
|
-
- `crm.objects.owners.read`
|
|
472
|
-
- `crm.objects.companies.read`
|
|
473
|
-
- `crm.objects.contacts.read`
|
|
474
|
-
- `crm.objects.deals.read`
|
|
475
|
-
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).
|
|
476
|
-
4. Create the app, copy the token (`pat-...`), then: `echo "$TOKEN" | fullstackgtm login hubspot --private-token`.
|
|
477
|
-
|
|
478
|
-
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.
|
|
479
|
-
|
|
480
|
-
### Salesforce: a Connected App (one-time, usually needs an admin)
|
|
481
|
-
|
|
482
|
-
Device-flow login requires a Connected App in your org — if you're not an admin, this is the step to ask one for:
|
|
483
|
-
|
|
484
|
-
1. **Setup → App Manager → New Connected App**, enable OAuth settings.
|
|
485
|
-
2. Check **Enable Device Flow**.
|
|
486
|
-
3. OAuth scopes: **Manage user data via APIs (`api`)** and **Perform requests at any time (`refresh_token`)** — the CLI requests exactly these.
|
|
487
|
-
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>`.
|
|
488
|
-
|
|
489
|
-
Writeback needs no extra OAuth scope — applies are gated by the logged-in user's normal object/field permissions.
|
|
490
|
-
|
|
491
|
-
**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
|
|
492
238
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
sf org display --target-org <alias> --verbose --json
|
|
497
|
-
# then hand the accessToken + instanceUrl to the CLI (token on stdin, never a flag):
|
|
498
|
-
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
|
|
499
242
|
```
|
|
500
243
|
|
|
501
|
-
|
|
244
|
+
Nine tools are exposed over stdio.
|
|
502
245
|
|
|
503
|
-
|
|
246
|
+
**Read-only:** `fullstackgtm_audit`, `fullstackgtm_capabilities`, `fullstackgtm_rules`, `fullstackgtm_suggest`, `fullstackgtm_call_parse`, `fullstackgtm_resolve`, and `fullstackgtm_market_worksheet`.
|
|
504
247
|
|
|
505
|
-
|
|
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.
|
|
506
249
|
|
|
507
|
-
|
|
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.
|
|
508
251
|
|
|
509
|
-
|
|
510
|
-
|---|---|
|
|
511
|
-
| **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. |
|
|
512
|
-
| **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. |
|
|
513
|
-
| **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. |
|
|
514
|
-
| **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. |
|
|
515
|
-
| **Patch plan run** | The audit record of one apply attempt: per-operation applied/failed/skipped results. |
|
|
252
|
+
## Library
|
|
516
253
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
```ts
|
|
520
|
-
import {
|
|
521
|
-
auditSnapshot, auditFindingId, builtinAuditRules, defaultPolicy,
|
|
522
|
-
type GtmAuditRule,
|
|
523
|
-
} from "fullstackgtm";
|
|
524
|
-
|
|
525
|
-
const missingAmount: GtmAuditRule = {
|
|
526
|
-
id: "missing-deal-amount",
|
|
527
|
-
title: "Deal has no amount",
|
|
528
|
-
description: "Amountless deals make forecast coverage meaningless.",
|
|
529
|
-
evaluate: ({ snapshot }) => ({
|
|
530
|
-
findings: snapshot.deals
|
|
531
|
-
.filter((deal) => !deal.amount)
|
|
532
|
-
.map((deal) => ({
|
|
533
|
-
id: auditFindingId("missing-deal-amount", deal.id),
|
|
534
|
-
objectType: "deal", objectId: deal.id,
|
|
535
|
-
ruleId: "missing-deal-amount",
|
|
536
|
-
title: "Deal has no amount", severity: "warning",
|
|
537
|
-
summary: `${deal.name} has no amount.`,
|
|
538
|
-
recommendation: "Set an amount or close the deal out.",
|
|
539
|
-
})),
|
|
540
|
-
operations: [],
|
|
541
|
-
}),
|
|
542
|
-
};
|
|
543
|
-
|
|
544
|
-
const plan = auditSnapshot(snapshot, defaultPolicy(), [...builtinAuditRules, missingAmount]);
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
## Use a connector programmatically
|
|
254
|
+
The framework-independent package exports the same canonical model, rule engine, plan contract, connectors, and apply orchestrator used by the CLI.
|
|
548
255
|
|
|
549
256
|
```ts
|
|
550
257
|
import { applyPatchPlan, auditSnapshot, createHubspotConnector } from "fullstackgtm";
|
|
@@ -554,89 +261,36 @@ const hubspot = createHubspotConnector({
|
|
|
554
261
|
});
|
|
555
262
|
|
|
556
263
|
const snapshot = await hubspot.fetchSnapshot();
|
|
557
|
-
const plan = auditSnapshot(snapshot);
|
|
264
|
+
const plan = auditSnapshot(snapshot); // read-only
|
|
558
265
|
|
|
559
|
-
// Later, after
|
|
266
|
+
// Later, after review:
|
|
560
267
|
const run = await applyPatchPlan(hubspot, plan, {
|
|
561
268
|
approvedOperationIds: ["op_abc123"],
|
|
562
|
-
valueOverrides: { op_abc123: "9001" },
|
|
563
269
|
});
|
|
564
270
|
```
|
|
565
271
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
```ts
|
|
569
|
-
import type { GtmConnector } from "fullstackgtm";
|
|
570
|
-
|
|
571
|
-
const myConnector: GtmConnector = {
|
|
572
|
-
provider: "my-crm",
|
|
573
|
-
fetchSnapshot: async () => ({ /* canonical snapshot */ }),
|
|
574
|
-
applyOperation: async (operation) => ({ operationId: operation.id, status: "applied" }),
|
|
575
|
-
};
|
|
576
|
-
```
|
|
577
|
-
|
|
578
|
-
## MCP server
|
|
579
|
-
|
|
580
|
-
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:
|
|
581
|
-
|
|
582
|
-
```bash
|
|
583
|
-
# In a project
|
|
584
|
-
npm install fullstackgtm @modelcontextprotocol/sdk zod
|
|
585
|
-
HUBSPOT_ACCESS_TOKEN=pat-... npx fullstackgtm-mcp
|
|
586
|
-
|
|
587
|
-
# Zero-install (the fullstackgtm-mcp wrapper package bundles the MCP peers)
|
|
588
|
-
npx -y fullstackgtm-mcp
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
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).
|
|
592
273
|
|
|
593
|
-
|
|
594
|
-
claude mcp add fullstackgtm -e HUBSPOT_ACCESS_TOKEN=pat-... -- npx -y fullstackgtm-mcp
|
|
595
|
-
```
|
|
596
|
-
|
|
597
|
-
Or configure any MCP client (Cursor, Claude Desktop, …) with:
|
|
598
|
-
|
|
599
|
-
```json
|
|
600
|
-
{
|
|
601
|
-
"mcpServers": {
|
|
602
|
-
"fullstackgtm": {
|
|
603
|
-
"command": "npx",
|
|
604
|
-
"args": ["-y", "fullstackgtm-mcp"],
|
|
605
|
-
"env": { "HUBSPOT_ACCESS_TOKEN": "pat-..." }
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
```
|
|
274
|
+
## Appendix: additional modules and recipes
|
|
610
275
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
**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).
|
|
614
|
-
|
|
615
|
-
**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).
|
|
616
|
-
|
|
617
|
-
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.
|
|
618
277
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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).
|
|
623
286
|
|
|
624
|
-
##
|
|
287
|
+
## License, boundary, and development
|
|
625
288
|
|
|
626
|
-
|
|
627
|
-
2. Every proposed write is a typed patch operation with before/after values, a reason, and a risk level.
|
|
628
|
-
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.
|
|
629
290
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
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.
|
|
633
|
-
|
|
634
|
-
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.
|
|
635
|
-
|
|
636
|
-
## Development
|
|
291
|
+
Public API stability and the open-core boundary are documented in [docs/api.md](./docs/api.md) and [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
637
292
|
|
|
638
293
|
```bash
|
|
639
|
-
npm run build
|
|
294
|
+
npm run build
|
|
295
|
+
npm test
|
|
640
296
|
```
|
|
641
|
-
|
|
642
|
-
Tests live in the repository root `tests/` directory and run with `npm test`.
|