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/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-
|
|
220
|
-
`(≈$${((apiSkippedCrm + apiSkippedSeen) * costPerRecord).toFixed(2)}
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
*
|
|
564
|
-
*
|
|
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
|
|
750
|
-
//
|
|
751
|
-
//
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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(
|
|
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
|
|
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
|
+
}
|