fullstackgtm 0.47.0 → 0.48.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 (52) hide show
  1. package/CHANGELOG.md +38 -6
  2. package/INSTALL_FOR_AGENTS.md +3 -3
  3. package/README.md +16 -8
  4. package/dist/audit.js +7 -0
  5. package/dist/backfill.js +2 -16
  6. package/dist/cli/fix.d.ts +3 -0
  7. package/dist/cli/fix.js +70 -1
  8. package/dist/cli/help.d.ts +6 -0
  9. package/dist/cli/help.js +76 -2
  10. package/dist/cli/suggest.d.ts +2 -1
  11. package/dist/cli/suggest.js +49 -3
  12. package/dist/cli.js +40 -15
  13. package/dist/connectors/hubspot.d.ts +2 -1
  14. package/dist/connectors/salesforce.d.ts +2 -1
  15. package/dist/connectors/signalSources.js +2 -11
  16. package/dist/format.js +31 -5
  17. package/dist/freeEmailDomains.d.ts +1 -0
  18. package/dist/freeEmailDomains.js +14 -0
  19. package/dist/hierarchy.d.ts +29 -0
  20. package/dist/hierarchy.js +164 -0
  21. package/dist/index.d.ts +6 -2
  22. package/dist/index.js +5 -1
  23. package/dist/mcp.js +19 -15
  24. package/dist/relationships.d.ts +39 -0
  25. package/dist/relationships.js +101 -0
  26. package/dist/route.d.ts +46 -0
  27. package/dist/route.js +228 -0
  28. package/dist/rules.d.ts +1 -0
  29. package/dist/rules.js +172 -12
  30. package/dist/types.d.ts +15 -0
  31. package/docs/api.md +20 -3
  32. package/docs/architecture.md +5 -2
  33. package/package.json +1 -1
  34. package/skills/fullstackgtm/SKILL.md +2 -2
  35. package/src/audit.ts +7 -0
  36. package/src/backfill.ts +2 -17
  37. package/src/cli/fix.ts +68 -1
  38. package/src/cli/help.ts +83 -2
  39. package/src/cli/suggest.ts +58 -4
  40. package/src/cli.ts +38 -13
  41. package/src/connectors/hubspot.ts +3 -1
  42. package/src/connectors/salesforce.ts +3 -1
  43. package/src/connectors/signalSources.ts +2 -12
  44. package/src/format.ts +33 -5
  45. package/src/freeEmailDomains.ts +14 -0
  46. package/src/hierarchy.ts +185 -0
  47. package/src/index.ts +25 -0
  48. package/src/mcp.ts +22 -16
  49. package/src/relationships.ts +115 -0
  50. package/src/route.ts +278 -0
  51. package/src/rules.ts +192 -12
  52. package/src/types.ts +17 -0
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@ import { activeProfile, listProfiles, setActiveProfile } from "./credentials.js"
2
2
  import { capabilitiesCommand, printCommandHelpJson, robotDocsCommand, unknownCommandEnvelope } from "./cli/capabilities.js";
3
3
  import { BESPOKE_HELP, commandHelp, HELP, shortUsage, stylizeShortUsage, usage } from "./cli/help.js";
4
4
  import { readPackageInfo } from "./cli/shared.js";
5
- import { correctedCommand, detectFlagTypo, suggestCommand, unknownFlagEnvelope } from "./cli/suggest.js";
5
+ import { correctedCommand, detectFlagTypo, detectUnknownFlag, suggestCommand, unknownFlagEnvelope } from "./cli/suggest.js";
6
6
  import { colorEnabled, paint } from "./cli/ui.js";
7
7
  // Verb modules load lazily inside their dispatch branch below. The dispatcher
8
8
  // used to import all of them eagerly, so `--version` compiled the full
@@ -56,16 +56,22 @@ export async function runCli(argv) {
56
56
  }
