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.
Files changed (66) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +7 -0
  3. package/dist/cli/audit.js +6 -1
  4. package/dist/cli/auth.js +49 -4
  5. package/dist/cli/backfill.js +4 -0
  6. package/dist/cli/call.js +28 -6
  7. package/dist/cli/draft.js +11 -1
  8. package/dist/cli/enrich.js +124 -57
  9. package/dist/cli/fix.js +6 -3
  10. package/dist/cli/help.js +31 -28
  11. package/dist/cli/icp.js +15 -1
  12. package/dist/cli/init.js +3 -3
  13. package/dist/cli/market.js +14 -1
  14. package/dist/cli/planOutput.d.ts +5 -0
  15. package/dist/cli/planOutput.js +27 -0
  16. package/dist/cli/plans.js +184 -33
  17. package/dist/cli/schedule.js +22 -11
  18. package/dist/cli/signals.js +22 -3
  19. package/dist/cli/tam.d.ts +1 -1
  20. package/dist/cli/tam.js +14 -2
  21. package/dist/cli/ui.js +14 -6
  22. package/dist/connectors/clay.d.ts +33 -0
  23. package/dist/connectors/clay.js +123 -0
  24. package/dist/connectors/hubspot.d.ts +4 -0
  25. package/dist/connectors/hubspot.js +36 -18
  26. package/dist/connectors/prospectSources.d.ts +12 -0
  27. package/dist/connectors/prospectSources.js +1 -1
  28. package/dist/contactProviders.d.ts +44 -0
  29. package/dist/contactProviders.js +100 -0
  30. package/dist/enrich.d.ts +7 -1
  31. package/dist/enrich.js +46 -1
  32. package/dist/hostedPatchPlan.js +6 -1
  33. package/dist/icp.d.ts +2 -0
  34. package/dist/icp.js +49 -0
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.js +2 -0
  37. package/dist/init.d.ts +1 -1
  38. package/dist/init.js +1 -1
  39. package/docs/api.md +18 -1
  40. package/package.json +1 -1
  41. package/src/cli/audit.ts +5 -1
  42. package/src/cli/auth.ts +46 -4
  43. package/src/cli/backfill.ts +3 -0
  44. package/src/cli/call.ts +25 -6
  45. package/src/cli/draft.ts +9 -1
  46. package/src/cli/enrich.ts +132 -56
  47. package/src/cli/fix.ts +5 -3
  48. package/src/cli/help.ts +31 -28
  49. package/src/cli/icp.ts +13 -1
  50. package/src/cli/init.ts +3 -3
  51. package/src/cli/market.ts +12 -1
  52. package/src/cli/planOutput.ts +30 -0
  53. package/src/cli/plans.ts +174 -29
  54. package/src/cli/schedule.ts +21 -15
  55. package/src/cli/signals.ts +22 -3
  56. package/src/cli/tam.ts +12 -3
  57. package/src/cli/ui.ts +15 -6
  58. package/src/connectors/clay.ts +155 -0
  59. package/src/connectors/hubspot.ts +41 -17
  60. package/src/connectors/prospectSources.ts +7 -1
  61. package/src/contactProviders.ts +141 -0
  62. package/src/enrich.ts +51 -2
  63. package/src/hostedPatchPlan.ts +6 -1
  64. package/src/icp.ts +51 -0
  65. package/src/index.ts +25 -0
  66. package/src/init.ts +2 -2
package/src/cli/enrich.ts CHANGED
@@ -9,6 +9,8 @@ import { createFilePlanStore } from "../planStore.ts";
9
9
  import { buildAcquirePlan, buildEnrichPlan, createFileEnrichRunStore, DEFAULT_STALE_DAYS, ENRICH_CONFIG_FILE_NAME, builtinAcquirePreset, builtinEnrichPreset, enrichRunId, inferIngestObjectType, latestStamps, loadEnrichConfig, parseCsv, resolveCrmField, selectStaleWork, stagedSourceRecords, staleDaysFor, type EnrichConfig, type EnrichCounts, type EnrichObjectType, type EnrichRun, type EnrichRunStore, type EnrichSourceRecord } from "../enrich.ts";
10
10
  import { loadMeter, remaining, type AcquireRemaining } from "../acquireMeter.ts";
11
11
  import { crmContactKeys, fetchExploriumProspects, fetchPipe0CrustdataProspectPage, partitionFreshProspects, pipe0ResolveCompanyDomains, pipe0ResolveWorkEmails, prospectIdentityKeys, type Prospect } from "../connectors/prospectSources.ts";
12
+ import { createClaySearch, runClayPeopleSearchPage } from "../connectors/clay.ts";
13
+ import { runContactWaterfall, type ContactProviderAdapter, type ContactWaterfallStep } from "../contactProviders.ts";
12
14
  import { loadSeen, recordSeen } from "../acquireSeen.ts";
13
15
  import { acquireCheckpointId, createFileAcquireCheckpointStore, type AcquireCheckpointKey } from "../acquireCheckpoint.ts";
14
16
  import { readHostedAcquireCheckpoint, writeHostedAcquireCheckpoint } from "../hostedAcquireCheckpoint.ts";
@@ -16,7 +18,7 @@ import { uploadHostedPatchPlan } from "../hostedPatchPlan.ts";
16
18
  import { progressReporter, reportCounts, reportEvent } from "../runReport.ts";
17
19
  import { ACQUIRE_STAGES, composeListeners, createProgressEmitter } from "../progress.ts";
18
20
  import { createLinkedInProvider, discoverLinkedInProspects } from "../acquireLinkedIn.ts";
