fullstackgtm 0.10.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +381 -0
  2. package/INSTALL_FOR_AGENTS.md +87 -0
  3. package/LICENSE +202 -0
  4. package/README.md +230 -0
  5. package/dist/audit.d.ts +7 -0
  6. package/dist/audit.js +202 -0
  7. package/dist/bin.d.ts +2 -0
  8. package/dist/bin.js +6 -0
  9. package/dist/cli.d.ts +38 -0
  10. package/dist/cli.js +915 -0
  11. package/dist/config.d.ts +36 -0
  12. package/dist/config.js +85 -0
  13. package/dist/connector.d.ts +30 -0
  14. package/dist/connector.js +94 -0
  15. package/dist/connectors/hubspot.d.ts +20 -0
  16. package/dist/connectors/hubspot.js +409 -0
  17. package/dist/connectors/hubspotAuth.d.ts +42 -0
  18. package/dist/connectors/hubspotAuth.js +189 -0
  19. package/dist/connectors/salesforce.d.ts +26 -0
  20. package/dist/connectors/salesforce.js +318 -0
  21. package/dist/connectors/salesforceAuth.d.ts +44 -0
  22. package/dist/connectors/salesforceAuth.js +120 -0
  23. package/dist/connectors/stripe.d.ts +27 -0
  24. package/dist/connectors/stripe.js +176 -0
  25. package/dist/credentials.d.ts +75 -0
  26. package/dist/credentials.js +197 -0
  27. package/dist/demo.d.ts +20 -0
  28. package/dist/demo.js +169 -0
  29. package/dist/diff.d.ts +46 -0
  30. package/dist/diff.js +107 -0
  31. package/dist/format.d.ts +3 -0
  32. package/dist/format.js +109 -0
  33. package/dist/index.d.ts +18 -0
  34. package/dist/index.js +17 -0
  35. package/dist/mappings.d.ts +8 -0
  36. package/dist/mappings.js +123 -0
  37. package/dist/mcp-bin.d.ts +2 -0
  38. package/dist/mcp-bin.js +33 -0
  39. package/dist/mcp.d.ts +1 -0
  40. package/dist/mcp.js +140 -0
  41. package/dist/merge.d.ts +48 -0
  42. package/dist/merge.js +145 -0
  43. package/dist/planStore.d.ts +31 -0
  44. package/dist/planStore.js +116 -0
  45. package/dist/rules.d.ts +24 -0
  46. package/dist/rules.js +512 -0
  47. package/dist/sampleData.d.ts +2 -0
  48. package/dist/sampleData.js +115 -0
  49. package/dist/types.d.ts +294 -0
  50. package/dist/types.js +8 -0
  51. package/docs/api.md +72 -0
  52. package/docs/roadmap-to-1.0.md +121 -0
  53. package/llms.txt +25 -0
  54. package/package.json +76 -0
  55. package/src/audit.ts +242 -0
  56. package/src/bin.ts +7 -0
  57. package/src/cli.ts +1042 -0
  58. package/src/config.ts +113 -0
  59. package/src/connector.ts +140 -0
  60. package/src/connectors/hubspot.ts +528 -0
  61. package/src/connectors/hubspotAuth.ts +246 -0
  62. package/src/connectors/salesforce.ts +420 -0
  63. package/src/connectors/salesforceAuth.ts +167 -0
  64. package/src/connectors/stripe.ts +215 -0
  65. package/src/credentials.ts +282 -0
  66. package/src/demo.ts +200 -0
  67. package/src/diff.ts +158 -0
  68. package/src/format.ts +162 -0
  69. package/src/index.ts +129 -0
  70. package/src/mappings.ts +157 -0
  71. package/src/mcp-bin.ts +32 -0
  72. package/src/mcp.ts +185 -0
  73. package/src/merge.ts +235 -0
  74. package/src/planStore.ts +155 -0
  75. package/src/rules.ts +539 -0
  76. package/src/sampleData.ts +117 -0
  77. package/src/types.ts +372 -0