57
57
  if (command === "help") {
58
58
  const [topic] = args;
59
- if (topic && topic !== "--full" && !topic.startsWith("-")) {
59
+ if (topic && !topic.startsWith("-") && args.includes("--json")) {
60
60
  // `help <cmd> --json` → machine-readable help derived from the same
61
- // HELP entry the plain text renders from. Plain output is unchanged.
62
- if (args.includes("--json"))
63
- printCommandHelpJson(topic);
64
- else
65
- console.log(commandHelp(topic));
61
+ // HELP entry the plain text renders from. JSON takes precedence over
62
+ // --full so automated callers get a stable shape.
63
+ printCommandHelpJson(topic);
64
+ }
65
+ else if (args.includes("--full")) {
66
+ // --full is the global escape hatch to the complete reference, even
67
+ // when a topic is given (help <cmd> --full), unless --json is requested.
68
+ console.log(usage());
69
+ }
70
+ else if (topic && !topic.startsWith("-")) {
71
+ console.log(commandHelp(topic));
66
72
  }
67
73
  else {
68
- console.log(args.includes("--full") || topic === "--full" ? usage() : styledShortUsage());
74
+ console.log(styledShortUsage());
69
75
  }
70
76
  return;
71
77
  }
@@ -90,20 +96,27 @@ export async function runCli(argv) {
90
96
  }
91
97
  // Flag typos used to be silently dropped by the per-verb parsers —
92
98
  // `audit --demo --jsn` exited 0 and printed markdown where the agent asked
93
- // for JSON. Near-miss unknown flags (≤ 1 edit from a flag documented in
94
- // help.ts) now stop with the exact corrected command. Suggest-only: never
95
- // auto-execute the correction, so a typo can never change what a
96
- // write-shaped invocation stages.
99
+ // for JSON. Every flag-shaped token is now checked against the per-command
100
+ // registry in help.ts, so unknown flags fail closed instead of being ignored.
101
+ // Suggest-only: never auto-execute the correction, so a typo can never
102
+ // silently change what a write-shaped invocation stages.
97
103
  if (command in HELP) {
98
- const typo = detectFlagTypo(args);
104
+ const typo = detectUnknownFlag(command, args);
99
105
  if (typo) {
100
106
  if (args.includes("--json")) {
101
107
  console.log(JSON.stringify(unknownFlagEnvelope(command, args, typo), null, 2));
102
108
  }
103
109
  else {
110
+ console.error(`Unknown flag for ${command}: ${typo.given}`);
111
+ // Keep the old near-miss shape too; existing agents key off it.
104
112
  console.error(`Unknown flag: ${typo.given}`);
105
- console.error(`Did you mean: ${typo.suggestion}`);
106
- console.error(`Try: ${correctedCommand(command, args, typo)}`);
113
+ if (typo.suggestion) {
114
+ console.error(`Did you mean ${typo.suggestion}?`);
115
+ console.error(`Did you mean: ${typo.suggestion}`);
116
+ if (typo.replacement.length > 0) {
117
+ console.error(`Try: ${correctedCommand(command, args, typo)}`);
118
+ }
119
+ }
107
120
  }
108
121
  process.exitCode = 1;
109
122
  return;
@@ -161,6 +174,18 @@ export async function runCli(argv) {
161
174
  await (await import("./cli/fix.js")).resolveCommand(args);
162
175
  return;
163
176
  }
177
+ if (command === "route") {
178
+ await (await import("./cli/fix.js")).routeCommand(args);
179
+ return;
180
+ }
181
+ if (command === "hierarchy") {
182
+ await (await import("./cli/fix.js")).hierarchyCommand(args);
183
+ return;
184
+ }
185
+ if (command === "relationships") {
186
+ await (await import("./cli/fix.js")).relationshipsCommand(args);
187
+ return;
188
+ }
164
189
  if (command === "bulk-update") {
165
190
  await (await import("./cli/fix.js")).bulkUpdateCommand(args);
166
191
  return;
@@ -1,6 +1,7 @@
1
1
  import { type FieldMappings } from "../mappings.ts";
2
2
  import type { GtmConnector, SnapshotProgress } from "../types.ts";
3
3
  import { type ProgressEmitter } from "../progress.ts";
4
+ export type HubspotWritableConnector = GtmConnector & Required<Pick<GtmConnector, "applyOperation" | "readField">>;
4
5
  export type HubspotConnectorOptions = {
5
6
  /** Returns a HubSpot access token (private app token or OAuth access token). */
6
7
  getAccessToken: () => string | Promise<string>;
@@ -26,4 +27,4 @@ export type HubspotConnectorOptions = {
26
27
  * amountless deals — so audit rules can surface the gaps instead of hiding
27
28
  * them.
28
29
  */
29
- export declare function createHubspotConnector(options: HubspotConnectorOptions): GtmConnector;
30
+ export declare function createHubspotConnector(options: HubspotConnectorOptions): HubspotWritableConnector;
@@ -6,6 +6,7 @@ export type SalesforceConnection = {
6
6
  /** e.g. https://yourorg.my.salesforce.com */
7
7
  instanceUrl: string;
8
8
  };
9
+ export type SalesforceWritableConnector = GtmConnector & Required<Pick<GtmConnector, "applyOperation" | "readField">>;
9
10
  export type SalesforceConnectorOptions = {
10
11
  /** Returns an access token plus the instance URL it belongs to. */
11
12
  getConnection: () => SalesforceConnection | Promise<SalesforceConnection>;
@@ -32,4 +33,4 @@ export type SalesforceConnectorOptions = {
32
33
  * surface the gaps). Probabilities are normalized to 0..1 to match the
33
34
  * canonical model.
34
35
  */
35
- export declare function createSalesforceConnector(options: SalesforceConnectorOptions): GtmConnector;
36
+ export declare function createSalesforceConnector(options: SalesforceConnectorOptions): SalesforceWritableConnector;
@@ -20,6 +20,7 @@
20
20
  */
21
21
  import { readFileSync, statSync } from "node:fs";
22
22
  import { resolve } from "node:path";
23
+ import { FREE_EMAIL_DOMAINS } from "../freeEmailDomains.js";
23
24
  import { SIGNAL_BUCKETS } from "../signals.js";
24
25
  import { parseSpoolText, spoolFilesIn } from "../spoolFiles.js";
25
26
  // ---------------------------------------------------------------------------
@@ -243,22 +244,12 @@ function fieldValue(values, name) {
243
244
  }
244
245
  return "";
245
246
  }
246
- const FREE_MAIL = new Set([
247
- "gmail.com",
248
- "yahoo.com",
249
- "hotmail.com",
250
- "outlook.com",
251
- "icloud.com",
252
- "aol.com",
253
- "proton.me",
254
- "protonmail.com",
255
- ]);
256
247
  /** Company domain from an email, or "" for free-mail / no email. */
257
248
  function corporateDomain(email) {
258
249
  if (!email.includes("@"))
259
250
  return "";
260
251
  const domain = email.split("@").at(-1).trim().toLowerCase();
261
- if (!domain || FREE_MAIL.has(domain))
252
+ if (!domain || FREE_EMAIL_DOMAINS.has(domain))
262
253
  return "";
263
254
  return domain;
264
255
  }
package/dist/format.js CHANGED
@@ -1,8 +1,7 @@
1
- // `summary: true` renders only the header + the Findings-by-Rule table + an
2
- // operation count, for read verbs like `audit` where the full per-operation
3
- // dump (1,400+ lines on a real portal) buries the signal. Defaults to the full
4
- // view so write-preview callers (bulk-update, fix, dedupe, plans show, MCP) —
5
- // where you approve specific operations — keep every operation's detail.
1
+ // `summary: true` renders the compact audit view: header, findings summary,
2
+ // assumptions/open questions, and an operation count. Defaults to the full view
3
+ // so write-preview callers (bulk-update, fix, dedupe, plans show, MCP) where
4
+ // you approve specific operations keep every operation's detail.
6
5
  export function patchPlanToMarkdown(plan, opts = {}) {
7
6
  const lines = [
8
7
  `# ${plan.title}`,
@@ -25,6 +24,7 @@ export function patchPlanToMarkdown(plan, opts = {}) {
25
24
  });
26
25
  lines.push("");
27
26
  }
27
+ appendAssumptionsAndDecisionPoints(lines, plan);
28
28
  if (opts.summary) {
29
29
  const ops = plan.operations.length;
30
30
  lines.push(ops === 0
@@ -96,6 +96,32 @@ export function formatPatchPlanRun(run) {
96
96
  }
97
97
  return `${lines.join("\n")}\n`;
98
98
  }
99
+ function appendAssumptionsAndDecisionPoints(lines, plan) {
100
+ const assumptions = [...(plan.assumptions ?? [])].sort((left, right) => {
101
+ if (left.confidence === "guess" && right.confidence !== "guess")
102
+ return -1;
103
+ if (left.confidence !== "guess" && right.confidence === "guess")
104
+ return 1;
105
+ return 0;
106
+ });
107
+ const openQuestions = plan.openQuestions ?? [];
108
+ if (assumptions.length === 0 && openQuestions.length === 0)
109
+ return;
110
+ lines.push("## Assumptions & decision points", "");
111
+ for (const assumption of assumptions) {
112
+ const warning = assumption.confidence === "guess" ? "⚠️ " : "";
113
+ lines.push(`- ${warning}${assumption.text}${assumption.source ? ` _(source: ${assumption.source})_` : ""}`);
114
+ }
115
+ if (openQuestions.length > 0) {
116
+ if (assumptions.length > 0)
117
+ lines.push("");
118
+ lines.push("### Open questions", "");
119
+ for (const question of openQuestions) {
120
+ lines.push(`- ${question}`);
121
+ }
122
+ }
123
+ lines.push("");
124
+ }
99
125
  function formatValue(value) {
100
126
  if (value === undefined || value === null || value === "")
101
127
  return "unset";
@@ -0,0 +1 @@
1
+ export declare const FREE_EMAIL_DOMAINS: Set<string>;
@@ -0,0 +1,14 @@
1
+ // Shared free-mail domains that should never be treated as company domains.
2
+ export const FREE_EMAIL_DOMAINS = new Set([
3
+ "gmail.com",
4
+ "googlemail.com",
5
+ "yahoo.com",
6
+ "outlook.com",
7
+ "hotmail.com",
8
+ "live.com",
9
+ "icloud.com",
10
+ "me.com",
11
+ "aol.com",
12
+ "proton.me",
13
+ "protonmail.com",
14
+ ]);
@@ -0,0 +1,29 @@
1
+ import type { CanonicalGtmSnapshot } from "./types.ts";
2
+ export type AccountHierarchyNode = {
3
+ accountId: string;
4
+ name: string;
5
+ domain?: string;
6
+ parentAccountId?: string;
7
+ parentReason?: string;
8
+ children: AccountHierarchyNode[];
9
+ };
10
+ export type AccountHierarchyConflict = {
11
+ type: "duplicate_domain" | "ambiguous_parent" | "cycle";
12
+ accountIds: string[];
13
+ key: string;
14
+ detail: string;
15
+ };
16
+ export type AccountHierarchyReport = {
17
+ generatedAt: string;
18
+ roots: AccountHierarchyNode[];
19
+ orphanAccounts: string[];
20
+ conflicts: AccountHierarchyConflict[];
21
+ counts: {
22
+ accounts: number;
23
+ roots: number;
24
+ linkedChildren: number;
25
+ conflicts: number;
26
+ };
27
+ };
28
+ export declare function buildAccountHierarchy(snapshot: CanonicalGtmSnapshot): AccountHierarchyReport;
29
+ export declare function accountHierarchyToMarkdown(report: AccountHierarchyReport): string;
@@ -0,0 +1,164 @@
1
+ import { normalizeDomain } from "./merge.js";
2
+ function rawString(account, keys) {
3
+ if (!account.raw || typeof account.raw !== "object")
4
+ return undefined;
5
+ const raw = account.raw;
6
+ for (const key of keys) {
7
+ const value = raw[key];
8
+ if (typeof value === "string" && value.trim())
9
+ return value.trim();
10
+ if (value && typeof value === "object") {
11
+ const nested = value;
12
+ for (const nestedKey of ["id", "value", "name"]) {
13
+ if (typeof nested[nestedKey] === "string" && String(nested[nestedKey]).trim())
14
+ return String(nested[nestedKey]).trim();
15
+ }
16
+ }
17
+ }
18
+ return undefined;
19
+ }
20
+ function parentHint(account) {
21
+ return rawString(account, ["parentAccountId", "parent_account_id", "ParentId", "parentCompanyId", "hs_parent_company_id"]);
22
+ }
23
+ function cloneNode(account) {
24
+ return { accountId: String(account.id), name: account.name, ...(account.domain ? { domain: account.domain } : {}), children: [] };
25
+ }
26
+ function looksLikePublicSuffix(domain) {
27
+ const parts = domain.split(".");
28
+ if (parts.length !== 2)
29
+ return false;
30
+ const [label, tld] = parts;
31
+ return tld.length === 2 && ["ac", "co", "com", "edu", "gov", "net", "org"].includes(label);
32
+ }
33
+ function domainParent(childDomain, domains) {
34
+ const parts = childDomain.split(".");
35
+ for (let i = 1; i < parts.length - 1; i++) {
36
+ const candidate = parts.slice(i).join(".");
37
+ if (candidate.split(".").length < 2 || looksLikePublicSuffix(candidate))
38
+ continue;
39
+ const unique = new Map((domains.get(candidate) ?? []).map((account) => [String(account.id), account]));
40
+ if (unique.size > 1)
41
+ return "ambiguous";
42
+ if (unique.size === 1)
43
+ return [...unique.values()][0];
44
+ }
45
+ return null;
46
+ }
47
+ function detectCycles(parentByChild) {
48
+ const cyclic = new Set();
49
+ const cycles = [];
50
+ const emitted = new Set();
51
+ for (const start of parentByChild.keys()) {
52
+ const path = [];
53
+ const indexById = new Map();
54
+ let cursor = start;
55
+ while (cursor && parentByChild.has(cursor)) {
56
+ const seenAt = indexById.get(cursor);
57
+ if (seenAt !== undefined) {
58
+ const cycle = path.slice(seenAt);
59
+ for (const id of cycle)
60
+ cyclic.add(id);
61
+ const key = [...cycle].sort().join("|");
62
+ if (!emitted.has(key)) {
63
+ emitted.add(key);
64
+ cycles.push(cycle);
65
+ }
66
+ break;
67
+ }
68
+ indexById.set(cursor, path.length);
69
+ path.push(cursor);
70
+ cursor = parentByChild.get(cursor)?.parentId;
71
+ }
72
+ }
73
+ return { accountIds: cyclic, cycles };
74
+ }
75
+ export function buildAccountHierarchy(snapshot) {
76
+ const byId = new Map(snapshot.accounts.map((account) => [String(account.id), account]));
77
+ const accounts = [...byId.values()];
78
+ const domains = new Map();
79
+ for (const account of accounts) {
80
+ const domain = normalizeDomain(account.domain);
81
+ if (domain)
82
+ domains.set(domain, [...(domains.get(domain) ?? []), account]);
83
+ }
84
+ const conflicts = [];
85
+ for (const [domain, accounts] of domains) {
86
+ const ids = [...new Set(accounts.map((account) => String(account.id)))];
87
+ if (ids.length > 1)
88
+ conflicts.push({ type: "duplicate_domain", accountIds: ids.sort(), key: domain, detail: `${ids.length} accounts share domain ${domain}; hierarchy inference will not choose between them.` });
89
+ }
90
+ const nodes = new Map(accounts.map((account) => [String(account.id), cloneNode(account)]));
91
+ const parentByChild = new Map();
92
+ for (const account of accounts) {
93
+ const id = String(account.id);
94
+ const hint = parentHint(account);
95
+ if (hint && byId.has(hint) && hint !== id) {
96
+ parentByChild.set(id, { parentId: hint, reason: "provider-parent" });
97
+ continue;
98
+ }
99
+ const domain = normalizeDomain(account.domain);
100
+ if (!domain)
101
+ continue;
102
+ const inferred = domainParent(domain, domains);
103
+ if (inferred === "ambiguous")
104
+ conflicts.push({ type: "ambiguous_parent", accountIds: [id], key: domain, detail: `Multiple possible parent accounts found for ${domain}; leaving account as a root.` });
105
+ else if (inferred && String(inferred.id) !== id)
106
+ parentByChild.set(id, { parentId: String(inferred.id), reason: "domain-subdomain" });
107
+ }
108
+ const cyclicAccounts = detectCycles(parentByChild);
109
+ for (const cycle of cyclicAccounts.cycles) {
110
+ const ordered = [...cycle].sort();
111
+ const key = ordered.join(" -> ");
112
+ conflicts.push({
113
+ type: "cycle",
114
+ accountIds: ordered,
115
+ key,
116
+ detail: `Cyclic parent hints detected among ${ordered.join(", ")}; leaving those accounts as roots instead of hiding them in a loop.`,
117
+ });
118
+ }
119
+ for (const accountId of cyclicAccounts.accountIds)
120
+ parentByChild.delete(accountId);
121
+ const roots = [];
122
+ const orphanAccounts = [];
123
+ const linkedChildren = new Set();
124
+ for (const account of accounts) {
125
+ const id = String(account.id);
126
+ const node = nodes.get(id);
127
+ const parent = parentByChild.get(id);
128
+ if (parent && nodes.has(parent.parentId)) {
129
+ node.parentAccountId = parent.parentId;
130
+ node.parentReason = parent.reason;
131
+ nodes.get(parent.parentId).children.push(node);
132
+ linkedChildren.add(id);
133
+ }
134
+ else {
135
+ roots.push(node);
136
+ if (!account.domain && !parentHint(account))
137
+ orphanAccounts.push(id);
138
+ }
139
+ }
140
+ const sortTree = (list) => {
141
+ list.sort((a, b) => a.name.localeCompare(b.name) || a.accountId.localeCompare(b.accountId));
142
+ for (const node of list)
143
+ sortTree(node.children);
144
+ };
145
+ sortTree(roots);
146
+ return { generatedAt: snapshot.generatedAt, roots, orphanAccounts: orphanAccounts.sort(), conflicts, counts: { accounts: accounts.length, roots: roots.length, linkedChildren: linkedChildren.size, conflicts: conflicts.length } };
147
+ }
148
+ export function accountHierarchyToMarkdown(report) {
149
+ const lines = ["# Account hierarchy", "", `${report.counts.accounts} account(s), ${report.counts.roots} root(s), ${report.counts.linkedChildren} inferred child link(s), ${report.counts.conflicts} conflict(s).`, ""];
150
+ const walk = (nodes, depth) => {
151
+ for (const node of nodes) {
152
+ const suffix = [node.domain, node.parentReason ? `via ${node.parentReason}` : undefined].filter(Boolean).join("; ");
153
+ lines.push(`${" ".repeat(depth)}- ${node.name} (${node.accountId}${suffix ? ` — ${suffix}` : ""})`);
154
+ walk(node.children, depth + 1);
155
+ }
156
+ };
157
+ walk(report.roots, 0);
158
+ if (report.conflicts.length) {
159
+ lines.push("", "## Conflicts");
160
+ for (const conflict of report.conflicts)
161
+ lines.push(`- ${conflict.type} ${conflict.key}: ${conflict.detail}`);
162
+ }
163
+ return lines.join("\n");
164
+ }
package/dist/index.d.ts CHANGED
@@ -3,6 +3,9 @@ export { matchesTerritory, parseAssignmentPolicy, resolveAssignment, type Assign
3
3
  export { buildBulkUpdatePlan, isFilterableField, parseWhere, type BulkUpdateOptions } from "./bulkUpdate.ts";
4
4
  export { buildDedupePlan, dedupeKey, type DedupeOptions } from "./dedupe.ts";
5
5
  export { buildReassignPlans, type ReassignObjectType, type ReassignOptions } from "./reassign.ts";
6
+ export { buildLeadRoutePlan, type LeadRouteCounts, type LeadRouteOptions, type LeadRouteResult, } from "./route.ts";
7
+ export { accountHierarchyToMarkdown, buildAccountHierarchy, type AccountHierarchyConflict, type AccountHierarchyNode, type AccountHierarchyReport, } from "./hierarchy.ts";
8
+ export { buildRelationshipMap, relationshipMapToMarkdown, type RelationshipMap, type StakeholderNode, type StakeholderRole, type StakeholderSentiment, } from "./relationships.ts";
6
9
  export { CONFIG_FILE_NAME, loadConfig, mergePolicy, resolveConfiguredRules, type FullstackgtmConfig, type LoadedConfig, } from "./config.ts";
7
10
  export { applyPatchPlan, type ApplyPatchPlanOptions } from "./connector.ts";
8
11
  export { APPLY_STAGES, BACKFILL_STRIPE_STAGES, composeListeners, createProgressEmitter, CRM_SYNC_STAGES, nullProgressEmitter, SNAPSHOT_PULL_STAGES, STRIPE_SNAPSHOT_STAGES, type ProgressEmitter, type ProgressEvent, type ProgressListener, type ProgressSnapshot, } from "./progress.ts";
@@ -23,11 +26,12 @@ export { mergeSnapshots, type MergeConflict, type MergeMatch, type MergeReport,
23
26
  export { createFilePlanStore, type PlanStore, type StoredPlan } from "./planStore.ts";
24
27
  export { computeApprovalDigests, loadOrCreateSigningKey, loadSigningKey, signApproval, verifyApprovalDigests, type ApprovalVerification, } from "./integrity.ts";
25
28
  export { buildAuditLog, verifyAuditLog, type AuditLogEntry, type AuditLogExport, type AuditLogVerification, } from "./auditLog.ts";
29
+ export { FREE_EMAIL_DOMAINS } from "./freeEmailDomains.ts";
26
30
  export { formatPatchPlanRun, patchPlanToMarkdown } from "./format.ts";
27
31
  export { computeHealth, summarizeHealth, healthToMarkdown, type HealthEntry, type HealthRollup, type HealthRuleDelta, } from "./health.ts";
28
32
  export { auditReportToHtml, auditReportToMarkdown, type ReportOptions } from "./report.ts";
29
33
  export { HUBSPOT_DEFAULT_FIELD_MAPPINGS, SALESFORCE_DEFAULT_FIELD_MAPPINGS, mappedField, mappedFields, normalizeFieldMappings, readMappedValue, type CrmObjectType, type FieldMappings, } from "./mappings.ts";
30
- export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, duplicateAccountDomainRule, duplicateContactEmailRule, duplicateOpenDealRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, provenanceSummary, requiresHumanInput, staleDealRule, } from "./rules.ts";
34
+ export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, isFindingsOnlyRule, duplicateAccountDomainRule, duplicateContactEmailRule, duplicateOpenDealRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, provenanceSummary, requiresHumanInput, staleDealRule, } from "./rules.ts";
31
35
  export { extractCallInsights, normalizeTranscript, parseCall, parseTranscript, suggestCallDeal, summarizeInsights, type CallDealSuggestion, type CallInsightType, type ExtractedCallInsight, type ParsedCall, type ParsedTranscriptSegment, } from "./calls.ts";
32
36
  export { sampleSnapshot } from "./sampleData.ts";
33
37
  export { DEFAULT_MODELS, DEFAULT_RUBRIC, classifyCallLlm, detectProviderFromKey, extractInsightsLlm, forcedToolCall, parseRubric, resolveLlmCredential, scoreCallLlm, validateLlmKey, type CallScorecard, type LlmCallClassification, type LlmCredential, type LlmExtractedInsight, type LlmProvider, type Rubric, type RubricDimension, type ScoreBand, type ScoredDimension, } from "./llm.ts";
@@ -48,4 +52,4 @@ export { fetchAtsJobs, snippetFor, type AtsBoardSource, type AtsJob, } from "./c
48
52
  export { scoreBand, judgeRunId, normalizeSpan, isGroundedSpan, scoreAccount, accountRecentlyTouched, bestContactForAccount, deterministicDecision, deterministicWhyNow, JUDGE_SCHEMA, DEFAULT_JUDGE_PROMPT, judgeDecisionLlm, judgeSignals, judgeDir, createFileJudgeStore, type JudgeDecisionKind, type JudgeDecision, type JudgeRun, type JudgeBand, type AccountScore, type JudgeStore, } from "./judge.ts";
49
53
  export { draft, authorOpeners, draftOpenerLlm, deterministicOpener, creditedTriggerSignal, draftEvidence, sanitizeOpener, firstLine, firstLineGroundedInTrigger, hasBannedGreeting, isStaleTrigger, draftOperationId, draftEvidenceId, DRAFT_CHANNELS, DRAFT_SCHEMA, DEFAULT_DRAFT_PROMPT, DEFAULT_FRESHNESS_DAYS, type Draft, type RejectedDraft, type DraftResult, type DraftChannel, } from "./draft.ts";
50
54
  export { DEFAULT_GOLDEN_SET, DEFAULT_MIN_ACCURACY, HOT_SCORE, defaultJudgeFn, gradeAgainstOutcomes, gradeJudge, parseGoldenSet, type Confusion, type EvalJudgeFn, type GoldenHistory, type GoldenRow, type GradeResult, type OutcomeCalibration, } from "./judgeEval.ts";
51
- 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";
55
+ 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, PatchPlanAssumption, PatchPlanAssumptionConfidence, PatchPlanRun, PatchPlanRunStatus, PatchVerification, PipelineFinding, PipelineFindingStatus, PipelineFindingType, ProviderIdentity, RiskLevel, SourceFreshness, } from "./types.ts";
package/dist/index.js CHANGED
@@ -3,6 +3,9 @@ export { matchesTerritory, parseAssignmentPolicy, resolveAssignment, } from "./a
3
3
  export { buildBulkUpdatePlan, isFilterableField, parseWhere } from "./bulkUpdate.js";
4
4
  export { buildDedupePlan, dedupeKey } from "./dedupe.js";
5
5
  export { buildReassignPlans } from "./reassign.js";
6
+ export { buildLeadRoutePlan, } from "./route.js";
7
+ export { accountHierarchyToMarkdown, buildAccountHierarchy, } from "./hierarchy.js";
8
+ export { buildRelationshipMap, relationshipMapToMarkdown, } from "./relationships.js";
6
9
  export { CONFIG_FILE_NAME, loadConfig, mergePolicy, resolveConfiguredRules, } from "./config.js";
7
10
  export { applyPatchPlan } from "./connector.js";
8
11
  export { APPLY_STAGES, BACKFILL_STRIPE_STAGES, composeListeners, createProgressEmitter, CRM_SYNC_STAGES, nullProgressEmitter, SNAPSHOT_PULL_STAGES, STRIPE_SNAPSHOT_STAGES, } from "./progress.js";
@@ -23,11 +26,12 @@ export { mergeSnapshots, } from "./merge.js";
23
26
  export { createFilePlanStore } from "./planStore.js";
24
27
  export { computeApprovalDigests, loadOrCreateSigningKey, loadSigningKey, signApproval, verifyApprovalDigests, } from "./integrity.js";
25
28
  export { buildAuditLog, verifyAuditLog, } from "./auditLog.js";
29
+ export { FREE_EMAIL_DOMAINS } from "./freeEmailDomains.js";
26
30
  export { formatPatchPlanRun, patchPlanToMarkdown } from "./format.js";
27
31
  export { computeHealth, summarizeHealth, healthToMarkdown, } from "./health.js";
28
32
  export { auditReportToHtml, auditReportToMarkdown } from "./report.js";
29
33
  export { HUBSPOT_DEFAULT_FIELD_MAPPINGS, SALESFORCE_DEFAULT_FIELD_MAPPINGS, mappedField, mappedFields, normalizeFieldMappings, readMappedValue, } from "./mappings.js";
30
- export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, duplicateAccountDomainRule, duplicateContactEmailRule, duplicateOpenDealRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, provenanceSummary, requiresHumanInput, staleDealRule, } from "./rules.js";
34
+ export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, isFindingsOnlyRule, duplicateAccountDomainRule, duplicateContactEmailRule, duplicateOpenDealRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, provenanceSummary, requiresHumanInput, staleDealRule, } from "./rules.js";
31
35
  export { extractCallInsights, normalizeTranscript, parseCall, parseTranscript, suggestCallDeal, summarizeInsights, } from "./calls.js";
32
36
  export { sampleSnapshot } from "./sampleData.js";
33
37
  export { DEFAULT_MODELS, DEFAULT_RUBRIC, classifyCallLlm, detectProviderFromKey, extractInsightsLlm, forcedToolCall, parseRubric, resolveLlmCredential, scoreCallLlm, validateLlmKey, } from "./llm.js";
package/dist/mcp.js CHANGED
@@ -129,6 +129,9 @@ function packageVersion() {
129
129
  return "0.0.0";
130
130
  }
131
131
  }
132
+ function strictSchema(shape) {
133
+ return z.object(shape).strict();
134
+ }
132
135
  // Single registration table. startMcpServer() registers exactly this list,
133
136
  // and fullstackgtm_capabilities reports its names from the same array — the
134
137
  // advertised inventory cannot drift from what is actually registered.
@@ -143,7 +146,7 @@ const toolDefinitions = [
143
146
  description: "Run a dry-run GTM hygiene audit and return a reviewable patch plan. " +
144
147
  "Sources: the realistic zero-credential demo CRM (provider: \"demo\" — richest test data), " +
145
148
  "the minimal sample dataset, a snapshot file, or a live provider.",
146
- inputSchema: {
149
+ inputSchema: strictSchema({
147
150
  provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
148
151
  inputPath: z.string().optional(),
149
152
  configPath: z.string().optional(),
@@ -151,7 +154,7 @@ const toolDefinitions = [
151
154
  output: z.enum(["json", "markdown"]).optional(),
152
155
  today: z.string().optional(),
153
156
  staleDealDays: z.number().int().positive().optional(),
154
- },
157
+ }),
155
158
  },
156
159
  handler: async ({ provider, inputPath, configPath, rules, output, today, staleDealDays }) => {
157
160
  const loaded = configPath ? loadConfig(configPath) : null;
@@ -176,11 +179,11 @@ const toolDefinitions = [
176
179
  description: "Derive values for a plan's requires_human_* placeholder operations from snapshot " +
177
180
  "evidence (account-name matching, contact associations), with confidence levels and " +
178
181
  "reasons. Read-only; feed accepted values into fullstackgtm_apply's valueOverrides.",
179
- inputSchema: {
182
+ inputSchema: strictSchema({
180
183
  planPath: z.string(),
181
184
  provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
182
185
  inputPath: z.string().optional(),
183
- },
186
+ }),
184
187
  },
185
188
  handler: async ({ planPath, provider, inputPath }) => {
186
189
  const plan = JSON.parse(readFileSync(resolve(process.cwd(), planPath), "utf8"));
@@ -198,14 +201,14 @@ const toolDefinitions = [
198
201
  "uses LLM extraction when an Anthropic/OpenAI key is configured in the server " +
199
202
  "environment or credential store, else the free deterministic keyword baseline; " +
200
203
  "'llm' and 'deterministic' force either. Read-only; every insight is provenance-marked.",
201
- inputSchema: {
204
+ inputSchema: strictSchema({
202
205
  transcript: z.string().optional(),
203
206
  transcriptPath: z.string().optional(),
204
207
  title: z.string().optional(),
205
208
  source: z.enum(["gong", "chorus", "fathom", "manual", "csv", "unknown"]).optional(),
206
209
  extractor: z.enum(["auto", "llm", "deterministic"]).optional(),
207
210
  model: z.string().optional(),
208
- },
211
+ }),
209
212
  },
210
213
  handler: async ({ transcript, transcriptPath, title, source, extractor, model }) => {
211
214
  const raw = transcript ??
@@ -238,7 +241,7 @@ const toolDefinitions = [
238
241
  "(exists | ambiguous | safe_to_create) with matches and a reason, using the same " +
239
242
  "identity keys as the audit/merge engines (account domain, contact email, open-deal " +
240
243
  "key). Read-only. Never create on 'exists' or 'ambiguous'.",
241
- inputSchema: {
244
+ inputSchema: strictSchema({
242
245
  objectType: z.enum(["account", "contact", "deal"]),
243
246
  name: z.string().optional(),
244
247
  domain: z.string().optional(),
@@ -246,7 +249,7 @@ const toolDefinitions = [
246
249
  accountId: z.string().optional(),
247
250
  provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
248
251
  inputPath: z.string().optional(),
249
- },
252
+ }),
250
253
  },
251
254
  handler: async ({ objectType, name, domain, email, accountId, provider, inputPath }) => {
252
255
  const snapshot = await readSnapshot(provider, inputPath);
@@ -259,7 +262,7 @@ const toolDefinitions = [
259
262
  config: {
260
263
  title: "List Audit Rules",
261
264
  description: "List the built-in deterministic audit rules with ids and descriptions.",
262
- inputSchema: {},
265
+ inputSchema: strictSchema({}),
263
266
  },
264
267
  handler: async () => {
265
268
  return content(builtinAuditRules.map(({ id, title, description }) => ({ id, title, description })));
@@ -277,13 +280,13 @@ const toolDefinitions = [
277
280
  "the store's HMAC approval digests — ids not approved with `plans approve` are " +
278
281
  "refused — and the run is recorded onto the stored plan so `plans show` and " +
279
282
  "`audit-log export` include it.",
280
- inputSchema: {
283
+ inputSchema: strictSchema({
281
284
  provider: z.enum(["hubspot", "salesforce"]),
282
285
  planPath: z.string(),
283
286
  approvedOperationIds: z.array(z.string()).min(1),
284
287
  valueOverrides: z.record(z.string(), z.string()).optional(),
285
288
  output: z.enum(["json", "markdown"]).optional(),
286
- },
289
+ }),
287
290
  },
288
291
  handler: async ({ provider, planPath, approvedOperationIds, valueOverrides, output }) => {
289
292
  // The file may be a raw PatchPlan (audit --out) or a StoredPlan envelope
@@ -400,11 +403,11 @@ const toolDefinitions = [
400
403
  "and quote verbatim spans (≤300 chars) for every loud/quiet reading. Submit the full " +
401
404
  "ObservationSet via fullstackgtm_market_observe — quotes are verified character-for-" +
402
405
  "character against the captures, so never paraphrase.",
403
- inputSchema: {
406
+ inputSchema: strictSchema({
404
407
  vendorId: z.string(),
405
408
  configPath: z.string().optional().describe("Path to market.config.json (default ./market.config.json)"),
406
409
  captureRun: z.string().optional(),
407
- },
410
+ }),
408
411
  },
409
412
  handler: async ({ vendorId, configPath, captureRun }) => {
410
413
  const config = loadMarketConfigOrHint(resolve(process.cwd(), configPath ?? "market.config.json"));
@@ -420,10 +423,10 @@ const toolDefinitions = [
420
423
  "Validates coverage, the verbatim-evidence rule, and mechanically verifies every quoted " +
421
424
  "span against the stored capture it cites. Returns problems if rejected; nothing is " +
422
425
  "stored unless the whole set passes. Observations are append-only — use a new runLabel.",
423
- inputSchema: {
426
+ inputSchema: strictSchema({
424
427
  observationsPath: z.string().describe("Path to the ObservationSet JSON file"),
425
428
  configPath: z.string().optional().describe("Path to market.config.json (default ./market.config.json)"),
426
- },
429
+ }),
427
430
  },
428
431
  handler: async ({ observationsPath, configPath }) => {
429
432
  const config = loadMarketConfigOrHint(resolve(process.cwd(), configPath ?? "market.config.json"));
@@ -454,6 +457,7 @@ toolDefinitions.unshift({
454
457
  description: "Machine-readable contract for this MCP server: the tool inventory (derived from the " +
455
458
  "server's own registration table, with per-tool CRM-write flags), safety defaults, and " +
456
459
  "the CLI entrypoints for everything the tools don't cover.",
460
+ inputSchema: strictSchema({}),
457
461
  },
458
462
  handler: async () => content({
459
463
  ok: true,