fullstackgtm 0.56.1 → 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 +43 -0
- package/README.md +163 -492
- package/dist/cli/enrich.d.ts +2 -2
- package/dist/cli/enrich.js +39 -12
- package/dist/cli/help.js +4 -4
- package/dist/cli/icp.js +99 -6
- package/dist/cli/signals.js +19 -4
- 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/hostedArtifacts.d.ts +35 -1
- package/dist/hostedArtifacts.js +35 -10
- package/dist/icp.d.ts +3 -0
- package/dist/icp.js +7 -4
- package/dist/icpSync.d.ts +55 -0
- package/dist/icpSync.js +93 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/runReport.js +11 -0
- package/docs/api.md +1 -1
- package/docs/architecture.md +5 -0
- package/llms.txt +3 -2
- package/package.json +2 -2
- package/src/cli/enrich.ts +40 -13
- package/src/cli/help.ts +4 -4
- package/src/cli/icp.ts +80 -5
- package/src/cli/signals.ts +20 -3
- package/src/enrich.ts +2 -2
- package/src/enrichZoomInfo.ts +197 -0
- package/src/hostedArtifacts.ts +51 -22
- package/src/icp.ts +10 -4
- package/src/icpSync.ts +74 -0
- package/src/index.ts +24 -0
- package/src/runReport.ts +12 -0
package/src/cli/icp.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Extracted verbatim from src/cli.ts (mechanical split, no behavior change).
|
|
2
2
|
|
|
3
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { emitKeypressEvents } from "node:readline";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { resolve } from "node:path";
|
|
@@ -9,14 +9,16 @@ import { fitThreshold, icpFromAnswers, icpToCrustdataFilters, icpToExploriumFilt
|
|
|
9
9
|
import { createFileSignalStore, DEFAULT_SIGNALS_CONFIG, type SignalsConfig } from "../signals.ts";
|
|
10
10
|
import { createFileJudgeStore, DEFAULT_JUDGE_PROMPT, judgeRunId, judgeSignals } from "../judge.ts";
|
|
11
11
|
import { DEFAULT_GOLDEN_NOW_ISO, DEFAULT_GOLDEN_SET, DEFAULT_MIN_ACCURACY, defaultJudgeFn, gradeAgainstOutcomes, gradeJudge, parseGoldenSet } from "../judgeEval.ts";
|
|
12
|
-
import { loadIcp, numericOption, option, readSecret, readSnapshot, resolveLlmBaseUrls, saveRequested } from "./shared.ts";
|
|
12
|
+
import { loadIcp, numericOption, option, readPackageInfo, readSecret, readSnapshot, resolveLlmBaseUrls, saveRequested } from "./shared.ts";
|
|
13
13
|
import { createStatusLine } from "./ui.ts";
|
|
14
14
|
import { box, colorEnabled, paint, truncateToWidth } from "./ui.ts";
|
|
15
15
|
import { getCredential, storeCredential } from "../credentials.ts";
|
|
16
16
|
import { deriveWebsiteIcp, icpReviewSegments, OPENROUTER_API_BASE, type WebsiteIcpDerivation } from "../icpDerive.ts";
|
|
17
17
|
import type { Icp } from "../icp.ts";
|
|
18
18
|
import { unknownSubcommandError } from "./suggest.ts";
|
|
19
|
-
import { writeHostedArtifact } from "../hostedArtifacts.ts";
|
|
19
|
+
import { readHostedArtifact, writeHostedArtifact } from "../hostedArtifacts.ts";
|
|
20
|
+
import { artifactKeyForDomain, extractHostedIcp, getIcpSyncStatus, markIcpSynced, pushIcp, readIcpSyncState } from "../icpSync.ts";
|
|
21
|
+
import { writeSecureFileAtomic } from "../secureFile.ts";
|
|
20
22
|
|
|
21
23
|
function renderJudgeDecisions(decisions: Awaited<ReturnType<typeof judgeSignals>>): string {
|
|
22
24
|
if (decisions.length === 0) return "No accounts cleared the score threshold.";
|
|
@@ -178,6 +180,14 @@ export async function icpCommand(args: string[]) {
|
|
|
178
180
|
fullstackgtm icp interview emit the interview spec (an agent drives it with AskUserQuestion)
|
|
179
181
|
fullstackgtm icp derive --domain <site> [--model <id>] [--out icp.json] [--json]
|
|
180
182
|
derive an evidence-backed ICP from a public website (OpenRouter/OpenAI/Anthropic)
|
|
183
|
+
fullstackgtm icp status --domain <site> [--icp icp.json] [--json]
|
|
184
|
+
compare local and hosted revisions without changing either
|
|
185
|
+
fullstackgtm icp pull --domain <site> [--out icp.json] [--force] [--json]
|
|
186
|
+
explicitly download hosted canonical ICP; refuses to replace an existing file without --force
|
|
187
|
+
fullstackgtm icp push --domain <site> [--icp icp.json] [--change-summary <text>] [--json]
|
|
188
|
+
publish the local ICP with revision conflict protection
|
|
189
|
+
fullstackgtm icp sync --domain <site> [--icp icp.json] [--json]
|
|
190
|
+
reconcile safe one-sided changes; hosted changes become a sibling review file
|
|
181
191
|
fullstackgtm icp set <answers.json> [--name <n>] [--out <path>] write icp.json from interview answers
|
|
182
192
|
fullstackgtm icp show [--icp <path>] render the ICP + the discovery filters it produces
|
|
183
193
|
fullstackgtm icp judge [--signals-from latest|<label>] [--with-history] [--prompt <path>] [--min-score 0] [--save] rank unjudged signals into send/nurture/skip decisions (timing x fit x memory)
|
|
@@ -235,14 +245,79 @@ ops. Develop one by interview, then \`enrich acquire\` picks up ./icp.json.
|
|
|
235
245
|
}
|
|
236
246
|
const mirrored = await writeHostedArtifact({
|
|
237
247
|
kind: "icp", key: `icp:${derived.company.domain}`, label: derived.icp.name,
|
|
238
|
-
domain: derived.company.domain, document: derived,
|
|
248
|
+
domain: derived.company.domain, document: derived, sourceVersion: readPackageInfo().version,
|
|
239
249
|
});
|
|
240
|
-
if (mirrored.status === "saved")
|
|
250
|
+
if (mirrored.status === "saved") {
|
|
251
|
+
if (out) markIcpSynced(resolve(process.cwd(), out), derived.company.domain, mirrored, derived.icp);
|
|
252
|
+
console.error(`Mirrored the reviewed ICP to the paired hosted workspace at revision ${mirrored.revision}.`);
|
|
253
|
+
}
|
|
254
|
+
else if (mirrored.status === "conflict") console.error(`Warning: ${mirrored.reason}. Run \`fullstackgtm icp sync --domain ${derived.company.domain}${out ? ` --icp ${out}` : ""}\` to reconcile.`);
|
|
241
255
|
else if (mirrored.status === "unavailable") console.error(`Warning: ${mirrored.reason}. The local ICP is still authoritative.`);
|
|
242
256
|
if (rest.includes("--json")) console.log(JSON.stringify(derived, null, 2));
|
|
243
257
|
else if (!reviewedInteractively) console.log(renderDerivedIcp(derived));
|
|
244
258
|
return;
|
|
245
259
|
}
|
|
260
|
+
if (["status", "pull", "push", "sync"].includes(sub)) {
|
|
261
|
+
const domain = option(rest, "--domain");
|
|
262
|
+
if (!domain) throw new Error(`icp ${sub}: --domain <company.com> is required.`);
|
|
263
|
+
const asJson = rest.includes("--json");
|
|
264
|
+
const icpPath = resolve(process.cwd(), option(rest, "--icp") ?? option(rest, "--out") ?? "icp.json");
|
|
265
|
+
|
|
266
|
+
if (sub === "status") {
|
|
267
|
+
if (!existsSync(icpPath)) throw new Error(`icp status: local ICP not found at ${icpPath}.`);
|
|
268
|
+
const checked = await getIcpSyncStatus(icpPath, domain);
|
|
269
|
+
if (asJson) console.log(JSON.stringify({ path: icpPath, domain, ...checked.status }, null, 2));
|
|
270
|
+
else console.log(`ICP ${checked.status.state.replaceAll("_", " ")} · local ${checked.status.localIcpSha256.slice(0, 12)}${checked.status.hostedRevision ? ` · hosted r${checked.status.hostedRevision}` : ""}${checked.status.trackedRevision ? ` · last synced r${checked.status.trackedRevision}` : ""}`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (sub === "pull") {
|
|
275
|
+
const out = resolve(process.cwd(), option(rest, "--out") ?? "icp.json");
|
|
276
|
+
const hosted = await readHostedArtifact("icp", artifactKeyForDomain(domain));
|
|
277
|
+
if (hosted.status !== "found") throw new Error(hosted.status === "missing" ? `No hosted ICP exists for ${domain}.` : hosted.status === "unpaired" ? "This CLI is not paired with a hosted workspace." : hosted.reason);
|
|
278
|
+
const icp = extractHostedIcp(hosted.state);
|
|
279
|
+
if (existsSync(out) && !rest.includes("--force")) throw new Error(`Refusing to replace ${out}. Re-run with --force after reviewing hosted revision ${hosted.state.revision}.`);
|
|
280
|
+
writeSecureFileAtomic(out, `${JSON.stringify(icp, null, 2)}\n`);
|
|
281
|
+
markIcpSynced(out, domain, hosted.state, icp);
|
|
282
|
+
if (asJson) console.log(JSON.stringify({ status: "pulled", path: out, revision: hosted.state.revision, documentSha256: hosted.state.documentSha256 }, null, 2));
|
|
283
|
+
else console.log(`Pulled hosted ICP revision ${hosted.state.revision} to ${out}.`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!existsSync(icpPath)) throw new Error(`icp ${sub}: local ICP not found at ${icpPath}.`);
|
|
288
|
+
if (sub === "push") {
|
|
289
|
+
const pushed = await pushIcp(icpPath, domain, option(rest, "--change-summary") ?? undefined);
|
|
290
|
+
if (pushed.status === "conflict") throw new Error(`ICP conflict: hosted revision ${pushed.checked.status.hostedRevision ?? "?"} changed since local revision ${pushed.checked.status.trackedRevision ?? "untracked"}. Run \`icp sync\` to write a review copy.`);
|
|
291
|
+
if (pushed.result.status !== "saved") throw new Error(pushed.result.status === "unpaired" ? "This CLI is not paired with a hosted workspace." : pushed.result.status === "conflict" ? pushed.result.reason : pushed.result.reason);
|
|
292
|
+
if (asJson) console.log(JSON.stringify({ status: "pushed", revision: pushed.result.revision, documentSha256: pushed.result.documentSha256, unchanged: pushed.result.unchanged }, null, 2));
|
|
293
|
+
else console.log(`${pushed.result.unchanged ? "Already at" : "Published"} hosted ICP revision ${pushed.result.revision}.`);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const checked = await getIcpSyncStatus(icpPath, domain);
|
|
298
|
+
if (checked.status.state === "local_changed" || checked.status.state === "missing_hosted") {
|
|
299
|
+
const pushed = await pushIcp(icpPath, domain, "Synchronized local ICP");
|
|
300
|
+
if (pushed.result?.status !== "saved") throw new Error("ICP sync could not publish the local change.");
|
|
301
|
+
if (asJson) console.log(JSON.stringify({ status: "pushed", revision: pushed.result.revision }, null, 2));
|
|
302
|
+
else console.log(`Published local ICP as hosted revision ${pushed.result.revision}.`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if ((checked.status.state === "hosted_changed" || checked.status.state === "conflict" || checked.status.state === "untracked") && checked.hosted) {
|
|
306
|
+
const reviewPath = `${icpPath}.hosted-r${checked.hosted.revision}.json`;
|
|
307
|
+
writeSecureFileAtomic(reviewPath, `${JSON.stringify(extractHostedIcp(checked.hosted), null, 2)}\n`);
|
|
308
|
+
const result = { status: checked.status.state, reviewPath, hostedRevision: checked.hosted.revision, trackedRevision: readIcpSyncState(icpPath)?.revision };
|
|
309
|
+
if (asJson) console.log(JSON.stringify(result, null, 2));
|
|
310
|
+
else console.log(`Hosted changes need review. Wrote revision ${checked.hosted.revision} to ${reviewPath}; your local ICP was not replaced.`);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (checked.status.state === "in_sync") {
|
|
314
|
+
if (!readIcpSyncState(icpPath) && checked.hosted) markIcpSynced(icpPath, domain, checked.hosted, checked.local);
|
|
315
|
+
if (asJson) console.log(JSON.stringify({ status: "in_sync", revision: checked.status.hostedRevision }, null, 2));
|
|
316
|
+
else console.log(`ICP is in sync${checked.status.hostedRevision ? ` at revision ${checked.status.hostedRevision}` : ""}.`);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
throw new Error(checked.status.reason ?? `ICP sync unavailable (${checked.status.state}).`);
|
|
320
|
+
}
|
|
246
321
|
if (sub === "set") {
|
|
247
322
|
const file = rest.find((a) => !a.startsWith("--"));
|
|
248
323
|
if (!file) throw new Error("Usage: fullstackgtm icp set <answers.json> [--name <n>] [--out <path>]");
|
package/src/cli/signals.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { loadIcp, option, readSnapshot, repeatedOption, saveRequested } from "./
|
|
|
12
12
|
import { createStatusLine } from "./ui.ts";
|
|
13
13
|
import { unknownSubcommandError } from "./suggest.ts";
|
|
14
14
|
import { writeHostedArtifact } from "../hostedArtifacts.ts";
|
|
15
|
+
import { readIcpSyncState } from "../icpSync.ts";
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -178,7 +179,18 @@ from the credential ladder, never argv; --connector-opt carries non-secret knobs
|
|
|
178
179
|
await store.appendRun({ id: signalRunId(runLabel), runLabel, startedAt: now.toISOString(), completedAt: new Date().toISOString(),
|
|
179
180
|
buckets: ["job"], counts: { fetched: discovered.summary.rawResults, new: ranked.length, deduped: deduped.length }, signals: ranked });
|
|
180
181
|
console.error(`Saved signal run "${runLabel}". Next: \`fullstackgtm icp judge --signals-from ${runLabel} --save\`.`);
|
|
181
|
-
await mirrorSignalRun(runLabel, now, ["job"], { fetched: discovered.summary.rawResults, new: ranked.length, deduped: deduped.length }, ranked
|
|
182
|
+
await mirrorSignalRun(runLabel, now, ["job"], { fetched: discovered.summary.rawResults, new: ranked.length, deduped: deduped.length }, ranked, rest, {
|
|
183
|
+
provider: discovered.summary.provider,
|
|
184
|
+
searchesUsed: discovered.summary.searchesUsed,
|
|
185
|
+
searchLimit: maxSearches,
|
|
186
|
+
rawResults: discovered.summary.rawResults,
|
|
187
|
+
matchedEvidence: discovered.summary.matchedEvidence,
|
|
188
|
+
resolvedAccounts: discovered.summary.resolvedAccounts,
|
|
189
|
+
unresolvedAccounts: discovered.summary.unresolvedAccounts,
|
|
190
|
+
costUsd: discovered.summary.costUsd,
|
|
191
|
+
costLimitUsd: maxUsd,
|
|
192
|
+
warnings: discovered.summary.warnings,
|
|
193
|
+
});
|
|
182
194
|
} else {
|
|
183
195
|
console.error("(not saved — re-run with --save to persist this evidence to the signal ledger)");
|
|
184
196
|
}
|
|
@@ -317,7 +329,7 @@ from the credential ladder, never argv; --connector-opt carries non-secret knobs
|
|
|
317
329
|
signals: ranked,
|
|
318
330
|
});
|
|
319
331
|
console.error(`Saved signal run "${runLabel}" (${fresh.length} fresh). Next: \`fullstackgtm icp judge --save\`.`);
|
|
320
|
-
await mirrorSignalRun(runLabel, now, buckets, { fetched, new: fresh.length, deduped: deduped.length }, ranked);
|
|
332
|
+
await mirrorSignalRun(runLabel, now, buckets, { fetched, new: fresh.length, deduped: deduped.length }, ranked, rest);
|
|
321
333
|
} else {
|
|
322
334
|
console.error("(not saved — re-run with --save to persist this run to the signal ledger)");
|
|
323
335
|
}
|
|
@@ -412,10 +424,15 @@ async function mirrorSignalRun(
|
|
|
412
424
|
buckets: SignalBucket[],
|
|
413
425
|
counts: { fetched: number; new: number; deduped: number },
|
|
414
426
|
signals: Signal[],
|
|
427
|
+
args: string[],
|
|
428
|
+
discovery?: Record<string, unknown>,
|
|
415
429
|
) {
|
|
430
|
+
const icpPath = resolve(process.cwd(), option(args, "--icp") ?? "icp.json");
|
|
431
|
+
const tracked = existsSync(icpPath) ? readIcpSyncState(icpPath) : null;
|
|
416
432
|
const mirrored = await writeHostedArtifact({
|
|
417
433
|
kind: "signal_run", key: `signals:${signalRunId(runLabel)}`, label: runLabel,
|
|
418
|
-
document: { runLabel, startedAt: startedAt.toISOString(), completedAt: new Date().toISOString(), buckets, counts, signals
|
|
434
|
+
document: { runLabel, startedAt: startedAt.toISOString(), completedAt: new Date().toISOString(), buckets, counts, signals, ...discovery,
|
|
435
|
+
icpRef: tracked ? { artifactId: tracked.artifactId, domain: tracked.domain, revision: tracked.revision, localIcpSha256: tracked.localIcpSha256 } : undefined },
|
|
419
436
|
});
|
|
420
437
|
if (mirrored.status === "saved") console.error("Mirrored the signal run to the paired hosted workspace.");
|
|
421
438
|
else if (mirrored.status === "unavailable") console.error(`Warning: ${mirrored.reason}. The local signal ledger is still authoritative.`);
|
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/hostedArtifacts.ts
CHANGED
|
@@ -1,21 +1,38 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Version-aware hosted artifact transport for a human-paired CLI profile. */
|
|
2
2
|
import { getCredential } from "./credentials.ts";
|
|
3
3
|
|
|
4
4
|
const ENDPOINT = "/api/cli/artifact";
|
|
5
5
|
const DEFAULT_TIMEOUT_MS = 4000;
|
|
6
6
|
|
|
7
|
+
export type HostedArtifactKind = "icp" | "signal_run" | "lead_run";
|
|
7
8
|
export type HostedArtifact = {
|
|
8
|
-
kind:
|
|
9
|
+
kind: HostedArtifactKind;
|
|
9
10
|
key: string;
|
|
10
11
|
label: string;
|
|
11
12
|
domain?: string;
|
|
12
13
|
document: unknown;
|
|
13
14
|
sourceVersion?: string;
|
|
15
|
+
expectedRevision?: number;
|
|
16
|
+
changeSummary?: string;
|
|
17
|
+
};
|
|
18
|
+
export type HostedArtifactState = HostedArtifact & {
|
|
19
|
+
artifactId: string;
|
|
20
|
+
revision: number;
|
|
21
|
+
documentSha256?: string;
|
|
22
|
+
origin: "cli" | "hosted";
|
|
23
|
+
updatedAt: number;
|
|
14
24
|
};
|
|
15
25
|
|
|
16
26
|
export type HostedArtifactResult =
|
|
17
27
|
| { status: "unpaired" }
|
|
18
|
-
| { status: "saved"; created: boolean; updatedAt: number }
|
|
28
|
+
| { status: "saved"; artifactId: string; created: boolean; updatedAt: number; revision: number; documentSha256: string; unchanged: boolean }
|
|
29
|
+
| { status: "conflict"; reason: string; currentRevision?: number; documentSha256?: string }
|
|
30
|
+
| { status: "unavailable"; reason: string };
|
|
31
|
+
|
|
32
|
+
export type HostedArtifactReadResult =
|
|
33
|
+
| { status: "unpaired" }
|
|
34
|
+
| { status: "found"; state: HostedArtifactState }
|
|
35
|
+
| { status: "missing" }
|
|
19
36
|
| { status: "unavailable"; reason: string };
|
|
20
37
|
|
|
21
38
|
function broker(): { baseUrl: string; accessToken: string } | null {
|
|
@@ -29,29 +46,41 @@ function broker(): { baseUrl: string; accessToken: string } | null {
|
|
|
29
46
|
} catch { return null; }
|
|
30
47
|
}
|
|
31
48
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
49
|
+
function requestOptions(accessToken: string, timeoutMs: number): Pick<RequestInit, "headers" | "signal"> {
|
|
50
|
+
return { headers: { Authorization: `Bearer ${accessToken}` }, signal: AbortSignal.timeout(timeoutMs) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function readHostedArtifact(kind: HostedArtifactKind, key: string, options: { fetchImpl?: typeof fetch; timeoutMs?: number } = {}): Promise<HostedArtifactReadResult> {
|
|
54
|
+
const paired = broker();
|
|
55
|
+
if (!paired) return { status: "unpaired" };
|
|
56
|
+
try {
|
|
57
|
+
const url = new URL(`${paired.baseUrl}${ENDPOINT}`);
|
|
58
|
+
url.searchParams.set("kind", kind); url.searchParams.set("key", key);
|
|
59
|
+
const response = await (options.fetchImpl ?? fetch)(url, requestOptions(paired.accessToken, options.timeoutMs ?? DEFAULT_TIMEOUT_MS));
|
|
60
|
+
if (response.status === 404) return { status: "missing" };
|
|
61
|
+
if (!response.ok) return { status: "unavailable", reason: `hosted artifact request failed (HTTP ${response.status})` };
|
|
62
|
+
const state = await response.json() as HostedArtifactState;
|
|
63
|
+
if (!state.artifactId || !Number.isSafeInteger(state.revision) || !state.document) return { status: "unavailable", reason: "hosted artifact response was invalid" };
|
|
64
|
+
return { status: "found", state };
|
|
65
|
+
} catch { return { status: "unavailable", reason: "hosted artifact request was unavailable" }; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function writeHostedArtifact(artifact: HostedArtifact, options: { fetchImpl?: typeof fetch; timeoutMs?: number } = {}): Promise<HostedArtifactResult> {
|
|
69
|
+
if (!artifact.key || artifact.key.length > 256 || !artifact.label || artifact.label.length > 200) throw new Error("Hosted artifact key/label is invalid.");
|
|
39
70
|
const paired = broker();
|
|
40
71
|
if (!paired) return { status: "unpaired" };
|
|
41
72
|
try {
|
|
42
73
|
const response = await (options.fetchImpl ?? fetch)(`${paired.baseUrl}${ENDPOINT}`, {
|
|
43
|
-
method: "POST",
|
|
44
|
-
headers: { Authorization: `Bearer ${paired.accessToken}`, "Content-Type": "application/json" },
|
|
45
|
-
body: JSON.stringify(artifact),
|
|
46
|
-
signal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
74
|
+
method: "POST", ...requestOptions(paired.accessToken, options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
75
|
+
headers: { Authorization: `Bearer ${paired.accessToken}`, "Content-Type": "application/json" }, body: JSON.stringify(artifact),
|
|
47
76
|
});
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return { status: "unavailable", reason: "hosted artifact response was invalid" };
|
|
77
|
+
if (response.status === 409) {
|
|
78
|
+
const body = await response.json() as { currentRevision?: number; documentSha256?: string };
|
|
79
|
+
return { status: "conflict", reason: "hosted ICP changed since the last sync", currentRevision: body.currentRevision, documentSha256: body.documentSha256 };
|
|
52
80
|
}
|
|
53
|
-
return { status: "
|
|
54
|
-
|
|
55
|
-
return { status: "unavailable", reason: "hosted artifact
|
|
56
|
-
|
|
81
|
+
if (!response.ok) return { status: "unavailable", reason: `hosted artifact request failed (HTTP ${response.status})` };
|
|
82
|
+
const body = await response.json() as { artifactId?: unknown; created?: unknown; updatedAt?: unknown; revision?: unknown; documentSha256?: unknown; unchanged?: unknown };
|
|
83
|
+
if (typeof body.artifactId !== "string" || typeof body.created !== "boolean" || typeof body.updatedAt !== "number" || typeof body.revision !== "number" || typeof body.documentSha256 !== "string") return { status: "unavailable", reason: "hosted artifact response was invalid" };
|
|
84
|
+
return { status: "saved", artifactId: body.artifactId, created: body.created, updatedAt: body.updatedAt, revision: body.revision, documentSha256: body.documentSha256, unchanged: body.unchanged === true };
|
|
85
|
+
} catch { return { status: "unavailable", reason: "hosted artifact request was unavailable" }; }
|
|
57
86
|
}
|
package/src/icp.ts
CHANGED
|
@@ -73,6 +73,9 @@ export type Icp = {
|
|
|
73
73
|
scoring?: {
|
|
74
74
|
/** minimum fit (0..1) for a prospect to become a create_record op. Default 0.5. */
|
|
75
75
|
threshold?: number;
|
|
76
|
+
/** Require a literal persona.titleKeywords phrase in title/headline. This
|
|
77
|
+
* disables the broader function fallback for high-precision sourcing. */
|
|
78
|
+
requireTitleKeyword?: boolean;
|
|
76
79
|
};
|
|
77
80
|
};
|
|
78
81
|
|
|
@@ -456,17 +459,20 @@ export function scoreProspectAgainstIcp(
|
|
|
456
459
|
const keywords = (icp.persona.titleKeywords ?? []).map((k) => k.toLowerCase());
|
|
457
460
|
const levels = (icp.persona.jobLevels ?? []).map((l) => l.toLowerCase());
|
|
458
461
|
const depts = (icp.persona.departments ?? []).map((d) => d.toLowerCase());
|
|
462
|
+
const exactTitleKeyword = keywords.find((keyword) => title.includes(keyword));
|
|
463
|
+
if (icp.scoring?.requireTitleKeyword && keywords.length && !exactTitleKeyword) {
|
|
464
|
+
return { score: 0, reasons: ["title does not contain a required ICP keyword"] };
|
|
465
|
+
}
|
|
459
466
|
|
|
460
467
|
let score = 0;
|
|
461
468
|
let weightSum = 0;
|
|
462
469
|
|
|
463
470
|
if (keywords.length) {
|
|
464
471
|
weightSum += 0.6;
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
if (exact) {
|
|
472
|
+
const functional = exactTitleKeyword ? undefined : roleKeywords(icp).find((keyword) => title.includes(keyword));
|
|
473
|
+
if (exactTitleKeyword) {
|
|
468
474
|
score += 0.6;
|
|
469
|
-
reasons.push(`title matches ICP keyword "${
|
|
475
|
+
reasons.push(`title matches ICP keyword "${exactTitleKeyword}"`);
|
|
470
476
|
} else if (functional) {
|
|
471
477
|
score += 0.45;
|
|
472
478
|
reasons.push(`title matches ICP function "${functional}"`);
|
package/src/icpSync.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { parseIcp, type Icp } from "./icp.ts";
|
|
5
|
+
import { readSecureRegularFile, writeSecureFileAtomic } from "./secureFile.ts";
|
|
6
|
+
import { readHostedArtifact, writeHostedArtifact, type HostedArtifactState } from "./hostedArtifacts.ts";
|
|
7
|
+
|
|
8
|
+
export type IcpSyncState = { version: 1; artifactId: string; key: string; domain: string; revision: number; localIcpSha256: string; hostedDocumentSha256?: string; syncedAt: string };
|
|
9
|
+
export type IcpSyncStatus = { state: "unpaired" | "missing_hosted" | "in_sync" | "local_changed" | "hosted_changed" | "conflict" | "untracked" | "unavailable"; localIcpSha256: string; hostedRevision?: number; trackedRevision?: number; reason?: string };
|
|
10
|
+
|
|
11
|
+
export function classifyIcpSync(localHash: string, hostedRevision: number, hostedHash: string, tracked: IcpSyncState | null): IcpSyncStatus["state"] {
|
|
12
|
+
if (!tracked) return localHash === hostedHash ? "in_sync" : "untracked";
|
|
13
|
+
const localChanged = localHash !== tracked.localIcpSha256;
|
|
14
|
+
const hostedChanged = hostedRevision !== tracked.revision;
|
|
15
|
+
return localChanged && hostedChanged ? "conflict" : localChanged ? "local_changed" : hostedChanged ? "hosted_changed" : "in_sync";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function canonicalJson(value: unknown): string {
|
|
19
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
20
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
21
|
+
const object = value as Record<string, unknown>;
|
|
22
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(",")}}`;
|
|
23
|
+
}
|
|
24
|
+
export const icpSha256 = (icp: Icp) => createHash("sha256").update(canonicalJson(icp)).digest("hex");
|
|
25
|
+
export function artifactKeyForDomain(domain: string): string {
|
|
26
|
+
const candidate = /^https?:\/\//i.test(domain.trim()) ? domain.trim() : `https://${domain.trim()}`;
|
|
27
|
+
let parsed: URL; try { parsed = new URL(candidate); } catch { throw new Error(`Invalid company domain: ${domain}`); }
|
|
28
|
+
const hostname = parsed.hostname.toLowerCase().replace(/^www\./, "").replace(/\.$/, "");
|
|
29
|
+
if (!hostname.includes(".")) throw new Error(`Invalid public company domain: ${domain}`);
|
|
30
|
+
return `icp:${hostname}`;
|
|
31
|
+
}
|
|
32
|
+
export const sidecarPathFor = (icpPath: string) => resolve(dirname(resolve(icpPath)), ".fullstackgtm", `${resolve(icpPath).split(/[\\/]/).pop()}.sync.json`);
|
|
33
|
+
|
|
34
|
+
export function readLocalIcp(path: string): Icp { return parseIcp(readSecureRegularFile(resolve(path))); }
|
|
35
|
+
export function extractHostedIcp(state: HostedArtifactState): Icp {
|
|
36
|
+
const root = state.document && typeof state.document === "object" && !Array.isArray(state.document) ? state.document as Record<string, unknown> : {};
|
|
37
|
+
return parseIcp(JSON.stringify(root.icp ?? state.document));
|
|
38
|
+
}
|
|
39
|
+
export function readIcpSyncState(icpPath: string): IcpSyncState | null {
|
|
40
|
+
const path = sidecarPathFor(icpPath); if (!existsSync(path)) return null;
|
|
41
|
+
try { const value = JSON.parse(readSecureRegularFile(path)) as IcpSyncState; return value?.version === 1 ? value : null; } catch { return null; }
|
|
42
|
+
}
|
|
43
|
+
export function writeIcpSyncState(icpPath: string, state: IcpSyncState): void {
|
|
44
|
+
const path = sidecarPathFor(icpPath); mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
45
|
+
writeSecureFileAtomic(path, `${JSON.stringify(state, null, 2)}\n`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function getIcpSyncStatus(icpPath: string, domain: string): Promise<{ status: IcpSyncStatus; hosted?: HostedArtifactState; local: Icp }> {
|
|
49
|
+
const local = readLocalIcp(icpPath); const localHash = icpSha256(local); const tracked = readIcpSyncState(icpPath);
|
|
50
|
+
const read = await readHostedArtifact("icp", artifactKeyForDomain(domain));
|
|
51
|
+
if (read.status === "unpaired") return { local, status: { state: "unpaired", localIcpSha256: localHash } };
|
|
52
|
+
if (read.status === "missing") return { local, status: { state: "missing_hosted", localIcpSha256: localHash } };
|
|
53
|
+
if (read.status === "unavailable") return { local, status: { state: "unavailable", localIcpSha256: localHash, reason: read.reason } };
|
|
54
|
+
const hostedHash = icpSha256(extractHostedIcp(read.state));
|
|
55
|
+
const state = classifyIcpSync(localHash, read.state.revision, hostedHash, tracked);
|
|
56
|
+
return { local, hosted: read.state, status: { state, localIcpSha256: localHash, hostedRevision: read.state.revision, trackedRevision: tracked?.revision } };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function markIcpSynced(icpPath: string, domain: string, hosted: HostedArtifactState | { artifactId: string; revision: number; documentSha256?: string }, icp: Icp): void {
|
|
60
|
+
const key = artifactKeyForDomain(domain); const normalizedDomain = key.slice("icp:".length);
|
|
61
|
+
writeIcpSyncState(icpPath, { version: 1, artifactId: hosted.artifactId, key, domain: normalizedDomain, revision: hosted.revision, localIcpSha256: icpSha256(icp), hostedDocumentSha256: hosted.documentSha256, syncedAt: new Date().toISOString() });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function pushIcp(icpPath: string, domain: string, changeSummary?: string) {
|
|
65
|
+
const checked = await getIcpSyncStatus(icpPath, domain);
|
|
66
|
+
if (checked.status.state === "conflict" || checked.status.state === "hosted_changed" || checked.status.state === "untracked") return { status: "conflict" as const, checked };
|
|
67
|
+
const current = checked.hosted;
|
|
68
|
+
const root = current?.document && typeof current.document === "object" && !Array.isArray(current.document) ? current.document as Record<string, unknown> : {};
|
|
69
|
+
const key = artifactKeyForDomain(domain); const normalizedDomain = key.slice("icp:".length);
|
|
70
|
+
const document = Object.keys(root).length ? { ...root, icp: checked.local, humanReview: { reviewedAt: new Date().toISOString(), source: "cli" } } : { company: { domain: normalizedDomain }, icp: checked.local, evidence: [], humanReview: { reviewedAt: new Date().toISOString(), source: "cli" } };
|
|
71
|
+
const result = await writeHostedArtifact({ kind: "icp", key, label: checked.local.name, domain: normalizedDomain, document, expectedRevision: current?.revision ?? 0, changeSummary });
|
|
72
|
+
if (result.status === "saved") markIcpSynced(icpPath, domain, { artifactId: result.artifactId, revision: result.revision, documentSha256: result.documentSha256 }, checked.local);
|
|
73
|
+
return { status: result.status, result, checked };
|
|
74
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,19 @@ export {
|
|
|
11
11
|
type IcpReviewSegment,
|
|
12
12
|
type IcpDerivationProgress,
|
|
13
13
|
} from "./icpDerive.ts";
|
|
14
|
+
export {
|
|
15
|
+
artifactKeyForDomain,
|
|
16
|
+
classifyIcpSync,
|
|
17
|
+
extractHostedIcp,
|
|
18
|
+
getIcpSyncStatus,
|
|
19
|
+
icpSha256,
|
|
20
|
+
markIcpSynced,
|
|
21
|
+
pushIcp,
|
|
22
|
+
readIcpSyncState,
|
|
23
|
+
sidecarPathFor,
|
|
24
|
+
type IcpSyncState,
|
|
25
|
+
type IcpSyncStatus,
|
|
26
|
+
} from "./icpSync.ts";
|
|
14
27
|
|
|
15
28
|
export {
|
|
16
29
|
acquireCheckpointId,
|
|
@@ -263,6 +276,17 @@ export {
|
|
|
263
276
|
type ApolloPullKey,
|
|
264
277
|
type ApolloPullResult,
|
|
265
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";
|
|
266
290
|
export {
|
|
267
291
|
diffFindings,
|
|
268
292
|
diffSnapshots,
|