fullstackgtm 0.50.0 → 0.51.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 +45 -0
- package/README.md +7 -0
- package/dist/cli/audit.js +6 -1
- package/dist/cli/auth.js +49 -4
- package/dist/cli/backfill.js +4 -0
- package/dist/cli/call.js +28 -6
- package/dist/cli/draft.js +11 -1
- package/dist/cli/enrich.js +124 -57
- package/dist/cli/fix.js +6 -3
- package/dist/cli/help.js +31 -28
- package/dist/cli/icp.js +15 -1
- package/dist/cli/init.js +3 -3
- package/dist/cli/market.js +14 -1
- package/dist/cli/planOutput.d.ts +5 -0
- package/dist/cli/planOutput.js +27 -0
- package/dist/cli/plans.js +184 -33
- package/dist/cli/schedule.js +22 -11
- package/dist/cli/signals.js +22 -3
- package/dist/cli/tam.d.ts +1 -1
- package/dist/cli/tam.js +14 -2
- package/dist/cli/ui.js +14 -6
- package/dist/connectors/clay.d.ts +33 -0
- package/dist/connectors/clay.js +123 -0
- package/dist/connectors/hubspot.d.ts +4 -0
- package/dist/connectors/hubspot.js +36 -18
- package/dist/connectors/prospectSources.d.ts +12 -0
- package/dist/connectors/prospectSources.js +1 -1
- package/dist/contactProviders.d.ts +44 -0
- package/dist/contactProviders.js +100 -0
- package/dist/enrich.d.ts +7 -1
- package/dist/enrich.js +46 -1
- package/dist/hostedPatchPlan.js +6 -1
- package/dist/icp.d.ts +2 -0
- package/dist/icp.js +49 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/init.d.ts +1 -1
- package/dist/init.js +1 -1
- package/docs/api.md +18 -1
- package/package.json +1 -1
- package/src/cli/audit.ts +5 -1
- package/src/cli/auth.ts +46 -4
- package/src/cli/backfill.ts +3 -0
- package/src/cli/call.ts +25 -6
- package/src/cli/draft.ts +9 -1
- package/src/cli/enrich.ts +132 -56
- package/src/cli/fix.ts +5 -3
- package/src/cli/help.ts +31 -28
- package/src/cli/icp.ts +13 -1
- package/src/cli/init.ts +3 -3
- package/src/cli/market.ts +12 -1
- package/src/cli/planOutput.ts +30 -0
- package/src/cli/plans.ts +174 -29
- package/src/cli/schedule.ts +21 -15
- package/src/cli/signals.ts +22 -3
- package/src/cli/tam.ts +12 -3
- package/src/cli/ui.ts +15 -6
- package/src/connectors/clay.ts +155 -0
- package/src/connectors/hubspot.ts +41 -17
- package/src/connectors/prospectSources.ts +7 -1
- package/src/contactProviders.ts +141 -0
- package/src/enrich.ts +51 -2
- package/src/hostedPatchPlan.ts +6 -1
- package/src/icp.ts +51 -0
- package/src/index.ts +25 -0
- package/src/init.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,51 @@ The path to 1.0 is planned in [docs/roadmap-to-1.0.md](https://github.com/fullst
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.51.0] — 2026-07-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Native Clay Public API authentication and people search for governed lead
|
|
15
|
+
acquisition, including ICP-to-Clay filter translation and resumable search
|
|
16
|
+
checkpoints.
|
|
17
|
+
- A provider-neutral, fill-only contact waterfall contract for work email,
|
|
18
|
+
mobile, and direct-dial enrichment, with Pipe0 as the first registered
|
|
19
|
+
provider and compatibility for existing Pipe0 email resolution.
|
|
20
|
+
- Clay acquisition presets, initialization, doctor diagnostics, public API
|
|
21
|
+
exports, strategy documentation, and live-safe credential validation.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- `enrich acquire` now uses the shared decision-first presentation contract:
|
|
26
|
+
compact cards by default, full evidence with `--verbose`, and a stable JSON
|
|
27
|
+
envelope with counts, cost, meter, and persistence state under `--json`.
|
|
28
|
+
- Pre-enrichment deduplication and avoided-spend reporting are provider-neutral
|
|
29
|
+
so the same acquisition pipeline can support additional contact sources.
|
|
30
|
+
|
|
31
|
+
## [0.50.1] — 2026-07-10
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- Decision-first CLI presentation across plan previews, plan queues, apply
|
|
36
|
+
outcomes, doctor, schedules, signals, ICP, market, TAM, and call analysis.
|
|
37
|
+
Human defaults use compact cards and ranked summaries; `--verbose` restores
|
|
38
|
+
full audit detail and `--json` retains the stable machine contract.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- HubSpot 429/5xx responses now retry with bounded backoff, honor
|
|
43
|
+
`Retry-After`, and report accurate live throttle feedback.
|
|
44
|
+
- Multi-line progress boards no longer scroll intermediate paint frames into
|
|
45
|
+
terminal history.
|
|
46
|
+
- `plans sync` reconciles historical workspaces concurrently with visible
|
|
47
|
+
progress, skips absent legacy mirrors, and avoids echoing hosted-origin
|
|
48
|
+
receipts back to hosted.
|
|
49
|
+
- CLI apply reports only the exact approved receipt subset to hosted while
|
|
50
|
+
retaining excluded rows in the complete local audit trail.
|
|
51
|
+
- Plan and apply output now distinguish approved, applied, excluded, skipped,
|
|
52
|
+
and failed operations without exposing the immutable document's stale
|
|
53
|
+
creation-time status as the current lifecycle state.
|
|
54
|
+
|
|
10
55
|
## [0.50.0] — 2026-07-10
|
|
11
56
|
|
|
12
57
|
### Added
|
package/README.md
CHANGED
|
@@ -243,6 +243,13 @@ replica learns what happened rather than inferring it from plan status. A
|
|
|
243
243
|
conflicting plan body or approval revision is surfaced for review and is never
|
|
244
244
|
merged by expanding authority.
|
|
245
245
|
|
|
246
|
+
Interactive command output is decision-first by default: compact cards show
|
|
247
|
+
status, selected scope, expected effect, per-record outcomes, hosted links, and
|
|
248
|
+
the next safe command. Use `--verbose` on supported human-facing commands for
|
|
249
|
+
the complete findings/evidence/payload document; use `--json` for the stable
|
|
250
|
+
machine-readable contract. Progress and warnings remain on stderr, while JSON
|
|
251
|
+
and requested report data remain clean on stdout.
|
|
252
|
+
|
|
246
253
|
Local signing keys and HMAC digests never upload. Operation before/after values
|
|
247
254
|
do upload to the paired organization because they are required for informed
|
|
248
255
|
review and hosted execution.
|
package/dist/cli/audit.js
CHANGED
|
@@ -11,6 +11,7 @@ import { auditReportToHtml, auditReportToMarkdown } from "../report.js";
|
|
|
11
11
|
import { reportCounts, reportCrm, reportFindings } from "../runReport.js";
|
|
12
12
|
import { connectorFor, numericOption, option, readSnapshot, saveRequested, selectedRules } from "./shared.js";
|
|
13
13
|
import { colorEnabled, createChecklist, formatCount, paint, scoreColor, sparkline, stylizePlanMarkdown, table } from "./ui.js";
|
|
14
|
+
import { compactPlan, verbosePlanRequested } from "./planOutput.js";
|
|
14
15
|
const SEVERITY_RANK = {
|
|
15
16
|
info: 0,
|
|
16
17
|
warning: 1,
|
|
@@ -241,11 +242,15 @@ export async function audit(args) {
|
|
|
241
242
|
if (args.includes("--json")) {
|
|
242
243
|
console.log(JSON.stringify(plan, null, 2));
|
|
243
244
|
}
|
|
245
|
+
else if (!verbosePlanRequested(args)) {
|
|
246
|
+
console.log(compactPlan(plan, { saved: saveRequested(args) }));
|
|
247
|
+
console.error(`\n${auditNextStep(args, plan)}`);
|
|
248
|
+
}
|
|
244
249
|
else {
|
|
245
250
|
// Default to the summary view (rule table + counts); the full per-operation
|
|
246
251
|
// dump is opt-in via --full or, for a deliverable, `report`. (dx-punch-list #3)
|
|
247
252
|
// Interactive terminals get the styled rendering; piped output is unchanged.
|
|
248
|
-
console.log(stylizePlanMarkdown(patchPlanToMarkdown(plan, { summary:
|
|
253
|
+
console.log(stylizePlanMarkdown(patchPlanToMarkdown(plan, { summary: false }), paint(colorEnabled(process.stdout))));
|
|
249
254
|
console.error(`\n${auditNextStep(args, plan)}`);
|
|
250
255
|
}
|
|
251
256
|
if (threshold &&
|
package/dist/cli/auth.js
CHANGED
|
@@ -7,6 +7,7 @@ import { activeProfile, credentialsPath, DEFAULT_PROFILE, deleteCredential, getC
|
|
|
7
7
|
import { activeWorkspaceProfile, readHealthTimeline, summarizeHealth } from "../health.js";
|
|
8
8
|
import { resolveLlmCredential, validateLlmKey } from "../llm.js";
|
|
9
9
|
import { createFilePlanStore } from "../planStore.js";
|
|
10
|
+
import { validateClayApiKey } from "../connectors/clay.js";
|
|
10
11
|
import { isOptionValue, numericOption, option, readPackageInfo, readSecret } from "./shared.js";
|
|
11
12
|
import { box, colorEnabled, paint, scoreColor, sparkline } from "./ui.js";
|
|
12
13
|
/**
|
|
@@ -351,6 +352,22 @@ export async function login(args) {
|
|
|
351
352
|
console.log(`Stored Apollo API key in ${credentialsPath()}. \`fullstackgtm enrich append|refresh\` use it automatically.`);
|
|
352
353
|
return;
|
|
353
354
|
}
|
|
355
|
+
if (provider === "clay") {
|
|
356
|
+
rejectArgvSecret(args, "--token", "--key", "--api-key");
|
|
357
|
+
const key = await readSecret("Clay Public API key");
|
|
358
|
+
if (!key)
|
|
359
|
+
throw new Error("No Clay Public API key provided.");
|
|
360
|
+
if (!args.includes("--no-validate")) {
|
|
361
|
+
const validation = await validateClayApiKey(key);
|
|
362
|
+
if (!validation.ok)
|
|
363
|
+
throw new Error(validation.detail);
|
|
364
|
+
console.log(validation.detail);
|
|
365
|
+
}
|
|
366
|
+
const stamp = new Date().toISOString();
|
|
367
|
+
storeCredential("clay", { kind: "api_key", accessToken: key, createdAt: stamp, updatedAt: stamp });
|
|
368
|
+
console.log(`Stored Clay Public API key in ${credentialsPath()}. Clay connector commands resolve it automatically.`);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
354
371
|
if (provider === "pipe0" || provider === "explorium" || provider === "heyreach" || provider === "theirstack") {
|
|
355
372
|
rejectArgvSecret(args, "--token", "--key", "--api-key");
|
|
356
373
|
const key = await readSecret(`${provider} API key`);
|
|
@@ -365,7 +382,7 @@ export async function login(args) {
|
|
|
365
382
|
return;
|
|
366
383
|
}
|
|
367
384
|
if (provider !== "hubspot") {
|
|
368
|
-
throw new Error("login supports: hubspot, salesforce, stripe, anthropic, openai, apollo, pipe0, explorium, or --via <hosted url>. Usage: fullstackgtm login <provider> | fullstackgtm login --via https://gtm.example.com");
|
|
385
|
+
throw new Error("login supports: hubspot, salesforce, stripe, anthropic, openai, apollo, clay, pipe0, explorium, heyreach, theirstack, or --via <hosted url>. Usage: fullstackgtm login <provider> | fullstackgtm login --via https://gtm.example.com");
|
|
369
386
|
}
|
|
370
387
|
const now = new Date().toISOString();
|
|
371
388
|
rejectArgvSecret(args, "--token");
|
|
@@ -450,6 +467,9 @@ export function doctorReport(env = process.env) {
|
|
|
450
467
|
stripe: env.STRIPE_SECRET_KEY
|
|
451
468
|
? { source: "env", detail: "STRIPE_SECRET_KEY" }
|
|
452
469
|
: providerStatus("stripe", broker),
|
|
470
|
+
clay: env.CLAY_API_KEY
|
|
471
|
+
? { source: "env", detail: "CLAY_API_KEY" }
|
|
472
|
+
: providerStatus("clay", null),
|
|
453
473
|
};
|
|
454
474
|
const llm = resolveLlmCredential(env);
|
|
455
475
|
const missingPeers = ["@modelcontextprotocol/sdk", "zod"].filter((name) => {
|
|
@@ -461,13 +481,13 @@ export function doctorReport(env = process.env) {
|
|
|
461
481
|
return true;
|
|
462
482
|
}
|
|
463
483
|
});
|
|
464
|
-
const
|
|
465
|
-
const nextSteps =
|
|
484
|
+
const connectedCrm = ["hubspot", "salesforce"].filter((provider) => providers[provider].source !== "none");
|
|
485
|
+
const nextSteps = connectedCrm.length === 0
|
|
466
486
|
? [
|
|
467
487
|
"fullstackgtm audit --demo # no credentials needed",
|
|
468
488
|
"fullstackgtm login hubspot # hosted browser OAuth (default; or: login salesforce)",
|
|
469
489
|
]
|
|
470
|
-
: [`fullstackgtm audit --provider ${
|
|
490
|
+
: [`fullstackgtm audit --provider ${connectedCrm[0]}`];
|
|
471
491
|
return {
|
|
472
492
|
package: packageInfo,
|
|
473
493
|
node: { version: process.versions.node, ok: nodeMajor >= 20, required: ">=20" },
|
|
@@ -579,6 +599,31 @@ export async function doctorCommand(args) {
|
|
|
579
599
|
const healthLine = workspace.auditCount === 0
|
|
580
600
|
? p.dim("no saved audits yet (fullstackgtm audit --provider <name> --save starts the timeline)")
|
|
581
601
|
: `${scoreColor(workspace.healthScore ?? 0, p)}/100${delta} — ${workspace.auditCount} audit(s) saved, last ${workspace.lastAuditAt}${healthSparkline(workspace.profile, p.enabled)}`;
|
|
602
|
+
const connectedProviders = Object.entries(report.providers).filter(([, provider]) => provider.source !== "none");
|
|
603
|
+
const blockers = [
|
|
604
|
+
...(!report.node.ok ? [`Node v${report.node.version} is unsupported; install ${report.node.required}.`] : []),
|
|
605
|
+
...(connectedProviders.length === 0 ? ["No CRM connected."] : []),
|
|
606
|
+
...(workspace.pendingPlans.length > 0 ? [`${workspace.pendingPlans.length} plan${workspace.pendingPlans.length === 1 ? "" : "s"} awaiting approval.`] : []),
|
|
607
|
+
];
|
|
608
|
+
if (!args.includes("--verbose")) {
|
|
609
|
+
const ready = report.node.ok && connectedProviders.length > 0;
|
|
610
|
+
const compact = [
|
|
611
|
+
`${ready ? p.green("Ready") : p.yellow("Setup needed")} · ${report.package.name} ${report.package.version} · profile ${report.profile}`,
|
|
612
|
+
`CRM ${connectedProviders.length > 0 ? connectedProviders.map(([name]) => name).join(", ") : "not connected"}`,
|
|
613
|
+
`Health ${healthLine}`,
|
|
614
|
+
`Plans ${workspace.pendingPlans.length === 0 ? "none awaiting approval" : `${workspace.pendingPlans.length} awaiting approval · ${workspace.pendingPlans[0].id}`}`,
|
|
615
|
+
...(blockers.length > 0 ? ["", "Attention", ...blockers.map((blocker) => ` ${blocker}`)] : []),
|
|
616
|
+
"",
|
|
617
|
+
"Next",
|
|
618
|
+
...nextSteps.map((step) => ` ${step}`),
|
|
619
|
+
"",
|
|
620
|
+
"Run `fullstackgtm doctor --verbose` for credential paths and optional tooling checks.",
|
|
621
|
+
];
|
|
622
|
+
console.log(p.enabled ? box(compact, p, "Workspace readiness").join("\n") : compact.join("\n"));
|
|
623
|
+
if (!report.node.ok)
|
|
624
|
+
process.exitCode = 1;
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
582
627
|
const lines = [
|
|
583
628
|
`Package: ${p.bold(`${report.package.name} ${report.package.version}`)}`,
|
|
584
629
|
`Node: v${report.node.version} (${report.node.required} required) ${mark(report.node.ok)}`,
|
package/dist/cli/backfill.js
CHANGED
|
@@ -14,6 +14,7 @@ import { progressReporter } from "../runReport.js";
|
|
|
14
14
|
import { unknownSubcommandError } from "./suggest.js";
|
|
15
15
|
import { option, readSnapshot, saveRequested } from "./shared.js";
|
|
16
16
|
import { colorEnabled, createProgressRenderer, paint, stylizePlanMarkdown, table } from "./ui.js";
|
|
17
|
+
import { compactPlan, verbosePlanRequested } from "./planOutput.js";
|
|
17
18
|
/** STRIPE_SECRET_KEY env ∨ stored `login stripe` credential (same ladder as `--provider stripe`). */
|
|
18
19
|
function resolveStripeKey() {
|
|
19
20
|
const key = process.env.STRIPE_SECRET_KEY ?? getCredential("stripe")?.accessToken;
|
|
@@ -73,6 +74,9 @@ export async function backfillCommand(args) {
|
|
|
73
74
|
if (rest.includes("--json")) {
|
|
74
75
|
console.log(JSON.stringify({ plan: result.plan, counts: result.counts, unmatched: result.unmatched, proposedAccounts: result.proposedAccounts }, null, 2));
|
|
75
76
|
}
|
|
77
|
+
else if (!verbosePlanRequested(rest)) {
|
|
78
|
+
console.log(compactPlan(result.plan, { saved: save && result.plan.operations.length > 0 }));
|
|
79
|
+
}
|
|
76
80
|
else {
|
|
77
81
|
// TTY-only styling; piped output stays byte-identical plain text.
|
|
78
82
|
console.log(stylizePlanMarkdown(patchPlanToMarkdown(result.plan), paint(colorEnabled(process.stdout))));
|
package/dist/cli/call.js
CHANGED
|
@@ -161,11 +161,13 @@ score always needs a key (scoring is LLM work).`);
|
|
|
161
161
|
console.log(JSON.stringify(parsed, null, 2));
|
|
162
162
|
return;
|
|
163
163
|
}
|
|
164
|
-
console.log(`Call ${parsed.id}${parsed.title ? ` — ${parsed.title}` : ""}
|
|
165
|
-
console.log(`${parsed.segments.length} segments · ${parsed.insights.length} insights
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
164
|
+
console.log(`Call ${parsed.id}${parsed.title ? ` — ${parsed.title}` : ""}`);
|
|
165
|
+
console.log(`${parsed.segments.length} segments · ${parsed.insights.length} insights · ${parsed.summary.highImportance} high priority`);
|
|
166
|
+
const visible = rest.includes("--verbose") ? parsed.insights : parsed.insights.filter((insight) => insight.importance >= 3);
|
|
167
|
+
for (const insight of visible)
|
|
168
|
+
console.log(`\n${insight.type.replaceAll("_", " ")} · priority ${insight.importance}\n ${insight.text.replace(/\s+/g, " ").trim()}`);
|
|
169
|
+
if (!rest.includes("--verbose") && visible.length < parsed.insights.length)
|
|
170
|
+
console.log(`\n${parsed.insights.length - visible.length} lower-priority insight(s) hidden; use --verbose to show all.`);
|
|
169
171
|
return;
|
|
170
172
|
}
|
|
171
173
|
if (subcommand === "link") {
|
|
@@ -284,11 +286,31 @@ score always needs a key (scoring is LLM work).`);
|
|
|
284
286
|
console.log(JSON.stringify(scorecard, null, 2));
|
|
285
287
|
return;
|
|
286
288
|
}
|
|
287
|
-
console.log(renderScorecard(scorecard, title));
|
|
289
|
+
console.log(rest.includes("--verbose") ? renderScorecard(scorecard, title) : renderCompactScorecard(scorecard, title));
|
|
288
290
|
return;
|
|
289
291
|
}
|
|
290
292
|
throw new Error(`call supports: parse, classify, link, plan, score (got ${subcommand ?? "nothing"})`);
|
|
291
293
|
}
|
|
294
|
+
function renderCompactScorecard(scorecard, title) {
|
|
295
|
+
const lines = [
|
|
296
|
+
`${title ?? "Call"} · ${scorecard.overallScore}/${scorecard.scale}${scorecard.band ? ` · ${scorecard.band.label}` : ""}`,
|
|
297
|
+
scorecard.rubricName ? `${scorecard.rubricName}${scorecard.callType ? ` (${scorecard.callType})` : ""}` : "",
|
|
298
|
+
"",
|
|
299
|
+
].filter((line, index) => line || index === 2);
|
|
300
|
+
for (const dimension of scorecard.dimensions) {
|
|
301
|
+
lines.push(`${dimension.score}/${dimension.maxScore} ${dimension.name}`);
|
|
302
|
+
lines.push(` ${dimension.coachingNote.replace(/\s+/g, " ").trim()}`);
|
|
303
|
+
}
|
|
304
|
+
if (scorecard.missedItems.length) {
|
|
305
|
+
lines.push("", "Focus next");
|
|
306
|
+
for (const item of scorecard.missedItems)
|
|
307
|
+
lines.push(` ${item}`);
|
|
308
|
+
}
|
|
309
|
+
if (scorecard.highlights.length)
|
|
310
|
+
lines.push("", `Strengths: ${scorecard.highlights.join(" · ")}`);
|
|
311
|
+
lines.push("", "Use --verbose for the full coaching scorecard.");
|
|
312
|
+
return lines.join("\n");
|
|
313
|
+
}
|
|
292
314
|
function renderScorecard(scorecard, title) {
|
|
293
315
|
const bandText = scorecard.band ? ` — ${scorecard.band.label}` : "";
|
|
294
316
|
const rubricLine = scorecard.rubricName ? `Rubric: ${scorecard.rubricName}${scorecard.callType ? ` (${scorecard.callType})` : ""}` : "";
|
package/dist/cli/draft.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createFileSignalStore } from "../signals.js";
|
|
|
7
7
|
import { createFileJudgeStore } from "../judge.js";
|
|
8
8
|
import { authorOpeners, DEFAULT_DRAFT_PROMPT, DRAFT_CHANNELS, draft } from "../draft.js";
|
|
9
9
|
import { numericOption, option, resolveLlmBaseUrls, saveRequested } from "./shared.js";
|
|
10
|
+
import { compactPlan, verbosePlanRequested } from "./planOutput.js";
|
|
10
11
|
/**
|
|
11
12
|
* `draft` — author ONE trigger-grounded opener per hot judge decision as a
|
|
12
13
|
* governed create_task plan. Structurally a proposal: never sends, never writes
|
|
@@ -68,7 +69,16 @@ LLM key it emits a clearly-labeled stub rather than fake authored copy.`);
|
|
|
68
69
|
channel,
|
|
69
70
|
openers,
|
|
70
71
|
});
|
|
71
|
-
|
|
72
|
+
if (args.includes("--json")) {
|
|
73
|
+
console.log(JSON.stringify({ drafts, rejected }, null, 2));
|
|
74
|
+
}
|
|
75
|
+
else if (verbosePlanRequested(args)) {
|
|
76
|
+
console.log(JSON.stringify({ drafts, rejected }, null, 2));
|
|
77
|
+
console.log(compactPlan(plan, { saved: save }));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
console.log(compactPlan(plan, { saved: save }));
|
|
81
|
+
}
|
|
72
82
|
const stale = drafts.filter((d) => d.staleTrigger);
|
|
73
83
|
console.error(`${drafts.length} opener(s) staged as create_task proposals (channel ${channel}, min score ${minScore})` +
|
|
74
84
|
`${rejected.length ? `; ${rejected.length} rejected (ungrounded first line)` : ""}` +
|
package/dist/cli/enrich.js
CHANGED
|
@@ -8,6 +8,8 @@ import { createFilePlanStore } from "../planStore.js";
|
|
|
8
8
|
import { buildAcquirePlan, buildEnrichPlan, createFileEnrichRunStore, DEFAULT_STALE_DAYS, ENRICH_CONFIG_FILE_NAME, builtinAcquirePreset, builtinEnrichPreset, enrichRunId, inferIngestObjectType, latestStamps, loadEnrichConfig, parseCsv, resolveCrmField, selectStaleWork, stagedSourceRecords, staleDaysFor } from "../enrich.js";
|
|
9
9
|
import { loadMeter, remaining } from "../acquireMeter.js";
|
|
10
10
|
import { crmContactKeys, fetchExploriumProspects, fetchPipe0CrustdataProspectPage, partitionFreshProspects, pipe0ResolveCompanyDomains, pipe0ResolveWorkEmails, prospectIdentityKeys } from "../connectors/prospectSources.js";
|
|
11
|
+
import { createClaySearch, runClayPeopleSearchPage } from "../connectors/clay.js";
|
|
12
|
+
import { runContactWaterfall } from "../contactProviders.js";
|
|
11
13
|
import { loadSeen, recordSeen } from "../acquireSeen.js";
|
|
12
14
|
import { acquireCheckpointId, createFileAcquireCheckpointStore } from "../acquireCheckpoint.js";
|
|
13
15
|
import { readHostedAcquireCheckpoint, writeHostedAcquireCheckpoint } from "../hostedAcquireCheckpoint.js";
|
|
@@ -15,13 +17,14 @@ import { uploadHostedPatchPlan } from "../hostedPatchPlan.js";
|
|
|
15
17
|
import { progressReporter, reportCounts, reportEvent } from "../runReport.js";
|
|
16
18
|
import { ACQUIRE_STAGES, composeListeners, createProgressEmitter } from "../progress.js";
|
|
17
19
|
import { createLinkedInProvider, discoverLinkedInProspects } from "../acquireLinkedIn.js";
|
|
18
|
-
import { fitThreshold, icpToCrustdataFilters, icpToExploriumFilters, scoreProspectAgainstIcp } from "../icp.js";
|
|
20
|
+
import { fitThreshold, icpToClayPeopleFilters, icpToCrustdataFilters, icpToExploriumFilters, scoreProspectAgainstIcp } from "../icp.js";
|
|
19
21
|
import { apolloPullKeysForAppend, apolloPullKeysForRefresh, createApolloClient, pullApolloRecords } from "../enrichApollo.js";
|
|
20
22
|
import { isSpoolPath, readSpoolPath } from "../spoolFiles.js";
|
|
21
23
|
import { isOptionValue, loadIcp, numericOption, option, readSnapshot, saveRequested } from "./shared.js";
|
|
22
24
|
import { providerKey } from "./tam.js";
|
|
23
25
|
import { unknownSubcommandError } from "./suggest.js";
|
|
24
26
|
import { colorEnabled, createProgressRenderer, createStatusLine, formatBar, formatDuration, paint } from "./ui.js";
|
|
27
|
+
import { compactPlan, verbosePlanRequested } from "./planOutput.js";
|
|
25
28
|
/**
|
|
26
29
|
* The enrich layer: governed append/refresh of third-party data (Apollo pull,
|
|
27
30
|
* Clay ingest) into the CRM through the normal dry-run → approval → apply
|
|
@@ -192,8 +195,8 @@ are phase 2. Recurring execution is the scheduler's job; enrich has no cron.`);
|
|
|
192
195
|
// Meter: how many MORE leads may we create right now? --max is an
|
|
193
196
|
// additional per-run ceiling, never a way to exceed the budget.
|
|
194
197
|
if (apiSkippedCrm || apiSkippedSeen) {
|
|
195
|
-
console.error(`Pre-
|
|
196
|
-
`(≈$${((apiSkippedCrm + apiSkippedSeen) * costPerRecord).toFixed(2)}
|
|
198
|
+
console.error(`Pre-enrichment dedup: skipped ${apiSkippedCrm} already-in-CRM + ${apiSkippedSeen} previously seen ` +
|
|
199
|
+
`(≈$${((apiSkippedCrm + apiSkippedSeen) * costPerRecord).toFixed(2)} downstream spend avoided).`);
|
|
197
200
|
}
|
|
198
201
|
// `cap` is the desired create count bounded by the persistent meter. Raw
|
|
199
202
|
// discovery scanning has its own --scan-limit and may cross many duplicate-
|
|
@@ -263,16 +266,16 @@ are phase 2. Recurring execution is the scheduler's job; enrich has no cron.`);
|
|
|
263
266
|
const meterLine = formatAcquireMeter(headroom, costPerRecord);
|
|
264
267
|
const gaugeLine = acquireGaugeLine(headroom, config.acquire.budget ?? {}, paint(colorEnabled(process.stdout)));
|
|
265
268
|
if (!save) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
269
|
+
printAcquireOutput({
|
|
270
|
+
args: rest,
|
|
271
|
+
result,
|
|
272
|
+
meter: headroom,
|
|
273
|
+
meterLine,
|
|
274
|
+
gaugeLine,
|
|
275
|
+
saved: false,
|
|
276
|
+
planSaved: false,
|
|
277
|
+
hostedPlanUrl: null,
|
|
278
|
+
});
|
|
276
279
|
return;
|
|
277
280
|
}
|
|
278
281
|
const run = await openEnrichRun(store, source, "acquire", option(rest, "--run-label"), today);
|
|
@@ -318,28 +321,16 @@ are phase 2. Recurring execution is the scheduler's job; enrich has no cron.`);
|
|
|
318
321
|
if (acquireTelemetry && acquireCheckpointSync) {
|
|
319
322
|
await persistAcquireCheckpoint(acquireCheckpointSync.key, acquireTelemetry.discovery, acquireCheckpointSync.hostedVersion);
|
|
320
323
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
console.log("\nNext steps");
|
|
332
|
-
console.log(` Review fullstackgtm plans show ${result.plan.id}`);
|
|
333
|
-
console.log(` Approve fullstackgtm plans approve ${result.plan.id} --operations all`);
|
|
334
|
-
console.log(` Apply fullstackgtm apply --plan-id ${result.plan.id} --provider hubspot`);
|
|
335
|
-
console.log("\nThe meter is charged only when a create lands at apply.");
|
|
336
|
-
}
|
|
337
|
-
else {
|
|
338
|
-
console.log(meterLine);
|
|
339
|
-
if (gaugeLine)
|
|
340
|
-
console.log(gaugeLine);
|
|
341
|
-
console.log("No net-new leads to create (everything sourced already matched, or was withheld by the meter).");
|
|
342
|
-
}
|
|
324
|
+
printAcquireOutput({
|
|
325
|
+
args: rest,
|
|
326
|
+
result,
|
|
327
|
+
meter: headroom,
|
|
328
|
+
meterLine,
|
|
329
|
+
gaugeLine,
|
|
330
|
+
saved: true,
|
|
331
|
+
planSaved: planIds.length > 0,
|
|
332
|
+
hostedPlanUrl,
|
|
333
|
+
});
|
|
343
334
|
return;
|
|
344
335
|
}
|
|
345
336
|
const mode = subcommand === "refresh" ? "refresh" : "append";
|
|
@@ -460,7 +451,7 @@ are phase 2. Recurring execution is the scheduler's job; enrich has no cron.`);
|
|
|
460
451
|
console.log(JSON.stringify(result.plan, null, 2));
|
|
461
452
|
}
|
|
462
453
|
else {
|
|
463
|
-
console.log(patchPlanToMarkdown(result.plan));
|
|
454
|
+
console.log(verbosePlanRequested(rest) ? patchPlanToMarkdown(result.plan) : compactPlan(result.plan));
|
|
464
455
|
console.log(formatEnrichCounts(result.counts, result.ambiguities.length));
|
|
465
456
|
console.log("\nDry run — nothing written. Re-run with --save to persist the plan and the run record.");
|
|
466
457
|
}
|
|
@@ -500,8 +491,9 @@ function formatEnrichCounts(counts, ambiguities) {
|
|
|
500
491
|
/**
|
|
501
492
|
* Pull net-new prospects from an API acquire source into source records the
|
|
502
493
|
* acquire builder dedupes + turns into create_record ops. Explorium discovers;
|
|
503
|
-
*
|
|
504
|
-
*
|
|
494
|
+
* A field-level provider waterfall fills missing contact data. Existing
|
|
495
|
+
* email-keyed and resolveEmailsWith=pipe0 configs translate to an implicit
|
|
496
|
+
* Pipe0 work-email step. Only rows that carry the configured dedupe key survive.
|
|
505
497
|
*/
|
|
506
498
|
async function acquireFromApi(config, source, rest, icp, snapshot, seen, priorRun, targetFresh, persistCheckpoint, progress) {
|
|
507
499
|
const acquire = config.acquire;
|
|
@@ -520,7 +512,9 @@ async function acquireFromApi(config, source, rest, icp, snapshot, seen, priorRu
|
|
|
520
512
|
? (icp ? icpToExploriumFilters(icp) : (disc.filters ?? {}))
|
|
521
513
|
: disc.provider === "pipe0"
|
|
522
514
|
? (icp ? icpToCrustdataFilters(icp) : (disc.filters ?? {}))
|
|
523
|
-
:
|
|
515
|
+
: disc.provider === "clay"
|
|
516
|
+
? (icp ? icpToClayPeopleFilters(icp) : (disc.filters ?? {}))
|
|
517
|
+
: {};
|
|
524
518
|
const queryFingerprint = createHash("sha256")
|
|
525
519
|
.update(JSON.stringify({ source, provider: disc.provider, listId, filters, icp: icp ?? null }))
|
|
526
520
|
.digest("hex");
|
|
@@ -583,6 +577,20 @@ async function acquireFromApi(config, source, rest, icp, snapshot, seen, priorRu
|
|
|
583
577
|
cursor = result.nextCursor;
|
|
584
578
|
exhausted = result.nextCursor === null;
|
|
585
579
|
}
|
|
580
|
+
else if (disc.provider === "clay") {
|
|
581
|
+
if (!cursor) {
|
|
582
|
+
progress.note("creating Clay people-search iterator");
|
|
583
|
+
cursor = await createClaySearch({
|
|
584
|
+
apiKey: providerKey("clay"), sourceType: "people", filters,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
const result = await runClayPeopleSearchPage({
|
|
588
|
+
apiKey: providerKey("clay"), searchId: cursor, limit: requestSize,
|
|
589
|
+
});
|
|
590
|
+
page = result.prospects;
|
|
591
|
+
offset += page.length;
|
|
592
|
+
exhausted = !result.hasMore;
|
|
593
|
+
}
|
|
586
594
|
else if (disc.provider === "explorium") {
|
|
587
595
|
const pageNumber = offset + 1;
|
|
588
596
|
exploriumPage = pageNumber;
|
|
@@ -603,7 +611,7 @@ async function acquireFromApi(config, source, rest, icp, snapshot, seen, priorRu
|
|
|
603
611
|
exhausted = page.length < requestSize;
|
|
604
612
|
}
|
|
605
613
|
else {
|
|
606
|
-
throw new Error(`enrich acquire: unknown discovery provider "${disc.provider}" (explorium | pipe0 | linkedin).`);
|
|
614
|
+
throw new Error(`enrich acquire: unknown discovery provider "${disc.provider}" (clay | explorium | pipe0 | linkedin).`);
|
|
607
615
|
}
|
|
608
616
|
discovered += page.length;
|
|
609
617
|
progress.items(discovered, scanLimit);
|
|
@@ -649,23 +657,35 @@ async function acquireFromApi(config, source, rest, icp, snapshot, seen, priorRu
|
|
|
649
657
|
}
|
|
650
658
|
progress.stage(ACQUIRE_STAGES[1], 1, ACQUIRE_STAGES.length);
|
|
651
659
|
progress.note("checking work-email requirements");
|
|
652
|
-
// 3. Resolve
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
if (
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
660
|
+
// 3. Resolve contact fields through the ordered, fill-only waterfall. Keep
|
|
661
|
+
// legacy behavior: email-keyed acquisition and resolveEmailsWith=pipe0 imply
|
|
662
|
+
// a Pipe0 work-email step when no explicit waterfall is configured.
|
|
663
|
+
const implicitPipe0 = matchKey === "email" || disc.resolveEmailsWith === "pipe0";
|
|
664
|
+
const contactWaterfall = disc.contactWaterfall ?? (implicitPipe0 ? [{ provider: "pipe0", fields: ["work_email"] }] : []);
|
|
665
|
+
if (contactWaterfall.length > 0) {
|
|
666
|
+
const adapters = {
|
|
667
|
+
pipe0: {
|
|
668
|
+
provider: "pipe0",
|
|
669
|
+
resolve: async (candidates, fields) => {
|
|
670
|
+
let resolved = candidates;
|
|
671
|
+
if (fields.includes("work_email") && resolved.some((p) => !p.companyDomain && p.companyName)) {
|
|
672
|
+
progress.note(`resolving company domains for ${resolved.length} candidate(s)`);
|
|
673
|
+
resolved = await pipe0ResolveCompanyDomains({ apiKey: providerKey("pipe0"), prospects: resolved });
|
|
674
|
+
}
|
|
675
|
+
if (fields.includes("work_email")) {
|
|
676
|
+
progress.note(`resolving work emails with pipe0 for ${resolved.length} candidate(s)`);
|
|
677
|
+
resolved = await pipe0ResolveWorkEmails({ apiKey: providerKey("pipe0"), prospects: resolved });
|
|
678
|
+
}
|
|
679
|
+
return resolved;
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
const result = await runContactWaterfall({ prospects, steps: contactWaterfall, adapters });
|
|
684
|
+
prospects.splice(0, prospects.length, ...result.prospects);
|
|
685
|
+
for (const attempt of result.attempts) {
|
|
686
|
+
const added = Object.entries(attempt.added).map(([field, count]) => `${count} ${field}`).join(", ") || "0 fields";
|
|
687
|
+
progress.note(`${attempt.provider}: ${attempt.attempted} attempted · ${added} added`);
|
|
688
|
+
}
|
|
669
689
|
}
|
|
670
690
|
progress.items(prospects.length, prospects.length);
|
|
671
691
|
progress.note(`${prospects.length} candidate(s) ready for planning`);
|
|
@@ -755,6 +775,53 @@ function formatAcquireMeter(headroom, costPerRecord) {
|
|
|
755
775
|
`Spend left: ${money(headroom.spendUsd.day)}/day, ${money(headroom.spendUsd.month)}/month ` +
|
|
756
776
|
`(≈$${costPerRecord.toFixed(2)}/lead).`);
|
|
757
777
|
}
|
|
778
|
+
function printAcquireOutput(options) {
|
|
779
|
+
const { args, result, meter, meterLine, gaugeLine, saved, planSaved, hostedPlanUrl } = options;
|
|
780
|
+
if (args.includes("--json")) {
|
|
781
|
+
console.log(JSON.stringify({
|
|
782
|
+
plan: result.plan,
|
|
783
|
+
counts: result.counts,
|
|
784
|
+
estCostUsd: result.estCostUsd,
|
|
785
|
+
meter,
|
|
786
|
+
persistence: {
|
|
787
|
+
runSaved: saved,
|
|
788
|
+
planSaved,
|
|
789
|
+
planId: planSaved ? result.plan.id : null,
|
|
790
|
+
hostedPlanUrl,
|
|
791
|
+
},
|
|
792
|
+
}, null, 2));
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
if (verbosePlanRequested(args)) {
|
|
796
|
+
console.log(patchPlanToMarkdown(result.plan));
|
|
797
|
+
console.log(`\nAcquisition`);
|
|
798
|
+
console.log(` Leads ${result.counts.created} net-new · ${result.counts.unassigned} unassigned`);
|
|
799
|
+
console.log(` Est. cost $${result.estCostUsd.toFixed(2)}`);
|
|
800
|
+
console.log(` Budget ${gaugeLine ?? meterLine}`);
|
|
801
|
+
if (hostedPlanUrl)
|
|
802
|
+
console.log(` Plan ${hostedPlanUrl}`);
|
|
803
|
+
}
|
|
804
|
+
else if (planSaved || !saved) {
|
|
805
|
+
console.log(compactPlan(result.plan, { saved: planSaved }));
|
|
806
|
+
console.log(meterLine);
|
|
807
|
+
if (gaugeLine)
|
|
808
|
+
console.log(gaugeLine);
|
|
809
|
+
}
|
|
810
|
+
else {
|
|
811
|
+
console.log(meterLine);
|
|
812
|
+
if (gaugeLine)
|
|
813
|
+
console.log(gaugeLine);
|
|
814
|
+
console.log("No net-new leads to create (everything sourced already matched, or was withheld by the meter).");
|
|
815
|
+
}
|
|
816
|
+
if (planSaved) {
|
|
817
|
+
console.log(`\nApprove fullstackgtm plans approve ${result.plan.id} --operations all`);
|
|
818
|
+
console.log(`Apply fullstackgtm apply --plan-id ${result.plan.id} --provider <hubspot|salesforce>`);
|
|
819
|
+
const hostedRuns = hostedRunsUrl();
|
|
820
|
+
if (hostedRuns)
|
|
821
|
+
console.log(`Observe ${hostedRuns}`);
|
|
822
|
+
console.log("The meter is charged only when a create lands at apply.");
|
|
823
|
+
}
|
|
824
|
+
}
|
|
758
825
|
function hostedRunsUrl() {
|
|
759
826
|
const baseUrl = getCredential("broker")?.baseUrl?.replace(/\/+$/, "");
|
|
760
827
|
return baseUrl ? `${baseUrl}/dashboard/runs` : null;
|
package/dist/cli/fix.js
CHANGED
|
@@ -20,6 +20,7 @@ import { APPLY_STAGES, composeListeners, createProgressEmitter } from "../progre
|
|
|
20
20
|
import { progressReporter } from "../runReport.js";
|
|
21
21
|
import { confirmRequested, connectorFor, isOptionValue, numericOption, option, readSnapshot, repeatedOption, saveRequested } from "./shared.js";
|
|
22
22
|
import { box, colorEnabled, createProgressRenderer, paint } from "./ui.js";
|
|
23
|
+
import { compactPlan, verbosePlanRequested } from "./planOutput.js";
|
|
23
24
|
/**
|
|
24
25
|
* The resolve gate: exit 0 = safe to create, exit 2 = match found (exists or
|
|
25
26
|
* ambiguous — do NOT blind-create), exit 1 = error. Built for sync jobs and
|
|
@@ -101,9 +102,12 @@ async function emitPlan(plan, args) {
|
|
|
101
102
|
if (args.includes("--json")) {
|
|
102
103
|
console.log(JSON.stringify(plan, null, 2));
|
|
103
104
|
}
|
|
104
|
-
else {
|
|
105
|
+
else if (verbosePlanRequested(args)) {
|
|
105
106
|
console.log(patchPlanToMarkdown(plan));
|
|
106
107
|
}
|
|
108
|
+
else {
|
|
109
|
+
console.log(compactPlan(plan, { saved: saveRequested(args) }));
|
|
110
|
+
}
|
|
107
111
|
}
|
|
108
112
|
/**
|
|
109
113
|
* Governed duplicate cleanup: group by a normalized identity key, propose one
|
|
@@ -170,8 +174,7 @@ export async function reassignCommand(args) {
|
|
|
170
174
|
for (const plan of plans) {
|
|
171
175
|
if (store)
|
|
172
176
|
await store.save(plan);
|
|
173
|
-
console.log(
|
|
174
|
-
console.log(` ${plan.summary}`);
|
|
177
|
+
console.log(verbosePlanRequested(args) ? patchPlanToMarkdown(plan) : compactPlan(plan, { saved: Boolean(store) }));
|
|
175
178
|
}
|
|
176
179
|
if (store) {
|
|
177
180
|
console.log(`\nSaved ${plans.length} plan(s). For each: \`fullstackgtm plans show <id>\`, \`fullstackgtm plans approve <id> --operations <ids|all>\`, then \`fullstackgtm apply --plan-id <id> --provider <name>\`.`);
|