fullstackgtm 0.47.0 → 0.49.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 +76 -6
- package/CONTRIBUTING.md +23 -4
- package/DATA-FLOWS.md +7 -2
- package/INSTALL_FOR_AGENTS.md +15 -6
- package/README.md +28 -9
- package/SECURITY.md +27 -3
- package/dist/audit.js +7 -0
- package/dist/backfill.js +2 -16
- package/dist/cli/audit.js +10 -7
- package/dist/cli/auth.js +8 -4
- package/dist/cli/fix.d.ts +3 -0
- package/dist/cli/fix.js +90 -8
- package/dist/cli/help.d.ts +6 -0
- package/dist/cli/help.js +81 -3
- package/dist/cli/market.js +180 -2
- package/dist/cli/plans.js +56 -5
- package/dist/cli/suggest.d.ts +2 -1
- package/dist/cli/suggest.js +49 -3
- package/dist/cli/ui.js +2 -0
- package/dist/cli.js +41 -16
- package/dist/config.d.ts +13 -1
- package/dist/config.js +23 -2
- package/dist/connectors/hubspot.d.ts +2 -1
- package/dist/connectors/prospectSources.js +4 -11
- package/dist/connectors/salesforce.d.ts +2 -1
- package/dist/connectors/salesforce.js +12 -5
- package/dist/connectors/salesforceAuth.d.ts +9 -0
- package/dist/connectors/salesforceAuth.js +47 -5
- package/dist/connectors/signalSources.js +2 -11
- package/dist/connectors/theirstack.d.ts +0 -19
- package/dist/connectors/theirstack.js +3 -10
- package/dist/credentials.js +36 -26
- package/dist/format.js +31 -5
- package/dist/freeEmailDomains.d.ts +1 -0
- package/dist/freeEmailDomains.js +14 -0
- package/dist/hierarchy.d.ts +29 -0
- package/dist/hierarchy.js +164 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +8 -3
- package/dist/market.d.ts +1 -1
- package/dist/market.js +7 -127
- package/dist/marketClassify.d.ts +10 -0
- package/dist/marketClassify.js +52 -1
- package/dist/marketSourcing.js +7 -45
- package/dist/mcp.js +86 -130
- package/dist/planStore.d.ts +28 -1
- package/dist/planStore.js +195 -49
- package/dist/providerError.d.ts +21 -0
- package/dist/providerError.js +30 -0
- package/dist/publicHttp.d.ts +28 -0
- package/dist/publicHttp.js +143 -0
- package/dist/relationships.d.ts +39 -0
- package/dist/relationships.js +101 -0
- package/dist/route.d.ts +46 -0
- package/dist/route.js +228 -0
- package/dist/rules.d.ts +1 -0
- package/dist/rules.js +172 -12
- package/dist/secureFile.d.ts +15 -0
- package/dist/secureFile.js +164 -0
- package/dist/types.d.ts +16 -1
- package/docs/api.md +49 -5
- package/docs/architecture.md +18 -4
- package/llms.txt +7 -0
- package/package.json +5 -3
- package/skills/fullstackgtm/SKILL.md +9 -3
- package/src/audit.ts +7 -0
- package/src/backfill.ts +2 -17
- package/src/cli/audit.ts +10 -7
- package/src/cli/auth.ts +8 -4
- package/src/cli/fix.ts +96 -8
- package/src/cli/help.ts +88 -3
- package/src/cli/market.ts +181 -2
- package/src/cli/plans.ts +66 -5
- package/src/cli/suggest.ts +58 -4
- package/src/cli/ui.ts +1 -0
- package/src/cli.ts +39 -14
- package/src/config.ts +39 -1
- package/src/connectors/hubspot.ts +3 -1
- package/src/connectors/prospectSources.ts +4 -11
- package/src/connectors/salesforce.ts +15 -6
- package/src/connectors/salesforceAuth.ts +46 -6
- package/src/connectors/signalSources.ts +2 -12
- package/src/connectors/theirstack.ts +3 -10
- package/src/credentials.ts +47 -28
- package/src/format.ts +33 -5
- package/src/freeEmailDomains.ts +14 -0
- package/src/hierarchy.ts +185 -0
- package/src/index.ts +29 -0
- package/src/market.ts +7 -111
- package/src/marketClassify.ts +61 -1
- package/src/marketSourcing.ts +7 -43
- package/src/mcp.ts +115 -152
- package/src/planStore.ts +235 -57
- package/src/providerError.ts +37 -0
- package/src/publicHttp.ts +147 -0
- package/src/relationships.ts +115 -0
- package/src/route.ts +278 -0
- package/src/rules.ts +192 -12
- package/src/secureFile.ts +169 -0
- package/src/types.ts +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,10 +3,80 @@
|
|
|
3
3
|
All notable changes to the `fullstackgtm` package are documented here.
|
|
4
4
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
5
|
and the project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
|
-
The path to 1.0 is planned in
|
|
6
|
+
The path to 1.0 is planned in [docs/roadmap-to-1.0.md](https://github.com/fullstackgtm/core/blob/main/docs/roadmap-to-1.0.md).
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.49.0] — 2026-07-10
|
|
11
|
+
|
|
12
|
+
### Security
|
|
13
|
+
|
|
14
|
+
- MCP apply now accepts only store-backed, HMAC-approved plan ids; external
|
|
15
|
+
plan paths and caller-supplied approvals/value overrides are rejected.
|
|
16
|
+
- Apply uses an atomic single-owner claim and durable attempt journal.
|
|
17
|
+
Uncertain attempts remain fail-closed until an operator acknowledges provider
|
|
18
|
+
state; recovery clears every approval and never replays writes automatically.
|
|
19
|
+
- Salesforce credential-bearing endpoints are restricted to canonical HTTPS
|
|
20
|
+
Salesforce origins and redirects are handled without forwarding credentials.
|
|
21
|
+
- Executable rule packages require explicit CLI trust and are disabled entirely
|
|
22
|
+
in MCP. Implicit repository configuration cannot activate plugins.
|
|
23
|
+
- Sensitive local files use atomic no-follow writes and verified no-follow
|
|
24
|
+
reads; symlinked credential, plan, and managed-home paths are refused.
|
|
25
|
+
- Market capture/sourcing uses DNS-pinned public-only HTTP with per-hop redirect
|
|
26
|
+
validation, bounded responses, and cross-origin credential stripping.
|
|
27
|
+
- Provider HTTP failures expose typed status metadata without raw response
|
|
28
|
+
bodies, reflected secrets, or PII.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- **Breaking pre-1.0 plan-store contract:** `PlanStore` implementations now
|
|
33
|
+
participate in apply claims, attempt recovery, and claim-bound run recording;
|
|
34
|
+
`ApprovalStatus` includes `applying`. Custom stores must implement the new
|
|
35
|
+
lifecycle before upgrading.
|
|
36
|
+
- Public mirror generation is commit-derived, allowlisted, secret-scanned, and
|
|
37
|
+
fail-closed. npm publishing uses pinned actions/npm plus a protected release
|
|
38
|
+
environment and provenance verification.
|
|
39
|
+
- Package CI now validates Node 20/22, Linux/macOS/Windows compiled runtimes,
|
|
40
|
+
and actual installed tarballs with and without optional MCP peers.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- `bulk-update --help` and `-h` now return focused help before parsing an object
|
|
45
|
+
type or touching configuration, credentials, disk, or network.
|
|
46
|
+
- Installation guidance no longer pins a brittle exact demo finding count.
|
|
47
|
+
|
|
48
|
+
## [0.48.0] — 2026-07-08
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- Added `GtmAuditRule.emitsOperations` and `isFindingsOnlyRule` so hosts can derive findings-only rule behavior from the package registry instead of duplicating rule IDs.
|
|
53
|
+
- **Route, hierarchy, and relationships open surfaces.** `route leads` builds governed lead-to-account/owner patch plans; `hierarchy report` and `relationships account` provide read-only planning reports for account trees and stakeholder maps.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- **CLI human path now uses progressive disclosure by default.** Bare
|
|
58
|
+
`fullstackgtm`/`--help` shows a compact lifecycle-grouped command map instead
|
|
59
|
+
of the full flag wall; `help <command>` and `<command> --help` show focused
|
|
60
|
+
per-command guidance; `help --full` (including `help <command> --full`) keeps
|
|
61
|
+
the complete reference available. `audit` now defaults to a summary view with
|
|
62
|
+
`--full` for per-operation detail and prints next-step guidance on stderr for
|
|
63
|
+
human runs while keeping `--json` machine-clean. The dry-run footer now frames
|
|
64
|
+
approval/write safety as invariants rather than prototype copy.
|
|
65
|
+
- **Patch-plan assumptions and open questions are canonical.** Plan exports and
|
|
66
|
+
markdown use `assumptions` plus `openQuestions`; the duplicate
|
|
67
|
+
`decisionPoints` alias was removed.
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- **Route plan safety and batching.** Owner routing recognizes both canonical
|
|
72
|
+
user ids and provider CRM owner ids, skips name-only account matches as
|
|
73
|
+
ambiguous, avoids self-conflicting sibling preconditions, groups only contacts
|
|
74
|
+
with multiple operations, and uses one shared free-email domain list.
|
|
75
|
+
- **Hierarchy and relationships correctness.** Hierarchy reports dedupe duplicate
|
|
76
|
+
account ids, avoid common public-suffix parent guesses, and rely on a single
|
|
77
|
+
cycle-dedupe pass. Relationship `--domain` selectors normalize domains and
|
|
78
|
+
markdown preserves zero-value deal amounts.
|
|
79
|
+
|
|
10
80
|
## [0.47.0] — 2026-07-06
|
|
11
81
|
|
|
12
82
|
### Added
|
|
@@ -117,7 +187,7 @@ The path to 1.0 is planned in the roadmap doc in the repository.
|
|
|
117
187
|
doctor, rules, dedupe, resolve, snapshot, and error paths) and the public
|
|
118
188
|
import surface of `cli.ts` is unchanged. Profiling method, opportunity
|
|
119
189
|
matrix, and honest non-wins are documented in
|
|
120
|
-
[docs/perf-notes.md](
|
|
190
|
+
[docs/perf-notes.md](https://github.com/fullstackgtm/core/blob/main/docs/perf-notes.md).
|
|
121
191
|
|
|
122
192
|
## [0.45.0] — 2026-07-02
|
|
123
193
|
|
|
@@ -455,7 +525,7 @@ The path to 1.0 is planned in the roadmap doc in the repository.
|
|
|
455
525
|
Library: `connectors/signalSources.ts` (`listSignalSources`, `getSignalSource`,
|
|
456
526
|
`fileSource`, `serpapiNewsSource`, `hubspotFormsSource`), `stagedRowToSignal` +
|
|
457
527
|
`StagedSignalRow` from `signals.ts`. Design:
|
|
458
|
-
[docs/spec-connectors-signals-outbound.md](
|
|
528
|
+
[docs/spec-connectors-signals-outbound.md](https://github.com/fullstackgtm/core/blob/main/docs/spec-connectors-signals-outbound.md)
|
|
459
529
|
(the connector taxonomy + the Phase 2/3 webhook-spool and governed-channel plan).
|
|
460
530
|
- **Webhook spool format + conventional landing zone (signals Phase 2).**
|
|
461
531
|
`--connector file` with no path now reads the conventional spool directory
|
|
@@ -844,7 +914,7 @@ The path to 1.0 is planned in the roadmap doc in the repository.
|
|
|
844
914
|
you approve specific operations and run `apply`… These safety invariants are
|
|
845
915
|
not beta."), matching the README and agent skill.
|
|
846
916
|
|
|
847
|
-
See [docs/dx-punch-list.md](
|
|
917
|
+
See [docs/dx-punch-list.md](https://github.com/fullstackgtm/core/blob/main/docs/dx-punch-list.md) for the audit that drove this.
|
|
848
918
|
|
|
849
919
|
## [0.34.0] — 2026-06-18
|
|
850
920
|
|
|
@@ -1935,7 +2005,7 @@ milestones, not a frozen public contract. 0.10.0 continues the 0.x line
|
|
|
1935
2005
|
1.2.2 plus the status/docs corrections below. `fullstackgtm@1.2.1` and
|
|
1936
2006
|
`@1.2.2` were briefly on npm (2026-06-10) and were unpublished the same
|
|
1937
2007
|
week; those version numbers are permanently burned per npm policy. The real
|
|
1938
|
-
1.0 will be declared via [docs/roadmap-to-1.0.md](
|
|
2008
|
+
1.0 will be declared via [docs/roadmap-to-1.0.md](https://github.com/fullstackgtm/core/blob/main/docs/roadmap-to-1.0.md)
|
|
1939
2009
|
once the API surface has survived external usage.
|
|
1940
2010
|
|
|
1941
2011
|
### Changed
|
|
@@ -2230,7 +2300,7 @@ stability commitment attached.
|
|
|
2230
2300
|
|
|
2231
2301
|
- The hosted app's Convex plan tables are the second `PlanStore`
|
|
2232
2302
|
implementation target; unifying them onto these types is the remaining
|
|
2233
|
-
0.3.x work (see docs/roadmap-to-1.0.md).
|
|
2303
|
+
0.3.x work (see https://github.com/fullstackgtm/core/blob/main/docs/roadmap-to-1.0.md).
|
|
2234
2304
|
|
|
2235
2305
|
## [0.2.0] — Unreleased
|
|
2236
2306
|
|
package/CONTRIBUTING.md
CHANGED
|
@@ -44,6 +44,21 @@ node --experimental-strip-types --test tests/fullstackgtm*.test.ts # from the
|
|
|
44
44
|
node packages/fullstackgtm/src/bin.ts doctor # run the CLI locally
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
Before a release-facing change, verify the artifact rather than only its source
|
|
48
|
+
tree:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cd packages/fullstackgtm
|
|
52
|
+
npm run build
|
|
53
|
+
node scripts/verify-packed-install.mjs # isolated install, optional peers absent
|
|
54
|
+
node scripts/verify-packed-install.mjs --with-peers # isolated install, MCP peers present
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The verifier creates the real npm tarball, enforces required/prohibited paths,
|
|
58
|
+
imports the public export, checks its declaration entrypoint, and runs both
|
|
59
|
+
installed binaries. CI repeats compiled-runtime checks on Node 20 and 22 across
|
|
60
|
+
Linux, macOS, and Windows; source-level tests remain on Node 22.6+.
|
|
61
|
+
|
|
47
62
|
> **Tests live at the monorepo root `tests/`, not in the package.** Running
|
|
48
63
|
> `npm test` *inside* `packages/fullstackgtm` deliberately fails with a pointer
|
|
49
64
|
> (a `pretest` guard) rather than silently passing with zero tests. On the
|
|
@@ -78,13 +93,17 @@ the development monorepo). From the monorepo:
|
|
|
78
93
|
|
|
79
94
|
1. Bump `packages/fullstackgtm/package.json` version + add a `CHANGELOG.md`
|
|
80
95
|
entry; merge to `main`.
|
|
81
|
-
2. `scripts/sync-oss.sh
|
|
82
|
-
|
|
96
|
+
2. Run `scripts/sync-oss.sh` to export an allowlisted mirror from the full
|
|
97
|
+
source commit, build `dist/`, scan for secrets, and inspect the complete
|
|
98
|
+
staged diff. Then rerun with `--push` after review. Clone failures stop the
|
|
99
|
+
release; only a genuine first publication may use the separately confirmed
|
|
100
|
+
`--initialize-public-repo` mode.
|
|
83
101
|
3. Re-check the mirror's `package.json` version, then tag `vX.Y.Z` on the mirror
|
|
84
102
|
and push the tag.
|
|
85
103
|
4. GitHub Actions (`release.yml`) publishes to npm via **OIDC trusted
|
|
86
|
-
publishing** — no tokens.
|
|
87
|
-
|
|
104
|
+
publishing** — no tokens. The protected `npm-production` environment must
|
|
105
|
+
require an independent reviewer. The workflow rebuilds from source and
|
|
106
|
+
refuses to publish if committed `dist/` differs (supply-chain gate).
|
|
88
107
|
|
|
89
108
|
**Access a new co-maintainer needs to cut a release:** write access to
|
|
90
109
|
`github.com/fullstackgtm/core` (for `sync-oss.sh --push` and the tag push). No
|
package/DATA-FLOWS.md
CHANGED
|
@@ -10,7 +10,8 @@ is no fullstackgtm-operated server in the data path for the open package.**
|
|
|
10
10
|
|
|
11
11
|
- CRM snapshots, patch plans, approvals, apply-run records, market captures and
|
|
12
12
|
observations, enrich run state, and the signing/credential stores all live
|
|
13
|
-
under `$FSGTM_HOME` (default `~/.fullstackgtm`), `0600`/`0700
|
|
13
|
+
under `$FSGTM_HOME` (default `~/.fullstackgtm`), `0600`/`0700`, using atomic
|
|
14
|
+
no-follow file I/O. Nothing is
|
|
14
15
|
uploaded to Full Stack GTM.
|
|
15
16
|
- No telemetry, analytics, or phone-home. The core package has zero runtime
|
|
16
17
|
dependencies; the only network calls are the ones listed below, all to
|
|
@@ -23,12 +24,16 @@ is no fullstackgtm-operated server in the data path for the open package.**
|
|
|
23
24
|
| `snapshot`, `audit`, `apply`, `resolve`, `bulk-update`, `dedupe`, `reassign`, `fix`, `enrich` (writeback) | **Your CRM** (HubSpot / Salesforce / Stripe API) | Reads: your CRM records. Writes: only approved patch operations. | Your CRM token (env / stored / broker) |
|
|
24
25
|
| `call parse`, `call score`, `market classify`, `market refresh` | **Your LLM provider** (api.anthropic.com or api.openai.com) | The call transcript / captured competitor page text you point at, plus the extraction prompt | Your `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` (BYO) |
|
|
25
26
|
| `enrich append --source apollo`, `enrich refresh` | **Apollo** (api.apollo.io) | The company domain / contact email being enriched | Your `APOLLO_API_KEY` (BYO) |
|
|
26
|
-
| `market capture`, `market refresh` | **Public vendor websites** you list in `market.config.json` |
|
|
27
|
+
| `market capture`, `market refresh` | **Public vendor websites** you list in `market.config.json` | A bounded HTTP GET; DNS answers are validated and pinned, mixed/private/reserved answers are refused, and every redirect is revalidated | none |
|
|
27
28
|
| `login --via <url>` (optional) | **Your hosted deployment's broker** | A pairing handshake; the broker mints short-lived CRM tokens | broker pairing token |
|
|
28
29
|
|
|
29
30
|
Commands not listed (`plans`, `rules`, `doctor`, `schedule`, `audit-log`,
|
|
30
31
|
`diff`, `merge`, report rendering) make **no network calls**.
|
|
31
32
|
|
|
33
|
+
Provider HTTP failures persist only typed provider/operation/status/retryability
|
|
34
|
+
metadata. Raw third-party response bodies—which may reflect credentials,
|
|
35
|
+
filters, or PII—are not written to run records or normal CLI/MCP errors.
|
|
36
|
+
|
|
32
37
|
## Avoiding third-party data egress
|
|
33
38
|
|
|
34
39
|
- **LLM verbs are optional.** `call parse --deterministic` uses a free,
|
package/INSTALL_FOR_AGENTS.md
CHANGED
|
@@ -33,9 +33,11 @@ machine — that is normal and does not block the next step.
|
|
|
33
33
|
fullstackgtm audit --demo --json
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Expect a JSON patch plan with `dryRun: true
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
Expect a JSON patch plan with `dryRun: true`, a non-empty `findings` array, and
|
|
37
|
+
matching proposed operations over a generated, deliberately messy CRM. The
|
|
38
|
+
exact count can grow as built-in rules are added; the stable contract is
|
|
39
|
+
determinism per release and seed. `--seed 7` is the default, so two runs on the
|
|
40
|
+
same version produce identical finding and operation ids. Exit code 0.
|
|
39
41
|
|
|
40
42
|
This proves the whole pipeline (snapshot → audit → plan) without any account.
|
|
41
43
|
|
|
@@ -131,12 +133,18 @@ the server resolves them from there (peer-dependency semantics) — so this
|
|
|
131
133
|
works from inside existing projects too.
|
|
132
134
|
|
|
133
135
|
Tools exposed over stdio — read-only: `fullstackgtm_audit`,
|
|
134
|
-
`
|
|
135
|
-
`fullstackgtm_resolve`, `fullstackgtm_market_worksheet`. Gated:
|
|
136
|
-
`fullstackgtm_apply` (
|
|
136
|
+
`fullstackgtm_capabilities`, `fullstackgtm_rules`, `fullstackgtm_suggest`,
|
|
137
|
+
`fullstackgtm_call_parse`, `fullstackgtm_resolve`, `fullstackgtm_market_worksheet`. Gated:
|
|
138
|
+
`fullstackgtm_apply` (stored `planId` only; approvals and values must already be
|
|
139
|
+
human-approved and HMAC-signed in the local plan store),
|
|
137
140
|
`fullstackgtm_market_observe` (every quoted span is verified against the
|
|
138
141
|
stored captures before anything is appended).
|
|
139
142
|
|
|
143
|
+
MCP cannot execute rule packages, apply external plan files, or supply its own
|
|
144
|
+
approvals/value overrides. For an interrupted apply, reconcile provider state,
|
|
145
|
+
then run `fullstackgtm plans recover <id> --acknowledge-uncertain-writes`; this
|
|
146
|
+
replays nothing and clears approvals so a fresh review is required.
|
|
147
|
+
|
|
140
148
|
## Troubleshooting
|
|
141
149
|
|
|
142
150
|
| Symptom | Fix |
|
|
@@ -145,3 +153,4 @@ stored captures before anything is appended).
|
|
|
145
153
|
| `No HubSpot credentials` (exit 1) | Set `HUBSPOT_ACCESS_TOKEN` or have a human run `fullstackgtm login hubspot --hosted` |
|
|
146
154
|
| MCP server prints peer-dependency help | Install `@modelcontextprotocol/sdk` and `zod` (see step 6) |
|
|
147
155
|
| Need machine state for orchestration | `fullstackgtm doctor --json` |
|
|
156
|
+
| Plan is `applying` after an interrupted request | Reconcile provider state, then run `fullstackgtm plans recover <id> --acknowledge-uncertain-writes`; review and approve again |
|
package/README.md
CHANGED
|
@@ -2,19 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/fullstackgtm/core/actions/workflows/ci.yml) [](https://www.npmjs.com/package/fullstackgtm) [](./LICENSE)
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Guardrails for AI agents working in your CRM.**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Let Claude or Codex loose on Salesforce or HubSpot and it will make predictable mistakes — duplicates, bad batch edits, silent overwrites. fullstackgtm is the wrapper that knows those mistakes: every change becomes a reviewable plan before anything is written.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Instead of overwriting records, an agent using fullstackgtm sets out a **patch plan**: what it wants to change, field by field, with before/after values and the reason. You review it, roll it out incrementally (approve 10 operations, then 100, then all), and every write records the before value — so a mistake is visible and reversible, not silently absorbed. Think `terraform plan` for your CRM.
|
|
10
|
+
|
|
11
|
+
**What it catches:** duplicates, orphaned records, bad batch edits, stale deals, missing owners — deterministic rules, not LLM guesses. Works as a CLI, a library, or an MCP server your agent calls directly.
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
Open source (Apache-2.0), zero runtime dependencies. Beta (0.x): APIs may shift before 1.0; the safety invariants never do. Connectors: HubSpot, Salesforce (read/write), Stripe (read-only).
|
|
12
14
|
|
|
13
15
|
## Install
|
|
14
16
|
|
|
15
17
|
```bash
|
|
18
|
+
npx fullstackgtm audit --demo # zero-install try-it path
|
|
16
19
|
npm install fullstackgtm # library + CLI in a project
|
|
17
|
-
npx fullstackgtm audit --demo # or zero-install via npx
|
|
18
20
|
npm install github:fullstackgtm/core # or straight from this repo (project-local)
|
|
19
21
|
npx github:fullstackgtm/core audit --demo # zero-install from the repo
|
|
20
22
|
```
|
|
@@ -315,7 +317,7 @@ fullstackgtm diff --before old.json --after new.json --fail-on-new-findings
|
|
|
315
317
|
- `--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.
|
|
316
318
|
- Exit codes: `0` success, `1` error, `2` findings at/above `--fail-on`.
|
|
317
319
|
|
|
318
|
-
"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](
|
|
320
|
+
"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).
|
|
319
321
|
|
|
320
322
|
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.
|
|
321
323
|
|
|
@@ -371,6 +373,12 @@ fullstackgtm logout hubspot # or: salesforce | broker
|
|
|
371
373
|
|
|
372
374
|
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.
|
|
373
375
|
|
|
376
|
+
Salesforce credential-bearing URLs are restricted to canonical HTTPS
|
|
377
|
+
Salesforce-owned production, sandbox, and My Domain origins. Lookalike hosts,
|
|
378
|
+
userinfo, paths/query/fragment, nonstandard ports, unsafe token-response
|
|
379
|
+
instance URLs, and credential-forwarding redirects are refused both when the
|
|
380
|
+
credential is stored and every time it is used.
|
|
381
|
+
|
|
374
382
|
## Connect your CRM
|
|
375
383
|
|
|
376
384
|
What each provider actually requires before `audit --provider <name>` works on your data.
|
|
@@ -533,20 +541,31 @@ Or configure any MCP client (Cursor, Claude Desktop, …) with:
|
|
|
533
541
|
}
|
|
534
542
|
```
|
|
535
543
|
|
|
536
|
-
|
|
544
|
+
Nine tools are exposed over stdio.
|
|
537
545
|
|
|
538
|
-
**Read-only:** `fullstackgtm_audit` (sample, demo, file, or live provider sources with optional rule scoping), `fullstackgtm_rules` (rule discovery), `fullstackgtm_suggest` (deterministic placeholder values with confidence + reasons), `fullstackgtm_call_parse` (transcripts → provenance-marked segments, insights, and evidence), `fullstackgtm_resolve` (the create gate: exists / ambiguous / safe_to_create), and `fullstackgtm_market_worksheet` (the classification packet for one vendor: claims, judging rules, captured page texts).
|
|
546
|
+
**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).
|
|
539
547
|
|
|
540
|
-
**Gated:** `fullstackgtm_apply` (
|
|
548
|
+
**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).
|
|
541
549
|
|
|
542
550
|
Tokens stored via `fullstackgtm login` are picked up automatically — the env var is only needed when no stored login exists.
|
|
543
551
|
|
|
552
|
+
Rule packages are executable JavaScript and are disabled by default. In the CLI,
|
|
553
|
+
review the modules and use `--config <path> --allow-plugins`; use `--no-plugins`
|
|
554
|
+
to apply only declarative config. MCP never executes rule packages: mutable
|
|
555
|
+
tool-call paths are not a durable code-trust boundary.
|
|
556
|
+
|
|
544
557
|
## Safety model
|
|
545
558
|
|
|
546
559
|
1. Reads are safe by default; audits never mutate anything.
|
|
547
560
|
2. Every proposed write is a typed patch operation with before/after values, a reason, and a risk level.
|
|
548
561
|
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.
|
|
549
562
|
|
|
563
|
+
## License & boundary
|
|
564
|
+
|
|
565
|
+
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.
|
|
566
|
+
|
|
567
|
+
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.
|
|
568
|
+
|
|
550
569
|
## Development
|
|
551
570
|
|
|
552
571
|
```bash
|
package/SECURITY.md
CHANGED
|
@@ -21,7 +21,10 @@ newest version, not backported (the project is pre-1.0).
|
|
|
21
21
|
**Credentials.** API tokens are never accepted as command-line arguments
|
|
22
22
|
(they would leak into the process table and shell history); they come from an
|
|
23
23
|
environment variable or stdin only, and are stored `0600` under a `0700` home
|
|
24
|
-
(`$FSGTM_HOME`, default `~/.fullstackgtm`)
|
|
24
|
+
(`$FSGTM_HOME`, default `~/.fullstackgtm`). Writes use exclusive temporary
|
|
25
|
+
files, fsync, and atomic rename; reads require regular files opened without
|
|
26
|
+
following symlinks. Managed-home, credential, and plan-store symlink paths are
|
|
27
|
+
refused before chmod/read/write. This is the
|
|
25
28
|
same custody model as the `gcloud`/`aws` CLIs. The hosted broker
|
|
26
29
|
(`login --via`) exists so a team can connect a CRM once, server-side, and hand
|
|
27
30
|
laptops only a revocable pairing token instead of a long-lived super-admin key;
|
|
@@ -57,13 +60,34 @@ approved on one machine cannot be applied on another (the key does not travel).
|
|
|
57
60
|
Documented boundary: this defends the plan file, not an attacker who already
|
|
58
61
|
holds the signing key (same directory and permissions as the credential store).
|
|
59
62
|
|
|
63
|
+
**Single-owner apply and recovery.** Store-backed CLI and MCP writes atomically
|
|
64
|
+
claim an approved plan and journal the provider, caller surface, and attempt
|
|
65
|
+
state before provider I/O. A second process cannot apply the same approval.
|
|
66
|
+
Failures after provider I/O remain `applying`/uncertain because a timeout cannot
|
|
67
|
+
prove no remote write landed. Recovery never auto-replays: after reconciling CRM
|
|
68
|
+
state, `plans recover <id> --acknowledge-uncertain-writes` clears every approval
|
|
69
|
+
and signature and requires a fresh human review.
|
|
70
|
+
|
|
71
|
+
**Executable configuration.** `rulePackages` are arbitrary JavaScript. An
|
|
72
|
+
implicitly discovered repository config cannot activate them; CLI execution
|
|
73
|
+
requires explicit `--config <path> --allow-plugins` (`--no-plugins` disables
|
|
74
|
+
them). MCP never executes rule packages or accepts external plans/caller
|
|
75
|
+
approvals.
|
|
76
|
+
|
|
77
|
+
**Credential origins.** Salesforce access and refresh tokens are sent only to
|
|
78
|
+
canonical HTTPS Salesforce-owned origins. Userinfo, paths/query/fragment,
|
|
79
|
+
nonstandard ports, lookalike domains, unsafe token-response instance URLs, and
|
|
80
|
+
credential-preserving redirects are refused at input, storage, and use.
|
|
81
|
+
|
|
60
82
|
**Scheduling never auto-approves.** Scheduled (cron) runs are restricted to a
|
|
61
83
|
read/plan-side allowlist plus `apply --plan-id` whose approved status and
|
|
62
84
|
signatures are re-checked at every firing. Arbitrary shell is not schedulable.
|
|
63
85
|
|
|
64
86
|
**Untrusted input.** Competitor pages fetched by `market capture` are guarded
|
|
65
|
-
against SSRF
|
|
66
|
-
|
|
87
|
+
against SSRF: every DNS answer must be globally routable, validated answers are
|
|
88
|
+
pinned into socket creation, mixed/private/reserved answers are refused, every
|
|
89
|
+
redirect is revalidated, sensitive headers are stripped cross-origin, and
|
|
90
|
+
time/body/redirect limits are enforced. LLM-extracted call insights and market
|
|
67
91
|
classifications are mechanically verified verbatim against the source text
|
|
68
92
|
before they can drive a writeback, so a prompt-injected transcript or page
|
|
69
93
|
cannot fabricate a grounded-looking change. CSV/formula-injection in ingested
|
package/dist/audit.js
CHANGED
|
@@ -33,6 +33,7 @@ export function auditSnapshot(snapshot, policy = defaultPolicy(), rules = builti
|
|
|
33
33
|
const context = { snapshot, policy, index: buildSnapshotIndex(snapshot) };
|
|
34
34
|
const findings = [];
|
|
35
35
|
const operations = [];
|
|
36
|
+
const assumptionsById = new Map();
|
|
36
37
|
for (const rule of rules) {
|
|
37
38
|
try {
|
|
38
39
|
onRule?.(rule.id, "start");
|
|
@@ -43,6 +44,10 @@ export function auditSnapshot(snapshot, policy = defaultPolicy(), rules = builti
|
|
|
43
44
|
const result = rule.evaluate(context);
|
|
44
45
|
findings.push(...result.findings);
|
|
45
46
|
operations.push(...result.operations);
|
|
47
|
+
for (const assumption of result.assumptions ?? []) {
|
|
48
|
+
if (!assumptionsById.has(assumption.id))
|
|
49
|
+
assumptionsById.set(assumption.id, assumption);
|
|
50
|
+
}
|
|
46
51
|
try {
|
|
47
52
|
onRule?.(rule.id, "done", result.findings.length);
|
|
48
53
|
}
|
|
@@ -50,6 +55,7 @@ export function auditSnapshot(snapshot, policy = defaultPolicy(), rules = builti
|
|
|
50
55
|
// progress is presentation-only
|
|
51
56
|
}
|
|
52
57
|
}
|
|
58
|
+
const assumptions = Array.from(assumptionsById.values());
|
|
53
59
|
const evidence = buildEvidence(snapshot, findings, policy.today);
|
|
54
60
|
const pipelineFindings = buildPipelineFindings(findings, operations, evidence, policy.today);
|
|
55
61
|
linkOperationContext(operations, findings, evidence);
|
|
@@ -63,6 +69,7 @@ export function auditSnapshot(snapshot, policy = defaultPolicy(), rules = builti
|
|
|
63
69
|
findings,
|
|
64
70
|
pipelineFindings,
|
|
65
71
|
evidence,
|
|
72
|
+
assumptions: assumptions.length > 0 ? assumptions : undefined,
|
|
66
73
|
operations,
|
|
67
74
|
};
|
|
68
75
|
}
|
package/dist/backfill.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* connector re-resolves each invoice id (resolve-first) and creates only on
|
|
12
12
|
* a confirmed miss.
|
|
13
13
|
*/
|
|
14
|
+
import { FREE_EMAIL_DOMAINS } from "./freeEmailDomains.js";
|
|
14
15
|
import { normalizeDomain } from "./merge.js";
|
|
15
16
|
// Mirrors stableHash in rules.ts (FNV-1a); duplicated to keep backfill.ts
|
|
16
17
|
// importable without pulling the audit engine (the enrich.ts precedent).
|
|
@@ -23,21 +24,6 @@ function fnv1a(value) {
|
|
|
23
24
|
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
24
25
|
}
|
|
25
26
|
export const DEFAULT_BACKFILL_MATCH_PROPERTY = "stripe_invoice_id";
|
|
26
|
-
// Freemail domains never become a company domain: stamping "gmail.com" on an
|
|
27
|
-
// account (or resolving a company BY gmail.com) would collapse unrelated
|
|
28
|
-
// customers onto one record. Mirrors FREE_MAIL in connectors/signalSources.ts
|
|
29
|
-
// (duplicated to keep backfill.ts importable without connector code, the
|
|
30
|
-
// fnv1a precedent above).
|
|
31
|
-
const FREE_MAIL = new Set([
|
|
32
|
-
"gmail.com",
|
|
33
|
-
"yahoo.com",
|
|
34
|
-
"hotmail.com",
|
|
35
|
-
"outlook.com",
|
|
36
|
-
"icloud.com",
|
|
37
|
-
"aol.com",
|
|
38
|
-
"proton.me",
|
|
39
|
-
"protonmail.com",
|
|
40
|
-
]);
|
|
41
27
|
/** `Invoice <number || id> — <customer name>` (the plan-time dedupe name). */
|
|
42
28
|
function backfillDealName(invoice, companyName) {
|
|
43
29
|
return `Invoice ${invoice.number ?? invoice.id} — ${invoice.customerName ?? companyName}`;
|
|
@@ -102,7 +88,7 @@ export function buildStripeBackfillPlan(invoices, snapshot, opts = {}) {
|
|
|
102
88
|
let companyDomain = normalizeDomain(account?.domain);
|
|
103
89
|
let accountIsNew = false;
|
|
104
90
|
if (!account && createMissingAccounts) {
|
|
105
|
-
const usableDomain = domain && !
|
|
91
|
+
const usableDomain = domain && !FREE_EMAIL_DOMAINS.has(domain) ? domain : undefined;
|
|
106
92
|
const name = invoice.customerName?.trim() || usableDomain;
|
|
107
93
|
if (name) {
|
|
108
94
|
companyName = name;
|
package/dist/cli/audit.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { auditSnapshot, defaultPolicy } from "../audit.js";
|
|
5
|
-
import { loadConfig, mergePolicy, resolveConfiguredRules } from "../config.js";
|
|
5
|
+
import { loadConfig, mergePolicy, resolveConfiguredRules, rulePackageTrustFromCli } from "../config.js";
|
|
6
6
|
import { getCredential, resolveHubspotConnection } from "../credentials.js";
|
|
7
7
|
import { patchPlanToMarkdown } from "../format.js";
|
|
8
8
|
import { appendHealthEntry, computeHealth, healthToMarkdown, readHealthTimeline, saveWorkspaceSnapshot, summarizeHealth, activeWorkspaceProfile } from "../health.js";
|
|
@@ -169,8 +169,9 @@ async function registerHubspotWithBroker() {
|
|
|
169
169
|
}
|
|
170
170
|
export async function audit(args) {
|
|
171
171
|
const threshold = failOnThreshold(args);
|
|
172
|
-
const
|
|
173
|
-
const
|
|
172
|
+
const explicitConfig = option(args, "--config") ?? undefined;
|
|
173
|
+
const loaded = loadConfig(explicitConfig);
|
|
174
|
+
const rules = selectedRules(args, await resolveConfiguredRules(loaded, undefined, rulePackageTrustFromCli(args, explicitConfig)));
|
|
174
175
|
const snapshot = await readSnapshot(args);
|
|
175
176
|
const policy = mergePolicy(defaultPolicy(), loaded?.config);
|
|
176
177
|
const today = option(args, "--today");
|
|
@@ -326,8 +327,9 @@ function shortDay(at) {
|
|
|
326
327
|
* re-fetching (useful for a plan produced earlier or by another machine).
|
|
327
328
|
*/
|
|
328
329
|
export async function reportCommand(args) {
|
|
329
|
-
const
|
|
330
|
-
const
|
|
330
|
+
const explicitConfig = option(args, "--config") ?? undefined;
|
|
331
|
+
const loaded = loadConfig(explicitConfig);
|
|
332
|
+
const configuredRules = await resolveConfiguredRules(loaded, undefined, rulePackageTrustFromCli(args, explicitConfig));
|
|
331
333
|
let plan;
|
|
332
334
|
let snapshot;
|
|
333
335
|
const planPath = option(args, "--plan");
|
|
@@ -375,8 +377,9 @@ export async function reportCommand(args) {
|
|
|
375
377
|
}
|
|
376
378
|
}
|
|
377
379
|
export async function rulesCommand(args) {
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
+
const explicitConfig = option(args, "--config") ?? undefined;
|
|
381
|
+
const loaded = loadConfig(explicitConfig);
|
|
382
|
+
const rules = await resolveConfiguredRules(loaded, undefined, rulePackageTrustFromCli(args, explicitConfig));
|
|
380
383
|
if (args.includes("--json")) {
|
|
381
384
|
console.log(JSON.stringify(rules.map(({ id, title, description, category }) => ({
|
|
382
385
|
id,
|
package/dist/cli/auth.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { DEFAULT_LOOPBACK_PORT, openInBrowser, runHubspotLoopbackLogin, validateHubspotToken } from "../connectors/hubspotAuth.js";
|
|
5
|
-
import { pollSalesforceDeviceLogin, startSalesforceDeviceLogin, validateSalesforceToken } from "../connectors/salesforceAuth.js";
|
|
5
|
+
import { pollSalesforceDeviceLogin, startSalesforceDeviceLogin, validateSalesforceOrigin, validateSalesforceToken } from "../connectors/salesforceAuth.js";
|
|
6
6
|
import { activeProfile, credentialsPath, DEFAULT_PROFILE, deleteCredential, getCredential, storeCredential } from "../credentials.js";
|
|
7
7
|
import { activeWorkspaceProfile, readHealthTimeline, summarizeHealth } from "../health.js";
|
|
8
8
|
import { resolveLlmCredential, validateLlmKey } from "../llm.js";
|
|
@@ -229,7 +229,10 @@ async function salesforceLogin(args) {
|
|
|
229
229
|
await guidedProviderLogin("salesforce", args, "--device requires --client-id (the consumer key of a Connected App with device flow enabled).");
|
|
230
230
|
return;
|
|
231
231
|
}
|
|
232
|
-
const
|
|
232
|
+
const loginUrlInput = option(args, "--login-url") ?? undefined;
|
|
233
|
+
const loginUrl = loginUrlInput
|
|
234
|
+
? validateSalesforceOrigin(loginUrlInput, "Salesforce --login-url")
|
|
235
|
+
: undefined;
|
|
233
236
|
const authorization = await startSalesforceDeviceLogin({ clientId, loginUrl });
|
|
234
237
|
console.error(`\nPairing code: ${authorization.userCode}\n\nConfirm this code at:\n\n ${authorization.verificationUri}\n`);
|
|
235
238
|
void openInBrowser(authorization.verificationUri);
|
|
@@ -254,11 +257,12 @@ async function salesforceLogin(args) {
|
|
|
254
257
|
console.log("Tokens refresh silently; no further browser interaction is needed.");
|
|
255
258
|
return;
|
|
256
259
|
}
|
|
257
|
-
const
|
|
258
|
-
if (!
|
|
260
|
+
const instanceUrlInput = option(args, "--instance-url");
|
|
261
|
+
if (!instanceUrlInput) {
|
|
259
262
|
await guidedProviderLogin("salesforce", args, "Salesforce login needs hosted OAuth, --device --client-id <consumer key>, or --instance-url <https://yourorg.my.salesforce.com> with the access token piped on stdin.");
|
|
260
263
|
return;
|
|
261
264
|
}
|
|
265
|
+
const instanceUrl = validateSalesforceOrigin(instanceUrlInput, "Salesforce --instance-url");
|
|
262
266
|
const token = await readSecret("Salesforce access token");
|
|
263
267
|
if (!token)
|
|
264
268
|
throw new Error("No access token provided.");
|
package/dist/cli/fix.d.ts
CHANGED
|
@@ -31,3 +31,6 @@ export declare function reassignCommand(args: string[]): Promise<void>;
|
|
|
31
31
|
*/
|
|
32
32
|
export declare function fixCommand(args: string[]): Promise<void>;
|
|
33
33
|
export declare function suggest(args: string[]): Promise<void>;
|
|
34
|
+
export declare function routeCommand(args: string[]): Promise<void>;
|
|
35
|
+
export declare function hierarchyCommand(args: string[]): Promise<void>;
|
|
36
|
+
export declare function relationshipsCommand(args: string[]): Promise<void>;
|