fullstackgtm 0.57.0 → 0.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +162 -508
- package/dist/cli/enrich.d.ts +2 -2
- package/dist/cli/enrich.js +27 -14
- package/dist/cli/help.js +2 -2
- package/dist/cli/signals.js +14 -3
- package/dist/enrich.d.ts +1 -1
- package/dist/enrich.js +1 -1
- package/dist/enrichZoomInfo.d.ts +33 -0
- package/dist/enrichZoomInfo.js +144 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/runReport.js +11 -0
- package/docs/api.md +1 -1
- package/llms.txt +3 -2
- package/package.json +2 -2
- package/src/cli/enrich.ts +30 -15
- package/src/cli/help.ts +2 -2
- package/src/cli/signals.ts +14 -2
- package/src/enrich.ts +2 -2
- package/src/enrichZoomInfo.ts +197 -0
- package/src/index.ts +11 -0
- package/src/runReport.ts +12 -0
package/src/enrich.ts
CHANGED
|
@@ -20,7 +20,7 @@ import type {
|
|
|
20
20
|
*
|
|
21
21
|
* Every enrichment vendor ships fire-and-forget writeback — data lands without
|
|
22
22
|
* a diff, without approval, over whatever a human typed. This layer inverts
|
|
23
|
-
* that: a source (Apollo pull, Clay ingest) feeds a deterministic matcher,
|
|
23
|
+
* that: a source (Apollo/ZoomInfo pull, Clay ingest) feeds a deterministic matcher,
|
|
24
24
|
* the matcher feeds a fill-blanks-only patch plan, and the plan goes through
|
|
25
25
|
* the existing dry-run → approval → apply contract. Every proposed value is
|
|
26
26
|
* traceable to the source payload that produced it (`GtmEvidence` on the
|
|
@@ -154,7 +154,7 @@ const MATCH_KEYS: Record<EnrichObjectType, string[]> = {
|
|
|
154
154
|
};
|
|
155
155
|
|
|
156
156
|
/** API source ids the MVP can pull from. */
|
|
157
|
-
export const SUPPORTED_API_SOURCES = ["apollo", "explorium", "pipe0", "clay", "linkedin"];
|
|
157
|
+
export const SUPPORTED_API_SOURCES = ["apollo", "zoominfo", "explorium", "pipe0", "clay", "linkedin"];
|
|
158
158
|
|
|
159
159
|
/**
|
|
160
160
|
* Canonical fields enrich may target, plus the HubSpot property spellings the
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import type { EnrichObjectType, EnrichSourceRecord } from "./enrich.ts";
|
|
3
|
+
import type { ApolloPullKey as EnrichPullKey, ApolloPullOptions as EnrichPullOptions, ApolloPullResult as EnrichPullResult } from "./enrichApollo.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ZoomInfo source adapter for governed enrichment.
|
|
7
|
+
*
|
|
8
|
+
* ZoomInfo owns authentication and credit accounting in its official `gtm`
|
|
9
|
+
* CLI. fullstackgtm invokes that CLI as a read-only source, normalizes the
|
|
10
|
+
* JSON response, then sends proposed CRM changes through the normal
|
|
11
|
+
* diff → patch plan → approval → apply lifecycle. No ZoomInfo credential is
|
|
12
|
+
* copied into fullstackgtm and this adapter never writes to the CRM directly.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type ZoomInfoCommandResult = {
|
|
16
|
+
stdout: string;
|
|
17
|
+
stderr: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type ZoomInfoCommandRunner = (
|
|
21
|
+
executable: string,
|
|
22
|
+
args: string[],
|
|
23
|
+
) => Promise<ZoomInfoCommandResult>;
|
|
24
|
+
|
|
25
|
+
export type ZoomInfoClientOptions = {
|
|
26
|
+
/** Official ZoomInfo CLI executable. Defaults to `gtm`. */
|
|
27
|
+
executable?: string;
|
|
28
|
+
/** Injectable command runner for tests. */
|
|
29
|
+
run?: ZoomInfoCommandRunner;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type ZoomInfoClient = {
|
|
33
|
+
enrichCompany(domain: string): Promise<unknown>;
|
|
34
|
+
enrichContact(email: string): Promise<unknown>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const DEFAULT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
38
|
+
|
|
39
|
+
const runCommand: ZoomInfoCommandRunner = (executable, args) =>
|
|
40
|
+
new Promise((resolve, reject) => {
|
|
41
|
+
execFile(executable, args, { encoding: "utf8", maxBuffer: DEFAULT_MAX_BUFFER }, (error, stdout, stderr) => {
|
|
42
|
+
if (error) {
|
|
43
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
44
|
+
if (code === "ENOENT") {
|
|
45
|
+
reject(
|
|
46
|
+
new Error(
|
|
47
|
+
"ZoomInfo CLI not found. Install it with `brew install zoominfo/gtm-ai/gtm-ai-cli` " +
|
|
48
|
+
"or `npm install -g @zoominfo/gtm-ai-cli`, then run `gtm auth login`.",
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Do not echo stdout/stderr: provider errors can contain submitted
|
|
54
|
+
// company domains, contact emails, or tenant-specific data and this
|
|
55
|
+
// message may be persisted in a scheduled-run record.
|
|
56
|
+
reject(
|
|
57
|
+
new Error(
|
|
58
|
+
`ZoomInfo CLI failed${typeof code === "string" || typeof code === "number" ? ` (${code})` : ""}. ` +
|
|
59
|
+
"Run `gtm auth whoami`, then retry the same `gtm ... enrich` command directly if needed.",
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
resolve({ stdout, stderr });
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function parseOutput(stdout: string): unknown {
|
|
69
|
+
const text = stdout.trim();
|
|
70
|
+
if (!text) return null;
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(text);
|
|
73
|
+
} catch {
|
|
74
|
+
throw new Error(
|
|
75
|
+
"ZoomInfo CLI returned non-JSON output. Upgrade `gtm` and verify that `gtm ... enrich -f json` works directly.",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createZoomInfoClient(options: ZoomInfoClientOptions = {}): ZoomInfoClient {
|
|
81
|
+
const executable = options.executable ?? process.env.ZOOMINFO_GTM_BIN ?? "gtm";
|
|
82
|
+
const run = options.run ?? runCommand;
|
|
83
|
+
|
|
84
|
+
async function invoke(args: string[]): Promise<unknown> {
|
|
85
|
+
const result = await run(executable, [...args, "-f", "json"]);
|
|
86
|
+
return parseOutput(result.stdout);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
enrichCompany(domain) {
|
|
91
|
+
return invoke(["companies", "enrich", "--domain", domain]);
|
|
92
|
+
},
|
|
93
|
+
enrichContact(email) {
|
|
94
|
+
return invoke(["contacts", "enrich", "--email", email]);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function recordCandidates(value: unknown): Record<string, unknown>[] {
|
|
100
|
+
if (Array.isArray(value)) {
|
|
101
|
+
return value.filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object" && !Array.isArray(item));
|
|
102
|
+
}
|
|
103
|
+
if (!value || typeof value !== "object") return [];
|
|
104
|
+
const object = value as Record<string, unknown>;
|
|
105
|
+
if (Array.isArray(object.data)) return recordCandidates(object.data);
|
|
106
|
+
if (object.data && typeof object.data === "object") return recordCandidates(object.data);
|
|
107
|
+
if (Array.isArray(object.companies)) return recordCandidates(object.companies);
|
|
108
|
+
if (Array.isArray(object.contacts)) return recordCandidates(object.contacts);
|
|
109
|
+
return [object];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function valueAt(record: Record<string, unknown>, paths: string[]): string | undefined {
|
|
113
|
+
for (const path of paths) {
|
|
114
|
+
let current: unknown = record;
|
|
115
|
+
for (const segment of path.split(".")) {
|
|
116
|
+
if (!current || typeof current !== "object") {
|
|
117
|
+
current = undefined;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
current = (current as Record<string, unknown>)[segment];
|
|
121
|
+
}
|
|
122
|
+
if (typeof current === "string" && current.trim()) return current.trim();
|
|
123
|
+
if (typeof current === "number" && Number.isFinite(current)) return String(current);
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalizeRecord(payload: unknown, key: EnrichPullKey): EnrichSourceRecord | undefined {
|
|
129
|
+
const record = recordCandidates(payload)[0];
|
|
130
|
+
if (!record) return undefined;
|
|
131
|
+
const id = valueAt(record, ["id", "attributes.id", "companyId", "personId", "attributes.companyId", "attributes.personId"]);
|
|
132
|
+
if (key.objectType === "company") {
|
|
133
|
+
return {
|
|
134
|
+
id: `zoominfo:company_${id ?? key.value}`,
|
|
135
|
+
objectType: "company",
|
|
136
|
+
keys: {
|
|
137
|
+
domain: valueAt(record, [
|
|
138
|
+
"attributes.domain",
|
|
139
|
+
"attributes.website",
|
|
140
|
+
"attributes.companyWebsite",
|
|
141
|
+
"domain",
|
|
142
|
+
"website",
|
|
143
|
+
"companyWebsite",
|
|
144
|
+
]) ?? key.value,
|
|
145
|
+
name: valueAt(record, ["attributes.name", "attributes.companyName", "name", "companyName"]),
|
|
146
|
+
},
|
|
147
|
+
payload: record,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
id: `zoominfo:contact_${id ?? key.value}`,
|
|
152
|
+
objectType: "contact",
|
|
153
|
+
keys: {
|
|
154
|
+
email: valueAt(record, [
|
|
155
|
+
"attributes.email",
|
|
156
|
+
"attributes.emailAddress",
|
|
157
|
+
"attributes.contactEmail",
|
|
158
|
+
"email",
|
|
159
|
+
"emailAddress",
|
|
160
|
+
"contactEmail",
|
|
161
|
+
]) ?? key.value,
|
|
162
|
+
name: valueAt(record, ["attributes.fullName", "attributes.name", "fullName", "name"]),
|
|
163
|
+
},
|
|
164
|
+
payload: record,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Pull snapshot-driven enrichment keys through ZoomInfo's official CLI. */
|
|
169
|
+
export async function pullZoomInfoRecords(
|
|
170
|
+
client: ZoomInfoClient,
|
|
171
|
+
keys: EnrichPullKey[],
|
|
172
|
+
options: EnrichPullOptions = {},
|
|
173
|
+
): Promise<EnrichPullResult> {
|
|
174
|
+
const records: EnrichSourceRecord[] = [];
|
|
175
|
+
const misses: EnrichPullKey[] = [];
|
|
176
|
+
const resumeIndex = options.resumeAfter ? keys.findIndex((key) => key.value === options.resumeAfter) : -1;
|
|
177
|
+
|
|
178
|
+
for (const key of keys.slice(resumeIndex + 1)) {
|
|
179
|
+
const payload = key.objectType === "company"
|
|
180
|
+
? await client.enrichCompany(key.value)
|
|
181
|
+
: await client.enrichContact(key.value);
|
|
182
|
+
const record = normalizeRecord(payload, key);
|
|
183
|
+
if (record) records.push(record);
|
|
184
|
+
else misses.push(key);
|
|
185
|
+
await options.onProgress?.({
|
|
186
|
+
lastKeyValue: key.value,
|
|
187
|
+
record,
|
|
188
|
+
miss: record ? undefined : key,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return { records, misses };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export type ZoomInfoPullKey = EnrichPullKey;
|
|
195
|
+
export type ZoomInfoPullOptions = EnrichPullOptions;
|
|
196
|
+
export type ZoomInfoPullResult = EnrichPullResult;
|
|
197
|
+
export type ZoomInfoObjectType = EnrichObjectType;
|
package/src/index.ts
CHANGED
|
@@ -276,6 +276,17 @@ export {
|
|
|
276
276
|
type ApolloPullKey,
|
|
277
277
|
type ApolloPullResult,
|
|
278
278
|
} from "./enrichApollo.ts";
|
|
279
|
+
export {
|
|
280
|
+
createZoomInfoClient,
|
|
281
|
+
pullZoomInfoRecords,
|
|
282
|
+
type ZoomInfoClient,
|
|
283
|
+
type ZoomInfoClientOptions,
|
|
284
|
+
type ZoomInfoCommandResult,
|
|
285
|
+
type ZoomInfoCommandRunner,
|
|
286
|
+
type ZoomInfoPullKey,
|
|
287
|
+
type ZoomInfoPullOptions,
|
|
288
|
+
type ZoomInfoPullResult,
|
|
289
|
+
} from "./enrichZoomInfo.ts";
|
|
279
290
|
export {
|
|
280
291
|
diffFindings,
|
|
281
292
|
diffSnapshots,
|
package/src/runReport.ts
CHANGED
|
@@ -126,6 +126,10 @@ let inflightHeartbeat: AbortController | null = null;
|
|
|
126
126
|
* commands simply never stream.
|
|
127
127
|
*/
|
|
128
128
|
export function beginRunReport(args: string[], startedAt: number): void {
|
|
129
|
+
counts = undefined;
|
|
130
|
+
findings = undefined;
|
|
131
|
+
crm = undefined;
|
|
132
|
+
events.length = 0;
|
|
129
133
|
lastHeartbeatAt = 0;
|
|
130
134
|
heartbeatBroker = undefined;
|
|
131
135
|
inflightHeartbeat = null;
|
|
@@ -241,6 +245,14 @@ export async function flushRunReport(
|
|
|
241
245
|
// replay of local history never duplicates.
|
|
242
246
|
const identity = runIdentity(args, startedAt);
|
|
243
247
|
if (!identity) return;
|
|
248
|
+
// A successful command with no result, finding, event, or live progress is
|
|
249
|
+
// terminal telemetry rather than something a person can explore. Keep it
|
|
250
|
+
// out of Runs. Errors always report; heartbeating commands still send their
|
|
251
|
+
// terminal status so an in-progress row cannot be stranded.
|
|
252
|
+
const hasUsefulOutput = Boolean(
|
|
253
|
+
(counts && Object.keys(counts).length > 0) || findings?.length || events.length,
|
|
254
|
+
);
|
|
255
|
+
if (status === "success" && !hasUsefulOutput && lastHeartbeatAt === 0) return;
|
|
244
256
|
const broker = getCredential("broker");
|
|
245
257
|
if (!broker?.baseUrl || !broker.accessToken) return; // opt-in: only when paired
|
|
246
258
|
|