fullstackgtm 0.50.1 → 0.52.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 +40 -0
- package/dist/cli/auth.js +42 -11
- package/dist/cli/enrich.js +122 -56
- package/dist/cli/help.js +10 -7
- package/dist/cli/icp.js +155 -1
- package/dist/cli/init.js +3 -3
- package/dist/cli/tam.d.ts +1 -1
- package/dist/cli/tam.js +4 -1
- package/dist/connectors/clay.d.ts +33 -0
- package/dist/connectors/clay.js +123 -0
- 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/icp.d.ts +2 -0
- package/dist/icp.js +53 -0
- package/dist/icpDerive.d.ts +51 -0
- package/dist/icpDerive.js +146 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/init.d.ts +1 -1
- package/dist/init.js +1 -1
- package/dist/llm.d.ts +4 -0
- package/dist/llm.js +77 -6
- package/dist/publicHttp.js +6 -0
- package/docs/api.md +18 -1
- package/package.json +1 -1
- package/src/cli/auth.ts +37 -10
- package/src/cli/enrich.ts +130 -55
- package/src/cli/help.ts +10 -7
- package/src/cli/icp.ts +144 -3
- package/src/cli/init.ts +3 -3
- package/src/cli/tam.ts +4 -2
- package/src/connectors/clay.ts +155 -0
- package/src/connectors/prospectSources.ts +7 -1
- package/src/contactProviders.ts +141 -0
- package/src/enrich.ts +51 -2
- package/src/icp.ts +55 -0
- package/src/icpDerive.ts +158 -0
- package/src/index.ts +38 -0
- package/src/init.ts +2 -2
- package/src/llm.ts +71 -6
- package/src/publicHttp.ts +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,46 @@ 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.52.0] — 2026-07-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `fullstackgtm icp derive --domain <site>` derives an evidence-backed ICP
|
|
15
|
+
from a public website with OpenRouter, OpenAI, or Anthropic, then offers an
|
|
16
|
+
interactive arrow-key review before writing `icp.json`.
|
|
17
|
+
- `fullstackgtm login openrouter` stores and validates a profile-scoped API key
|
|
18
|
+
for website-to-ICP derivation.
|
|
19
|
+
- OpenRouter derivation streams reasoning activity and provider-authored
|
|
20
|
+
reasoning summaries into the CLI status line during the model's longest wait.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Public website fetching retains DNS pinning while disabling Node's automatic
|
|
25
|
+
address-family selection, avoiding custom-lookup failures on current Node.
|
|
26
|
+
- ICP-to-Clay filters map only supported catalog industries rather than
|
|
27
|
+
inventing unsupported provider labels.
|
|
28
|
+
|
|
29
|
+
## [0.51.0] — 2026-07-11
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- Native Clay Public API authentication and people search for governed lead
|
|
34
|
+
acquisition, including ICP-to-Clay filter translation and resumable search
|
|
35
|
+
checkpoints.
|
|
36
|
+
- A provider-neutral, fill-only contact waterfall contract for work email,
|
|
37
|
+
mobile, and direct-dial enrichment, with Pipe0 as the first registered
|
|
38
|
+
provider and compatibility for existing Pipe0 email resolution.
|
|
39
|
+
- Clay acquisition presets, initialization, doctor diagnostics, public API
|
|
40
|
+
exports, strategy documentation, and live-safe credential validation.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- `enrich acquire` now uses the shared decision-first presentation contract:
|
|
45
|
+
compact cards by default, full evidence with `--verbose`, and a stable JSON
|
|
46
|
+
envelope with counts, cost, meter, and persistence state under `--json`.
|
|
47
|
+
- Pre-enrichment deduplication and avoided-spend reporting are provider-neutral
|
|
48
|
+
so the same acquisition pipeline can support additional contact sources.
|
|
49
|
+
|
|
10
50
|
## [0.50.1] — 2026-07-10
|
|
11
51
|
|
|
12
52
|
### Added
|
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
|
/**
|
|
@@ -316,20 +317,28 @@ export async function login(args) {
|
|
|
316
317
|
console.log(`Logged in to Stripe. Credentials stored in ${credentialsPath()}.`);
|
|
317
318
|
return;
|
|
318
319
|
}
|
|
319
|
-
if (provider === "anthropic" || provider === "openai") {
|
|
320
|
+
if (provider === "anthropic" || provider === "openai" || provider === "openrouter") {
|
|
320
321
|
rejectArgvSecret(args, "--token", "--key", "--api-key");
|
|
321
|
-
const key = await readSecret(`${provider} API key (${provider === "anthropic" ? "sk-ant-..." : "sk-..."})`);
|
|
322
|
+
const key = await readSecret(`${provider} API key (${provider === "anthropic" ? "sk-ant-..." : provider === "openrouter" ? "sk-or-..." : "sk-..."})`);
|
|
322
323
|
if (!key)
|
|
323
324
|
throw new Error(`No ${provider} key provided.`);
|
|
324
325
|
if (!args.includes("--no-validate")) {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
326
|
+
if (provider === "openrouter") {
|
|
327
|
+
const response = await fetch("https://openrouter.ai/api/v1/key", { headers: { Authorization: `Bearer ${key}` } });
|
|
328
|
+
if (!response.ok)
|
|
329
|
+
throw new Error(`openrouter rejected the key: ${safeStatus(response)}`);
|
|
330
|
+
console.log("Key accepted by OpenRouter.");
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
const validation = await validateLlmKey(provider, key);
|
|
334
|
+
if (!validation.ok)
|
|
335
|
+
throw new Error(`${provider} rejected the key: ${validation.detail}`);
|
|
336
|
+
console.log(validation.detail);
|
|
337
|
+
}
|
|
329
338
|
}
|
|
330
339
|
const stamp = new Date().toISOString();
|
|
331
340
|
storeCredential(provider, { kind: "api_key", accessToken: key, createdAt: stamp, updatedAt: stamp });
|
|
332
|
-
console.log(`Stored ${provider} API key in ${credentialsPath()}. \`fullstackgtm
|
|
341
|
+
console.log(`Stored ${provider} API key in ${credentialsPath()}. \`fullstackgtm icp derive\` uses it automatically${provider === "openrouter" ? "." : "; call parse/score use it too."}`);
|
|
333
342
|
return;
|
|
334
343
|
}
|
|
335
344
|
if (provider === "apollo") {
|
|
@@ -351,6 +360,22 @@ export async function login(args) {
|
|
|
351
360
|
console.log(`Stored Apollo API key in ${credentialsPath()}. \`fullstackgtm enrich append|refresh\` use it automatically.`);
|
|
352
361
|
return;
|
|
353
362
|
}
|
|
363
|
+
if (provider === "clay") {
|
|
364
|
+
rejectArgvSecret(args, "--token", "--key", "--api-key");
|
|
365
|
+
const key = await readSecret("Clay Public API key");
|
|
366
|
+
if (!key)
|
|
367
|
+
throw new Error("No Clay Public API key provided.");
|
|
368
|
+
if (!args.includes("--no-validate")) {
|
|
369
|
+
const validation = await validateClayApiKey(key);
|
|
370
|
+
if (!validation.ok)
|
|
371
|
+
throw new Error(validation.detail);
|
|
372
|
+
console.log(validation.detail);
|
|
373
|
+
}
|
|
374
|
+
const stamp = new Date().toISOString();
|
|
375
|
+
storeCredential("clay", { kind: "api_key", accessToken: key, createdAt: stamp, updatedAt: stamp });
|
|
376
|
+
console.log(`Stored Clay Public API key in ${credentialsPath()}. Clay connector commands resolve it automatically.`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
354
379
|
if (provider === "pipe0" || provider === "explorium" || provider === "heyreach" || provider === "theirstack") {
|
|
355
380
|
rejectArgvSecret(args, "--token", "--key", "--api-key");
|
|
356
381
|
const key = await readSecret(`${provider} API key`);
|
|
@@ -365,7 +390,7 @@ export async function login(args) {
|
|
|
365
390
|
return;
|
|
366
391
|
}
|
|
367
392
|
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");
|
|
393
|
+
throw new Error("login supports: hubspot, salesforce, stripe, anthropic, openai, openrouter, apollo, clay, pipe0, explorium, heyreach, theirstack, or --via <hosted url>. Usage: fullstackgtm login <provider> | fullstackgtm login --via https://gtm.example.com");
|
|
369
394
|
}
|
|
370
395
|
const now = new Date().toISOString();
|
|
371
396
|
rejectArgvSecret(args, "--token");
|
|
@@ -450,6 +475,12 @@ export function doctorReport(env = process.env) {
|
|
|
450
475
|
stripe: env.STRIPE_SECRET_KEY
|
|
451
476
|
? { source: "env", detail: "STRIPE_SECRET_KEY" }
|
|
452
477
|
: providerStatus("stripe", broker),
|
|
478
|
+
clay: env.CLAY_API_KEY
|
|
479
|
+
? { source: "env", detail: "CLAY_API_KEY" }
|
|
480
|
+
: providerStatus("clay", null),
|
|
481
|
+
openrouter: env.OPENROUTER_API_KEY
|
|
482
|
+
? { source: "env", detail: "OPENROUTER_API_KEY" }
|
|
483
|
+
: providerStatus("openrouter", null),
|
|
453
484
|
};
|
|
454
485
|
const llm = resolveLlmCredential(env);
|
|
455
486
|
const missingPeers = ["@modelcontextprotocol/sdk", "zod"].filter((name) => {
|
|
@@ -461,13 +492,13 @@ export function doctorReport(env = process.env) {
|
|
|
461
492
|
return true;
|
|
462
493
|
}
|
|
463
494
|
});
|
|
464
|
-
const
|
|
465
|
-
const nextSteps =
|
|
495
|
+
const connectedCrm = ["hubspot", "salesforce"].filter((provider) => providers[provider].source !== "none");
|
|
496
|
+
const nextSteps = connectedCrm.length === 0
|
|
466
497
|
? [
|
|
467
498
|
"fullstackgtm audit --demo # no credentials needed",
|
|
468
499
|
"fullstackgtm login hubspot # hosted browser OAuth (default; or: login salesforce)",
|
|
469
500
|
]
|
|
470
|
-
: [`fullstackgtm audit --provider ${
|
|
501
|
+
: [`fullstackgtm audit --provider ${connectedCrm[0]}`];
|
|
471
502
|
return {
|
|
472
503
|
package: packageInfo,
|
|
473
504
|
node: { version: process.versions.node, ok: nodeMajor >= 20, required: ">=20" },
|
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,7 +17,7 @@ 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";
|
|
@@ -193,8 +195,8 @@ are phase 2. Recurring execution is the scheduler's job; enrich has no cron.`);
|
|
|
193
195
|
// Meter: how many MORE leads may we create right now? --max is an
|
|
194
196
|
// additional per-run ceiling, never a way to exceed the budget.
|
|
195
197
|
if (apiSkippedCrm || apiSkippedSeen) {
|
|
196
|
-
console.error(`Pre-
|
|
197
|
-
`(≈$${((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).`);
|
|
198
200
|
}
|
|
199
201
|
// `cap` is the desired create count bounded by the persistent meter. Raw
|
|
200
202
|
// discovery scanning has its own --scan-limit and may cross many duplicate-
|
|
@@ -264,16 +266,16 @@ are phase 2. Recurring execution is the scheduler's job; enrich has no cron.`);
|
|
|
264
266
|
const meterLine = formatAcquireMeter(headroom, costPerRecord);
|
|
265
267
|
const gaugeLine = acquireGaugeLine(headroom, config.acquire.budget ?? {}, paint(colorEnabled(process.stdout)));
|
|
266
268
|
if (!save) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
269
|
+
printAcquireOutput({
|
|
270
|
+
args: rest,
|
|
271
|
+
result,
|
|
272
|
+
meter: headroom,
|
|
273
|
+
meterLine,
|
|
274
|
+
gaugeLine,
|
|
275
|
+
saved: false,
|
|
276
|
+
planSaved: false,
|
|
277
|
+
hostedPlanUrl: null,
|
|
278
|
+
});
|
|
277
279
|
return;
|
|
278
280
|
}
|
|
279
281
|
const run = await openEnrichRun(store, source, "acquire", option(rest, "--run-label"), today);
|
|
@@ -319,28 +321,16 @@ are phase 2. Recurring execution is the scheduler's job; enrich has no cron.`);
|
|
|
319
321
|
if (acquireTelemetry && acquireCheckpointSync) {
|
|
320
322
|
await persistAcquireCheckpoint(acquireCheckpointSync.key, acquireTelemetry.discovery, acquireCheckpointSync.hostedVersion);
|
|
321
323
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
console.log("\nNext steps");
|
|
333
|
-
console.log(` Review fullstackgtm plans show ${result.plan.id}`);
|
|
334
|
-
console.log(` Approve fullstackgtm plans approve ${result.plan.id} --operations all`);
|
|
335
|
-
console.log(` Apply fullstackgtm apply --plan-id ${result.plan.id} --provider hubspot`);
|
|
336
|
-
console.log("\nThe meter is charged only when a create lands at apply.");
|
|
337
|
-
}
|
|
338
|
-
else {
|
|
339
|
-
console.log(meterLine);
|
|
340
|
-
if (gaugeLine)
|
|
341
|
-
console.log(gaugeLine);
|
|
342
|
-
console.log("No net-new leads to create (everything sourced already matched, or was withheld by the meter).");
|
|
343
|
-
}
|
|
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
|
+
});
|
|
344
334
|
return;
|
|
345
335
|
}
|
|
346
336
|
const mode = subcommand === "refresh" ? "refresh" : "append";
|
|
@@ -501,8 +491,9 @@ function formatEnrichCounts(counts, ambiguities) {
|
|
|
501
491
|
/**
|
|
502
492
|
* Pull net-new prospects from an API acquire source into source records the
|
|
503
493
|
* acquire builder dedupes + turns into create_record ops. Explorium discovers;
|
|
504
|
-
*
|
|
505
|
-
*
|
|
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.
|
|
506
497
|
*/
|
|
507
498
|
async function acquireFromApi(config, source, rest, icp, snapshot, seen, priorRun, targetFresh, persistCheckpoint, progress) {
|
|
508
499
|
const acquire = config.acquire;
|
|
@@ -521,7 +512,9 @@ async function acquireFromApi(config, source, rest, icp, snapshot, seen, priorRu
|
|
|
521
512
|
? (icp ? icpToExploriumFilters(icp) : (disc.filters ?? {}))
|
|
522
513
|
: disc.provider === "pipe0"
|
|
523
514
|
? (icp ? icpToCrustdataFilters(icp) : (disc.filters ?? {}))
|
|
524
|
-
:
|
|
515
|
+
: disc.provider === "clay"
|
|
516
|
+
? (icp ? icpToClayPeopleFilters(icp) : (disc.filters ?? {}))
|
|
517
|
+
: {};
|
|
525
518
|
const queryFingerprint = createHash("sha256")
|
|
526
519
|
.update(JSON.stringify({ source, provider: disc.provider, listId, filters, icp: icp ?? null }))
|
|
527
520
|
.digest("hex");
|
|
@@ -584,6 +577,20 @@ async function acquireFromApi(config, source, rest, icp, snapshot, seen, priorRu
|
|
|
584
577
|
cursor = result.nextCursor;
|
|
585
578
|
exhausted = result.nextCursor === null;
|
|
586
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
|
+
}
|
|
587
594
|
else if (disc.provider === "explorium") {
|
|
588
595
|
const pageNumber = offset + 1;
|
|
589
596
|
exploriumPage = pageNumber;
|
|
@@ -604,7 +611,7 @@ async function acquireFromApi(config, source, rest, icp, snapshot, seen, priorRu
|
|
|
604
611
|
exhausted = page.length < requestSize;
|
|
605
612
|
}
|
|
606
613
|
else {
|
|
607
|
-
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).`);
|
|
608
615
|
}
|
|
609
616
|
discovered += page.length;
|
|
610
617
|
progress.items(discovered, scanLimit);
|
|
@@ -650,23 +657,35 @@ async function acquireFromApi(config, source, rest, icp, snapshot, seen, priorRu
|
|
|
650
657
|
}
|
|
651
658
|
progress.stage(ACQUIRE_STAGES[1], 1, ACQUIRE_STAGES.length);
|
|
652
659
|
progress.note("checking work-email requirements");
|
|
653
|
-
// 3. Resolve
|
|
654
|
-
//
|
|
655
|
-
//
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
if (
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
+
}
|
|
670
689
|
}
|
|
671
690
|
progress.items(prospects.length, prospects.length);
|
|
672
691
|
progress.note(`${prospects.length} candidate(s) ready for planning`);
|
|
@@ -756,6 +775,53 @@ function formatAcquireMeter(headroom, costPerRecord) {
|
|
|
756
775
|
`Spend left: ${money(headroom.spendUsd.day)}/day, ${money(headroom.spendUsd.month)}/month ` +
|
|
757
776
|
`(≈$${costPerRecord.toFixed(2)}/lead).`);
|
|
758
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
|
+
}
|
|
759
825
|
function hostedRunsUrl() {
|
|
760
826
|
const baseUrl = getCredential("broker")?.baseUrl?.replace(/\/+$/, "");
|
|
761
827
|
return baseUrl ? `${baseUrl}/dashboard/runs` : null;
|
package/dist/cli/help.js
CHANGED
|
@@ -16,13 +16,16 @@ Usage:
|
|
|
16
16
|
fullstackgtm login salesforce --instance-url <url> [--no-validate] advanced: paste access token
|
|
17
17
|
fullstackgtm login stripe [--no-validate]
|
|
18
18
|
fullstackgtm login anthropic | openai store an LLM API key for call parse/score
|
|
19
|
-
fullstackgtm login apollo
|
|
19
|
+
fullstackgtm login apollo | clay store a data-provider Public API key
|
|
20
|
+
fullstackgtm login pipe0 | explorium | theirstack store a discovery-provider key (theirstack = technographic TAM)
|
|
21
|
+
fullstackgtm login heyreach store a HeyReach key for enrich acquire --source linkedin
|
|
22
|
+
fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|clay|pipe0|explorium|heyreach|broker>
|
|
20
23
|
|
|
21
24
|
Secrets (tokens, client secrets) are NEVER passed as flags — they leak via
|
|
22
25
|
the process list and shell history. Pipe them on stdin or enter them at the
|
|
23
26
|
interactive prompt:
|
|
24
27
|
echo "$HUBSPOT_TOKEN" | fullstackgtm login hubspot --private-token
|
|
25
|
-
fullstackgtm init [--source pipe0|explorium|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]
|
|
28
|
+
fullstackgtm init [--source pipe0|explorium|clay|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]
|
|
26
29
|
cold start: scaffold icp.json + enrich.config.json + a
|
|
27
30
|
PLAYBOOK wired for this workspace (the CLI ships primitives,
|
|
28
31
|
your agent is the orchestrator — see docs/recipes.md)
|
|
@@ -72,7 +75,7 @@ Usage:
|
|
|
72
75
|
fullstackgtm tam estimate [--name <n>] [--icp <path>] (--accounts <n> | --source theirstack|explorium) (--acv <annual-usd> | --acv-from-crm --deal-period monthly|quarterly|annual) [--acv-basis account|buyer] [--buyers-per-account <n>] [--cross-checks <file.json>] [source options] [--json]
|
|
73
76
|
fullstackgtm tam status [--name <n>] <source options> [--save] [--json]
|
|
74
77
|
fullstackgtm tam report [--name <n>] [--out <path>]
|
|
75
|
-
fullstackgtm tam populate [--name <n>] --cron "<expr>" [--source pipe0|explorium|linkedin] [--provider hubspot|salesforce] [--label <l>]
|
|
78
|
+
fullstackgtm tam populate [--name <n>] --cron "<expr>" [--source pipe0|explorium|clay|linkedin] [--provider hubspot|salesforce] [--label <l>]
|
|
76
79
|
size the reachable market FROM the ICP (a real account count ×
|
|
77
80
|
ACV; buyers/account = the contact population target), then fill
|
|
78
81
|
it: populate schedules plan-only enrich acquire --save (apply
|
|
@@ -276,7 +279,7 @@ export const HELP = {
|
|
|
276
279
|
summary: "scaffold a workspace (icp.json + enrich.config.json + PLAYBOOK)",
|
|
277
280
|
phase: "Setup",
|
|
278
281
|
synopsis: [
|
|
279
|
-
"fullstackgtm init [--source pipe0|explorium|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]",
|
|
282
|
+
"fullstackgtm init [--source pipe0|explorium|clay|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]",
|
|
280
283
|
],
|
|
281
284
|
detail: "Cold start: writes a starter ICP, an acquire-ready enrich.config.json (with a visible assign seam so leads are never ownerless), and a PLAYBOOK wired with the cold-start + outbound-loop recipes for this workspace. Pure file-writer — no network, keeps existing files unless --force. The CLI ships governed primitives; your coding agent is the orchestrator (see docs/recipes.md).",
|
|
282
285
|
seeAlso: ["icp", "enrich", "signals"],
|
|
@@ -289,7 +292,7 @@ export const HELP = {
|
|
|
289
292
|
"fullstackgtm login hubspot | salesforce hosted browser OAuth (default)",
|
|
290
293
|
"fullstackgtm login --via <hosted url> pair with a team deployment",
|
|
291
294
|
"fullstackgtm login stripe",
|
|
292
|
-
"fullstackgtm login anthropic | openai | apollo",
|
|
295
|
+
"fullstackgtm login anthropic | openai | apollo | clay",
|
|
293
296
|
],
|
|
294
297
|
detail: "HubSpot/Salesforce use hosted browser OAuth by default; BYO app/token paths are advanced. Secrets are NEVER passed as flags — pipe on stdin or enter at the prompt: `echo \"$TOKEN\" | fullstackgtm login hubspot --private-token`.",
|
|
295
298
|
seeAlso: ["doctor", "logout", "profiles"],
|
|
@@ -297,7 +300,7 @@ export const HELP = {
|
|
|
297
300
|
logout: {
|
|
298
301
|
summary: "remove stored credentials for a provider",
|
|
299
302
|
phase: "Setup",
|
|
300
|
-
synopsis: ["fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|pipe0|explorium|broker>"],
|
|
303
|
+
synopsis: ["fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|clay|pipe0|explorium|broker>"],
|
|
301
304
|
seeAlso: ["login", "doctor"],
|
|
302
305
|
},
|
|
303
306
|
doctor: {
|
|
@@ -633,7 +636,7 @@ export const COMMAND_FLAGS = {
|
|
|
633
636
|
merge: ["--input", "--out", "--json"],
|
|
634
637
|
market: ["--category", "--config", "--vendor", "--snapshot", "--task-account", "--task-deal", "--capture-run", "--run", "--prior-run", "--diff", "--from", "--calls", "--anchor", "--min-mentions", "--max-claims", "--promote-lift", "--model", "--format", "--auto", "--unverified", "--save", "--dry-run", "--verbose", "--json", "--out"],
|
|
635
638
|
tam: [...SOURCE_FLAGS, "--source", "--name", "--icp", "--accounts", "--acv", "--acv-basis", "--acv-from-crm", "--deal-period", "--buyers-per-account", "--cross-checks", "--max", "--max-credits", "--usd-per-credit", "--dry-run", "--confirm", "--cron", "--label", "--save", "--verbose", "--json", "--out"],
|
|
636
|
-
icp: [...SOURCE_FLAGS, "--name", "--signals-from", "--with-history", "--prompt", "--min-score", "--from-judge", "--golden", "--against-outcomes", "--min-accuracy", "--model", "--label", "--save", "--verbose", "--json", "--out"],
|
|
639
|
+
icp: [...SOURCE_FLAGS, "--name", "--icp", "--domain", "--no-interactive", "--signals-from", "--with-history", "--prompt", "--min-score", "--from-judge", "--golden", "--against-outcomes", "--min-accuracy", "--model", "--label", "--save", "--verbose", "--json", "--out"],
|
|
637
640
|
signals: ["--bucket", "--source", "--connector", "--connector-opt", "--watchlist", "--keywords", "--from", "--label", "--since", "--account", "--contact", "--unjudged", "--touch", "--result", "--save", "--verbose", "--json", "--explain"],
|
|
638
641
|
draft: ["--from-judge", "--min-score", "--prompt", "--model", "--channel", "--save", "--dry-run", "--verbose", "--json", "--out"],
|
|
639
642
|
schedule: ["--cron", "--label", "--provider", "--trigger", "--timer", "--runs", "--verbose", "--json"],
|