19
- import { fitThreshold, icpToCrustdataFilters, icpToExploriumFilters, scoreProspectAgainstIcp, type Icp } from "../icp.ts";
21
+ import { fitThreshold, icpToClayPeopleFilters, icpToCrustdataFilters, icpToExploriumFilters, scoreProspectAgainstIcp, type Icp } from "../icp.ts";
20
22
  import { apolloPullKeysForAppend, apolloPullKeysForRefresh, createApolloClient, pullApolloRecords, type ApolloPullKey } from "../enrichApollo.ts";
21
23
  import type { CanonicalGtmSnapshot } from "../types.ts";
22
24
  import { isSpoolPath, readSpoolPath } from "../spoolFiles.ts";
@@ -24,6 +26,7 @@ import { isOptionValue, loadIcp, numericOption, option, readSnapshot, saveReques
24
26
  import { providerKey } from "./tam.ts";
25
27
  import { unknownSubcommandError } from "./suggest.ts";
26
28
  import { colorEnabled, createProgressRenderer, createStatusLine, formatBar, formatDuration, paint, type Paint } from "./ui.ts";
29
+ import { compactPlan, verbosePlanRequested } from "./planOutput.ts";
27
30
  import type { AcquireBudget } from "../acquireMeter.ts";
28
31
 
29
32
 
@@ -216,8 +219,8 @@ are phase 2. Recurring execution is the scheduler's job; enrich has no cron.`);
216
219
  // additional per-run ceiling, never a way to exceed the budget.
217
220
  if (apiSkippedCrm || apiSkippedSeen) {
218
221
  console.error(
219
- `Pre-email dedup: skipped ${apiSkippedCrm} already-in-CRM + ${apiSkippedSeen} seen before paying for emails ` +
220
- `(≈$${((apiSkippedCrm + apiSkippedSeen) * costPerRecord).toFixed(2)} enrichment saved).`,
222
+ `Pre-enrichment dedup: skipped ${apiSkippedCrm} already-in-CRM + ${apiSkippedSeen} previously seen ` +
223
+ `(≈$${((apiSkippedCrm + apiSkippedSeen) * costPerRecord).toFixed(2)} downstream spend avoided).`,
221
224
  );
222
225
  }
223
226
  // `cap` is the desired create count bounded by the persistent meter. Raw
@@ -300,20 +303,16 @@ are phase 2. Recurring execution is the scheduler's job; enrich has no cron.`);
300
303
  const meterLine = formatAcquireMeter(headroom, costPerRecord);
301
304
  const gaugeLine = acquireGaugeLine(headroom, config.acquire.budget ?? {}, paint(colorEnabled(process.stdout)));
302
305
  if (!save) {
303
- if (rest.includes("--json")) {
304
- console.log(
305
- JSON.stringify(
306
- { plan: result.plan, counts: result.counts, estCostUsd: result.estCostUsd, meter: headroom },
307
- null,
308
- 2,
309
- ),
310
- );
311
- } else {
312
- console.log(patchPlanToMarkdown(result.plan));
313
- console.log(meterLine);
314
- if (gaugeLine) console.log(gaugeLine);
315
- console.log("\nDry run — nothing written. Re-run with --save to persist the plan, then approve + apply.");
316
- }
306
+ printAcquireOutput({
307
+ args: rest,
308
+ result,
309
+ meter: headroom,
310
+ meterLine,
311
+ gaugeLine,
312
+ saved: false,
313
+ planSaved: false,
314
+ hostedPlanUrl: null,
315
+ });
317
316
  return;
318
317
  }
319
318
 
@@ -362,24 +361,16 @@ are phase 2. Recurring execution is the scheduler's job; enrich has no cron.`);
362
361
  acquireCheckpointSync.hostedVersion,
363
362
  );
364
363
  }
365
- if (planIds.length > 0) {
366
- const hostedRuns = hostedRunsUrl();
367
- console.log(`\nSaved plan ${result.plan.id}`);
368
- console.log(` Leads ${result.counts.created} net-new`);
369
- console.log(` Est. cost $${result.estCostUsd.toFixed(2)}`);
370
- console.log(` Budget ${gaugeLine ?? meterLine}`);
371
- if (hostedPlanUrl) console.log(` Plan ${hostedPlanUrl}`);
372
- if (hostedRuns) console.log(` Observe ${hostedRuns}`);
373
- console.log("\nNext steps");
374
- console.log(` Review fullstackgtm plans show ${result.plan.id}`);
375
- console.log(` Approve fullstackgtm plans approve ${result.plan.id} --operations all`);
376
- console.log(` Apply fullstackgtm apply --plan-id ${result.plan.id} --provider hubspot`);
377
- console.log("\nThe meter is charged only when a create lands at apply.");
378
- } else {
379
- console.log(meterLine);
380
- if (gaugeLine) console.log(gaugeLine);
381
- console.log("No net-new leads to create (everything sourced already matched, or was withheld by the meter).");
382
- }
364
+ printAcquireOutput({
365
+ args: rest,
366
+ result,
367
+ meter: headroom,
368
+ meterLine,
369
+ gaugeLine,
370
+ saved: true,
371
+ planSaved: planIds.length > 0,
372
+ hostedPlanUrl,
373
+ });
383
374
  return;
384
375
  }
385
376
 
@@ -514,7 +505,7 @@ are phase 2. Recurring execution is the scheduler's job; enrich has no cron.`);
514
505
  if (rest.includes("--json")) {
515
506
  console.log(JSON.stringify(result.plan, null, 2));
516
507
  } else {
517
- console.log(patchPlanToMarkdown(result.plan));
508
+ console.log(verbosePlanRequested(rest) ? patchPlanToMarkdown(result.plan) : compactPlan(result.plan));
518
509
  console.log(formatEnrichCounts(result.counts, result.ambiguities.length));
519
510
  console.log("\nDry run — nothing written. Re-run with --save to persist the plan and the run record.");
520
511
  }
