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/src/cli/auth.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { activeProfile, credentialsPath, DEFAULT_PROFILE, deleteCredential, getC
|
|
|
8
8
|
import { activeWorkspaceProfile, readHealthTimeline, summarizeHealth } from "../health.ts";
|
|
9
9
|
import { resolveLlmCredential, validateLlmKey } from "../llm.ts";
|
|
10
10
|
import { createFilePlanStore, type StoredPlan } from "../planStore.ts";
|
|
11
|
+
import { validateClayApiKey } from "../connectors/clay.ts";
|
|
11
12
|
import { isOptionValue, numericOption, option, readPackageInfo, readSecret } from "./shared.ts";
|
|
12
13
|
import { box, colorEnabled, paint, scoreColor, sparkline } from "./ui.ts";
|
|
13
14
|
|
|
@@ -346,18 +347,24 @@ export async function login(args: string[]) {
|
|
|
346
347
|
console.log(`Logged in to Stripe. Credentials stored in ${credentialsPath()}.`);
|
|
347
348
|
return;
|
|
348
349
|
}
|
|
349
|
-
if (provider === "anthropic" || provider === "openai") {
|
|
350
|
+
if (provider === "anthropic" || provider === "openai" || provider === "openrouter") {
|
|
350
351
|
rejectArgvSecret(args, "--token", "--key", "--api-key");
|
|
351
|
-
const key = await readSecret(`${provider} API key (${provider === "anthropic" ? "sk-ant-..." : "sk-..."})`);
|
|
352
|
+
const key = await readSecret(`${provider} API key (${provider === "anthropic" ? "sk-ant-..." : provider === "openrouter" ? "sk-or-..." : "sk-..."})`);
|
|
352
353
|
if (!key) throw new Error(`No ${provider} key provided.`);
|
|
353
354
|
if (!args.includes("--no-validate")) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
355
|
+
if (provider === "openrouter") {
|
|
356
|
+
const response = await fetch("https://openrouter.ai/api/v1/key", { headers: { Authorization: `Bearer ${key}` } });
|
|
357
|
+
if (!response.ok) throw new Error(`openrouter rejected the key: ${safeStatus(response)}`);
|
|
358
|
+
console.log("Key accepted by OpenRouter.");
|
|
359
|
+
} else {
|
|
360
|
+
const validation = await validateLlmKey(provider, key);
|
|
361
|
+
if (!validation.ok) throw new Error(`${provider} rejected the key: ${validation.detail}`);
|
|
362
|
+
console.log(validation.detail);
|
|
363
|
+
}
|
|
357
364
|
}
|
|
358
365
|
const stamp = new Date().toISOString();
|
|
359
366
|
storeCredential(provider, { kind: "api_key", accessToken: key, createdAt: stamp, updatedAt: stamp });
|
|
360
|
-
console.log(`Stored ${provider} API key in ${credentialsPath()}. \`fullstackgtm
|
|
367
|
+
console.log(`Stored ${provider} API key in ${credentialsPath()}. \`fullstackgtm icp derive\` uses it automatically${provider === "openrouter" ? "." : "; call parse/score use it too."}`);
|
|
361
368
|
return;
|
|
362
369
|
}
|
|
363
370
|
if (provider === "apollo") {
|
|
@@ -378,6 +385,20 @@ export async function login(args: string[]) {
|
|
|
378
385
|
console.log(`Stored Apollo API key in ${credentialsPath()}. \`fullstackgtm enrich append|refresh\` use it automatically.`);
|
|
379
386
|
return;
|
|
380
387
|
}
|
|
388
|
+
if (provider === "clay") {
|
|
389
|
+
rejectArgvSecret(args, "--token", "--key", "--api-key");
|
|
390
|
+
const key = await readSecret("Clay Public API key");
|
|
391
|
+
if (!key) throw new Error("No Clay Public API key provided.");
|
|
392
|
+
if (!args.includes("--no-validate")) {
|
|
393
|
+
const validation = await validateClayApiKey(key);
|
|
394
|
+
if (!validation.ok) throw new Error(validation.detail);
|
|
395
|
+
console.log(validation.detail);
|
|
396
|
+
}
|
|
397
|
+
const stamp = new Date().toISOString();
|
|
398
|
+
storeCredential("clay", { kind: "api_key", accessToken: key, createdAt: stamp, updatedAt: stamp });
|
|
399
|
+
console.log(`Stored Clay Public API key in ${credentialsPath()}. Clay connector commands resolve it automatically.`);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
381
402
|
if (provider === "pipe0" || provider === "explorium" || provider === "heyreach" || provider === "theirstack") {
|
|
382
403
|
rejectArgvSecret(args, "--token", "--key", "--api-key");
|
|
383
404
|
const key = await readSecret(`${provider} API key`);
|
|
@@ -393,7 +414,7 @@ export async function login(args: string[]) {
|
|
|
393
414
|
}
|
|
394
415
|
if (provider !== "hubspot") {
|
|
395
416
|
throw new Error(
|
|
396
|
-
"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",
|
|
417
|
+
"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",
|
|
397
418
|
);
|
|
398
419
|
}
|
|
399
420
|
const now = new Date().toISOString();
|
|
@@ -491,6 +512,12 @@ export function doctorReport(env: Record<string, string | undefined> = process.e
|
|
|
491
512
|
stripe: env.STRIPE_SECRET_KEY
|
|
492
513
|
? { source: "env", detail: "STRIPE_SECRET_KEY" }
|
|
493
514
|
: providerStatus("stripe", broker),
|
|
515
|
+
clay: env.CLAY_API_KEY
|
|
516
|
+
? { source: "env", detail: "CLAY_API_KEY" }
|
|
517
|
+
: providerStatus("clay", null),
|
|
518
|
+
openrouter: env.OPENROUTER_API_KEY
|
|
519
|
+
? { source: "env", detail: "OPENROUTER_API_KEY" }
|
|
520
|
+
: providerStatus("openrouter", null),
|
|
494
521
|
};
|
|
495
522
|
|
|
496
523
|
const llm = resolveLlmCredential(env);
|
|
@@ -503,14 +530,14 @@ export function doctorReport(env: Record<string, string | undefined> = process.e
|
|
|
503
530
|
}
|
|
504
531
|
});
|
|
505
532
|
|
|
506
|
-
const
|
|
533
|
+
const connectedCrm = ["hubspot", "salesforce"].filter((provider) => providers[provider].source !== "none");
|
|
507
534
|
const nextSteps =
|
|
508
|
-
|
|
535
|
+
connectedCrm.length === 0
|
|
509
536
|
? [
|
|
510
537
|
"fullstackgtm audit --demo # no credentials needed",
|
|
511
538
|
"fullstackgtm login hubspot # hosted browser OAuth (default; or: login salesforce)",
|
|
512
539
|
]
|
|
513
|
-
: [`fullstackgtm audit --provider ${
|
|
540
|
+
: [`fullstackgtm audit --provider ${connectedCrm[0]}`];
|
|
514
541
|
|
|
515
542
|
return {
|
|
516
543
|
package: packageInfo,
|
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";
|
|
@@ -217,8 +219,8 @@ are phase 2. Recurring execution is the scheduler's job; enrich has no cron.`);
|
|
|
217
219
|
// additional per-run ceiling, never a way to exceed the budget.
|
|
218
220
|
if (apiSkippedCrm || apiSkippedSeen) {
|
|
219
221
|
console.error(
|
|
220
|
-
`Pre-
|
|
221
|
-
`(≈$${((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).`,
|
|
222
224
|
);
|
|
223
225
|
}
|
|
224
226
|
// `cap` is the desired create count bounded by the persistent meter. Raw
|
|
@@ -301,20 +303,16 @@ are phase 2. Recurring execution is the scheduler's job; enrich has no cron.`);
|
|
|
301
303
|
const meterLine = formatAcquireMeter(headroom, costPerRecord);
|
|
302
304
|
const gaugeLine = acquireGaugeLine(headroom, config.acquire.budget ?? {}, paint(colorEnabled(process.stdout)));
|
|
303
305
|
if (!save) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
console.log(meterLine);
|
|
315
|
-
if (gaugeLine) console.log(gaugeLine);
|
|
316
|
-
console.log("\nDry run — nothing written. Re-run with --save to persist the plan, then approve + apply.");
|
|
317
|
-
}
|
|
306
|
+
printAcquireOutput({
|
|
307
|
+
args: rest,
|
|
308
|
+
result,
|
|
309
|
+
meter: headroom,
|
|
310
|
+
meterLine,
|
|
311
|
+
gaugeLine,
|
|
312
|
+
saved: false,
|
|
313
|
+
planSaved: false,
|
|
314
|
+
hostedPlanUrl: null,
|
|
315
|
+
});
|
|
318
316
|
return;
|
|
319
317
|
}
|
|
320
318
|
|
|
@@ -363,24 +361,16 @@ are phase 2. Recurring execution is the scheduler's job; enrich has no cron.`);
|
|
|
363
361
|
acquireCheckpointSync.hostedVersion,
|
|
364
362
|
);
|
|
365
363
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
console.log(` Approve fullstackgtm plans approve ${result.plan.id} --operations all`);
|
|
377
|
-
console.log(` Apply fullstackgtm apply --plan-id ${result.plan.id} --provider hubspot`);
|
|
378
|
-
console.log("\nThe meter is charged only when a create lands at apply.");
|
|
379
|
-
} else {
|
|
380
|
-
console.log(meterLine);
|
|
381
|
-
if (gaugeLine) console.log(gaugeLine);
|
|
382
|
-
console.log("No net-new leads to create (everything sourced already matched, or was withheld by the meter).");
|
|
383
|
-
}
|
|
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
|
+
});
|
|
384
374
|
return;
|
|
385
375
|
}
|
|
386
376
|
|
|
@@ -561,8 +551,9 @@ function formatEnrichCounts(counts: EnrichCounts, ambiguities: number) {
|
|
|
561
551
|
/**
|
|
562
552
|
* Pull net-new prospects from an API acquire source into source records the
|
|
563
553
|
* acquire builder dedupes + turns into create_record ops. Explorium discovers;
|
|
564
|
-
*
|
|
565
|
-
*
|
|
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.
|
|
566
557
|
*/
|
|
567
558
|
async function acquireFromApi(
|
|
568
559
|
config: EnrichConfig,
|
|
@@ -602,7 +593,9 @@ async function acquireFromApi(
|
|
|
602
593
|
? (icp ? icpToExploriumFilters(icp) : (disc.filters ?? {}))
|
|
603
594
|
: disc.provider === "pipe0"
|
|
604
595
|
? (icp ? icpToCrustdataFilters(icp) : (disc.filters ?? {}))
|
|
605
|
-
:
|
|
596
|
+
: disc.provider === "clay"
|
|
597
|
+
? (icp ? icpToClayPeopleFilters(icp) : (disc.filters ?? {}))
|
|
598
|
+
: {};
|
|
606
599
|
const queryFingerprint = createHash("sha256")
|
|
607
600
|
.update(JSON.stringify({ source, provider: disc.provider, listId, filters, icp: icp ?? null }))
|
|
608
601
|
.digest("hex");
|
|
@@ -677,6 +670,19 @@ async function acquireFromApi(
|
|
|
677
670
|
page = result.prospects;
|
|
678
671
|
cursor = result.nextCursor;
|
|
679
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;
|
|
680
686
|
} else if (disc.provider === "explorium") {
|
|
681
687
|
const pageNumber = offset + 1;
|
|
682
688
|
exploriumPage = pageNumber;
|
|
@@ -695,7 +701,7 @@ async function acquireFromApi(
|
|
|
695
701
|
offset += page.length;
|
|
696
702
|
exhausted = page.length < requestSize;
|
|
697
703
|
} else {
|
|
698
|
-
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).`);
|
|
699
705
|
}
|
|
700
706
|
discovered += page.length;
|
|
701
707
|
progress.items(discovered, scanLimit);
|
|
@@ -747,23 +753,37 @@ async function acquireFromApi(
|
|
|
747
753
|
progress.stage(ACQUIRE_STAGES[1], 1, ACQUIRE_STAGES.length);
|
|
748
754
|
progress.note("checking work-email requirements");
|
|
749
755
|
|
|
750
|
-
// 3. Resolve
|
|
751
|
-
//
|
|
752
|
-
//
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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`);
|
|
763
786
|
}
|
|
764
|
-
progress.note(`resolving work emails for ${prospects.length} candidate(s)`);
|
|
765
|
-
const resolved = await pipe0ResolveWorkEmails({ apiKey: providerKey("pipe0"), prospects });
|
|
766
|
-
prospects.splice(0, prospects.length, ...resolved);
|
|
767
787
|
}
|
|
768
788
|
progress.items(prospects.length, prospects.length);
|
|
769
789
|
progress.note(`${prospects.length} candidate(s) ready for planning`);
|
|
@@ -876,6 +896,61 @@ function formatAcquireMeter(headroom: AcquireRemaining, costPerRecord: number):
|
|
|
876
896
|
);
|
|
877
897
|
}
|
|
878
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
|
+
|
|
879
954
|
function hostedRunsUrl(): string | null {
|
|
880
955
|
const baseUrl = getCredential("broker")?.baseUrl?.replace(/\/+$/, "");
|
|
881
956
|
return baseUrl ? `${baseUrl}/dashboard/runs` : null;
|
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
|
|
@@ -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: {
|
|
@@ -695,7 +698,7 @@ export const COMMAND_FLAGS: Record<string, string[]> = {
|
|
|
695
698
|
merge: ["--input", "--out", "--json"],
|
|
696
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"],
|
|
697
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"],
|
|
698
|
-
icp: [...SOURCE_FLAGS, "--name", "--signals-from", "--with-history", "--prompt", "--min-score", "--from-judge", "--golden", "--against-outcomes", "--min-accuracy", "--model", "--label", "--save", "--verbose", "--json", "--out"],
|
|
701
|
+
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"],
|
|
699
702
|
signals: ["--bucket", "--source", "--connector", "--connector-opt", "--watchlist", "--keywords", "--from", "--label", "--since", "--account", "--contact", "--unjudged", "--touch", "--result", "--save", "--verbose", "--json", "--explain"],
|
|
700
703
|
draft: ["--from-judge", "--min-score", "--prompt", "--model", "--channel", "--save", "--dry-run", "--verbose", "--json", "--out"],
|
|
701
704
|
schedule: ["--cron", "--label", "--provider", "--trigger", "--timer", "--runs", "--verbose", "--json"],
|
package/src/cli/icp.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
// Extracted verbatim from src/cli.ts (mechanical split, no behavior change).
|
|
2
2
|
|
|
3
3
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { emitKeypressEvents } from "node:readline";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
4
6
|
import { resolve } from "node:path";
|
|
5
|
-
import { resolveLlmCredential } from "../llm.ts";
|
|
7
|
+
import { resolveLlmCredential, type LlmCallOptions } from "../llm.ts";
|
|
6
8
|
import { fitThreshold, icpFromAnswers, icpToCrustdataFilters, icpToExploriumFilters, INTERVIEW_SPEC } from "../icp.ts";
|
|
7
9
|
import { createFileSignalStore, DEFAULT_SIGNALS_CONFIG, type SignalsConfig } from "../signals.ts";
|
|
8
10
|
import { createFileJudgeStore, DEFAULT_JUDGE_PROMPT, judgeRunId, judgeSignals } from "../judge.ts";
|
|
9
11
|
import { DEFAULT_GOLDEN_NOW_ISO, DEFAULT_GOLDEN_SET, DEFAULT_MIN_ACCURACY, defaultJudgeFn, gradeAgainstOutcomes, gradeJudge, parseGoldenSet } from "../judgeEval.ts";
|
|
10
|
-
import
|
|
11
|
-
import { loadIcp, numericOption, option, readSnapshot, resolveLlmBaseUrls, saveRequested } from "./shared.ts";
|
|
12
|
+
import { loadIcp, numericOption, option, readSecret, readSnapshot, resolveLlmBaseUrls, saveRequested } from "./shared.ts";
|
|
12
13
|
import { createStatusLine } from "./ui.ts";
|
|
14
|
+
import { box, colorEnabled, paint } from "./ui.ts";
|
|
15
|
+
import { getCredential, storeCredential } from "../credentials.ts";
|
|
16
|
+
import { deriveWebsiteIcp, icpReviewSegments, OPENROUTER_API_BASE, type WebsiteIcpDerivation } from "../icpDerive.ts";
|
|
17
|
+
import type { Icp } from "../icp.ts";
|
|
13
18
|
import { unknownSubcommandError } from "./suggest.ts";
|
|
14
19
|
|
|
15
20
|
function renderJudgeDecisions(decisions: Awaited<ReturnType<typeof judgeSignals>>): string {
|
|
@@ -24,6 +29,110 @@ function renderJudgeDecisions(decisions: Awaited<ReturnType<typeof judgeSignals>
|
|
|
24
29
|
return lines.join("\n");
|
|
25
30
|
}
|
|
26
31
|
|
|
32
|
+
function updateReviewSegment(icp: Icp, id: string, raw: string): Icp {
|
|
33
|
+
const values = raw.split(",").map((value) => value.trim()).filter(Boolean);
|
|
34
|
+
if (id === "threshold") return { ...icp, scoring: { ...icp.scoring, threshold: Number(raw) } };
|
|
35
|
+
if (["industries", "employeeBands", "geos", "technologies"].includes(id)) {
|
|
36
|
+
return { ...icp, firmographics: { ...icp.firmographics, [id]: values } };
|
|
37
|
+
}
|
|
38
|
+
if (["titleKeywords", "jobLevels", "departments"].includes(id)) {
|
|
39
|
+
return { ...icp, persona: { ...icp.persona, [id]: values } };
|
|
40
|
+
}
|
|
41
|
+
return { ...icp, signals: { ...icp.signals, intentTopics: values } };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function renderDerivedIcp(result: WebsiteIcpDerivation, selected = -1): string {
|
|
45
|
+
const p = paint(colorEnabled());
|
|
46
|
+
const segments = icpReviewSegments(result.icp);
|
|
47
|
+
const lines = [
|
|
48
|
+
`${result.company.name} ${result.company.domain}`,
|
|
49
|
+
`${Math.round(result.confidence * 100)}% confidence · ${result.derivation.model}`,
|
|
50
|
+
"",
|
|
51
|
+
...segments.map((segment, index) => `${index === selected ? "›" : " "} ${segment.label.padEnd(15)} ${segment.value}`),
|
|
52
|
+
"",
|
|
53
|
+
selected >= 0 ? "↑/↓ select · enter edit · s save · q cancel" : `${result.evidence.length} website-verifiable evidence quote(s)`,
|
|
54
|
+
];
|
|
55
|
+
return box(lines, p, "ICP preview").join("\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function resolveIcpDeriveLlm(args: string[]): Promise<LlmCallOptions> {
|
|
59
|
+
const requested = option(args, "--provider")?.toLowerCase();
|
|
60
|
+
if (requested && !["openrouter", "openai", "anthropic"].includes(requested)) {
|
|
61
|
+
throw new Error(`icp derive --provider must be openrouter, openai, or anthropic; got "${requested}".`);
|
|
62
|
+
}
|
|
63
|
+
const openRouterKey = process.env.OPENROUTER_API_KEY ?? getCredential("openrouter")?.accessToken;
|
|
64
|
+
const normal = resolveLlmCredential();
|
|
65
|
+
if ((!requested || requested === "openrouter") && openRouterKey) {
|
|
66
|
+
return { provider: "openai", apiKey: openRouterKey, openaiBaseUrl: OPENROUTER_API_BASE };
|
|
67
|
+
}
|
|
68
|
+
if (requested === "openai" || requested === "anthropic") {
|
|
69
|
+
const envKey = requested === "openai" ? process.env.OPENAI_API_KEY : process.env.ANTHROPIC_API_KEY;
|
|
70
|
+
const key = envKey ?? getCredential(requested)?.accessToken;
|
|
71
|
+
if (key) return { provider: requested, apiKey: key, ...resolveLlmBaseUrls() };
|
|
72
|
+
}
|
|
73
|
+
if (normal && (!requested || requested === normal.provider)) return { ...normal, ...resolveLlmBaseUrls() };
|
|
74
|
+
const provider = requested as "openrouter" | "openai" | "anthropic" | undefined;
|
|
75
|
+
if (!process.stdin.isTTY || process.env.CI) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
"ICP derivation needs an LLM key. Run one of:\n" +
|
|
78
|
+
" fullstackgtm login openrouter\n fullstackgtm login openai\n fullstackgtm login anthropic\n" +
|
|
79
|
+
"Then rerun the same icp derive command.",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
83
|
+
const answer = provider ?? ((await rl.question("LLM provider [openrouter/openai/anthropic] (openrouter): ")).trim().toLowerCase() || "openrouter");
|
|
84
|
+
rl.close();
|
|
85
|
+
if (!['openrouter', 'openai', 'anthropic'].includes(answer)) throw new Error(`Unknown LLM provider "${answer}".`);
|
|
86
|
+
const selectedProvider = answer as "openrouter" | "openai" | "anthropic";
|
|
87
|
+
const key = await readSecret(`${selectedProvider} API key`);
|
|
88
|
+
if (!key) throw new Error(`No ${answer} API key provided.`);
|
|
89
|
+
const now = new Date().toISOString();
|
|
90
|
+
storeCredential(selectedProvider, { kind: "api_key", accessToken: key, createdAt: now, updatedAt: now });
|
|
91
|
+
console.error(`Stored ${selectedProvider} key in the active FullStackGTM profile.`);
|
|
92
|
+
return selectedProvider === "openrouter"
|
|
93
|
+
? { provider: "openai", apiKey: key, openaiBaseUrl: OPENROUTER_API_BASE }
|
|
94
|
+
: { provider: selectedProvider, apiKey: key };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function reviewDerivedIcp(result: WebsiteIcpDerivation): Promise<WebsiteIcpDerivation | null> {
|
|
98
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return result;
|
|
99
|
+
let current = result;
|
|
100
|
+
let selected = 0;
|
|
101
|
+
const render = () => process.stdout.write(`\u001b[2J\u001b[H${renderDerivedIcp(current, selected)}\n`);
|
|
102
|
+
emitKeypressEvents(process.stdin);
|
|
103
|
+
process.stdin.setRawMode?.(true);
|
|
104
|
+
process.stdin.resume();
|
|
105
|
+
render();
|
|
106
|
+
return new Promise((resolveReview) => {
|
|
107
|
+
const finish = (value: WebsiteIcpDerivation | null) => {
|
|
108
|
+
process.stdin.off("keypress", onKey);
|
|
109
|
+
process.stdin.setRawMode?.(false);
|
|
110
|
+
process.stdin.pause();
|
|
111
|
+
resolveReview(value);
|
|
112
|
+
};
|
|
113
|
+
const onKey = async (_input: string, key: { name?: string; ctrl?: boolean }) => {
|
|
114
|
+
const segments = icpReviewSegments(current.icp);
|
|
115
|
+
if (key.ctrl && key.name === "c" || key.name === "q") return finish(null);
|
|
116
|
+
if (key.name === "up") { selected = (selected - 1 + segments.length) % segments.length; return render(); }
|
|
117
|
+
if (key.name === "down") { selected = (selected + 1) % segments.length; return render(); }
|
|
118
|
+
if (key.name === "s") return finish(current);
|
|
119
|
+
if (key.name !== "return") return;
|
|
120
|
+
process.stdin.off("keypress", onKey);
|
|
121
|
+
process.stdin.setRawMode?.(false);
|
|
122
|
+
const segment = segments[selected];
|
|
123
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
124
|
+
const answer = await rl.question(`\n${segment.label} [${segment.value}]: `);
|
|
125
|
+
rl.close();
|
|
126
|
+
if (answer.trim()) current = { ...current, icp: updateReviewSegment(current.icp, segment.id, answer) };
|
|
127
|
+
emitKeypressEvents(process.stdin);
|
|
128
|
+
process.stdin.setRawMode?.(true);
|
|
129
|
+
process.stdin.on("keypress", onKey);
|
|
130
|
+
render();
|
|
131
|
+
};
|
|
132
|
+
process.stdin.on("keypress", onKey);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
27
136
|
|
|
28
137
|
/**
|
|
29
138
|
* `icp` — develop and inspect the Ideal Customer Profile that targets acquire.
|
|
@@ -38,6 +147,8 @@ export async function icpCommand(args: string[]) {
|
|
|
38
147
|
if (!sub || args.includes("--help") || args.includes("-h")) {
|
|
39
148
|
console.log(`Usage:
|
|
40
149
|
fullstackgtm icp interview emit the interview spec (an agent drives it with AskUserQuestion)
|
|
150
|
+
fullstackgtm icp derive --domain <site> [--model <id>] [--out icp.json] [--json]
|
|
151
|
+
derive an evidence-backed ICP from a public website (OpenRouter/OpenAI/Anthropic)
|
|
41
152
|
fullstackgtm icp set <answers.json> [--name <n>] [--out <path>] write icp.json from interview answers
|
|
42
153
|
fullstackgtm icp show [--icp <path>] render the ICP + the discovery filters it produces
|
|
43
154
|
fullstackgtm icp judge [--signals-from latest|<label>] [--with-history] [--prompt <path>] [--min-score 0] [--save] rank unjudged signals into send/nurture/skip decisions (timing x fit x memory)
|
|
@@ -65,6 +176,36 @@ ops. Develop one by interview, then \`enrich acquire\` picks up ./icp.json.
|
|
|
65
176
|
);
|
|
66
177
|
return;
|
|
67
178
|
}
|
|
179
|
+
if (sub === "derive") {
|
|
180
|
+
const domain = option(rest, "--domain");
|
|
181
|
+
if (!domain) throw new Error("Usage: fullstackgtm icp derive --domain <company.com> [--out icp.json] [--json]");
|
|
182
|
+
const llm = await resolveIcpDeriveLlm(rest);
|
|
183
|
+
const status = createStatusLine();
|
|
184
|
+
let derived: WebsiteIcpDerivation;
|
|
185
|
+
try {
|
|
186
|
+
derived = await deriveWebsiteIcp({
|
|
187
|
+
domain,
|
|
188
|
+
llm,
|
|
189
|
+
model: option(rest, "--model") ?? undefined,
|
|
190
|
+
onProgress: (event) => status.set(event.message),
|
|
191
|
+
});
|
|
192
|
+
} finally {
|
|
193
|
+
status.done();
|
|
194
|
+
}
|
|
195
|
+
if (!rest.includes("--json") && !rest.includes("--no-interactive")) {
|
|
196
|
+
const reviewed = await reviewDerivedIcp(derived);
|
|
197
|
+
if (!reviewed) throw new Error("ICP review cancelled; no file was written.");
|
|
198
|
+
derived = reviewed;
|
|
199
|
+
}
|
|
200
|
+
const out = option(rest, "--out");
|
|
201
|
+
if (out) {
|
|
202
|
+
const path = resolve(process.cwd(), out);
|
|
203
|
+
writeFileSync(path, `${JSON.stringify(derived.icp, null, 2)}\n`);
|
|
204
|
+
console.error(`Wrote reviewed ICP to ${path}. Next: fullstackgtm enrich acquire --source clay --icp ${path}`);
|
|
205
|
+
}
|
|
206
|
+
console.log(rest.includes("--json") ? JSON.stringify(derived, null, 2) : renderDerivedIcp(derived));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
68
209
|
if (sub === "set") {
|
|
69
210
|
const file = rest.find((a) => !a.startsWith("--"));
|
|
70
211
|
if (!file) throw new Error("Usage: fullstackgtm icp set <answers.json> [--name <n>] [--out <path>]");
|
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)) {
|