package/dist/demo.js ADDED
@@ -0,0 +1,169 @@
1
+ const FIRST_NAMES = [
2
+ "Ava", "Marcus", "Priya", "Diego", "Sofia", "Jordan",
3
+ "Mei", "Tomás", "Nia", "Ethan", "Lena", "Omar",
4
+ ];
5
+ const LAST_NAMES = [
6
+ "Calloway", "Reyes", "Iyer", "Novak", "Bennett", "Okafor",
7
+ "Lindqvist", "Moreau", "Tanaka", "Whitfield", "Drummond", "Vargas",
8
+ ];
9
+ const COMPANY_HEADS = [
10
+ "Halcyon", "Northwind", "Cobalt", "Ridgeline", "Lumen", "Vantage",
11
+ "Harbor", "Atlas", "Crestline", "Meridian", "Bluff", "Juniper",
12
+ "Granite", "Summit", "Beacon",
13
+ ];
14
+ const COMPANY_TAILS = [
15
+ "Analytics", "Logistics", "Biotech", "Robotics", "Software",
16
+ "Manufacturing", "Health", "Financial", "Media", "Systems",
17
+ "Foods", "Energy", "Labs", "Dynamics",
18
+ ];
19
+ const INDUSTRIES = [
20
+ "SaaS", "Logistics", "Healthcare", "Manufacturing", "Financial Services",
21
+ "Media", "Energy", "Retail",
22
+ ];
23
+ const OPEN_STAGES = ["discovery", "qualification", "proposal", "negotiation"];
24
+ const DEAL_LABELS = [
25
+ "Platform Subscription", "Enterprise Rollout", "Pilot Expansion",
26
+ "Renewal", "Multi-Year Agreement", "Team Upgrade", "Add-On Seats",
27
+ ];
28
+ /** Deterministic PRNG (mulberry32) so demo data is stable across runs. */
29
+ function mulberry32(seed) {
30
+ let state = seed >>> 0;
31
+ return () => {
32
+ state = (state + 0x6d2b79f5) >>> 0;
33
+ let t = state;
34
+ t = Math.imul(t ^ (t >>> 15), t | 1);
35
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
36
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
37
+ };
38
+ }
39
+ function shiftDate(today, days) {
40
+ const base = Date.parse(`${today}T00:00:00Z`);
41
+ return new Date(base + days * 86_400_000).toISOString().slice(0, 10);
42
+ }
43
+ /**
44
+ * Generate a realistic, deliberately messy mid-market CRM snapshot.
45
+ *
46
+ * The mess is injected at fixed indices so audits over a given seed produce
47
+ * stable, testable findings, while the PRNG varies names, amounts, and dates:
48
+ * - every 8th account is an orphan (no contacts, no deals)
49
+ * - every 9th deal references a departed owner that no longer exists
50
+ * - every 11th deal lost its account association
51
+ * - open deals carry a realistic spread of past close dates and stale activity
52
+ */
53
+ export function generateDemoSnapshot(options = {}) {
54
+ const seed = options.seed ?? 7;
55
+ const today = options.today ?? "2026-06-09";
56
+ const accountCount = options.accounts ?? 56;
57
+ const dealCount = options.deals ?? 80;
58
+ const random = mulberry32(seed);
59
+ const pick = (items) => items[Math.floor(random() * items.length)];
60
+ const between = (min, max) => min + Math.floor(random() * (max - min + 1));
61
+ const users = FIRST_NAMES.map((firstName, index) => ({
62
+ id: `user_${String(index + 1).padStart(2, "0")}`,
63
+ provider: "mock",
64
+ crmId: `${9000 + index}`,
65
+ name: `${firstName} ${LAST_NAMES[index]}`,
66
+ email: `${firstName.toLowerCase()}.${LAST_NAMES[index].toLowerCase()}@example.com`,
67
+ title: index === 0 ? "VP Sales" : index === 1 ? "Sales Manager" : "Account Executive",
68
+ active: index < 10,
69
+ }));
70
+ const activeReps = users.slice(2, 10);
71
+ const accounts = [];
72
+ for (let index = 0; index < accountCount; index += 1) {
73
+ const name = `${COMPANY_HEADS[index % COMPANY_HEADS.length]} ${pick(COMPANY_TAILS)}`;
74
+ accounts.push({
75
+ id: `acct_${String(index + 1).padStart(3, "0")}`,
76
+ provider: "mock",
77
+ crmId: `${10_000 + index}`,
78
+ name,
79
+ domain: `${name.toLowerCase().replace(/[^a-z]/g, "")}.com`,
80
+ industry: pick(INDUSTRIES),
81
+ ownerId: random() < 0.85 ? pick(activeReps).id : undefined,
82
+ employeeCount: between(40, 4000),
83
+ annualRevenue: between(2, 400) * 1_000_000,
84
+ lastSyncAt: shiftDate(today, -between(0, 3)),
85
+ });
86
+ }
87
+ const orphanAccountIds = new Set(accounts.filter((_, index) => index % 8 === 0).map((account) => account.id));
88
+ const linkableAccounts = accounts.filter((account) => !orphanAccountIds.has(account.id));
89
+ const contacts = [];
90
+ for (const account of linkableAccounts) {
91
+ const count = between(1, 3);
92
+ for (let index = 0; index < count; index += 1) {
93
+ const firstName = pick(FIRST_NAMES);
94
+ const lastName = pick(LAST_NAMES);
95
+ contacts.push({
96
+ id: `contact_${String(contacts.length + 1).padStart(3, "0")}`,
97
+ provider: "mock",
98
+ crmId: `${20_000 + contacts.length}`,
99
+ accountId: account.id,
100
+ firstName,
101
+ lastName,
102
+ email: `${firstName.toLowerCase()}@${account.domain}`,
103
+ title: pick(["CTO", "VP Operations", "Head of RevOps", "Director of IT", "CFO"]),
104
+ ownerId: account.ownerId,
105
+ lastSyncAt: account.lastSyncAt,
106
+ });
107
+ }
108
+ }
109
+ const deals = [];
110
+ const activities = [];
111
+ for (let index = 0; index < dealCount; index += 1) {
112
+ const account = linkableAccounts[index % linkableAccounts.length];
113
+ const departedOwner = index % 9 === 0;
114
+ const unlinked = index % 11 === 0;
115
+ const stageRoll = random();
116
+ const stage = stageRoll < 0.7 ? pick(OPEN_STAGES) : stageRoll < 0.88 ? "closedwon" : "closedlost";
117
+ const isWon = stage === "closedwon";
118
+ const isClosed = isWon || stage === "closedlost";
119
+ const closeDate = isClosed
120
+ ? shiftDate(today, -between(5, 120))
121
+ : shiftDate(today, between(-60, 150));
122
+ const daysSinceActivity = isClosed ? between(5, 60) : between(0, 75);
123
+ const lastActivityAt = shiftDate(today, -daysSinceActivity);
124
+ const deal = {
125
+ id: `deal_${String(index + 1).padStart(3, "0")}`,
126
+ provider: "mock",
127
+ crmId: `${30_000 + index}`,
128
+ accountId: unlinked ? undefined : account.id,
129
+ ownerId: departedOwner ? `user_departed_${index % 2}` : (account.ownerId ?? pick(activeReps).id),
130
+ name: `${account.name} — ${pick(DEAL_LABELS)}`,
131
+ amount: between(8, 480) * 500,
132
+ currency: "USD",
133
+ stage,
134
+ closeDate,
135
+ forecastCategory: isWon ? "closed_won" : isClosed ? "closed_lost" : "pipeline",
136
+ probability: isClosed ? (isWon ? 1 : 0) : between(10, 80) / 100,
137
+ isClosed,
138
+ isWon,
139
+ lastActivityAt,
140
+ lastSyncAt: shiftDate(today, -between(0, 2)),
141
+ };
142
+ deals.push(deal);
143
+ const activityCount = Math.max(1, 3 - Math.floor(daysSinceActivity / 25));
144
+ for (let activityIndex = 0; activityIndex < activityCount; activityIndex += 1) {
145
+ activities.push({
146
+ id: `activity_${String(activities.length + 1).padStart(4, "0")}`,
147
+ provider: "mock",
148
+ dealId: deal.id,
149
+ accountId: deal.accountId,
150
+ ownerId: deal.ownerId,
151
+ type: pick(["call", "email", "meeting"]),
152
+ occurredAt: shiftDate(today, -(daysSinceActivity + activityIndex * between(3, 12))),
153
+ subject: pick([
154
+ "Discovery call", "Pricing discussion", "Security review",
155
+ "Champion sync", "Procurement follow-up", "Executive alignment",
156
+ ]),
157
+ });
158
+ }
159
+ }
160
+ return {
161
+ generatedAt: `${today}T00:00:00.000Z`,
162
+ provider: "mock",
163
+ users,
164
+ accounts,
165
+ contacts,
166
+ deals,
167
+ activities,
168
+ };
169
+ }
package/dist/diff.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ import type { AuditFinding, CanonicalGtmSnapshot, PatchPlan } from "./types.ts";
2
+ export type FieldChange = {
3
+ field: string;
4
+ before: unknown;
5
+ after: unknown;
6
+ };
7
+ export type RecordChange = {
8
+ id: string;
9
+ label: string;
10
+ changes: FieldChange[];
11
+ };
12
+ export type CollectionDiff = {
13
+ added: Array<{
14
+ id: string;
15
+ label: string;
16
+ }>;
17
+ removed: Array<{
18
+ id: string;
19
+ label: string;
20
+ }>;
21
+ changed: RecordChange[];
22
+ };
23
+ export type SnapshotDiff = {
24
+ before: {
25
+ provider: string;
26
+ generatedAt: string;
27
+ };
28
+ after: {
29
+ provider: string;
30
+ generatedAt: string;
31
+ };
32
+ users: CollectionDiff;
33
+ accounts: CollectionDiff;
34
+ contacts: CollectionDiff;
35
+ deals: CollectionDiff;
36
+ activities: CollectionDiff;
37
+ };
38
+ export declare function diffSnapshots(before: CanonicalGtmSnapshot, after: CanonicalGtmSnapshot): SnapshotDiff;
39
+ export type FindingsDrift = {
40
+ newFindings: AuditFinding[];
41
+ resolvedFindings: AuditFinding[];
42
+ persistingCount: number;
43
+ };
44
+ /** Hygiene drift between two plans, keyed on stable finding ids. */
45
+ export declare function diffFindings(before: PatchPlan, after: PatchPlan): FindingsDrift;
46
+ export declare function diffToMarkdown(diff: SnapshotDiff, drift?: FindingsDrift): string;
package/dist/diff.js ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Snapshot and audit drift. Record ids and finding ids are stable across
3
+ * runs, so two snapshots (or two plans) can be compared directly: what
4
+ * appeared, what disappeared, what changed — and whether hygiene regressed.
5
+ */
6
+ // Fields that change on every sync without semantic meaning.
7
+ const IGNORED_FIELDS = new Set(["raw", "lastSyncAt", "identities"]);
8
+ function labelOf(record) {
9
+ return record.name ?? record.email ?? record.id;
10
+ }
11
+ function diffCollection(before, after) {
12
+ const beforeById = new Map(before.map((record) => [record.id, record]));
13
+ const afterById = new Map(after.map((record) => [record.id, record]));
14
+ const added = after
15
+ .filter((record) => !beforeById.has(record.id))
16
+ .map((record) => ({ id: record.id, label: labelOf(record) }));
17
+ const removed = before
18
+ .filter((record) => !afterById.has(record.id))
19
+ .map((record) => ({ id: record.id, label: labelOf(record) }));
20
+ const changed = [];
21
+ for (const record of after) {
22
+ const previous = beforeById.get(record.id);
23
+ if (!previous)
24
+ continue;
25
+ const fields = new Set([...Object.keys(previous), ...Object.keys(record)]);
26
+ const changes = [];
27
+ for (const field of fields) {
28
+ if (IGNORED_FIELDS.has(field))
29
+ continue;
30
+ const beforeValue = previous[field];
31
+ const afterValue = record[field];
32
+ if (JSON.stringify(beforeValue) !== JSON.stringify(afterValue)) {
33
+ changes.push({ field, before: beforeValue ?? null, after: afterValue ?? null });
34
+ }
35
+ }
36
+ if (changes.length > 0) {
37
+ changed.push({ id: record.id, label: labelOf(record), changes });
38
+ }
39
+ }
40
+ return { added, removed, changed };
41
+ }
42
+ export function diffSnapshots(before, after) {
43
+ return {
44
+ before: { provider: before.provider, generatedAt: before.generatedAt },
45
+ after: { provider: after.provider, generatedAt: after.generatedAt },
46
+ users: diffCollection(before.users, after.users),
47
+ accounts: diffCollection(before.accounts, after.accounts),
48
+ contacts: diffCollection(before.contacts, after.contacts),
49
+ deals: diffCollection(before.deals, after.deals),
50
+ activities: diffCollection(before.activities, after.activities),
51
+ };
52
+ }
53
+ /** Hygiene drift between two plans, keyed on stable finding ids. */
54
+ export function diffFindings(before, after) {
55
+ const beforeIds = new Set(before.findings.map((finding) => finding.id));
56
+ const afterIds = new Set(after.findings.map((finding) => finding.id));
57
+ return {
58
+ newFindings: after.findings.filter((finding) => !beforeIds.has(finding.id)),
59
+ resolvedFindings: before.findings.filter((finding) => !afterIds.has(finding.id)),
60
+ persistingCount: after.findings.filter((finding) => beforeIds.has(finding.id)).length,
61
+ };
62
+ }
63
+ export function diffToMarkdown(diff, drift) {
64
+ const lines = [
65
+ "# Snapshot diff",
66
+ "",
67
+ `Before: ${diff.before.provider} @ ${diff.before.generatedAt}`,
68
+ `After: ${diff.after.provider} @ ${diff.after.generatedAt}`,
69
+ "",
70
+ "| Collection | Added | Removed | Changed |",
71
+ "| --- | --- | --- | --- |",
72
+ ];
73
+ for (const collection of ["users", "accounts", "contacts", "deals", "activities"]) {
74
+ const entry = diff[collection];
75
+ lines.push(`| ${collection} | ${entry.added.length} | ${entry.removed.length} | ${entry.changed.length} |`);
76
+ }
77
+ for (const collection of ["users", "accounts", "contacts", "deals", "activities"]) {
78
+ const entry = diff[collection];
79
+ if (entry.added.length + entry.removed.length + entry.changed.length === 0)
80
+ continue;
81
+ lines.push("", `## ${collection}`, "");
82
+ for (const record of entry.added)
83
+ lines.push(`- added ${record.label} (${record.id})`);
84
+ for (const record of entry.removed)
85
+ lines.push(`- removed ${record.label} (${record.id})`);
86
+ for (const record of entry.changed) {
87
+ lines.push(`- changed ${record.label} (${record.id}): ${record.changes
88
+ .map((change) => `${change.field} ${formatValue(change.before)} → ${formatValue(change.after)}`)
89
+ .join("; ")}`);
90
+ }
91
+ }
92
+ if (drift) {
93
+ lines.push("", "## Hygiene drift", "", `New findings: ${drift.newFindings.length}`, `Resolved findings: ${drift.resolvedFindings.length}`, `Persisting findings: ${drift.persistingCount}`);
94
+ for (const finding of drift.newFindings) {
95
+ lines.push(`- NEW [${finding.ruleId}] ${finding.summary}`);
96
+ }
97
+ for (const finding of drift.resolvedFindings) {
98
+ lines.push(`- resolved [${finding.ruleId}] ${finding.summary}`);
99
+ }
100
+ }
101
+ return `${lines.join("\n")}\n`;
102
+ }
103
+ function formatValue(value) {
104
+ if (value === undefined || value === null || value === "")
105
+ return "∅";
106
+ return typeof value === "string" ? value : JSON.stringify(value);
107
+ }
@@ -0,0 +1,3 @@
1
+ import type { PatchPlan, PatchPlanRun } from "./types.ts";
2
+ export declare function patchPlanToMarkdown(plan: PatchPlan): string;
3
+ export declare function formatPatchPlanRun(run: PatchPlanRun): string;
package/dist/format.js ADDED
@@ -0,0 +1,109 @@
1
+ export function patchPlanToMarkdown(plan) {
2
+ const lines = [
3
+ `# ${plan.title}`,
4
+ "",
5
+ `Status: ${plan.status}`,
6
+ `Dry run: ${plan.dryRun ? "yes" : "no"}`,
7
+ `Summary: ${plan.summary}`,
8
+ "",
9
+ ];
10
+ if (plan.findings.length > 0) {
11
+ const byRule = new Map();
12
+ for (const finding of plan.findings) {
13
+ const entry = byRule.get(finding.ruleId) ?? { count: 0, severity: finding.severity };
14
+ entry.count += 1;
15
+ byRule.set(finding.ruleId, entry);
16
+ }
17
+ lines.push("## Findings by Rule", "", "| Rule | Findings | Severity |", "| --- | --- | --- |");
18
+ byRule.forEach((entry, ruleId) => {
19
+ lines.push(`| ${ruleId} | ${entry.count} | ${entry.severity} |`);
20
+ });
21
+ lines.push("");
22
+ }
23
+ lines.push("## Findings", "");
24
+ const findings = plan.pipelineFindings ?? [];
25
+ if (findings.length > 0) {
26
+ for (const finding of findings) {
27
+ lines.push(`- **${finding.title}** (${finding.severity}, ${finding.status})`, ` - Finding: ${finding.type}`, ` - Object: ${finding.objectType}/${finding.objectId}`, ` - Summary: ${finding.summary}`, ` - Recommendation: ${finding.recommendation}`, ` - Evidence refs: ${formatRefs(finding.evidenceIds)}`, ` - Current CRM value: ${formatValue(finding.currentCrmValue)}`, ` - Proposed value: ${formatValue(finding.proposedValue)}`, ` - Source freshness: ${formatFreshness(finding.freshness)}`, ` - Patch eligible: ${finding.patchEligibility.eligible ? "yes" : "no"} (${finding.patchEligibility.reason})`);
28
+ }
29
+ const pipelineFindingIds = new Set(findings.map((finding) => finding.id));
30
+ const otherFindings = plan.findings.filter((finding) => !pipelineFindingIds.has(finding.id));
31
+ if (otherFindings.length > 0) {
32
+ lines.push("", "### Other Findings", "");
33
+ for (const finding of otherFindings) {
34
+ lines.push(`- **${finding.title}** (${finding.severity})`, ` - Object: ${finding.objectType}/${finding.objectId}`, ` - Rule: ${finding.ruleId}`, ` - Summary: ${finding.summary}`, ` - Recommendation: ${finding.recommendation}`);
35
+ }
36
+ }
37
+ }
38
+ else if (plan.findings.length === 0) {
39
+ lines.push("No findings.");
40
+ }
41
+ else {
42
+ for (const finding of plan.findings) {
43
+ lines.push(`- **${finding.title}** (${finding.severity})`, ` - Object: ${finding.objectType}/${finding.objectId}`, ` - Rule: ${finding.ruleId}`, ` - Summary: ${finding.summary}`, ` - Recommendation: ${finding.recommendation}`);
44
+ }
45
+ }
46
+ if (plan.evidence && plan.evidence.length > 0) {
47
+ lines.push("", "## Evidence", "");
48
+ for (const evidence of plan.evidence) {
49
+ lines.push(`- **${evidence.id}** ${evidence.sourceSystem}:${evidence.sourceObjectType}/${evidence.sourceObjectId}`, ` - Object: ${evidence.objectType ?? "unknown"}/${evidence.objectId ?? "unknown"}`, ` - Text: ${evidence.text}`, ` - Freshness: ${evidence.freshness ? formatFreshness(evidence.freshness) : "unknown"}`);
50
+ }
51
+ }
52
+ lines.push("", "## Proposed Patch Operations", "");
53
+ if (plan.operations.length === 0) {
54
+ lines.push("No patch operations proposed.");
55
+ }
56
+ else {
57
+ for (const operation of plan.operations) {
58
+ lines.push(`- **${operation.operation}** ${operation.objectType}/${operation.objectId}`, ` - ID: ${operation.id}`, ` - Field/action: ${operation.field ?? operation.operation}`, ` - Findings: ${formatRefs(operation.findingIds)}`, ` - Evidence refs: ${formatRefs(operation.evidenceIds)}`, ` - Before: ${formatValue(operation.beforeValue)}`, ` - After: ${formatValue(operation.afterValue)}`, ` - Reason: ${operation.reason}`, ` - Source rule/policy: ${operation.sourceRuleOrPolicy ?? "unspecified"}`, ` - Risk: ${operation.riskLevel}`, ` - Approval required: ${operation.approvalRequired ? "yes" : "no"}`, ` - Rollback: ${operation.rollback ?? "restore prior value if apply is rejected or verification fails"}`, ` - Verification: ${formatVerification(operation.verification)}`);
59
+ }
60
+ }
61
+ lines.push("", "> This prototype is dry-run only. Real CRM writes must require explicit human approval before connector adapters apply any operation.");
62
+ return `${lines.join("\n")}\n`;
63
+ }
64
+ export function formatPatchPlanRun(run) {
65
+ const lines = [
66
+ `# Patch plan run`,
67
+ "",
68
+ `Plan: ${run.planId}`,
69
+ `Provider: ${run.provider}`,
70
+ `Status: ${run.status}`,
71
+ `Started: ${run.startedAt}`,
72
+ `Finished: ${run.finishedAt}`,
73
+ "",
74
+ "## Operation Results",
75
+ "",
76
+ ];
77
+ if (run.results.length === 0) {
78
+ lines.push("No operations were attempted.");
79
+ }
80
+ else {
81
+ for (const result of run.results) {
82
+ lines.push(`- **${result.status}** ${result.operationId}${result.detail ? ` — ${result.detail}` : ""}`);
83
+ }
84
+ }
85
+ return `${lines.join("\n")}\n`;
86
+ }
87
+ function formatValue(value) {
88
+ if (value === undefined || value === null || value === "")
89
+ return "unset";
90
+ if (typeof value === "string")
91
+ return value;
92
+ return JSON.stringify(value);
93
+ }
94
+ function formatRefs(refs) {
95
+ return refs && refs.length > 0 ? refs.join(", ") : "none";
96
+ }
97
+ function formatFreshness(freshness) {
98
+ const age = freshness.ageDays === undefined ? "age unknown" : `${freshness.ageDays}d old`;
99
+ const source = freshness.sourceUpdatedAt ? `source ${freshness.sourceUpdatedAt}` : "source date unknown";
100
+ return `${freshness.state}, ${age}, ${source}, checked ${freshness.checkedAt}`;
101
+ }
102
+ function formatVerification(verification) {
103
+ if (!verification)
104
+ return "not started";
105
+ const observed = verification.observedValue === undefined
106
+ ? ""
107
+ : `, observed ${formatValue(verification.observedValue)}`;
108
+ return `${verification.status}${observed}${verification.auditText ? ` (${verification.auditText})` : ""}`;
109
+ }
@@ -0,0 +1,18 @@
1
+ export { auditSnapshot, defaultPolicy } from "./audit.ts";
2
+ export { CONFIG_FILE_NAME, loadConfig, mergePolicy, resolveConfiguredRules, type FullstackgtmConfig, type LoadedConfig, } from "./config.ts";
3
+ export { applyPatchPlan, type ApplyPatchPlanOptions } from "./connector.ts";
4
+ export { createHubspotConnector, type HubspotConnectorOptions } from "./connectors/hubspot.ts";
5
+ export { DEFAULT_LOOPBACK_PORT, DEFAULT_OAUTH_SCOPES, exchangeHubspotCode, refreshHubspotToken, runHubspotLoopbackLogin, validateHubspotToken, type HubspotTokenSet, type LoopbackLoginOptions, } from "./connectors/hubspotAuth.ts";
6
+ export { createSalesforceConnector, type SalesforceConnection, type SalesforceConnectorOptions, } from "./connectors/salesforce.ts";
7
+ export { pollSalesforceDeviceLogin, refreshSalesforceToken, startSalesforceDeviceLogin, validateSalesforceToken, type SalesforceDeviceAuthorization, type SalesforceTokenSet, } from "./connectors/salesforceAuth.ts";
8
+ export { createStripeConnector, type StripeConnectorOptions } from "./connectors/stripe.ts";
9
+ export { credentialsDir, credentialsPath, deleteCredential, getCredential, resolveHubspotAccessToken, resolveHubspotConnection, storeCredential, type HubspotConnection, type StoredCredential, } from "./credentials.ts";
10
+ export { generateDemoSnapshot, type DemoSnapshotOptions } from "./demo.ts";
11
+ export { diffFindings, diffSnapshots, diffToMarkdown, type CollectionDiff, type FieldChange, type FindingsDrift, type RecordChange, type SnapshotDiff, } from "./diff.ts";
12
+ export { mergeSnapshots, type MergeConflict, type MergeMatch, type MergeReport, type MergeSuggestion, } from "./merge.ts";
13
+ export { createFilePlanStore, type PlanStore, type StoredPlan } from "./planStore.ts";
14
+ export { formatPatchPlanRun, patchPlanToMarkdown } from "./format.ts";
15
+ export { HUBSPOT_DEFAULT_FIELD_MAPPINGS, SALESFORCE_DEFAULT_FIELD_MAPPINGS, mappedField, mappedFields, normalizeFieldMappings, readMappedValue, type CrmObjectType, type FieldMappings, } from "./mappings.ts";
16
+ export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, duplicateAccountDomainRule, duplicateContactEmailRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, requiresHumanInput, staleDealRule, } from "./rules.ts";
17
+ export { sampleSnapshot } from "./sampleData.ts";
18
+ export type { ApprovalStatus, AuditFinding, AuditFindingSeverity, CanonicalAccount, CanonicalActivity, CanonicalContact, CanonicalDeal, CanonicalGtmSnapshot, CanonicalUser, CrmProvider, GtmAuditRule, GtmConnector, GtmEvidence, GtmEvidenceSourceSystem, GtmObjectType, GtmPolicy, GtmRuleContext, GtmRuleResult, GtmSnapshotIndex, PatchOperation, PatchOperationResult, PatchOperationType, PatchPlan, PatchPlanRun, PatchPlanRunStatus, PatchVerification, PipelineFinding, PipelineFindingStatus, PipelineFindingType, ProviderIdentity, RiskLevel, SourceFreshness, } from "./types.ts";
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ export { auditSnapshot, defaultPolicy } from "./audit.js";
2
+ export { CONFIG_FILE_NAME, loadConfig, mergePolicy, resolveConfiguredRules, } from "./config.js";
3
+ export { applyPatchPlan } from "./connector.js";
4
+ export { createHubspotConnector } from "./connectors/hubspot.js";
5
+ export { DEFAULT_LOOPBACK_PORT, DEFAULT_OAUTH_SCOPES, exchangeHubspotCode, refreshHubspotToken, runHubspotLoopbackLogin, validateHubspotToken, } from "./connectors/hubspotAuth.js";
6
+ export { createSalesforceConnector, } from "./connectors/salesforce.js";
7
+ export { pollSalesforceDeviceLogin, refreshSalesforceToken, startSalesforceDeviceLogin, validateSalesforceToken, } from "./connectors/salesforceAuth.js";
8
+ export { createStripeConnector } from "./connectors/stripe.js";
9
+ export { credentialsDir, credentialsPath, deleteCredential, getCredential, resolveHubspotAccessToken, resolveHubspotConnection, storeCredential, } from "./credentials.js";
10
+ export { generateDemoSnapshot } from "./demo.js";
11
+ export { diffFindings, diffSnapshots, diffToMarkdown, } from "./diff.js";
12
+ export { mergeSnapshots, } from "./merge.js";
13
+ export { createFilePlanStore } from "./planStore.js";
14
+ export { formatPatchPlanRun, patchPlanToMarkdown } from "./format.js";
15
+ export { HUBSPOT_DEFAULT_FIELD_MAPPINGS, SALESFORCE_DEFAULT_FIELD_MAPPINGS, mappedField, mappedFields, normalizeFieldMappings, readMappedValue, } from "./mappings.js";
16
+ export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, duplicateAccountDomainRule, duplicateContactEmailRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, requiresHumanInput, staleDealRule, } from "./rules.js";
17
+ export { sampleSnapshot } from "./sampleData.js";
@@ -0,0 +1,8 @@
1
+ export type CrmObjectType = "owners" | "accounts" | "contacts" | "deals";
2
+ export type FieldMappings = Partial<Record<CrmObjectType | string, Record<string, string>>>;
3
+ export declare const HUBSPOT_DEFAULT_FIELD_MAPPINGS: Record<Exclude<CrmObjectType, "owners">, Record<string, string>>;
4
+ export declare const SALESFORCE_DEFAULT_FIELD_MAPPINGS: Record<CrmObjectType, Record<string, string>>;
5
+ export declare function normalizeFieldMappings(value: unknown): FieldMappings;
6
+ export declare function mappedField(mappings: FieldMappings | undefined, objectType: CrmObjectType, targetField: string, fallbackField: string): string;
7
+ export declare function mappedFields(mappings: FieldMappings | undefined, objectType: CrmObjectType, defaults: Record<string, string>): string[];
8
+ export declare function readMappedValue(source: Record<string, unknown>, mappings: FieldMappings | undefined, objectType: CrmObjectType, targetField: string, fallbackField: string): unknown;
@@ -0,0 +1,123 @@
1
+ export const HUBSPOT_DEFAULT_FIELD_MAPPINGS = {
2
+ accounts: {
3
+ name: "name",
4
+ domain: "domain",
5
+ industry: "industry",
6
+ employeeCount: "numberofemployees",
7
+ annualRevenue: "annualrevenue",
8
+ ownerId: "hubspot_owner_id",
9
+ },
10
+ contacts: {
11
+ firstName: "firstname",
12
+ lastName: "lastname",
13
+ email: "email",
14
+ phone: "phone",
15
+ title: "jobtitle",
16
+ ownerId: "hubspot_owner_id",
17
+ },
18
+ deals: {
19
+ name: "dealname",
20
+ amount: "amount",
21
+ closeDate: "closedate",
22
+ stage: "dealstage",
23
+ dealType: "dealtype",
24
+ ownerId: "hubspot_owner_id",
25
+ probability: "hs_deal_stage_probability",
26
+ lastActivityAt: "hs_last_sales_activity_timestamp",
27
+ nextStep: "hs_next_step",
28
+ },
29
+ };
30
+ export const SALESFORCE_DEFAULT_FIELD_MAPPINGS = {
31
+ owners: {
32
+ id: "Id",
33
+ name: "Name",
34
+ email: "Email",
35
+ title: "Title",
36
+ isActive: "IsActive",
37
+ },
38
+ accounts: {
39
+ id: "Id",
40
+ name: "Name",
41
+ domain: "Website",
42
+ industry: "Industry",
43
+ employeeCount: "NumberOfEmployees",
44
+ annualRevenue: "AnnualRevenue",
45
+ ownerId: "OwnerId",
46
+ },
47
+ contacts: {
48
+ id: "Id",
49
+ firstName: "FirstName",
50
+ lastName: "LastName",
51
+ email: "Email",
52
+ phone: "Phone",
53
+ title: "Title",
54
+ accountId: "AccountId",
55
+ ownerId: "OwnerId",
56
+ },
57
+ deals: {
58
+ id: "Id",
59
+ name: "Name",
60
+ amount: "Amount",
61
+ closeDate: "CloseDate",
62
+ stage: "StageName",
63
+ dealType: "Type",
64
+ probability: "Probability",
65
+ isClosed: "IsClosed",
66
+ isWon: "IsWon",
67
+ ownerId: "OwnerId",
68
+ accountId: "AccountId",
69
+ lastActivityAt: "LastActivityDate",
70
+ forecastCategoryName: "ForecastCategory",
71
+ nextStep: "NextStep",
72
+ },
73
+ };
74
+ const OBJECT_ALIASES = {
75
+ owners: ["owners", "users"],
76
+ accounts: ["accounts", "companies"],
77
+ contacts: ["contacts"],
78
+ deals: ["deals", "opportunities"],
79
+ };
80
+ export function normalizeFieldMappings(value) {
81
+ if (!value || typeof value !== "object" || Array.isArray(value))
82
+ return {};
83
+ const normalized = {};
84
+ for (const [objectName, mapping] of Object.entries(value)) {
85
+ if (!mapping || typeof mapping !== "object" || Array.isArray(mapping)) {
86
+ continue;
87
+ }
88
+ const clean = {};
89
+ for (const [targetField, providerField] of Object.entries(mapping)) {
90
+ if (typeof providerField === "string" && providerField.trim()) {
91
+ clean[targetField] = providerField.trim();
92
+ }
93
+ }
94
+ if (Object.keys(clean).length > 0)
95
+ normalized[objectName] = clean;
96
+ }
97
+ return normalized;
98
+ }
99
+ export function mappedField(mappings, objectType, targetField, fallbackField) {
100
+ const normalized = normalizeFieldMappings(mappings);
101
+ for (const alias of OBJECT_ALIASES[objectType]) {
102
+ const mapped = normalized[alias]?.[targetField];
103
+ if (mapped)
104
+ return mapped;
105
+ }
106
+ return fallbackField;
107
+ }
108
+ export function mappedFields(mappings, objectType, defaults) {
109
+ const fields = Object.entries(defaults).map(([targetField, fallbackField]) => mappedField(mappings, objectType, targetField, fallbackField));
110
+ return Array.from(new Set(fields)).filter(Boolean);
111
+ }
112
+ export function readMappedValue(source, mappings, objectType, targetField, fallbackField) {
113
+ return readPath(source, mappedField(mappings, objectType, targetField, fallbackField));
114
+ }
115
+ function readPath(source, path) {
116
+ if (Object.prototype.hasOwnProperty.call(source, path))
117
+ return source[path];
118
+ return path.split(".").reduce((value, segment) => {
119
+ if (!value || typeof value !== "object")
120
+ return undefined;
121
+ return value[segment];
122
+ }, source);
123
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ // The MCP server needs the optional peer dependencies. Import dynamically so
3
+ // a missing peer produces install guidance instead of a module-load stack
4
+ // trace — `npx fullstackgtm-mcp` alone never installs optional peers.
5
+ const MISSING_PEER_HELP = `The MCP server needs the optional peer dependencies @modelcontextprotocol/sdk and zod.
6
+
7
+ In a project:
8
+ npm install fullstackgtm @modelcontextprotocol/sdk zod
9
+
10
+ Zero-install:
11
+ npx -p fullstackgtm -p @modelcontextprotocol/sdk -p zod fullstackgtm-mcp`;
12
+ function isMissingPeerError(error) {
13
+ if (!(error instanceof Error))
14
+ return false;
15
+ const code = error.code;
16
+ if (code !== "ERR_MODULE_NOT_FOUND" && code !== "MODULE_NOT_FOUND")
17
+ return false;
18
+ return error.message.includes("@modelcontextprotocol/sdk") || error.message.includes("zod");
19
+ }
20
+ try {
21
+ const { startMcpServer } = await import("./mcp.js");
22
+ await startMcpServer();
23
+ }
24
+ catch (error) {
25
+ if (isMissingPeerError(error)) {
26
+ console.error(MISSING_PEER_HELP);
27
+ }
28
+ else {
29
+ console.error(error instanceof Error ? error.message : String(error));
30
+ }
31
+ process.exitCode = 1;
32
+ }
33
+ export {};
package/dist/mcp.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function startMcpServer(): Promise<void>;