@@ -560,8 +551,9 @@ function formatEnrichCounts(counts: EnrichCounts, ambiguities: number) {
560
551
  /**
561
552
  * Pull net-new prospects from an API acquire source into source records the
562
553
  * acquire builder dedupes + turns into create_record ops. Explorium discovers;
563
- * pipe0 (when resolveEmailsWith=pipe0) fills real work emails. Only rows that
564
- * carry the dedupe key (email) survive you cannot resolve-first without it.
554
+ * A field-level provider waterfall fills missing contact data. Existing
555
+ * email-keyed and resolveEmailsWith=pipe0 configs translate to an implicit
556
+ * Pipe0 work-email step. Only rows that carry the configured dedupe key survive.
565
557
  */
566
558
  async function acquireFromApi(
567
559
  config: EnrichConfig,
@@ -601,7 +593,9 @@ async function acquireFromApi(
601
593
  ? (icp ? icpToExploriumFilters(icp) : (disc.filters ?? {}))
602
594
  : disc.provider === "pipe0"
603
595
  ? (icp ? icpToCrustdataFilters(icp) : (disc.filters ?? {}))
604
- : {};
596
+ : disc.provider === "clay"
597
+ ? (icp ? icpToClayPeopleFilters(icp) : (disc.filters ?? {}))
598
+ : {};
605
599
  const queryFingerprint = createHash("sha256")
606
600
  .update(JSON.stringify({ source, provider: disc.provider, listId, filters, icp: icp ?? null }))
607
601
  .digest("hex");
@@ -676,6 +670,19 @@ async function acquireFromApi(
676
670
  page = result.prospects;
677
671
  cursor = result.nextCursor;
678
672
  exhausted = result.nextCursor === null;
673
+ } else if (disc.provider === "clay") {
674
+ if (!cursor) {
675
+ progress.note("creating Clay people-search iterator");
676
+ cursor = await createClaySearch({
677
+ apiKey: providerKey("clay"), sourceType: "people", filters,
678
+ });
679
+ }
680
+ const result = await runClayPeopleSearchPage({
681
+ apiKey: providerKey("clay"), searchId: cursor, limit: requestSize,
682
+ });
683
+ page = result.prospects;
684
+ offset += page.length;
685
+ exhausted = !result.hasMore;
679
686
  } else if (disc.provider === "explorium") {
680
687
  const pageNumber = offset + 1;
681
688
  exploriumPage = pageNumber;
@@ -694,7 +701,7 @@ async function acquireFromApi(
694
701
  offset += page.length;
695
702
  exhausted = page.length < requestSize;
696
703
  } else {
697
- throw new Error(`enrich acquire: unknown discovery provider "${disc.provider}" (explorium | pipe0 | linkedin).`);
704
+ throw new Error(`enrich acquire: unknown discovery provider "${disc.provider}" (clay | explorium | pipe0 | linkedin).`);
698
705
  }
699
706
  discovered += page.length;
700
707
  progress.items(discovered, scanLimit);
@@ -746,23 +753,37 @@ async function acquireFromApi(
746
753
  progress.stage(ACQUIRE_STAGES[1], 1, ACQUIRE_STAGES.length);
747
754
  progress.note("checking work-email requirements");
748
755
 
749
- // 3. Resolve real work emails. Triggered either when email IS the dedupe key
750
- // (Explorium's email is hashed, Crustdata returns none) or when a source
751
- // that keys on something else opts in via `resolveEmailsWith: "pipe0"`
752
- // (e.g. LinkedIn keys on the profile URL but still wants outreach emails).
753
- // pipe0 waterfall, chunked; resolves from name + company domain/name.
754
- if (matchKey === "email" || disc.resolveEmailsWith === "pipe0") {
755
- // The waterfall needs a company DOMAIN; LinkedIn/HeyReach lists carry only
756
- // names. Resolve domains first (pipe0 company:identity) so resolution can
757
- // actually land — without it, name-only resolution fails for every lead.
758
- if (prospects.some((p) => !p.companyDomain && p.companyName)) {
759
- progress.note(`resolving company domains for ${prospects.length} candidate(s)`);
760
- const resolved = await pipe0ResolveCompanyDomains({ apiKey: providerKey("pipe0"), prospects });
761
- prospects.splice(0, prospects.length, ...resolved);
756
+ // 3. Resolve contact fields through the ordered, fill-only waterfall. Keep
757
+ // legacy behavior: email-keyed acquisition and resolveEmailsWith=pipe0 imply
758
+ // a Pipe0 work-email step when no explicit waterfall is configured.
759
+ const implicitPipe0 = matchKey === "email" || disc.resolveEmailsWith === "pipe0";
760
+ const contactWaterfall: ContactWaterfallStep[] = disc.contactWaterfall ?? (
761
+ implicitPipe0 ? [{ provider: "pipe0", fields: ["work_email"] }] : []
762
+ );
763
+ if (contactWaterfall.length > 0) {
764
+ const adapters: Record<string, ContactProviderAdapter> = {
765
+ pipe0: {
766
+ provider: "pipe0",
767
+ resolve: async (candidates, fields) => {
768
+ let resolved = candidates;
769
+ if (fields.includes("work_email") && resolved.some((p) => !p.companyDomain && p.companyName)) {
770
+ progress.note(`resolving company domains for ${resolved.length} candidate(s)`);
771
+ resolved = await pipe0ResolveCompanyDomains({ apiKey: providerKey("pipe0"), prospects: resolved });
772
+ }
773
+ if (fields.includes("work_email")) {
774
+ progress.note(`resolving work emails with pipe0 for ${resolved.length} candidate(s)`);
775
+ resolved = await pipe0ResolveWorkEmails({ apiKey: providerKey("pipe0"), prospects: resolved });
776
+ }
777
+ return resolved;
778
+ },
779
+ },
780
+ };
781
+ const result = await runContactWaterfall({ prospects, steps: contactWaterfall, adapters });
782
+ prospects.splice(0, prospects.length, ...result.prospects);
783
+ for (const attempt of result.attempts) {
784
+ const added = Object.entries(attempt.added).map(([field, count]) => `${count} ${field}`).join(", ") || "0 fields";
785
+ progress.note(`${attempt.provider}: ${attempt.attempted} attempted · ${added} added`);
762
786
  }
763
- progress.note(`resolving work emails for ${prospects.length} candidate(s)`);
764
- const resolved = await pipe0ResolveWorkEmails({ apiKey: providerKey("pipe0"), prospects });
765
- prospects.splice(0, prospects.length, ...resolved);
766
787
  }
767
788
  progress.items(prospects.length, prospects.length);
768
789
  progress.note(`${prospects.length} candidate(s) ready for planning`);
@@ -875,6 +896,61 @@ function formatAcquireMeter(headroom: AcquireRemaining, costPerRecord: number):
875
896
  );
876
897
  }
877
898
 
899
+ function printAcquireOutput(options: {
900
+ args: readonly string[];
901
+ result: ReturnType<typeof buildAcquirePlan>;
902
+ meter: AcquireRemaining;
903
+ meterLine: string;
904
+ gaugeLine: string | null;
905
+ /** The acquisition run/checkpoint was persisted. */
906
+ saved: boolean;
907
+ /** A non-empty plan was persisted to the plan store. */
908
+ planSaved: boolean;
909
+ hostedPlanUrl: string | null;
910
+ }): void {
911
+ const { args, result, meter, meterLine, gaugeLine, saved, planSaved, hostedPlanUrl } = options;
912
+ if (args.includes("--json")) {
913
+ console.log(JSON.stringify({
914
+ plan: result.plan,
915
+ counts: result.counts,
916
+ estCostUsd: result.estCostUsd,
917
+ meter,
918
+ persistence: {
919
+ runSaved: saved,
920
+ planSaved,
921
+ planId: planSaved ? result.plan.id : null,
922
+ hostedPlanUrl,
923
+ },
924
+ }, null, 2));
925
+ return;
926
+ }
927
+
928
+ if (verbosePlanRequested(args)) {
929
+ console.log(patchPlanToMarkdown(result.plan));
930
+ console.log(`\nAcquisition`);
931
+ console.log(` Leads ${result.counts.created} net-new · ${result.counts.unassigned} unassigned`);
932
+ console.log(` Est. cost $${result.estCostUsd.toFixed(2)}`);
933
+ console.log(` Budget ${gaugeLine ?? meterLine}`);
934
+ if (hostedPlanUrl) console.log(` Plan ${hostedPlanUrl}`);
935
+ } else if (planSaved || !saved) {
936
+ console.log(compactPlan(result.plan, { saved: planSaved }));
937
+ console.log(meterLine);
938
+ if (gaugeLine) console.log(gaugeLine);
939
+ } else {
940
+ console.log(meterLine);
941
+ if (gaugeLine) console.log(gaugeLine);
942
+ console.log("No net-new leads to create (everything sourced already matched, or was withheld by the meter).");
943
+ }
944
+
945
+ if (planSaved) {
946
+ console.log(`\nApprove fullstackgtm plans approve ${result.plan.id} --operations all`);
947
+ console.log(`Apply fullstackgtm apply --plan-id ${result.plan.id} --provider <hubspot|salesforce>`);
948
+ const hostedRuns = hostedRunsUrl();
949
+ if (hostedRuns) console.log(`Observe ${hostedRuns}`);
950
+ console.log("The meter is charged only when a create lands at apply.");
951
+ }
952
+ }
953
+
878
954
  function hostedRunsUrl(): string | null {
879
955
  const baseUrl = getCredential("broker")?.baseUrl?.replace(/\/+$/, "");
880
956
  return baseUrl ? `${baseUrl}/dashboard/runs` : null;
package/src/cli/fix.ts CHANGED
@@ -22,6 +22,7 @@ import { progressReporter } from "../runReport.ts";
22
22
  import type { PatchPlan, PatchPlanRun } from "../types.ts";
23
23
  import { confirmRequested, connectorFor, isOptionValue, numericOption, option, readSnapshot, repeatedOption, saveRequested } from "./shared.ts";
24
24
  import { box, colorEnabled, createProgressRenderer, paint } from "./ui.ts";
25
+ import { compactPlan, verbosePlanRequested } from "./planOutput.ts";
25
26
 
26
27
 
27
28
  /**
@@ -107,8 +108,10 @@ async function emitPlan(plan: PatchPlan, args: string[]) {
107
108
  }
108
109
  if (args.includes("--json")) {
109
110
  console.log(JSON.stringify(plan, null, 2));
110
- } else {
111
+ } else if (verbosePlanRequested(args)) {
111
112
  console.log(patchPlanToMarkdown(plan));
113
+ } else {
114
+ console.log(compactPlan(plan, { saved: saveRequested(args) }));
112
115
  }
113
116
  }
114
117
 
@@ -181,8 +184,7 @@ export async function reassignCommand(args: string[]) {
181
184
  const store = saveRequested(args) ? createFilePlanStore() : null;
182
185
  for (const plan of plans) {
183
186
  if (store) await store.save(plan);
184
- console.log(`${plan.id} ${String(plan.operations.length).padStart(3)} operation(s) ${plan.title}`);
185
- console.log(` ${plan.summary}`);
187
+ console.log(verbosePlanRequested(args) ? patchPlanToMarkdown(plan) : compactPlan(plan, { saved: Boolean(store) }));
186
188
  }
187
189
  if (store) {
188
190
  console.log(
package/src/cli/help.ts CHANGED
@@ -20,13 +20,16 @@ Usage:
20
20
  fullstackgtm login salesforce --instance-url <url> [--no-validate] advanced: paste access token
21
21
  fullstackgtm login stripe [--no-validate]
22
22
  fullstackgtm login anthropic | openai store an LLM API key for call parse/score
23
- fullstackgtm login apollo store an Apollo API key for enrich pulls\n fullstackgtm login pipe0 | explorium | theirstack store a discovery-provider key (theirstack = technographic TAM)\n fullstackgtm login heyreach store a HeyReach key for enrich acquire --source linkedin\n fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|pipe0|explorium|heyreach|broker>
23
+ fullstackgtm login apollo | clay store a data-provider Public API key
24
+ fullstackgtm login pipe0 | explorium | theirstack store a discovery-provider key (theirstack = technographic TAM)
25
+ fullstackgtm login heyreach store a HeyReach key for enrich acquire --source linkedin
26
+ fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|clay|pipe0|explorium|heyreach|broker>
24
27
 
25
28
  Secrets (tokens, client secrets) are NEVER passed as flags — they leak via
26
29
  the process list and shell history. Pipe them on stdin or enter them at the
27
30
  interactive prompt:
28
31
  echo "$HUBSPOT_TOKEN" | fullstackgtm login hubspot --private-token
29
- fullstackgtm init [--source pipe0|explorium|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]
32
+ fullstackgtm init [--source pipe0|explorium|clay|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]
30
33
  cold start: scaffold icp.json + enrich.config.json + a
31
34
  PLAYBOOK wired for this workspace (the CLI ships primitives,
32
35
  your agent is the orchestrator — see docs/recipes.md)
@@ -76,7 +79,7 @@ Usage:
76
79
  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]
77
80
  fullstackgtm tam status [--name <n>] <source options> [--save] [--json]
78
81
  fullstackgtm tam report [--name <n>] [--out <path>]
79
- fullstackgtm tam populate [--name <n>] --cron "<expr>" [--source pipe0|explorium|linkedin] [--provider hubspot|salesforce] [--label <l>]
82
+ fullstackgtm tam populate [--name <n>] --cron "<expr>" [--source pipe0|explorium|clay|linkedin] [--provider hubspot|salesforce] [--label <l>]
80
83
  size the reachable market FROM the ICP (a real account count ×
81
84
  ACV; buyers/account = the contact population target), then fill
82
85
  it: populate schedules plan-only enrich acquire --save (apply
@@ -180,11 +183,11 @@ Usage:
180
183
  fullstackgtm suggest --plan-id <id> | --plan <path> [source options] [--json] [--out <path>]
181
184
  derive values for requires_human_* placeholders
182
185
  from snapshot evidence, with confidence + reasons
183
- fullstackgtm plans list [--status <s>] | show <id> | sync | reject <id>
186
+ fullstackgtm plans list [--status <s>] | show <id> [--verbose] | sync | reject <id>
184
187
  fullstackgtm plans approve <id> --operations <ids|all> [--value <opId>=<v>]
185
188
  fullstackgtm plans approve <id> --values-from <suggestions.json> [--min-confidence high|low] [--include-creates]
186
189
  fullstackgtm plans recover <id> --acknowledge-uncertain-writes
187
- fullstackgtm apply --plan-id <id> --provider <name>
190
+ fullstackgtm apply --plan-id <id> --provider <name> [--verbose|--json]
188
191
  fullstackgtm apply --plan-id <id> --channel outbox (render approved openers; transmits nothing)
189
192
  fullstackgtm apply --plan <path> --provider <name> --approve <ids|all> [options]
190
193
  fullstackgtm audit-log export [--out <path>] | verify --in <path> tamper-evident apply-run record
@@ -298,7 +301,7 @@ export const HELP: Record<string, HelpEntry> = {
298
301
  summary: "scaffold a workspace (icp.json + enrich.config.json + PLAYBOOK)",
299
302
  phase: "Setup",
300
303
  synopsis: [
301
- "fullstackgtm init [--source pipe0|explorium|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]",
304
+ "fullstackgtm init [--source pipe0|explorium|clay|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]",
302
305
  ],
303
306
  detail:
304
307
  "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).",
@@ -312,7 +315,7 @@ export const HELP: Record<string, HelpEntry> = {
312
315
  "fullstackgtm login hubspot | salesforce hosted browser OAuth (default)",
313
316
  "fullstackgtm login --via <hosted url> pair with a team deployment",
314
317
  "fullstackgtm login stripe",
315
- "fullstackgtm login anthropic | openai | apollo",
318
+ "fullstackgtm login anthropic | openai | apollo | clay",
316
319
  ],
317
320
  detail:
318
321
  "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`.",
@@ -321,7 +324,7 @@ export const HELP: Record<string, HelpEntry> = {
321
324
  logout: {
322
325
  summary: "remove stored credentials for a provider",
323
326
  phase: "Setup",
324
- synopsis: ["fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|pipe0|explorium|broker>"],
327
+ synopsis: ["fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|clay|pipe0|explorium|broker>"],
325
328
  seeAlso: ["login", "doctor"],
326
329
  },
327
330
  doctor: {
@@ -550,7 +553,7 @@ export const HELP: Record<string, HelpEntry> = {
550
553
  summary: "replicated plan lifecycle: list / show / sync / approve / reject",
551
554
  phase: "Govern",
552
555
  synopsis: [
553
- "fullstackgtm plans list [--status <s>] | show <id> | sync | reject <id>",
556
+ "fullstackgtm plans list [--status <s>] | show <id> [--verbose] | sync | reject <id>",
554
557
  "fullstackgtm plans approve <id> --operations <ids|all> [--value <opId>=<v>]",
555
558
  "fullstackgtm plans approve <id> --values-from <suggestions.json> [--min-confidence high|low]",
556
559
  "fullstackgtm plans recover <id> --acknowledge-uncertain-writes",
@@ -563,12 +566,12 @@ export const HELP: Record<string, HelpEntry> = {
563
566
  summary: "write ONLY explicitly approved operations to a provider",
564
567
  phase: "Govern / Verify",
565
568
  synopsis: [
566
- "fullstackgtm apply --plan-id <id> --provider <name>",
569
+ "fullstackgtm apply --plan-id <id> --provider <name> [--verbose|--json]",
567
570
  "fullstackgtm apply --plan-id <id> --channel outbox (render approved drafted openers to the outbox; transmits nothing)",
568
571
  "fullstackgtm apply --plan <path> --provider <name> --approve <ids|all> [--value <opId>=<v>]",
569
572
  ],
570
573
  detail:
571
- "The CLI CRM-write verb. Writes only operations approved locally or imported from the shared hosted replica, with an online execution claim when available, stable operation ids, compare-and-set, resolve-before-create, and readback. Hosted may execute the same synchronized plan when it has a compatible CRM connection; its exact operation receipts are imported on the next CLI check-in. Never writes requires_human_* placeholders without a --value override.",
574
+ "The CLI CRM-write verb. Writes only operations approved locally or imported from the shared hosted replica, with an online execution claim when available, stable operation ids, compare-and-set, resolve-before-create, and readback. Default output is a compact outcome card; --verbose prints the full run document and --json preserves structured output. Hosted may execute the same synchronized plan when it has a compatible CRM connection; its exact operation receipts are imported on the next CLI check-in. Never writes requires_human_* placeholders without a --value override.",
572
575
  seeAlso: ["plans", "suggest", "audit-log"],
573
576
  },
574
577
  "audit-log": {
@@ -658,7 +661,7 @@ export const BESPOKE_HELP = ["init", "call", "market", "tam", "enrich", "schedul
658
661
  export const GLOBAL_FLAGS = ["--help", "--full"];
659
662
  export const GLOBAL_SHORT_FLAGS = ["-h"];
660
663
  export const SOURCE_FLAGS = ["--provider", "--token-env", "--input", "--demo", "--sample", "--seed", "--today"];
661
- export const AUDIT_FLAGS = ["--config", "--allow-plugins", "--no-plugins", "--rules", "--stale-days", "--fail-on", "--save", "--dry-run", "--json", "--out", "--full"];
664
+ export const AUDIT_FLAGS = ["--config", "--allow-plugins", "--no-plugins", "--rules", "--stale-days", "--fail-on", "--save", "--dry-run", "--json", "--out", "--full", "--verbose"];
662
665
 
663
666
  // Complete per-command flag registry used by runCli's fail-closed flag
664
667
  // validation. Keep this next to HELP so focused help, machine capabilities,
@@ -667,7 +670,7 @@ export const COMMAND_FLAGS: Record<string, string[]> = {
667
670
  init: ["--source", "--provider", "--out", "--force"],
668
671
  login: ["--via", "--hosted", "--private-token", "--token", "--key", "--api-key", "--client-id", "--client-secret", "--scopes", "--port", "--oauth", "--device", "--instance-url", "--login-url", "--no-validate"],
669
672
  logout: [],
670
- doctor: ["--json"],
673
+ doctor: ["--verbose", "--json"],
671
674
  capabilities: ["--json"],
672
675
  "robot-docs": [],
673
676
  profiles: ["--json"],
@@ -681,24 +684,24 @@ export const COMMAND_FLAGS: Record<string, string[]> = {
681
684
  hierarchy: [...SOURCE_FLAGS, "--json", "--out"],
682
685
  relationships: [...SOURCE_FLAGS, "--account-id", "--domain", "--json", "--out"],
683
686
  route: [...SOURCE_FLAGS, "--match", "--no-inherit-owner", "--reassign-owned", "--policy", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
684
- fix: [...SOURCE_FLAGS, "--config", "--allow-plugins", "--no-plugins", "--rule", "--provider", "--min-confidence", "--include-creates", "--yes", "--confirm", "--dry-run"],
685
- "bulk-update": [...SOURCE_FLAGS, "--where", "--set", "--guard", "--require", "--create-task", "--archive", "--force-archive-duplicates", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
686
- dedupe: [...SOURCE_FLAGS, "--key", "--keep", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
687
- reassign: [...SOURCE_FLAGS, "--from", "--assign-unowned", "--to", "--objects", "--where", "--except-deal-stage", "--include-closed-deals", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
688
- backfill: [...SOURCE_FLAGS, "--since", "--pipeline", "--match-property", "--skip-unmatched", "--save", "--dry-run", "--json"],
689
- enrich: [...SOURCE_FLAGS, "--config", "--source", "--icp", "--input", "--provider", "--list", "--stale-days", "--assign-owner", "--objects", "--max", "--scan-limit", "--staged-run", "--run-label", "--label", "--runs", "--save", "--dry-run", "--json", "--out"],
690
- call: [...SOURCE_FLAGS, "--transcript", "--title", "--source", "--model", "--deterministic", "--heuristics", "--llm", "--json", "--ndjson", "--out", "--call", "--call-type", "--rubric", "--list", "--list-rubrics", "--attendees", "--domain", "--deal", "--save", "--dry-run"],
687
+ fix: [...SOURCE_FLAGS, "--config", "--allow-plugins", "--no-plugins", "--rule", "--provider", "--min-confidence", "--include-creates", "--yes", "--confirm", "--dry-run", "--verbose"],
688
+ "bulk-update": [...SOURCE_FLAGS, "--where", "--set", "--guard", "--require", "--create-task", "--archive", "--force-archive-duplicates", "--reason", "--max-operations", "--save", "--dry-run", "--verbose", "--json", "--out"],
689
+ dedupe: [...SOURCE_FLAGS, "--key", "--keep", "--reason", "--max-operations", "--save", "--dry-run", "--verbose", "--json", "--out"],
690
+ reassign: [...SOURCE_FLAGS, "--from", "--assign-unowned", "--to", "--objects", "--where", "--except-deal-stage", "--include-closed-deals", "--reason", "--max-operations", "--save", "--dry-run", "--verbose", "--json", "--out"],
691
+ backfill: [...SOURCE_FLAGS, "--since", "--pipeline", "--match-property", "--skip-unmatched", "--save", "--dry-run", "--verbose", "--json"],
692
+ enrich: [...SOURCE_FLAGS, "--config", "--source", "--icp", "--input", "--provider", "--list", "--stale-days", "--assign-owner", "--objects", "--max", "--scan-limit", "--staged-run", "--run-label", "--label", "--runs", "--save", "--dry-run", "--verbose", "--json", "--out"],
693
+ call: [...SOURCE_FLAGS, "--transcript", "--title", "--source", "--model", "--deterministic", "--heuristics", "--llm", "--verbose", "--json", "--ndjson", "--out", "--call", "--call-type", "--rubric", "--list", "--list-rubrics", "--attendees", "--domain", "--deal", "--save", "--dry-run"],
691
694
  suggest: [...SOURCE_FLAGS, "--plan-id", "--plan", "--json", "--out"],
692
- plans: ["--status", "--operations", "--value", "--values-from", "--min-confidence", "--include-creates", "--acknowledge-uncertain-writes", "--json"],
693
- apply: ["--plan", "--plan-id", "--provider", "--channel", "--token-env", "--approve", "--value", "--json", "--config"],
695
+ plans: ["--status", "--operations", "--value", "--values-from", "--min-confidence", "--include-creates", "--acknowledge-uncertain-writes", "--verbose", "--json"],
696
+ apply: ["--plan", "--plan-id", "--provider", "--channel", "--token-env", "--approve", "--value", "--verbose", "--json", "--config"],
694
697
  "audit-log": ["--in", "--out", "--json"],
695
698
  merge: ["--input", "--out", "--json"],
696
- 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", "--json", "--out"],
697
- 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", "--json", "--out"],
698
- icp: [...SOURCE_FLAGS, "--name", "--signals-from", "--with-history", "--prompt", "--min-score", "--from-judge", "--golden", "--against-outcomes", "--min-accuracy", "--model", "--label", "--save", "--json", "--out"],
699
- signals: ["--bucket", "--source", "--connector", "--connector-opt", "--watchlist", "--keywords", "--from", "--label", "--since", "--account", "--contact", "--unjudged", "--touch", "--result", "--save", "--json", "--explain"],
700
- draft: ["--from-judge", "--min-score", "--prompt", "--model", "--channel", "--save", "--dry-run", "--json", "--out"],
701
- schedule: ["--cron", "--label", "--provider", "--trigger", "--timer", "--runs", "--json"],
699
+ 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"],
700
+ 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"],
701
+ icp: [...SOURCE_FLAGS, "--name", "--signals-from", "--with-history", "--prompt", "--min-score", "--from-judge", "--golden", "--against-outcomes", "--min-accuracy", "--model", "--label", "--save", "--verbose", "--json", "--out"],
702
+ signals: ["--bucket", "--source", "--connector", "--connector-opt", "--watchlist", "--keywords", "--from", "--label", "--since", "--account", "--contact", "--unjudged", "--touch", "--result", "--save", "--verbose", "--json", "--explain"],
703
+ draft: ["--from-judge", "--min-score", "--prompt", "--model", "--channel", "--save", "--dry-run", "--verbose", "--json", "--out"],
704
+ schedule: ["--cron", "--label", "--provider", "--trigger", "--timer", "--runs", "--verbose", "--json"],
702
705
  };
703
706
 
704
707
  export const FLAGS_WITH_VALUES = new Set([
package/src/cli/icp.ts CHANGED
@@ -12,6 +12,18 @@ import { loadIcp, numericOption, option, readSnapshot, resolveLlmBaseUrls, saveR
12
12
  import { createStatusLine } from "./ui.ts";
13
13
  import { unknownSubcommandError } from "./suggest.ts";
14
14
 
15
+ function renderJudgeDecisions(decisions: Awaited<ReturnType<typeof judgeSignals>>): string {
16
+ if (decisions.length === 0) return "No accounts cleared the score threshold.";
17
+ const lines = [`ICP decisions (${decisions.length})`, ""];
18
+ for (const decision of decisions) {
19
+ const target = decision.contact?.email || decision.contact?.title;
20
+ lines.push(`${decision.decision.toUpperCase().padEnd(7)} ${String(decision.score).padStart(3)} ${decision.accountDomain}${target ? ` · ${target}` : ""}`);
21
+ if (decision.whyNow) lines.push(` ${decision.whyNow.replace(/\s+/g, " ").trim()}`);
22
+ if (decision.play) lines.push(` Next: ${decision.play.replace(/\s+/g, " ").trim()}`);
23
+ }
24
+ return lines.join("\n");
25
+ }
26
+
15
27
 
16
28
  /**
17
29
  * `icp` — develop and inspect the Ideal Customer Profile that targets acquire.
@@ -158,7 +170,7 @@ ops. Develop one by interview, then \`enrich acquire\` picks up ./icp.json.
158
170
  decisions = decisions.filter((d) => d.score >= minScore);
159
171
 
160
172
  // 5) Ranked decisions to stdout (JSON), guidance to stderr.
161
- console.log(JSON.stringify(decisions, null, 2));
173
+ console.log(rest.includes("--json") || rest.includes("--verbose") ? JSON.stringify(decisions, null, 2) : renderJudgeDecisions(decisions));
162
174
  console.error(
163
175
  `Judged ${unjudged.length} signal(s) across ${new Set(decisions.map((d) => d.accountDomain)).size} account(s): ` +
164
176
  `${decisions.filter((d) => d.decision === "send").length} send, ` +
package/src/cli/init.ts CHANGED
@@ -16,7 +16,7 @@ import { option } from "./shared.ts";
16
16
  export function initCommand(args: string[]) {
17
17
  if (args.includes("--help") || args.includes("-h")) {
18
18
  console.log(`Usage:
19
- fullstackgtm init [--source pipe0|explorium|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]
19
+ fullstackgtm init [--source pipe0|explorium|clay|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]
20
20
 
21
21
  Scaffolds a workspace so the first acquire/signals/judge/draft commands work:
22
22
  icp.json starter ICP (edit, or rebuild with \`icp interview\`)
@@ -28,8 +28,8 @@ docs/recipes.md for the full play set. Existing files are kept unless --force.`)
28
28
  return;
29
29
  }
30
30
  const source = (option(args, "--source") ?? "pipe0") as InitSource;
31
- if (!["pipe0", "explorium", "linkedin"].includes(source)) {
32
- throw new Error(`init: --source must be pipe0|explorium|linkedin (got "${source}")`);
31
+ if (!["pipe0", "explorium", "clay", "linkedin"].includes(source)) {
32
+ throw new Error(`init: --source must be pipe0|explorium|clay|linkedin (got "${source}")`);
33
33
  }
34
34
  const provider = (option(args, "--provider") ?? "hubspot") as InitProvider;
35
35
  if (!["hubspot", "salesforce"].includes(provider)) {
package/src/cli/market.ts CHANGED
@@ -499,8 +499,19 @@ Rules:
499
499
  if (outPath) {
500
500
  writeFileSync(resolve(process.cwd(), outPath), output);
501
501
  console.log(`Wrote ${outPath}`);
502
- } else {
502
+ } else if (rest.includes("--verbose") || option(rest, "--format")) {
503
503
  console.log(output);
504
+ } else {
505
+ const fronts = computeFrontStates(config, set);
506
+ const counts = new Map<string, number>();
507
+ for (const front of fronts) counts.set(front.state, (counts.get(front.state) ?? 0) + 1);
508
+ console.log(`${config.category} market · ${set.runLabel}`);
509
+ console.log(`${fronts.length} claim fronts · ${["open", "contested", "owned", "saturated", "vacant"].filter((state) => counts.has(state)).map((state) => `${counts.get(state)} ${state}`).join(" · ")}`);
510
+ for (const front of fronts) {
511
+ const owner = front.loudVendorIds.length ? ` · ${front.loudVendorIds.join(", ")}` : "";
512
+ console.log(`\n${front.state.toUpperCase()} ${front.claimId}${owner}`);
513
+ }
514
+ console.log("\nUse --verbose for the full evidence report, or --out <path> to write it.");
504
515
  }
505
516
  return;
506
517
  }
@@ -0,0 +1,30 @@
1
+ import type { PatchPlan } from "../types.ts";
2
+ import { box, colorEnabled, paint, truncateToWidth } from "./ui.ts";
3
+
4
+ export function compactPlan(plan: PatchPlan, options: { saved?: boolean } = {}): string {
5
+ const counts = new Map<string, number>();
6
+ for (const operation of plan.operations) {
7
+ const action = operation.operation || operation.field || "change";
8
+ counts.set(action, (counts.get(action) ?? 0) + 1);
9
+ }
10
+ const mix = [...counts]
11
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
12
+ .slice(0, 4)
13
+ .map(([action, count]) => `${count} ${action}`)
14
+ .join(" · ");
15
+ const lines = [
16
+ truncateToWidth(plan.title, 100),
17
+ `Status ${options.saved ? "SAVED · NEEDS APPROVAL" : "DRY RUN · NOTHING WRITTEN"}`,
18
+ truncateToWidth(`Scope ${plan.operations.length} operation${plan.operations.length === 1 ? "" : "s"}${mix ? ` · ${mix}` : ""}`, 100),
19
+ truncateToWidth(`Effect ${plan.summary}`, 100),
20
+ options.saved ? `Next fullstackgtm plans show ${plan.id}` : "Next re-run with --save to stage this plan for approval",
21
+ ];
22
+ return [
23
+ ...box(lines, paint(colorEnabled(process.stdout)), "Plan preview"),
24
+ `Details: re-run with --verbose${options.saved ? `; JSON: fullstackgtm plans show ${plan.id} --json` : "; machine output: --json"}`,
25
+ ].join("\n");
26
+ }
27
+
28
+ export function verbosePlanRequested(args: readonly string[]) {
29
+ return args.includes("--verbose") || args.includes("--full");
30
+ }