fullstackgtm 0.10.0 → 0.11.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 +106 -0
- package/INSTALL_FOR_AGENTS.md +28 -2
- package/README.md +74 -6
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +251 -16
- package/dist/connectors/hubspot.js +36 -11
- package/dist/connectors/hubspotAuth.js +10 -2
- package/dist/connectors/salesforce.js +34 -9
- package/dist/connectors/salesforceAuth.js +17 -3
- package/dist/credentials.d.ts +19 -0
- package/dist/credentials.js +69 -8
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/mcp.js +53 -5
- package/dist/report.d.ts +61 -0
- package/dist/report.js +331 -0
- package/dist/rules.d.ts +1 -0
- package/dist/rules.js +47 -0
- package/dist/suggest.d.ts +31 -0
- package/dist/suggest.js +148 -0
- package/docs/api.md +13 -1
- package/docs/roadmap-to-1.0.md +31 -3
- package/package.json +1 -1
- package/src/cli.ts +271 -14
- package/src/connectors/hubspot.ts +35 -11
- package/src/connectors/hubspotAuth.ts +9 -2
- package/src/connectors/salesforce.ts +35 -9
- package/src/connectors/salesforceAuth.ts +19 -6
- package/src/credentials.ts +71 -6
- package/src/index.ts +7 -0
- package/src/mcp.ts +55 -5
- package/src/report.ts +502 -0
- package/src/rules.ts +50 -0
- package/src/suggest.ts +202 -0
package/src/suggest.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { requiresHumanInput } from "./rules.ts";
|
|
2
|
+
import type { CanonicalGtmSnapshot, PatchOperation, PatchPlan } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Deterministic value suggestions for `requires_human_*` placeholder
|
|
6
|
+
* operations. The engine never invents data: every suggestion is derived
|
|
7
|
+
* from evidence already in the snapshot (account names, contact→account
|
|
8
|
+
* associations, the org's user list) and carries a confidence level plus a
|
|
9
|
+
* human-readable reason, so a reviewer — or an agent driving the CLI — can
|
|
10
|
+
* approve in bulk at a chosen confidence threshold.
|
|
11
|
+
*
|
|
12
|
+
* Born from dogfooding: an audit of a real portal produced 30
|
|
13
|
+
* `requires_human_account_selection` placeholders whose answers were all
|
|
14
|
+
* derivable from the snapshot itself.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export type SuggestionConfidence = "high" | "low" | "create" | "none";
|
|
18
|
+
|
|
19
|
+
export type ValueSuggestion = {
|
|
20
|
+
operationId: string;
|
|
21
|
+
objectType: string;
|
|
22
|
+
objectId: string;
|
|
23
|
+
/** Display name of the object the operation targets (e.g. the deal name). */
|
|
24
|
+
objectName?: string;
|
|
25
|
+
/** The requires_human_* placeholder being filled. */
|
|
26
|
+
placeholder: string;
|
|
27
|
+
/**
|
|
28
|
+
* The value to approve with, or null when no evidence supports one.
|
|
29
|
+
* `create:<Name>` proposes creating the missing record on apply.
|
|
30
|
+
*/
|
|
31
|
+
suggestedValue: string | null;
|
|
32
|
+
confidence: SuggestionConfidence;
|
|
33
|
+
reason: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function suggestValues(
|
|
37
|
+
plan: PatchPlan,
|
|
38
|
+
snapshot: CanonicalGtmSnapshot,
|
|
39
|
+
): ValueSuggestion[] {
|
|
40
|
+
const accountsByNorm = new Map<string, { id: string; name: string }>();
|
|
41
|
+
for (const account of snapshot.accounts) {
|
|
42
|
+
accountsByNorm.set(normalize(account.name), { id: account.id, name: account.name });
|
|
43
|
+
}
|
|
44
|
+
const accountsById = new Map(snapshot.accounts.map((a) => [a.id, a]));
|
|
45
|
+
const contactsByName = new Map<string, { id: string; accountId?: string; name: string }>();
|
|
46
|
+
for (const contact of snapshot.contacts) {
|
|
47
|
+
const name = normalize(`${contact.firstName ?? ""} ${contact.lastName ?? ""}`);
|
|
48
|
+
if (name) contactsByName.set(name, { id: contact.id, accountId: contact.accountId, name });
|
|
49
|
+
}
|
|
50
|
+
const dealsById = new Map(snapshot.deals.map((d) => [d.id, d]));
|
|
51
|
+
const activeUsers = snapshot.users.filter((user) => user.active !== false);
|
|
52
|
+
|
|
53
|
+
const suggestions: ValueSuggestion[] = [];
|
|
54
|
+
for (const operation of plan.operations) {
|
|
55
|
+
if (!requiresHumanInput(operation.afterValue)) continue;
|
|
56
|
+
const placeholder = String(operation.afterValue);
|
|
57
|
+
|
|
58
|
+
if (placeholder === "requires_human_account_selection" && operation.objectType === "deal") {
|
|
59
|
+
suggestions.push(
|
|
60
|
+
suggestDealAccount(operation, dealsById, accountsByNorm, accountsById, contactsByName),
|
|
61
|
+
);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (placeholder === "requires_human_owner_selection" && activeUsers.length === 1) {
|
|
66
|
+
suggestions.push({
|
|
67
|
+
operationId: operation.id,
|
|
68
|
+
objectType: operation.objectType,
|
|
69
|
+
objectId: operation.objectId,
|
|
70
|
+
objectName: dealsById.get(operation.objectId)?.name,
|
|
71
|
+
placeholder,
|
|
72
|
+
suggestedValue: activeUsers[0].id,
|
|
73
|
+
confidence: "high",
|
|
74
|
+
reason: `${activeUsers[0].name} is the only active user in the org.`,
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
suggestions.push({
|
|
80
|
+
operationId: operation.id,
|
|
81
|
+
objectType: operation.objectType,
|
|
82
|
+
objectId: operation.objectId,
|
|
83
|
+
objectName: dealsById.get(operation.objectId)?.name,
|
|
84
|
+
placeholder,
|
|
85
|
+
suggestedValue: null,
|
|
86
|
+
confidence: "none",
|
|
87
|
+
reason: `No deterministic heuristic for ${placeholder}; supply --value ${operation.id}=<value>.`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return suggestions;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function suggestDealAccount(
|
|
94
|
+
operation: PatchOperation,
|
|
95
|
+
dealsById: Map<string, { name: string }>,
|
|
96
|
+
accountsByNorm: Map<string, { id: string; name: string }>,
|
|
97
|
+
accountsById: Map<string, { id: string; name: string }>,
|
|
98
|
+
contactsByName: Map<string, { id: string; accountId?: string; name: string }>,
|
|
99
|
+
): ValueSuggestion {
|
|
100
|
+
const deal = dealsById.get(operation.objectId);
|
|
101
|
+
const base = {
|
|
102
|
+
operationId: operation.id,
|
|
103
|
+
objectType: operation.objectType,
|
|
104
|
+
objectId: operation.objectId,
|
|
105
|
+
objectName: deal?.name,
|
|
106
|
+
placeholder: "requires_human_account_selection",
|
|
107
|
+
};
|
|
108
|
+
if (!deal) {
|
|
109
|
+
return { ...base, suggestedValue: null, confidence: "none", reason: "Deal not found in the snapshot." };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Convention: "Contact Name - Company Name". Both signals below are
|
|
113
|
+
// independent; agreement upgrades confidence, conflict downgrades it.
|
|
114
|
+
const separatorIndex = deal.name.indexOf(" - ");
|
|
115
|
+
const left = separatorIndex >= 0 ? deal.name.slice(0, separatorIndex) : deal.name;
|
|
116
|
+
const right = separatorIndex >= 0 ? deal.name.slice(separatorIndex + 3).trim() : "";
|
|
117
|
+
|
|
118
|
+
// Signal 1: company-name match against account names.
|
|
119
|
+
let nameMatch: { id: string; name: string } | null = null;
|
|
120
|
+
let nameMatchKind = "";
|
|
121
|
+
if (right) {
|
|
122
|
+
nameMatch = accountsByNorm.get(normalize(right)) ?? null;
|
|
123
|
+
nameMatchKind = nameMatch ? "exact name match" : "";
|
|
124
|
+
if (!nameMatch) {
|
|
125
|
+
const rightNorm = normalize(right);
|
|
126
|
+
const candidates = [...accountsByNorm.entries()].filter(
|
|
127
|
+
([norm]) => norm.includes(rightNorm) || rightNorm.includes(norm),
|
|
128
|
+
);
|
|
129
|
+
if (candidates.length === 1) {
|
|
130
|
+
nameMatch = candidates[0][1];
|
|
131
|
+
nameMatchKind = "partial name match";
|
|
132
|
+
} else if (candidates.length > 1) {
|
|
133
|
+
return {
|
|
134
|
+
...base,
|
|
135
|
+
suggestedValue: null,
|
|
136
|
+
confidence: "none",
|
|
137
|
+
reason: `"${right}" matches ${candidates.length} accounts ambiguously: ${candidates.slice(0, 4).map(([, a]) => a.name).join(", ")}.`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Signal 2: the deal's named contact already belongs to an account.
|
|
144
|
+
const contact = contactsByName.get(normalize(left));
|
|
145
|
+
const contactAccount = contact?.accountId ? accountsById.get(contact.accountId) ?? null : null;
|
|
146
|
+
|
|
147
|
+
if (nameMatch && contactAccount) {
|
|
148
|
+
if (nameMatch.id === contactAccount.id) {
|
|
149
|
+
return {
|
|
150
|
+
...base,
|
|
151
|
+
suggestedValue: nameMatch.id,
|
|
152
|
+
confidence: "high",
|
|
153
|
+
reason: `${nameMatchKind} on "${nameMatch.name}", confirmed by contact "${left}"'s account association.`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
...base,
|
|
158
|
+
suggestedValue: null,
|
|
159
|
+
confidence: "none",
|
|
160
|
+
reason: `Signals conflict: name matches "${nameMatch.name}" but contact "${left}" belongs to "${contactAccount.name}".`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (nameMatch) {
|
|
164
|
+
return {
|
|
165
|
+
...base,
|
|
166
|
+
suggestedValue: nameMatch.id,
|
|
167
|
+
confidence: nameMatchKind === "exact name match" ? "high" : "low",
|
|
168
|
+
reason: `${nameMatchKind} on "${nameMatch.name}" (no contact signal to confirm).`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (contactAccount) {
|
|
172
|
+
return {
|
|
173
|
+
...base,
|
|
174
|
+
suggestedValue: contactAccount.id,
|
|
175
|
+
confidence: "low",
|
|
176
|
+
reason: `Contact "${left}" belongs to "${contactAccount.name}" (no account-name match to confirm).`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (contact && !contact.accountId && right) {
|
|
180
|
+
return {
|
|
181
|
+
...base,
|
|
182
|
+
suggestedValue: `create:${right}`,
|
|
183
|
+
confidence: "create",
|
|
184
|
+
reason: `No account named "${right}" exists and contact "${left}" has none — approving creates the company, then links.`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
...base,
|
|
189
|
+
suggestedValue: null,
|
|
190
|
+
confidence: "none",
|
|
191
|
+
reason: right
|
|
192
|
+
? `No account matches "${right}" and "${left}" is not a known contact. Supply --value ${operation.id}=<accountId> or --value ${operation.id}=create:<Company Name>.`
|
|
193
|
+
: `Deal name "${deal.name}" has no "Contact - Company" pattern to derive a company from. Supply --value ${operation.id}=<accountId> or create:<Company Name>.`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalize(value: string) {
|
|
198
|
+
return value
|
|
199
|
+
.toLowerCase()
|
|
200
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
201
|
+
.trim();
|
|
202
|
+
}
|